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

local DATA = ns.Data or {}
local MIDNIGHT_PROFESSION_SERVICE_QUESTS = DATA.MIDNIGHT_PROFESSION_SERVICE_QUESTS or {}
local MIDNIGHT_PROFESSION_TREATISE_QUESTS = DATA.MIDNIGHT_PROFESSION_TREATISE_QUESTS or {}
local DARKMOON_PROFESSION_QUESTS = DATA.DARKMOON_PROFESSION_QUESTS or {}
local DARKMOON_FAIRE_QUESTS = DATA.DARKMOON_FAIRE_QUESTS or {}
local PROFESSION_SKILL_LINE_TO_KEY = DATA.PROFESSION_SKILL_LINE_TO_KEY or {}

local Util = ns.Util or {}
local getWeeklyResetInfo = Util.GetWeeklyResetInfo

local Quest = ns.Quest or {}
local getQuestObjectiveLines = Quest.GetQuestObjectiveLines
local buildQuestPoolState = Quest.BuildQuestPoolState
local getQuestTitle = Quest.GetQuestTitle

local DARKMOON_MAP_IDS = {
    [407] = true,
    [974] = true,
}
local DARKMOON_CALENDAR_TEXTURES = {
    [235448] = true,
    [235447] = true,
    [235446] = true,
}

local darkmoonCacheExpiresAt = 0
local darkmoonCacheValue = nil
local darkmoonKnownStartTime = 0
local darkmoonKnownEndTime = 0
local darkmoonCalendarRequestAt = 0

local function copyCalendarTime(timeInfo)
    if type(timeInfo) ~= "table" then
        return nil
    end

    return {
        year = tonumber(timeInfo.year),
        month = tonumber(timeInfo.month),
        day = tonumber(timeInfo.monthDay or timeInfo.day),
        hour = tonumber(timeInfo.hour) or 0,
        min = tonumber(timeInfo.minute or timeInfo.min) or 0,
        sec = tonumber(timeInfo.second or timeInfo.sec) or 0,
    }
end

local function getCalendarEpoch(timeInfo)
    local copy = copyCalendarTime(timeInfo)
    if not copy or not copy.year or not copy.month or not copy.day then
        return nil
    end

    return time(copy)
end

local function requestDarkmoonCalendar()
    if not C_Calendar or type(C_Calendar.OpenCalendar) ~= "function" then
        return
    end

    local now = time()
    if darkmoonCalendarRequestAt > 0 and (now - darkmoonCalendarRequestAt) < 30 then
        return
    end

    darkmoonCalendarRequestAt = now
    pcall(C_Calendar.OpenCalendar)
end

local function isPlayerOnDarkmoonIsland()
    if C_Map and type(C_Map.GetBestMapForUnit) == "function" then
        local ok, mapID = pcall(C_Map.GetBestMapForUnit, "player")
        if ok and DARKMOON_MAP_IDS[tonumber(mapID)] then
            return true
        end
    end

    return false
end

local function isLoggingOut()
    return ns and ns.isPlayerLoggingOut == true
end

local function scanDarkmoonCalendar()
    if not C_Calendar
        or not C_DateAndTime
        or type(C_DateAndTime.GetCurrentCalendarTime) ~= "function"
        or type(C_Calendar.SetAbsMonth) ~= "function"
        or type(C_Calendar.GetNumDayEvents) ~= "function"
        or type(C_Calendar.GetHolidayInfo) ~= "function"
    then
        return false, nil
    end

    local ok, currentCalendarTime = pcall(C_DateAndTime.GetCurrentCalendarTime)
    if not ok or type(currentCalendarTime) ~= "table" then
        return false, nil
    end

    local currentEpoch = getCalendarEpoch(currentCalendarTime)
    if not currentEpoch then
        return false, nil
    end

    if darkmoonKnownStartTime > 0 and darkmoonKnownEndTime > 0 then
        if currentEpoch >= darkmoonKnownStartTime and currentEpoch <= darkmoonKnownEndTime then
            return true, darkmoonKnownEndTime
        end

        darkmoonKnownStartTime = 0
        darkmoonKnownEndTime = 0
    end

    local openMonth, openYear
    if CalendarFrame and type(CalendarFrame.IsShown) == "function" and CalendarFrame:IsShown()
        and type(C_Calendar.GetMonthInfo) == "function"
    then
        local monthOk, monthInfo = pcall(C_Calendar.GetMonthInfo)
        if monthOk and type(monthInfo) == "table" then
            openMonth, openYear = monthInfo.month, monthInfo.year
        end
    end

    pcall(C_Calendar.SetAbsMonth, currentCalendarTime.month, currentCalendarTime.year)

    local numOk, numDayEvents = pcall(C_Calendar.GetNumDayEvents, 0, currentCalendarTime.monthDay)
    if not numOk then
        numDayEvents = 0
    end

    local foundActive = false
    local foundEndTime = nil
    for index = 1, tonumber(numDayEvents) or 0 do
        local holidayOk, holidayData = pcall(C_Calendar.GetHolidayInfo, 0, currentCalendarTime.monthDay, index)
        local texture = holidayOk and type(holidayData) == "table" and tonumber(holidayData.texture) or nil
        if texture and DARKMOON_CALENDAR_TEXTURES[texture] then
            local startTime = getCalendarEpoch(holidayData.startTime)
            local endTime = getCalendarEpoch(holidayData.endTime)
            if startTime and endTime then
                darkmoonKnownStartTime = startTime
                darkmoonKnownEndTime = endTime
                if currentEpoch >= startTime and currentEpoch <= endTime then
                    foundActive = true
                    foundEndTime = endTime
                    break
                end
            else
                foundActive = true
                break
            end
        end
    end

    if openMonth and openYear then
        pcall(C_Calendar.SetAbsMonth, openMonth, openYear)
    end

    return foundActive, foundEndTime
end

function ns:RequestDarkmoonFaireCalendarRefresh()
    if isLoggingOut() then
        return
    end
    requestDarkmoonCalendar()
end

function ns:ClearDarkmoonFaireActiveCache()
    darkmoonCacheExpiresAt = 0
    darkmoonCacheValue = nil
end

function ns:IsDarkmoonFaireActive()
    local now = time()
    if isLoggingOut() then
        if darkmoonCacheValue ~= nil and now < darkmoonCacheExpiresAt then
            return darkmoonCacheValue == true
        end
        if darkmoonKnownStartTime > 0 and darkmoonKnownEndTime > 0 then
            return now >= darkmoonKnownStartTime and now <= darkmoonKnownEndTime
        end
        return false
    end

    if isPlayerOnDarkmoonIsland() then
        return true
    end

    if darkmoonCacheValue ~= nil and now < darkmoonCacheExpiresAt then
        return darkmoonCacheValue == true
    end

    requestDarkmoonCalendar()

    local active, activeUntil = scanDarkmoonCalendar()
    darkmoonCacheValue = active == true
    if darkmoonCacheValue and activeUntil and activeUntil > now then
        darkmoonCacheExpiresAt = math.min(activeUntil, now + 3600)
    else
        darkmoonCacheExpiresAt = now + 300
    end

    return darkmoonCacheValue == true
end

function ns:GetDarkmoonFaireResetKey()
    if darkmoonKnownStartTime > 0 and darkmoonKnownEndTime > 0 then
        return string.format("darkmoon:%d:%d", darkmoonKnownStartTime, darkmoonKnownEndTime)
    end

    if isLoggingOut() then
        return "darkmoon:" .. date("%Y:%m")
    end

    local currentCalendarTime
    if C_DateAndTime and type(C_DateAndTime.GetCurrentCalendarTime) == "function" then
        local ok, value = pcall(C_DateAndTime.GetCurrentCalendarTime)
        if ok and type(value) == "table" then
            currentCalendarTime = value
        end
    end

    if currentCalendarTime and currentCalendarTime.year and currentCalendarTime.month then
        return string.format("darkmoon:%d:%02d", currentCalendarTime.year, currentCalendarTime.month)
    end

    return "darkmoon:" .. date("%Y:%m")
end

local function isDarkmoonProfessionRow(row)
    if type(row) ~= "table" then
        return false
    end

    return row.resetType == "darkmoon"
        or (type(row.key) == "string" and row.key:match("^profession_darkmoon_") ~= nil)
end

local function filterUnavailableDarkmoonRows(state, darkmoonActive)
    if darkmoonActive == true or type(state) ~= "table" or type(state.rows) ~= "table" then
        return state
    end

    local rows = {}
    local completed = 0
    local open = 0
    for _, row in ipairs(state.rows) do
        if not isDarkmoonProfessionRow(row) then
            rows[#rows + 1] = row
            if row.completed then
                completed = completed + 1
            elseif row.seen then
                open = open + 1
            end
        end
    end

    local filtered = {}
    for key, value in pairs(state) do
        if key ~= "rows" and key ~= "summary" then
            filtered[key] = value
        end
    end

    local total = math.max(0, #rows == 1 and rows[1].key == "profession_none" and 0 or #rows)
    filtered.rows = rows
    filtered.summary = {
        completed = completed,
        open = open,
        total = total,
        text = string.format("%d/%d", completed, total),
    }

    return filtered
end

local PROFESSION_WEEKLY_STATUS_RANK = {
    missing = 1,
    open = 2,
    turnin = 3,
    complete = 4,
}

local function getProfessionWeeklyRowKey(row)
    if type(row) ~= "table" then
        return nil
    end

    if type(row.key) == "string" and row.key ~= "" then
        return row.key
    end

    local questID = tonumber(row.questID)
    return questID and tostring(questID) or nil
end

local function isProfessionWeeklyRowMoreInformative(previousRow, currentRow)
    if type(previousRow) ~= "table" or type(currentRow) ~= "table" then
        return false
    end

    if previousRow.completed == true and currentRow.completed ~= true then
        return true
    end

    local previousRank = PROFESSION_WEEKLY_STATUS_RANK[previousRow.status] or 0
    local currentRank = PROFESSION_WEEKLY_STATUS_RANK[currentRow.status] or 0
    if previousRank > currentRank then
        return true
    end

    return previousRow.seen == true and currentRow.seen ~= true and currentRow.status == "missing"
end

local function rebuildProfessionWeeklySummary(state)
    if type(state) ~= "table" or type(state.rows) ~= "table" then
        return
    end

    local completed = 0
    local open = 0
    for _, row in ipairs(state.rows) do
        if row.completed then
            completed = completed + 1
        elseif row.seen then
            open = open + 1
        end
    end

    local total = math.max(0, #state.rows == 1 and state.rows[1].key == "profession_none" and 0 or #state.rows)
    state.summary = {
        completed = completed,
        open = open,
        total = total,
        text = string.format("%d/%d", completed, total),
    }
end

local function preserveProfessionWeeklyProgress(previousState, currentState)
    if type(previousState) ~= "table"
        or type(previousState.rows) ~= "table"
        or type(currentState) ~= "table"
        or type(currentState.rows) ~= "table"
    then
        return currentState
    end

    local previousByKey = {}
    for _, row in ipairs(previousState.rows) do
        local key = getProfessionWeeklyRowKey(row)
        if key then
            previousByKey[key] = row
        end
    end

    local changed = false
    for index, row in ipairs(currentState.rows) do
        local previousRow = previousByKey[getProfessionWeeklyRowKey(row)]
        if isProfessionWeeklyRowMoreInformative(previousRow, row) then
            currentState.rows[index] = previousRow
            changed = true
        end
    end

    if changed then
        rebuildProfessionWeeklySummary(currentState)
    end

    return currentState
end

local function getCharacterProfessionList(character)
    if type(character) ~= "table" or type(character.professions) ~= "table" then
        return {}
    end

    local list = {}
    for _, entry in ipairs(character.professions) do
        local skillLineID = tonumber(entry and entry.skillLineID)
        local professionKey = skillLineID and PROFESSION_SKILL_LINE_TO_KEY[skillLineID] or nil
        if professionKey then
            list[#list + 1] = {
                key = professionKey,
                skillLineID = skillLineID,
                name = entry.name or professionKey,
            }
        end
    end

    table.sort(list, function(a, b)
        return (a.name or "") < (b.name or "")
    end)

    return list
end

local function buildProfessionWeeklyRow(profession, suffixLabel, questIDs, tooltipKeyPrefix)
    local row = buildQuestPoolState({
        key = string.format("profession_%s_%s", tooltipKeyPrefix, profession.key),
        fallbackLabel = string.format("%s - %s", profession.name or profession.key, suffixLabel),
        quests = questIDs,
    })

    row.professionKey = profession.key
    row.professionName = profession.name
    row.skillLineID = profession.skillLineID
    row.tooltipTitle = row.label
    row.tooltipLines = row.tooltipLines or {}

    if tooltipKeyPrefix == "service" then
        if row.status == "missing" then
            row.tooltipLines = { L.PROFESSIONS_TOOLTIP_SERVICE_MISSING or "This profession weekly can be accepted in Midnight." }
        elseif row.status == "turnin" then
            row.tooltipLines = { L.PROFESSIONS_TOOLTIP_TURNIN or "This quest is done and can now be turned in." }
        elseif row.status == "complete" then
            row.tooltipLines = { L.PROFESSIONS_TOOLTIP_DONE or "Already completed this week." }
        else
            row.tooltipLines = getQuestObjectiveLines(row.questID)
            if #row.tooltipLines == 0 then
                row.tooltipLines = { L.PROFESSIONS_TOOLTIP_SERVICE_PROGRESS or "This profession weekly is currently active." }
            end
        end
    else
        local itemHint = L.PROFESSIONS_TOOLTIP_TREATISE_ITEM_HINT or "Can be crafted by an Inscription player or requested through a Crafting Order."
        local lines = { itemHint }

        if row.status == "missing" then
            lines[#lines + 1] = L.PROFESSIONS_TOOLTIP_TREATISE_MISSING or "The weekly treatise quest is not accepted yet."
        elseif row.status == "turnin" then
            lines[#lines + 1] = L.PROFESSIONS_TOOLTIP_TURNIN or "This quest is done and can now be turned in."
        elseif row.status == "complete" then
            lines[#lines + 1] = L.PROFESSIONS_TOOLTIP_DONE or "Already completed this week."
        else
            local objectiveLines = getQuestObjectiveLines(row.questID)
            if #objectiveLines > 0 then
                for _, line in ipairs(objectiveLines) do
                    lines[#lines + 1] = line
                end
            else
                lines[#lines + 1] = L.PROFESSIONS_TOOLTIP_TREATISE_PROGRESS or "The weekly treatise quest is currently active."
            end
        end

        row.tooltipLines = lines
    end

    return row
end

local function getItemDisplayName(itemID)
    itemID = tonumber(itemID)
    if not itemID then
        return nil
    end

    if C_Item and type(C_Item.GetItemInfo) == "function" then
        local ok, itemName, itemLink = pcall(C_Item.GetItemInfo, itemID)
        if ok then
            if type(itemLink) == "string" and itemLink ~= "" then
                return itemLink
            end
            if type(itemName) == "string" and itemName ~= "" then
                return itemName
            end
        end
    end

    if type(GetItemInfo) == "function" then
        local ok, itemName, itemLink = pcall(GetItemInfo, itemID)
        if ok then
            if type(itemLink) == "string" and itemLink ~= "" then
                return itemLink
            end
            if type(itemName) == "string" and itemName ~= "" then
                return itemName
            end
        end
    end

    return string.format("Item #%d", itemID)
end

local function getItemCount(itemID)
    itemID = tonumber(itemID)
    if not itemID then
        return 0
    end

    if C_Item and type(C_Item.GetItemCount) == "function" then
        local ok, count = pcall(C_Item.GetItemCount, itemID, true)
        if ok then
            return math.max(0, tonumber(count) or 0)
        end
    end

    if type(GetItemCount) == "function" then
        local ok, count = pcall(GetItemCount, itemID, true)
        if ok then
            return math.max(0, tonumber(count) or 0)
        end
    end

    return 0
end

local function getLocalizedDarkmoonQuestTitle(questData)
    local titles = type(questData) == "table" and questData.title or nil
    local locale = type(GetLocale) == "function" and GetLocale() or "enUS"
    if type(titles) == "table" then
        return titles[locale] or titles.enUS or titles.deDE or questData.fallbackTitle
    end

    return questData and questData.fallbackTitle or nil
end

local function buildDarkmoonFaireQuestRow(questData)
    if type(questData) ~= "table" then
        return nil
    end

    local questID = tonumber(questData.id)
    if not questID then
        return nil
    end

    if C_QuestLog and type(C_QuestLog.RequestLoadQuestByID) == "function" then
        pcall(C_QuestLog.RequestLoadQuestByID, questID)
    end

    local fallbackTitle = getLocalizedDarkmoonQuestTitle(questData)
    local label = (getQuestTitle and getQuestTitle(questID, fallbackTitle)) or fallbackTitle or ("Quest #" .. tostring(questID))
    local row = buildQuestPoolState({
        key = "darkmoon_" .. tostring(questData.key or questID),
        fallbackLabel = label,
        quests = {
            { id = questID, fallbackTitle = fallbackTitle },
        },
    })

    if not row then
        return nil
    end

    local startItemID = tonumber(questData.startItemID)
    local costItemID = tonumber(questData.costItemID)
    local startItemCount = startItemID and getItemCount(startItemID) or 0
    local resetKind = questData.reset == "daily" and "daily" or "darkmoon"
    local lines = {}

    row.key = "darkmoon_" .. tostring(questData.key or questID)
    row.label = label
    row.questID = questID
    row.resetType = resetKind
    row.renownResetKind = resetKind
    row.renownXP = math.max(1, tonumber(questData.renownXP) or 2)
    row.category = questData.category
    row.tooltipTitle = label

    if startItemID then
        lines[#lines + 1] = string.format(
            L.PVE_DARKMOON_START_ITEM_HINT or "Starts from item: %s",
            getItemDisplayName(startItemID)
        )
        if startItemCount > 0 and row.status == "missing" then
            row.status = "turnin"
            row.seen = true
            row.text = L.STATUS_TURNIN or "Turn in"
            row.turnInQuestID = questID
        end
    end

    if costItemID then
        lines[#lines + 1] = string.format(
            L.PVE_DARKMOON_TOKEN_HINT or "Requires %s.",
            getItemDisplayName(costItemID)
        )
    end

    if resetKind == "daily" then
        lines[#lines + 1] = L.PVE_DARKMOON_DAILY_HINT or "Daily during the Darkmoon Faire."
    else
        lines[#lines + 1] = L.PVE_DARKMOON_MONTHLY_HINT or "Once per character per Darkmoon Faire."
    end

    if row.status == "missing" then
        lines[#lines + 1] = L.PVE_DARKMOON_ACCEPT_HINT or "Accept this on Darkmoon Island while the Faire is active."
    elseif row.status == "turnin" then
        lines[#lines + 1] = L.PROFESSIONS_TOOLTIP_TURNIN or "This quest is done and can now be turned in."
    elseif row.status == "complete" then
        lines[#lines + 1] = resetKind == "daily"
            and (L.PVE_DARKMOON_DONE_DAILY_HINT or "Already completed today.")
            or (L.PVE_DARKMOON_DONE_MONTHLY_HINT or "Already completed for this Darkmoon Faire.")
    else
        local objectiveLines = getQuestObjectiveLines(row.questID)
        if #objectiveLines > 0 then
            for _, line in ipairs(objectiveLines) do
                lines[#lines + 1] = line
            end
        else
            lines[#lines + 1] = L.PVE_DARKMOON_ACTIVE_HINT or "This Darkmoon quest is currently active."
        end
    end

    row.statusBadgeItemID = nil

    row.tooltipLines = lines
    return row
end

function ns:BuildCurrentDarkmoonFaireState()
    if not self:IsDarkmoonFaireActive() then
        return {
            active = false,
            rows = {},
            summary = {
                completed = 0,
                open = 0,
                total = 0,
                text = "0/0",
            },
        }
    end

    local rows = {}
    local completed = 0
    local open = 0

    for _, questData in ipairs(DARKMOON_FAIRE_QUESTS) do
        local row = buildDarkmoonFaireQuestRow(questData)
        if row then
            rows[#rows + 1] = row
            if row.completed then
                completed = completed + 1
            elseif row.seen then
                open = open + 1
            end
        end
    end

    return {
        active = true,
        rows = rows,
        summary = {
            completed = completed,
            open = open,
            total = #rows,
            text = string.format("%d/%d", completed, #rows),
        },
    }
end

function ns:RefreshCurrentCharacterDarkmoonFaireData()
    if isLoggingOut() then
        local record = self.GetStoredCharacterRecord and self:GetStoredCharacterRecord(nil, false) or nil
        return type(record) == "table"
            and type(record.darkmoonFaire) == "table"
            and type(record.darkmoonFaire.state) == "table"
            and record.darkmoonFaire.state
            or nil
    end

    if not self:IsDarkmoonFaireActive() then
        return nil
    end

    local record = self:RefreshCurrentCharacterRecord()
    self:ClearExpiredDarkmoonFaireState(record)

    local state = self:BuildCurrentDarkmoonFaireState()
    if not state then
        return nil
    end

    local dailyResetAt = time() + math.max(0, tonumber(self.GetDailyResetSeconds and self:GetDailyResetSeconds() or 0) or 0)
    record.darkmoonFaire = {
        dailyResetAt = dailyResetAt,
        darkmoonResetKey = self.GetDarkmoonFaireResetKey and self:GetDarkmoonFaireResetKey() or nil,
        state = state,
    }

    return state
end

function ns:GetLiveDarkmoonFaireState(keyOrCharacter, fallbackState)
    if not self:IsDarkmoonFaireActive() then
        return {
            active = false,
            rows = {},
            summary = {
                completed = 0,
                open = 0,
                total = 0,
                text = "0/0",
            },
        }
    end

    local key = keyOrCharacter
    if type(keyOrCharacter) == "table" then
        key = keyOrCharacter.key
    end

    if self.IsCurrentCharacterKey and self:IsCurrentCharacterKey(key) then
        local liveState = self:BuildCurrentDarkmoonFaireState()
        if liveState then
            return liveState
        end
    end

    local storedState = self.GetStoredDarkmoonFaireState and self:GetStoredDarkmoonFaireState(key)
    if storedState then
        return storedState
    end

    return fallbackState
end

local function appendDarkmoonItemLines(lines, questData)
    local items = type(questData) == "table" and questData.items or nil
    if type(items) ~= "table" or #items == 0 then
        return
    end

    lines[#lines + 1] = L.PROFESSIONS_TOOLTIP_DARKMOON_REQUIRES or "Bring:"

    for _, item in ipairs(items) do
        local itemID = tonumber(item and item.id)
        local count = math.max(1, tonumber(item and item.count) or 1)
        if itemID then
            lines[#lines + 1] = string.format("%dx %s", count, getItemDisplayName(itemID))
        end
    end
end

local function buildDarkmoonProfessionRow(profession)
    local questData = DARKMOON_PROFESSION_QUESTS[profession.key]
    if type(questData) ~= "table" then
        return nil
    end

    local questID = tonumber(questData.id)
    if not questID then
        return nil
    end

    local row = buildQuestPoolState({
        key = string.format("profession_darkmoon_%s", profession.key),
        fallbackLabel = string.format("%s - %s", profession.name or profession.key, L.PROFESSIONS_ROW_DARKMOON or "Darkmoon Faire"),
        quests = {
            { id = questID, fallbackTitle = questData.fallbackTitle },
        },
    })

    if not row then
        return nil
    end

    row.professionKey = profession.key
    row.professionName = profession.name
    row.skillLineID = profession.skillLineID
    row.resetType = "darkmoon"
    row.tooltipTitle = row.label

    local lines = {}
    appendDarkmoonItemLines(lines, questData)
    lines[#lines + 1] = L.PROFESSIONS_TOOLTIP_DARKMOON_REWARD or "Rewards profession skill and profession knowledge for your latest known expansion tier."
    lines[#lines + 1] = L.PROFESSIONS_TOOLTIP_DARKMOON_MONTHLY or "Available during the Darkmoon Faire and resets with the Faire."

    if row.status == "missing" then
        lines[#lines + 1] = L.PROFESSIONS_TOOLTIP_DARKMOON_MISSING or "Accept this profession quest on Darkmoon Island while the Faire is active."
    elseif row.status == "turnin" then
        lines[#lines + 1] = L.PROFESSIONS_TOOLTIP_TURNIN or "This quest is done and can now be turned in."
    elseif row.status == "complete" then
        lines[#lines + 1] = L.PROFESSIONS_TOOLTIP_DARKMOON_DONE or "Already completed for this Darkmoon Faire."
    else
        local objectiveLines = getQuestObjectiveLines(row.questID)
        if #objectiveLines > 0 then
            for _, line in ipairs(objectiveLines) do
                lines[#lines + 1] = line
            end
        else
            lines[#lines + 1] = L.PROFESSIONS_TOOLTIP_DARKMOON_PROGRESS or "This Darkmoon profession quest is currently active."
        end
    end

    row.tooltipLines = lines
    return row
end

function ns:BuildCurrentProfessionWeeklyState(character)
    local sourceCharacter = character
    if type(sourceCharacter) ~= "table" then
        sourceCharacter = self:RefreshCurrentCharacterRecord()
    end

    local professionList = getCharacterProfessionList(sourceCharacter)
    local rows = {}
    local completed = 0
    local open = 0
    local darkmoonActive = self:IsDarkmoonFaireActive()

    for _, profession in ipairs(professionList) do
        local servicePool = MIDNIGHT_PROFESSION_SERVICE_QUESTS[profession.key]

        if servicePool and #servicePool > 0 then
            local serviceQuestDefs = {}
            for _, questID in ipairs(servicePool) do
                serviceQuestDefs[#serviceQuestDefs + 1] = { id = questID }
            end
            local serviceRow = buildProfessionWeeklyRow(profession, L.PROFESSIONS_ROW_SERVICE or "Services Requested", serviceQuestDefs, "service")
            rows[#rows + 1] = serviceRow
        end
    end

    for _, profession in ipairs(professionList) do
        local treatiseQuestID = MIDNIGHT_PROFESSION_TREATISE_QUESTS[profession.key]

        if treatiseQuestID and treatiseQuestID > 0 then
            local treatiseRow = buildProfessionWeeklyRow(profession, L.PROFESSIONS_ROW_TREATISE or "Thalassian Treatise", { { id = treatiseQuestID } }, "treatise")
            rows[#rows + 1] = treatiseRow
        end
    end

    if darkmoonActive then
        for _, profession in ipairs(professionList) do
            local darkmoonRow = buildDarkmoonProfessionRow(profession)
            if darkmoonRow then
                rows[#rows + 1] = darkmoonRow
            end
        end
    end

    for _, row in ipairs(rows) do
        if row.completed then
            completed = completed + 1
        elseif row.seen then
            open = open + 1
        end
    end

    if #rows == 0 then
        rows[1] = {
            key = "profession_none",
            label = L.PROFESSIONS_EMPTY_LABEL or "Keine verfolgten Berufe",
            text = L.PROFESSIONS_EMPTY_VALUE or "--",
            status = "missing",
            completed = false,
            seen = false,
            tooltipTitle = L.PROFESSIONS_EMPTY_LABEL or "Keine verfolgten Berufe",
            tooltipLines = {
                L.PROFESSIONS_EMPTY_HINT or "Logge einen Charakter mit Hauptberufen ein, damit die Berufsweeklys erfasst werden.",
            },
        }
    end

    return {
        rows = rows,
        summary = {
            completed = completed,
            open = open,
            total = math.max(0, #rows == 1 and rows[1].key == "profession_none" and 0 or #rows),
            text = string.format("%d/%d", completed, math.max(0, #rows == 1 and rows[1].key == "profession_none" and 0 or #rows)),
        },
    }
end

function ns:RefreshCurrentCharacterProfessionWeeklyData()
    local record = self:RefreshCurrentCharacterRecord()
    self:ClearExpiredProfessionWeeklyState(record)
    local previousState = type(record.professionWeekly) == "table" and record.professionWeekly.state or nil

    if isLoggingOut() then
        return previousState
    end

    local state = self:BuildCurrentProfessionWeeklyState(record)
    if not state then
        return nil
    end
    state = preserveProfessionWeeklyProgress(previousState, state)

    local _, weeklyResetAt = getWeeklyResetInfo()
    record.professionWeekly = {
        resetAt = weeklyResetAt,
        state = state,
    }

    return state
end

function ns:GetLiveProfessionWeeklyState(keyOrCharacter, fallbackState)
    local key = keyOrCharacter
    if type(keyOrCharacter) == "table" then
        key = keyOrCharacter.key
    end

    if self:IsCurrentCharacterKey(key) then
        local liveState = self:BuildCurrentProfessionWeeklyState()
        if liveState then
            local storedState = self:GetStoredProfessionWeeklyState(key)
            return preserveProfessionWeeklyProgress(storedState, liveState)
        end
    end

    local storedState = self:GetStoredProfessionWeeklyState(key)
    if storedState then
        return filterUnavailableDarkmoonRows(storedState, self:IsDarkmoonFaireActive())
    end

    return filterUnavailableDarkmoonRows(fallbackState, self:IsDarkmoonFaireActive())
end
