local _, ns = ...
local L = ns.L or {}

local UIData = ns.UIData or {}
local HOUSING_COMMUNITY_COUPONS_CURRENCY_ID = UIData.HOUSING_COMMUNITY_COUPONS_CURRENCY_ID or 3363
local HOUSING_REQUEST_COOLDOWN = UIData.HOUSING_REQUEST_COOLDOWN or 2.5

local HousingView = {}
ns.UIHousingView = HousingView

local taskUI = {}
HousingView.TaskUI = taskUI

local housingRequestState = {
    houseListAt = 0,
    initiativeAt = 0,
    activityAt = 0,
    favorAtByGUID = {},
    activityNeighborhoodGUID = nil,
    viewingNeighborhoodGUID = nil,
    renderStage = nil,
    renderError = nil,
}
local housingUIRefreshPending = false

local function clamp(value, minimum, maximum)
    if value < minimum then
        return minimum
    end

    if value > maximum then
        return maximum
    end

    return value
end

local function formatElapsed(seconds)
    seconds = math.max(0, math.floor(seconds or 0))

    local days = math.floor(seconds / 86400)
    local hours = math.floor((seconds % 86400) / 3600)
    local minutes = math.floor((seconds % 3600) / 60)

    if days > 0 then
        return string.format(L.TIME_DAY_HOUR or "%d d %d h", days, hours)
    end

    if hours > 0 then
        return string.format(L.TIME_HOUR_MIN or "%d h %d m", hours, minutes)
    end

    return string.format(L.TIME_MIN or "%d m", minutes)
end
local function getHousingRequestNow()
    if type(GetTimePreciseSec) == "function" then
        local ok, now = pcall(GetTimePreciseSec)
        if ok and type(now) == "number" then
            return now
        end
    end

    return tonumber(time and time()) or 0
end

local function canRequestHousing(lastRequestAt)
    return (getHousingRequestNow() - (tonumber(lastRequestAt) or 0)) >= HOUSING_REQUEST_COOLDOWN
end

local function queueHousingUIRefresh(delaySeconds)
    if housingUIRefreshPending then
        return
    end

    housingUIRefreshPending = true
    C_Timer.After(delaySeconds or 1.0, function()
        housingUIRefreshPending = false
        local ui = ns.UI
        if ui
            and ui.frame
            and ui.frame:IsShown()
            and ns
            and ns.GetSelectedMainTab
            and ns:GetSelectedMainTab() == "housing"
            and ui.Refresh
        then
            ui:Refresh()
        end
    end)
end

local function requestHousingLiveData(options)
    options = type(options) == "table" and options or {}
    local houseGUID = options.houseGUID
    local neighborhoodGUID = options.neighborhoodGUID
    local requestedSomething = false

    if options.missingHouseList
        and type(C_Housing) == "table"
        and type(C_Housing.GetPlayerOwnedHouses) == "function"
        and canRequestHousing(housingRequestState.houseListAt)
    then
        housingRequestState.houseListAt = getHousingRequestNow()
        local ok, houseInfoList = pcall(C_Housing.GetPlayerOwnedHouses)
        if ok and type(houseInfoList) == "table" and ns.RememberHousingHouseList then
            ns:RememberHousingHouseList(houseInfoList)
        end
        requestedSomething = true
    end

    if type(C_NeighborhoodInitiative) == "table" then
        if neighborhoodGUID
            and type(C_NeighborhoodInitiative.SetViewingNeighborhood) == "function"
            and housingRequestState.viewingNeighborhoodGUID ~= neighborhoodGUID
        then
            housingRequestState.viewingNeighborhoodGUID = neighborhoodGUID
            pcall(C_NeighborhoodInitiative.SetViewingNeighborhood, neighborhoodGUID)
            requestedSomething = true
        end

        if options.missingInitiative
            and type(C_NeighborhoodInitiative.RequestNeighborhoodInitiativeInfo) == "function"
            and canRequestHousing(housingRequestState.initiativeAt)
        then
            housingRequestState.initiativeAt = getHousingRequestNow()
            pcall(C_NeighborhoodInitiative.RequestNeighborhoodInitiativeInfo)
            requestedSomething = true
        end

        if options.missingActivity
            and type(C_NeighborhoodInitiative.RequestInitiativeActivityLog) == "function"
            and canRequestHousing(housingRequestState.activityAt)
        then
            housingRequestState.activityAt = getHousingRequestNow()
            housingRequestState.activityNeighborhoodGUID = neighborhoodGUID or housingRequestState.viewingNeighborhoodGUID
            pcall(C_NeighborhoodInitiative.RequestInitiativeActivityLog)
            requestedSomething = true
        end
    end

    if options.missingFavor
        and houseGUID
        and type(C_Housing) == "table"
        and type(C_Housing.GetCurrentHouseLevelFavor) == "function"
        and canRequestHousing(housingRequestState.favorAtByGUID[houseGUID])
    then
        housingRequestState.favorAtByGUID[houseGUID] = getHousingRequestNow()
        local ok, houseLevelFavor = pcall(C_Housing.GetCurrentHouseLevelFavor, houseGUID)
        if ok and type(houseLevelFavor) == "table" and ns.RememberHousingLevelFavor then
            ns:RememberHousingLevelFavor(houseLevelFavor)
        end
        requestedSomething = true
    end

    if requestedSomething then
        queueHousingUIRefresh(1.0)
    end
end

local function normalizeHousingText(text)
    if type(text) ~= "string" then
        return text
    end

    local fixed = text
    fixed = fixed:gsub("ÃƒÂ¤", "ä")
    fixed = fixed:gsub("ÃƒÂ¶", "ö")
    fixed = fixed:gsub("ÃƒÂ¼", "ü")
    fixed = fixed:gsub("Ãƒâ€ž", "Ä")
    fixed = fixed:gsub("Ãƒâ€“", "Ö")
    fixed = fixed:gsub("ÃƒÅ“", "Ü")
    fixed = fixed:gsub("ÃƒÅ¸", "ß")
    fixed = fixed:gsub("Ã¤", "ä")
    fixed = fixed:gsub("Ã¶", "ö")
    fixed = fixed:gsub("Ã¼", "ü")
    fixed = fixed:gsub("Ã„", "Ä")
    fixed = fixed:gsub("Ã–", "Ö")
    fixed = fixed:gsub("Ãœ", "Ü")
    fixed = fixed:gsub("ÃŸ", "ß")
    return fixed
end

local function getHousingOwnedHouses()
    return (ns.GetHousingHouseList and ns:GetHousingHouseList()) or {}
end

local function getHousingActiveNeighborhoodGUID()
    if type(C_NeighborhoodInitiative) == "table" and type(C_NeighborhoodInitiative.GetActiveNeighborhood) == "function" then
        local ok, activeNeighborhoodGUID = pcall(C_NeighborhoodInitiative.GetActiveNeighborhood)
        if ok then
            return activeNeighborhoodGUID
        end
    end

    return nil
end

local function getHousingCurrentInitiativeInfo()
    if type(C_NeighborhoodInitiative) == "table" and type(C_NeighborhoodInitiative.GetNeighborhoodInitiativeInfo) == "function" then
        local ok, initiativeInfo = pcall(C_NeighborhoodInitiative.GetNeighborhoodInitiativeInfo)
        if ok and type(initiativeInfo) == "table" then
            return initiativeInfo
        end
    end

    return nil
end

local function getHousingCurrentActivityLogInfo()
    if type(C_NeighborhoodInitiative) == "table" and type(C_NeighborhoodInitiative.GetInitiativeActivityLogInfo) == "function" then
        local ok, activityLogInfo = pcall(C_NeighborhoodInitiative.GetInitiativeActivityLogInfo)
        if ok and type(activityLogInfo) == "table" then
            return activityLogInfo
        end
    end

    return nil
end

local function rememberHousingRenderError(stage, err)
    if not stage or err == nil then
        housingRequestState.renderStage = nil
        housingRequestState.renderError = nil
        return
    end

    local text = tostring(err or "")
    text = text:gsub("[\r\n]+", " ")
    if #text > 220 then
        text = text:sub(1, 217) .. "..."
    end

    housingRequestState.renderStage = tostring(stage)
    housingRequestState.renderError = text
end

function taskUI.canToggle()
    return type(C_NeighborhoodInitiative) == "table"
        and (
            type(C_NeighborhoodInitiative.AddTrackedInitiativeTask) == "function"
            or type(C_NeighborhoodInitiative.RemoveTrackedInitiativeTask) == "function"
        )
end

function taskUI.toggle(taskID, isTracked)
    if taskID == nil or type(C_NeighborhoodInitiative) ~= "table" then
        return
    end

    local didToggle = false
    if isTracked then
        if type(C_NeighborhoodInitiative.RemoveTrackedInitiativeTask) == "function" then
            didToggle = pcall(C_NeighborhoodInitiative.RemoveTrackedInitiativeTask, taskID)
        end
    else
        if type(C_NeighborhoodInitiative.AddTrackedInitiativeTask) == "function" then
            didToggle = pcall(C_NeighborhoodInitiative.AddTrackedInitiativeTask, taskID)
        end
    end

    if didToggle then
        queueHousingUIRefresh(0.15)
    end
end

function taskUI.buildTooltipLines(entry)
    local lines = {}
    for _, line in ipairs(type(entry and entry.tooltipLines) == "table" and entry.tooltipLines or {}) do
        if type(line) == "string" and line ~= "" then
            lines[#lines + 1] = line
        end
    end

    if entry and entry.id ~= nil and taskUI.canToggle() then
        lines[#lines + 1] = entry.tracked
            and (L.HOUSING_TASK_UNTRACK_HINT or "Left-click to remove this task.")
            or (L.HOUSING_TASK_TRACK_HINT or "Left-click to pin this task.")
    end

    return lines
end

local function collectHousingSequence(entries)
    local collected = {}
    if type(entries) ~= "table" then
        return collected
    end

    local numericKeys = {}
    for key, value in pairs(entries) do
        if type(key) == "number" and value ~= nil then
            numericKeys[#numericKeys + 1] = key
        end
    end

    if #numericKeys > 0 then
        table.sort(numericKeys)
        for _, key in ipairs(numericKeys) do
            collected[#collected + 1] = entries[key]
        end
        return collected
    end

    for _, value in pairs(entries) do
        if value ~= nil then
            collected[#collected + 1] = value
        end
    end

    return collected
end

local function findHousingHouseByNeighborhoodGUID(houseInfoList, neighborhoodGUID)
    if not neighborhoodGUID then
        return nil
    end

    for _, houseInfo in ipairs(houseInfoList or {}) do
        if houseInfo.neighborhoodGUID == neighborhoodGUID then
            return houseInfo
        end
    end

    return nil
end

local function resolveHousingTargetNeighborhoodGUID(houseInfoList, initiativeInfo, activeNeighborhoodGUID)
    if activeNeighborhoodGUID then
        return activeNeighborhoodGUID
    end

    if type(initiativeInfo) == "table"
        and initiativeInfo.isLoaded == true
        and initiativeInfo.neighborhoodGUID
    then
        return initiativeInfo.neighborhoodGUID
    end

    if housingRequestState.viewingNeighborhoodGUID then
        return housingRequestState.viewingNeighborhoodGUID
    end

    if houseInfoList and houseInfoList[1] then
        return houseInfoList[1].neighborhoodGUID
    end

    return nil
end

local function getHousingSelectedHouse(houseInfoList, activeNeighborhoodGUID, targetNeighborhoodGUID)
    local activeHouse = findHousingHouseByNeighborhoodGUID(houseInfoList, activeNeighborhoodGUID)
    if activeHouse then
        return activeHouse
    end

    local targetHouse = findHousingHouseByNeighborhoodGUID(houseInfoList, targetNeighborhoodGUID)
    if targetHouse then
        return targetHouse
    end

    return houseInfoList and houseInfoList[1] or nil
end

local function getHousingRenderableInitiativeInfo(initiativeInfo, targetNeighborhoodGUID)
    if type(initiativeInfo) ~= "table" then
        return nil, false
    end

    if initiativeInfo.isLoaded ~= true then
        return initiativeInfo, false
    end

    if targetNeighborhoodGUID
        and initiativeInfo.neighborhoodGUID
        and initiativeInfo.neighborhoodGUID ~= targetNeighborhoodGUID
    then
        return initiativeInfo, true
    end

    return initiativeInfo, false
end

local function getHousingRenderableActivityLogInfo(activityLogInfo, targetNeighborhoodGUID, activeNeighborhoodGUID)
    if type(activityLogInfo) == "table" and activityLogInfo.isLoaded == true then
        return activityLogInfo
    end

    return nil
end

local function getHousingHouseLevelFavor(houseGUID)
    if ns.GetHousingLevelFavor then
        return ns:GetHousingLevelFavor(houseGUID)
    end

    return nil
end

local function getHousingTaskProgress(task)
    if type(task.requirementsList) == "table" and #task.requirementsList > 0 then
        local requirement = task.requirementsList[1]
        local requirementText = requirement and requirement.requirementText
        if type(requirementText) == "string" and requirementText ~= "" then
            local current, maximum = requirementText:match("(%d+)%s*/%s*(%d+)")
            if current and maximum then
                return tonumber(current) or 0, tonumber(maximum) or 1, requirementText
            end
        end
    end

    return task.completed and 1 or 0, 1, nil
end

local function getHousingTaskCouponReward(task)
    if not task or not task.rewardQuestID or task.rewardQuestID == 0 then
        return 0
    end

    if C_QuestLog and type(C_QuestLog.GetQuestRewardCurrencies) == "function" then
        local ok, rewards = pcall(C_QuestLog.GetQuestRewardCurrencies, task.rewardQuestID)
        if ok and type(rewards) == "table" then
            for _, reward in ipairs(rewards) do
                if reward.currencyID == HOUSING_COMMUNITY_COUPONS_CURRENCY_ID then
                    return tonumber(reward.totalRewardAmount) or 0
                end
            end
        end
    end

    return 0
end


HousingView.RememberRenderError = rememberHousingRenderError

local function formatHousingValue(value)
    value = tonumber(value) or 0
    if math.abs(value - math.floor(value + 0.5)) < 0.01 then
        return tostring(math.floor(value + 0.5))
    end

    return string.format("%.1f", value)
end

local function buildHousingMilestoneEntries(initiativeInfo, currentProgress)
    local entries = {}
    local thresholds = {}
    local maxProgress = 0

    if type(initiativeInfo) == "table" and type(initiativeInfo.milestones) == "table" then
        for _, milestone in ipairs(collectHousingSequence(initiativeInfo.milestones)) do
            local threshold = tonumber(milestone and milestone.requiredContributionAmount) or 0
            if threshold > 0 then
                thresholds[#thresholds + 1] = threshold
                maxProgress = math.max(maxProgress, threshold)

                local remaining = math.max(0, threshold - currentProgress)
                entries[#entries + 1] = {
                    label = string.format(L.HOUSING_MILESTONE_LABEL or "Milestone %d", threshold),
                    text = remaining <= 0
                        and (L.HOUSING_MILESTONE_REACHED or "Reached")
                        or string.format(L.HOUSING_MILESTONE_LEFT or "%d left", remaining),
                    status = remaining <= 0 and "complete" or (remaining <= math.max(5, math.floor(threshold * 0.15)) and "turnin" or "open"),
                    tooltipTitle = string.format(L.HOUSING_MILESTONE_LABEL or "Milestone %d", threshold),
                    tooltipLines = {
                        string.format("%d / %d", currentProgress, threshold),
                    },
                }
            end
        end
    end

    table.sort(entries, function(a, b)
        return (tonumber(a and a.tooltipLines and a.tooltipLines[1] and a.tooltipLines[1]:match("/%s*(%d+)")) or 0)
            < (tonumber(b and b.tooltipLines and b.tooltipLines[1] and b.tooltipLines[1]:match("/%s*(%d+)")) or 0)
    end)

    return entries, thresholds, maxProgress
end

local function buildHousingActivityEntries(activityLogInfo)
    local entries = {}
    local taskActivity = type(activityLogInfo) == "table" and collectHousingSequence(activityLogInfo.taskActivity) or nil
    local playerName = UnitName and UnitName("player") or nil

    if type(taskActivity) ~= "table" then
        return entries
    end

    table.sort(taskActivity, function(a, b)
        return (tonumber(a and a.completionTime) or 0) > (tonumber(b and b.completionTime) or 0)
    end)

    for _, activity in ipairs(taskActivity) do
        if #entries >= 10 then
            break
        end

        if type(activity) == "table" then
            local taskName = normalizeHousingText(activity.taskName or (L.UNKNOWN or "Unknown"))
            local sourceName = normalizeHousingText(activity.playerName or (L.UNKNOWN or "Unknown"))
            local amount = formatHousingValue(activity.amount or 0)
            local completionTime = tonumber(activity.completionTime) or 0
            local tooltipLines = {
                sourceName,
                string.format(L.HOUSING_ACTIVITY_AMOUNT or "+%s XP", amount),
            }

            if completionTime > 0 then
                tooltipLines[#tooltipLines + 1] = date("%d.%m.%Y %H:%M", completionTime)
            end

            entries[#entries + 1] = {
                label = string.format(L.HOUSING_ACTIVITY_BY or "%s: %s", sourceName, taskName),
                text = string.format(L.HOUSING_ACTIVITY_AMOUNT or "+%s XP", amount),
                status = sourceName == playerName and "complete" or "open",
                tooltipTitle = taskName,
                tooltipLines = tooltipLines,
            }
        end
    end

    return entries
end

function HousingView.BuildEndeavorsView(character)
    if not character or not (ns.IsCurrentCharacterKey and ns:IsCurrentCharacterKey(character.key)) then
        local storedSnapshot = ns.GetStoredHousingSnapshot and ns:GetStoredHousingSnapshot(character) or nil
        if storedSnapshot then
            return storedSnapshot
        end

        return {
            available = false,
            liveOnly = true,
            message = L.HOUSING_NO_SNAPSHOT or "No saved Housing snapshot yet for this character. Log onto the character once to capture it.",
        }
    end

    if type(C_Housing) ~= "table"
        or type(C_NeighborhoodInitiative) ~= "table"
        or type(C_CurrencyInfo) ~= "table"
        or type(C_CurrencyInfo.GetCurrencyInfo) ~= "function"
    then
        return {
            available = false,
            message = L.HOUSING_UNAVAILABLE or "Housing data is not available yet.",
        }
    end

    local rawInitiativeInfo = getHousingCurrentInitiativeInfo()
    local activeNeighborhoodGUID = getHousingActiveNeighborhoodGUID()
    local houseInfoList = getHousingOwnedHouses()
    local targetNeighborhoodGUID = resolveHousingTargetNeighborhoodGUID(houseInfoList, rawInitiativeInfo, activeNeighborhoodGUID)
    local selectedHouse = getHousingSelectedHouse(
        houseInfoList,
        activeNeighborhoodGUID,
        targetNeighborhoodGUID
    )
    local houseGUID = selectedHouse and selectedHouse.houseGUID or nil
    if not targetNeighborhoodGUID and selectedHouse then
        targetNeighborhoodGUID = selectedHouse.neighborhoodGUID
    end

    local initiativeInfo, staleInitiativeInfo = getHousingRenderableInitiativeInfo(rawInitiativeInfo, targetNeighborhoodGUID)
    local houseLevelFavor = getHousingHouseLevelFavor(houseGUID)

    local currencyInfo = C_CurrencyInfo.GetCurrencyInfo(HOUSING_COMMUNITY_COUPONS_CURRENCY_ID) or {}
    local coupons = tonumber(currencyInfo.quantity) or 0
    local couponCap = tonumber(currencyInfo.maxQuantity) or 0
    local couponName = currencyInfo.name or (L.HOUSING_HOME_COUPONS or "Coupons")

    local rawInitiativeTasks = (initiativeInfo and type(initiativeInfo.tasks) == "table") and collectHousingSequence(initiativeInfo.tasks) or {}
    local needsTaskRefresh = initiativeInfo
        and initiativeInfo.isLoaded
        and (tonumber(initiativeInfo.initiativeID) or 0) > 0
        and #rawInitiativeTasks == 0

    local trackedLookup = {}
    if type(C_NeighborhoodInitiative.GetTrackedInitiativeTasks) == "function" then
        local ok, trackedInfo = pcall(C_NeighborhoodInitiative.GetTrackedInitiativeTasks)
        if ok and type(trackedInfo) == "table" and type(trackedInfo.trackedIDs) == "table" then
            for _, taskID in ipairs(trackedInfo.trackedIDs) do
                trackedLookup[tonumber(taskID) or taskID] = true
            end
        end
    end

    local activityLogInfo = getHousingRenderableActivityLogInfo(
        getHousingCurrentActivityLogInfo(),
        targetNeighborhoodGUID,
        activeNeighborhoodGUID
    )

    requestHousingLiveData({
        missingHouseList = #houseInfoList == 0,
        missingInitiative = targetNeighborhoodGUID ~= nil and (staleInitiativeInfo or not (initiativeInfo and initiativeInfo.isLoaded) or needsTaskRefresh),
        missingActivity = (not activeNeighborhoodGUID or targetNeighborhoodGUID == activeNeighborhoodGUID) and activityLogInfo == nil,
        neighborhoodGUID = targetNeighborhoodGUID,
        houseGUID = houseGUID,
        missingFavor = houseGUID ~= nil and houseLevelFavor == nil,
    })

    if targetNeighborhoodGUID and not (initiativeInfo and initiativeInfo.isLoaded) then
        queueHousingUIRefresh(3.0)
        return {
            available = false,
            loading = true,
            message = L.HOUSING_LOADING or "Housing data is loading. Give the game a moment to deliver house and neighborhood information.",
        }
    end

    if needsTaskRefresh then
        queueHousingUIRefresh(3.0)
    end

    local currentLevel = tonumber(houseLevelFavor and houseLevelFavor.houseLevel) or 0
    local currentFavor = tonumber(houseLevelFavor and houseLevelFavor.houseFavor) or 0
    local maxLevel = 0
    if type(C_Housing.GetMaxHouseLevel) == "function" then
        local ok, liveMaxLevel = pcall(C_Housing.GetMaxHouseLevel)
        if ok then
            maxLevel = tonumber(liveMaxLevel) or 0
        end
    end

    local nextLevelFavor = 0
    if currentLevel > 0
        and maxLevel > 0
        and currentLevel < maxLevel
        and type(C_Housing.GetHouseLevelFavorForLevel) == "function"
    then
        local ok, neededFavor = pcall(C_Housing.GetHouseLevelFavorForLevel, currentLevel + 1)
        if ok then
            nextLevelFavor = tonumber(neededFavor) or 0
        end
    end

    local playerContribution = tonumber(initiativeInfo and initiativeInfo.playerTotalContribution) or 0
    local currentProgress = tonumber(initiativeInfo and initiativeInfo.currentProgress) or 0
    local tasks = {}
    local trackedTasks = {}
    local completedTasks = 0

    local seenTaskIDs = {}
    local function appendHousingInitiativeTask(task, allowDuplicates)
        if type(task) ~= "table" then
            return
        end

        local taskID = tonumber(task.ID) or task.ID
        if not allowDuplicates and taskID ~= nil and seenTaskIDs[taskID] then
            return
        end

        local current, maximum, requirementText = getHousingTaskProgress(task)
        local tracked = trackedLookup[taskID] == true
        local completed = task.completed == true
        local couponReward = getHousingTaskCouponReward(task)
        local repeatable = (tonumber(task.taskType) or 0) > 0
        local houseXpReward = tonumber(task.progressContributionAmount) or 0
        local name = normalizeHousingText(task.taskName or (L.UNKNOWN or "Unknown"))
        local description = normalizeHousingText(task.description or "")
        local status = completed and "complete" or ((tonumber(current) or 0) > 0 and "turnin" or "open")
        local statusText = completed and (L.HOUSING_TASK_PROGRESS_DONE or L.STATUS_DONE or "Done")
            or ((maximum or 1) > 1 and string.format("%d/%d", current or 0, maximum or 1))
            or (tracked and (L.HOUSING_TASK_TRACKED or "Tracked") or (L.HOUSING_TASK_PROGRESS_OPEN or "Open"))
        local metaParts = {}
        local tooltipLines = {}

        if description ~= "" then
            tooltipLines[#tooltipLines + 1] = description
        end
        if requirementText and requirementText ~= "" then
            tooltipLines[#tooltipLines + 1] = requirementText
        end
        if houseXpReward > 0 then
            local rewardText = string.format(L.HOUSING_TASK_XP or "+%d house XP", houseXpReward)
            metaParts[#metaParts + 1] = rewardText
            tooltipLines[#tooltipLines + 1] = rewardText
        end
        if couponReward > 0 then
            local couponText = string.format(L.HOUSING_TASK_COUPONS or "+%d coupons", couponReward)
            metaParts[#metaParts + 1] = couponText
            tooltipLines[#tooltipLines + 1] = couponText
        end
        if repeatable then
            metaParts[#metaParts + 1] = L.HOUSING_TASK_REPEATABLE or "Repeatable"
        end
        if tracked then
            metaParts[#metaParts + 1] = L.HOUSING_TASK_TRACKED or "Tracked"
        end
        if repeatable and (tonumber(task.timesCompleted) or 0) > 0 then
            tooltipLines[#tooltipLines + 1] = string.format(
                L.HOUSING_TASK_TIMES or "%dx completed",
                tonumber(task.timesCompleted) or 0
            )
        end

        local entry = {
            id = taskID,
            title = name,
            meta = table.concat(metaParts, "  |  "),
            status = status,
            statusText = statusText,
            progressCurrent = tonumber(current) or 0,
            progressMax = tonumber(maximum) or 1,
            progressRatio = (tonumber(maximum) or 0) > 1 and clamp((tonumber(current) or 0) / (tonumber(maximum) or 1), 0, 1) or 0,
            tooltipTitle = name,
            tooltipLines = tooltipLines,
            completed = completed,
            tracked = tracked,
            sortOrder = tonumber(task.sortOrder) or 999,
        }

        if taskID ~= nil then
            seenTaskIDs[taskID] = true
        end
        tasks[#tasks + 1] = entry
        if tracked and not completed then
            trackedTasks[#trackedTasks + 1] = {
                id = taskID,
                label = name,
                text = (tonumber(maximum) or 1) > 1 and string.format("%d/%d", current or 0, maximum or 1)
                    or (L.HOUSING_TASK_TRACKED or "Tracked"),
                status = status,
                tooltipTitle = name,
                tooltipLines = tooltipLines,
                tracked = true,
            }
        end
        if completed then
            completedTasks = completedTasks + 1
        end
    end

    for _, task in ipairs(rawInitiativeTasks) do
        appendHousingInitiativeTask(task, false)
    end

    if #tasks == 0 and #rawInitiativeTasks > 0 then
        for _, task in ipairs(rawInitiativeTasks) do
            appendHousingInitiativeTask(task, true)
        end
    end

    table.sort(tasks, function(a, b)
        if a.completed ~= b.completed then
            return not a.completed
        end
        return (a.sortOrder or 999) < (b.sortOrder or 999)
    end)

    table.sort(trackedTasks, function(a, b)
        return (a.label or "") < (b.label or "")
    end)

    local milestoneEntries, milestoneThresholds, milestoneMaxProgress = buildHousingMilestoneEntries(initiativeInfo, currentProgress)
    local maxProgress = milestoneMaxProgress
    if maxProgress <= 0 then
        maxProgress = tonumber(initiativeInfo and initiativeInfo.progressRequired) or 100
    end

    local nextMilestoneRemaining
    for _, threshold in ipairs(milestoneThresholds or {}) do
        if threshold > currentProgress then
            nextMilestoneRemaining = threshold - currentProgress
            break
        end
    end

    local noteParts = {}
    if nextMilestoneRemaining then
        noteParts[#noteParts + 1] = string.format(L.HOUSING_PROGRESS_NEXT or "%d to next milestone", nextMilestoneRemaining)
    elseif #milestoneEntries > 0 then
        noteParts[#noteParts + 1] = L.HOUSING_PROGRESS_REACHED or "All milestones reached"
    end
    if (tonumber(initiativeInfo and initiativeInfo.duration) or 0) > 0 then
        noteParts[#noteParts + 1] = string.format(
            L.HOUSING_PROGRESS_DAYS or "%s remaining",
            formatElapsed(tonumber(initiativeInfo.duration) or 0)
        )
    end
    if initiativeInfo and initiativeInfo.neighborhoodGUID and activeNeighborhoodGUID and initiativeInfo.neighborhoodGUID ~= activeNeighborhoodGUID then
        noteParts[#noteParts + 1] = L.HOUSING_NOTE_NON_ACTIVE or "The client is currently viewing a different neighborhood than your active one."
    end

    local houseTitle = selectedHouse and selectedHouse.houseName
    if type(houseTitle) ~= "string" or houseTitle == "" then
        houseTitle = selectedHouse and selectedHouse.neighborhoodName or (L.HOUSING_HOME_NONE or "No house data yet")
    end
    local houseSubtitle = ""
    if selectedHouse
        and type(selectedHouse.neighborhoodName) == "string"
        and selectedHouse.neighborhoodName ~= ""
        and selectedHouse.neighborhoodName ~= houseTitle
    then
        houseSubtitle = selectedHouse.neighborhoodName
    end

    local homeStateText = ((selectedHouse and activeNeighborhoodGUID and selectedHouse.neighborhoodGUID == activeNeighborhoodGUID)
        or (not selectedHouse and targetNeighborhoodGUID and activeNeighborhoodGUID and targetNeighborhoodGUID == activeNeighborhoodGUID))
        and (L.HOUSING_HOME_ACTIVE or "Active neighborhood")
        or (L.HOUSING_HOME_VIEWING or "Viewing neighborhood")
    local weeklyEntry = ns.GetLiveHousingWeeklyState and ns:GetLiveHousingWeeklyState(character, nil) or nil
    local activityEntries = buildHousingActivityEntries(activityLogInfo)
    local taskCount = (#tasks > 0 and #tasks) or #rawInitiativeTasks
    local trackedCount = #trackedTasks

    local view = {
        available = true,
        updatedAt = time(),
        title = initiativeInfo and normalizeHousingText(initiativeInfo.title or "") or (L.HOUSING_TITLE or "Housing"),
        subtitle = "",
        statCards = {
            {
                label = L.HOUSING_SUMMARY_HOME_LEVEL or "House Level",
                value = currentLevel > 0 and string.format("%d/%d", currentLevel, maxLevel > 0 and maxLevel or currentLevel) or "--",
                meta = nextLevelFavor > 0 and string.format("%d / %d", currentFavor, nextLevelFavor)
                    or ((currentLevel > 0 and (L.HOUSING_HOME_MAX or "Max level reached")) or ""),
            },
            {
                label = couponName,
                value = tostring(coupons),
                meta = couponCap > 0 and string.format("%d / %d", coupons, couponCap) or couponName,
            },
            {
                label = L.HOUSING_SUMMARY_CONTRIBUTION or "Your Contribution",
                value = tostring(math.floor(playerContribution + 0.5)),
                meta = string.format("%d / %d", currentProgress, maxProgress),
            },
            {
                label = L.HOUSING_SUMMARY_TASKS or "Tasks",
                value = string.format("%d/%d", completedTasks, taskCount),
                meta = trackedCount > 0
                    and string.format("%d %s", trackedCount, L.HOUSING_TASK_TRACKED or "Tracked")
                    or (L.HOUSING_TRACKED_EMPTY or "No tracked housing tasks yet."),
            },
        },
        progress = {
            label = L.HOUSING_PROGRESS_LABEL or "Endeavor Progress",
            value = string.format("%d / %d", currentProgress, maxProgress),
            ratio = maxProgress > 0 and clamp(currentProgress / maxProgress, 0, 1) or 0,
            thresholds = milestoneThresholds,
            maxThreshold = maxProgress,
            note = table.concat(noteParts, "  |  "),
        },
        loadingTasks = needsTaskRefresh,
        house = {
            title = normalizeHousingText(houseTitle or (L.HOUSING_HOME_NONE or "No house data yet")),
            subtitle = normalizeHousingText(houseSubtitle or ""),
            meta = string.format(
                L.HOUSING_HOME_META or "%d houses  |  %s",
                #houseInfoList,
                homeStateText
            ),
            levelValue = currentLevel > 0 and string.format("%d/%d", currentLevel, maxLevel > 0 and maxLevel or currentLevel) or "--",
            favorValue = nextLevelFavor > 0 and string.format("%d / %d", currentFavor, nextLevelFavor)
                or ((currentLevel > 0 and (L.HOUSING_HOME_MAX or "Max level reached")) or "--"),
            favorRatio = nextLevelFavor > 0 and clamp(currentFavor / nextLevelFavor, 0, 1) or (currentLevel > 0 and 1 or 0),
            couponLabel = normalizeHousingText(couponName or (L.HOUSING_HOME_COUPONS or "Coupons")),
            couponValue = tostring(coupons),
            couponIcon = currencyInfo.iconFileID,
        },
        weekly = weeklyEntry,
        tasks = tasks,
        trackedTasks = trackedTasks,
        milestones = milestoneEntries,
        activity = activityEntries,
    }
    if ns.RememberHousingSnapshot then
        ns:RememberHousingSnapshot(view, character)
    end
    return view
end
