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

local DATA = ns.Data or {}
local PROFESSION_SKILL_LINE_TO_KEY = DATA.PROFESSION_SKILL_LINE_TO_KEY or {}
local PROFESSION_KNOWLEDGE_CACHE_SECONDS = 1.0

local cachedCurrentProfessionKnowledgeState = nil
local cachedCurrentProfessionKnowledgeAt = 0

local function getRuntimeTime()
    return type(GetTime) == "function" and GetTime() or 0
end

local function clearCurrentProfessionKnowledgeCache()
    cachedCurrentProfessionKnowledgeState = nil
    cachedCurrentProfessionKnowledgeAt = 0
end

local function getPrimaryProfessionTradeSkillLine(index)
    if type(GetProfessions) ~= "function"
        or type(GetProfessionInfo) ~= "function"
        or not (type(C_TradeSkillUI) == "table"
            and type(C_TradeSkillUI.GetAllProfessionTradeSkillLines) == "function"
            and type(C_TradeSkillUI.GetProfessionInfoBySkillLineID) == "function")
    then
        return nil, nil
    end

    local professionIndex = select(index, GetProfessions())
    if not professionIndex then
        return nil, nil
    end

    local professionName, _, _, _, _, _, baseSkillLineID, _, _, _, subcategoryName = GetProfessionInfo(professionIndex)
    local desiredName = (type(subcategoryName) == "string" and subcategoryName ~= "" and subcategoryName)
        or (type(professionName) == "string" and professionName ~= "" and professionName)
        or nil
    local candidateSkillLineID, candidateName

    local okSkillLines, skillLines = pcall(C_TradeSkillUI.GetAllProfessionTradeSkillLines)
    if okSkillLines and type(skillLines) == "table" then
        for _, rawSkillLineID in ipairs(skillLines) do
            local skillLineID = tonumber(rawSkillLineID)
            local okInfo, info = pcall(C_TradeSkillUI.GetProfessionInfoBySkillLineID, rawSkillLineID)
            if okInfo and type(info) == "table" then
                local infoName = type(info.professionName) == "string" and info.professionName or nil
                if desiredName and infoName == desiredName and skillLineID and skillLineID > 0 then
                    return skillLineID, infoName
                end

                if not candidateSkillLineID then
                    if (professionName and infoName == professionName) or (skillLineID and skillLineID == tonumber(baseSkillLineID)) then
                        candidateSkillLineID = skillLineID
                        candidateName = infoName or professionName or desiredName
                    end
                end
            end
        end
    end

    local resolvedSkillLineID = candidateSkillLineID or tonumber(baseSkillLineID)
    if resolvedSkillLineID and resolvedSkillLineID > 0 then
        return resolvedSkillLineID, candidateName or desiredName or professionName
    end

    return nil, desiredName or professionName
end

local function getProfessionUnspentKnowledge(skillLineID)
    skillLineID = tonumber(skillLineID)
    if not skillLineID or skillLineID <= 0 then
        return 0, false
    end

    if not (type(C_ProfSpecs) == "table"
        and type(C_ProfSpecs.GetConfigIDForSkillLine) == "function"
        and type(C_ProfSpecs.GetSpecTabIDsForSkillLine) == "function"
        and type(C_ProfSpecs.GetTabInfo) == "function"
        and type(C_ProfSpecs.GetSpendCurrencyForPath) == "function"
        and type(C_Traits) == "table"
        and type(C_Traits.GetTreeCurrencyInfo) == "function"
        and type(C_Traits.GetTreeNodes) == "function"
        and type(C_Traits.GetNodeInfo) == "function"
        and type(C_Traits.CanPurchaseRank) == "function")
    then
        return 0, false
    end

    local okConfig, configID = pcall(C_ProfSpecs.GetConfigIDForSkillLine, skillLineID)
    configID = okConfig and tonumber(configID) or nil
    if not configID or configID <= 0 then
        return 0, false
    end

    local okTabs, tabTreeIDs = pcall(C_ProfSpecs.GetSpecTabIDsForSkillLine, skillLineID)
    if not okTabs or type(tabTreeIDs) ~= "table" then
        return 0, false
    end

    local total = 0
    local anyPurchasableNode = false
    local seenCurrencies = {}

    for _, rawTreeID in ipairs(tabTreeIDs) do
        local treeID = tonumber(rawTreeID)
        if treeID and treeID > 0 then
            local spendCurrencyID = nil
            local okTabInfo, tabInfo = pcall(C_ProfSpecs.GetTabInfo, treeID)
            local rootNodeID = okTabInfo and type(tabInfo) == "table" and tonumber(tabInfo.rootNodeID) or nil
            if rootNodeID and rootNodeID > 0 then
                local okSpendCurrency, rawCurrencyID = pcall(C_ProfSpecs.GetSpendCurrencyForPath, rootNodeID)
                spendCurrencyID = okSpendCurrency and tonumber(rawCurrencyID) or nil
            end

            if spendCurrencyID and not seenCurrencies[spendCurrencyID] then
                seenCurrencies[spendCurrencyID] = true
                local okCurrencies, treeCurrencyInfo = pcall(C_Traits.GetTreeCurrencyInfo, configID, treeID, true)
                if okCurrencies and type(treeCurrencyInfo) == "table" then
                    for _, currencyInfo in ipairs(treeCurrencyInfo) do
                        if tonumber(type(currencyInfo) == "table" and currencyInfo.traitCurrencyID or 0) == spendCurrencyID then
                            total = total + math.max(0, tonumber(currencyInfo.quantity) or 0)
                            break
                        end
                    end
                end
            end

            local okNodes, nodeIDs = pcall(C_Traits.GetTreeNodes, treeID)
            if okNodes and type(nodeIDs) == "table" then
                for _, rawNodeID in ipairs(nodeIDs) do
                    local nodeID = tonumber(rawNodeID)
                    if nodeID and nodeID > 0 then
                        local okNodeInfo, nodeInfo = pcall(C_Traits.GetNodeInfo, configID, nodeID)
                        if okNodeInfo and type(nodeInfo) == "table" and nodeInfo.isVisible then
                            local activeEntryID = type(nodeInfo.activeEntry) == "table" and nodeInfo.activeEntry.entryID or nil
                            local okCanPurchase, canPurchase = pcall(C_Traits.CanPurchaseRank, configID, nodeID, activeEntryID)
                            if okCanPurchase and canPurchase then
                                anyPurchasableNode = true
                                break
                            end
                        end
                    end
                end
            end
        end
    end

    return total, anyPurchasableNode
end

function ns:BuildCurrentProfessionKnowledgeState(forceRefresh)
    local now = getRuntimeTime()
    if forceRefresh ~= true
        and self.professionKnowledgeDirty ~= true
        and type(cachedCurrentProfessionKnowledgeState) == "table"
        and now > 0
        and (now - cachedCurrentProfessionKnowledgeAt) < PROFESSION_KNOWLEDGE_CACHE_SECONDS
    then
        return cachedCurrentProfessionKnowledgeState
    end

    local state = {
        updatedAt = time(),
        totalPoints = 0,
        professionCount = 0,
        hasUnspent = false,
        professions = {},
    }

    for index = 1, 2 do
        local skillLineID, professionName = getPrimaryProfessionTradeSkillLine(index)
        if skillLineID then
            local points, hasPurchasableNode = getProfessionUnspentKnowledge(skillLineID)
            points = math.max(0, tonumber(points) or 0)
            local name = professionName or (L.UNKNOWN or "Unknown")
            local hasUnspent = hasPurchasableNode and points > 0
            state.professions[#state.professions + 1] = {
                slotIndex = index,
                skillLineID = skillLineID,
                professionKey = PROFESSION_SKILL_LINE_TO_KEY[skillLineID],
                name = name,
                points = points,
                hasUnspent = hasUnspent == true,
                canPurchase = hasPurchasableNode == true,
                chipText = string.format("%s +%d", name, points),
            }
            if hasUnspent then
                state.totalPoints = state.totalPoints + points
            end
        end
    end

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

    state.professionCount = #state.professions
    state.hasUnspent = state.professionCount > 0 and state.totalPoints > 0
    cachedCurrentProfessionKnowledgeState = state
    cachedCurrentProfessionKnowledgeAt = now
    self.professionKnowledgeDirty = false
    return state
end

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

    local record = self:GetStoredCharacterRecord(key, false)
    return type(record) == "table"
        and type(record.professionKnowledge) == "table"
        and type(record.professionKnowledge.state) == "table"
        and record.professionKnowledge.state
        or nil
end

function ns:RefreshCurrentCharacterProfessionKnowledgeData(forceRefresh)
    local record = self:RefreshCurrentCharacterRecord()
    local state = self:BuildCurrentProfessionKnowledgeState(forceRefresh == true)
    if not state then
        return nil
    end

    record.professionKnowledge = {
        updatedAt = time(),
        state = state,
    }

    return state
end

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

    if self:IsCurrentCharacterKey(key) then
        local liveState = self:BuildCurrentProfessionKnowledgeState()
        if liveState then
            return liveState
        end
    end

    local storedState = self:GetStoredProfessionKnowledgeState(key)
    if storedState then
        return storedState
    end

    return fallbackState
end

function ns:MarkCurrentCharacterProfessionsDirty()
    self.professionsDirty = true
    self.professionKnowledgeDirty = true
    clearCurrentProfessionKnowledgeCache()
end

function ns:MarkCurrentCharacterProfessionKnowledgeDirty()
    self.professionKnowledgeDirty = true
    clearCurrentProfessionKnowledgeCache()
end
