GuildHelper = GuildHelper or {}

local addon = GuildHelper

addon.ReadinessService = addon.ReadinessService or {}

local service = addon.ReadinessService
local DURABILITY_READY_THRESHOLD = 95

local function deepCopy(value)
    if type(value) ~= "table" then
        return value
    end

    local copy = {}

    for key, nestedValue in pairs(value) do
        copy[key] = deepCopy(nestedValue)
    end

    return copy
end

local function mergeKnown(target, source)
    if type(source) ~= "table" then
        return target
    end

    for key, value in pairs(source) do
        if target[key] ~= nil then
            if type(target[key]) == "table" and type(value) == "table" then
                mergeKnown(target[key], value)
            else
                target[key] = value
            end
        end
    end

    return target
end

local function normalizeStatus(value)
    value = type(value) == "string" and string.lower(value) or "unknown"
    return value == "ready" and 1 or (value == "issues" and 0 or nil)
end

local function normalizeFlag(value)
    if value == true then
        return 1
    end

    if value == false then
        return 0
    end

    return nil
end

local EMPTY_SOCKET_GLOBAL_KEYS = {
    "EMPTY_SOCKET_META",
    "EMPTY_SOCKET_RED",
    "EMPTY_SOCKET_YELLOW",
    "EMPTY_SOCKET_BLUE",
    "EMPTY_SOCKET_PRISMATIC",
    "EMPTY_SOCKET_COGWHEEL",
    "EMPTY_SOCKET_HYDRAULIC",
    "EMPTY_SOCKET_PUNCHCARDRED",
    "EMPTY_SOCKET_PUNCHCARDYELLOW",
    "EMPTY_SOCKET_PUNCHCARDBLUE",
    "EMPTY_SOCKET_DOMINATION",
}

local ALWAYS_ENCHANTABLE_SLOTS = {
    [5] = true,
    [8] = true,
    [9] = true,
    [11] = true,
    [12] = true,
    [15] = true,
}

local WEAPON_EQUIP_LOCS = {
    INVTYPE_2HWEAPON = true,
    INVTYPE_RANGED = true,
    INVTYPE_RANGEDRIGHT = true,
    INVTYPE_WEAPON = true,
    INVTYPE_WEAPONMAINHAND = true,
    INVTYPE_WEAPONOFFHAND = true,
}

local readinessTooltip = nil

local function trimString(value)
    return tostring(value or ""):match("^%s*(.-)%s*$") or ""
end

local function splitItemString(itemLink)
    local itemString = type(itemLink) == "string" and (string.match(itemLink, "|Hitem:([^|]+)|h") or string.match(itemLink, "^item:([^|]+)")) or nil

    if type(itemString) ~= "string" or itemString == "" then
        return nil
    end

    local fields = {}

    for field in string.gmatch(itemString, "([^:]+)") do
        fields[#fields + 1] = field
    end

    return fields
end

local function getReadinessTooltip()
    if readinessTooltip ~= nil then
        return readinessTooltip
    end

    if type(CreateFrame) ~= "function" then
        return nil
    end

    readinessTooltip = CreateFrame("GameTooltip", "GH3ReadinessScanTooltip", UIParent, "GameTooltipTemplate")

    if readinessTooltip and readinessTooltip.SetOwner then
        readinessTooltip:SetOwner(UIParent or WorldFrame or readinessTooltip, "ANCHOR_NONE")
    end

    return readinessTooltip
end

local function isEmptySocketText(text)
    local normalized = trimString(text)

    if normalized == "" then
        return false
    end

    for _, globalKey in ipairs(EMPTY_SOCKET_GLOBAL_KEYS) do
        if normalized == trimString(_G[globalKey]) then
            return true
        end
    end

    return false
end

local function countFilledGems(itemLink)
    local fields = splitItemString(itemLink)

    if type(fields) ~= "table" then
        return 0
    end

    local count = 0

    for fieldIndex = 4, 7 do
        if (tonumber(fields[fieldIndex]) or 0) > 0 then
            count = count + 1
        end
    end

    return count
end

local function countEmptySockets(itemLink)
    local tooltip = getReadinessTooltip()

    if type(tooltip) ~= "table" or type(tooltip.SetHyperlink) ~= "function" or type(tooltip.NumLines) ~= "function" then
        return 0
    end

    tooltip:ClearLines()

    local ok = pcall(tooltip.SetHyperlink, tooltip, itemLink)

    if not ok then
        tooltip:Hide()
        tooltip:ClearLines()
        return 0
    end

    local count = 0
    local tooltipName = tooltip:GetName()

    for lineIndex = 1, tooltip:NumLines() do
        local lineRegion = tooltipName and _G[tooltipName .. "TextLeft" .. lineIndex] or nil
        local lineText = lineRegion and lineRegion.GetText and lineRegion:GetText() or nil

        if isEmptySocketText(lineText) then
            count = count + 1
        end
    end

    tooltip:Hide()
    tooltip:ClearLines()
    return count
end

local function getEnchantId(itemLink)
    local fields = splitItemString(itemLink)
    return type(fields) == "table" and (tonumber(fields[3]) or 0) or 0
end

local function slotCanRequireEnchant(slotId, itemLink)
    if ALWAYS_ENCHANTABLE_SLOTS[slotId] then
        return true
    end

    if slotId ~= 16 and slotId ~= 17 then
        return false
    end

    if type(GetItemInfoInstant) ~= "function" then
        return true
    end

    local _, _, _, equipLoc = GetItemInfoInstant(itemLink)
    return equipLoc ~= nil and WEAPON_EQUIP_LOCS[equipLoc] == true
end

local function scanAuraNames()
    if type(UnitBuff) ~= "function" and type(UnitAura) ~= "function" then
        return nil
    end

    local auraNames = {}

    for auraIndex = 1, 40 do
        local auraName = nil

        if type(UnitBuff) == "function" then
            auraName = UnitBuff("player", auraIndex)
        elseif type(UnitAura) == "function" then
            auraName = UnitAura("player", auraIndex, "HELPFUL")
        end

        if trimString(auraName) == "" then
            break
        end

        auraNames[#auraNames + 1] = string.lower(auraName)
    end

    return auraNames
end

local function getContainerNumSlotsCompat(bagId)
    if C_Container and C_Container.GetContainerNumSlots then
        return C_Container.GetContainerNumSlots(bagId)
    end
    return GetContainerNumSlots and GetContainerNumSlots(bagId) or 0
end

local function getContainerItemLinkCompat(bagId, slotIndex)
    if C_Container and C_Container.GetContainerItemLink then
        return C_Container.GetContainerItemLink(bagId, slotIndex)
    end
    return GetContainerItemLink and GetContainerItemLink(bagId, slotIndex) or nil
end

local function scanBagConsumables()
    if type(GetItemInfo) ~= "function" then
        return nil
    end

    local stocked = { food = false, flask = false, potion = false, rune = false }

    for bagId = 0, 4 do
        for slotIndex = 1, math.max(0, tonumber(getContainerNumSlotsCompat(bagId)) or 0) do
            local itemLink = getContainerItemLinkCompat(bagId, slotIndex)
            if itemLink then
                local itemName, _, _, _, _, itemType, itemSubType = GetItemInfo(itemLink)
                local spellName = GetItemSpell and GetItemSpell(itemLink) or nil
                local typeText = string.lower(trimString(itemType))
                local subTypeText = string.lower(trimString(itemSubType))
                local haystack = string.lower(table.concat({ trimString(itemName), trimString(itemSubType), trimString(spellName) }, " "))

                if typeText == "consumable" or subTypeText ~= "" then
                    stocked.food = stocked.food or string.find(subTypeText, "food", 1, true) ~= nil or string.find(subTypeText, "drink", 1, true) ~= nil or string.find(haystack, "feast", 1, true) ~= nil
                    stocked.flask = stocked.flask or string.find(subTypeText, "flask", 1, true) ~= nil or string.find(haystack, "phial", 1, true) ~= nil
                    stocked.potion = stocked.potion or string.find(subTypeText, "potion", 1, true) ~= nil
                    stocked.rune = stocked.rune or string.find(haystack, "augmentation", 1, true) ~= nil or string.find(haystack, "rune", 1, true) ~= nil
                end
            end
        end
    end

    return stocked
end

function service:GetGemReadiness()
    if type(GetInventoryItemLink) ~= "function" then
        return { gems = { total = 0, missing = 0, status = "unknown" } }
    end

    local totalSockets = 0
    local missingSockets = 0

    for slotId = 1, 19 do
        if slotId ~= 4 then
            local itemLink = GetInventoryItemLink("player", slotId)

            if itemLink then
                local filledSockets = countFilledGems(itemLink)
                local emptySockets = countEmptySockets(itemLink)

                totalSockets = totalSockets + filledSockets + emptySockets
                missingSockets = missingSockets + emptySockets
            end
        end
    end

    if totalSockets <= 0 then
        return { gems = { total = 0, missing = 0, status = "unknown" } }
    end

    return {
        gems = {
            total = totalSockets,
            missing = missingSockets,
            status = missingSockets == 0 and "ready" or "issues",
        },
    }
end

function service:GetEnchantReadiness()
    if type(GetInventoryItemLink) ~= "function" then
        return { enchants = { total = 0, missing = 0, status = "unknown" } }
    end

    local totalEnchants = 0
    local missingEnchants = 0

    for slotId = 1, 19 do
        local itemLink = GetInventoryItemLink("player", slotId)

        if itemLink and slotCanRequireEnchant(slotId, itemLink) then
            totalEnchants = totalEnchants + 1

            if getEnchantId(itemLink) <= 0 then
                missingEnchants = missingEnchants + 1
            end
        end
    end

    if totalEnchants <= 0 then
        return { enchants = { total = 0, missing = 0, status = "unknown" } }
    end

    return {
        enchants = {
            total = totalEnchants,
            missing = missingEnchants,
            status = missingEnchants == 0 and "ready" or "issues",
        },
    }
end

function service:GetConsumableReadiness()
    local auraNames = scanAuraNames()
    local bagConsumables = scanBagConsumables()
    local auraNameList = {}

    if type(auraNames) == "table" then
        auraNameList = auraNames
    end

    if type(auraNames) ~= "table" and type(bagConsumables) ~= "table" then
        return { consumables = { food = nil, flask = nil, potion = nil, rune = nil, status = "unknown" } }
    end

    local hasFood = false
    local hasFlask = false
    local hasPotion = false
    local hasRune = false

    for _, auraName in ipairs(auraNameList) do
        if string.find(auraName, "well fed", 1, true) then
            hasFood = true
        end

        if string.find(auraName, "flask", 1, true) or string.find(auraName, "phial", 1, true) then
            hasFlask = true
        end

        if string.find(auraName, "potion", 1, true) then
            hasPotion = true
        end

        if string.find(auraName, "augmentation", 1, true) or string.find(auraName, "rune", 1, true) then
            hasRune = true
        end
    end

    hasFood = hasFood or (type(bagConsumables) == "table" and bagConsumables.food == true) or false
    hasFlask = hasFlask or (type(bagConsumables) == "table" and bagConsumables.flask == true) or false
    hasPotion = hasPotion or (type(bagConsumables) == "table" and bagConsumables.potion == true) or false
    hasRune = hasRune or (type(bagConsumables) == "table" and bagConsumables.rune == true) or false

    return {
        consumables = {
            food = hasFood,
            flask = hasFlask,
            potion = hasPotion,
            rune = hasRune,
            status = (hasFood and hasFlask) and "ready" or "issues",
        },
    }
end

function service:GetDurabilityReadiness()
    if type(GetInventoryItemDurability) ~= "function" then
        return { durability = { minPercent = nil, avgPercent = nil, status = "unknown" } }
    end

    local minPercent = nil
    local totalPercent = 0
    local count = 0

    for slotId = 1, 19 do
        local current, maximum = GetInventoryItemDurability(slotId)

        if type(current) == "number" and type(maximum) == "number" and maximum > 0 then
            local percent = math.floor(((current / maximum) * 100) + 0.5)

            if minPercent == nil or percent < minPercent then
                minPercent = percent
            end

            totalPercent = totalPercent + percent
            count = count + 1
        end
    end

    if count == 0 then
        return { durability = { minPercent = nil, avgPercent = nil, status = "unknown" } }
    end

    return {
        durability = {
            minPercent = minPercent,
            avgPercent = math.floor((totalPercent / count) + 0.5),
            status = minPercent >= DURABILITY_READY_THRESHOLD and "ready" or "issues",
        },
    }
end

function service:GetDefaultReadiness()
    return {
        gems = { total = 0, missing = 0, status = "unknown" },
        enchants = { total = 0, missing = 0, status = "unknown" },
        consumables = { food = nil, flask = nil, potion = nil, rune = nil, status = "unknown" },
        durability = { minPercent = 0, avgPercent = 0, status = "unknown" },
        talents = { status = "unknown", label = "Unknown", name = "", configID = 0 },
        weeklyVault = { status = "unknown", label = "Unknown", progress = 0, total = 0, available = false },
        preferences = {
            trackGems = true,
            trackEnchants = true,
            trackConsumables = true,
            trackDurability = true,
            weeklyVault = true,
            durabilityAlertBelow = DURABILITY_READY_THRESHOLD,
        },
        buffed = nil,
        fed = nil,
        flask = nil,
        readinessPercent = 0,
        statusText = "No Readiness Data",
        updatedAt = 0,
        source = "unknown",
    }
end

function service:GetTalentReadiness()
    if type(C_ClassTalents) ~= "table" or type(C_ClassTalents.GetActiveConfigID) ~= "function" then
        return { talents = { status = "unknown", label = "Unknown", name = "", configID = 0 } }
    end

    local configID = C_ClassTalents.GetActiveConfigID()
    if type(configID) ~= "number" or configID <= 0 then
        return { talents = { status = "unknown", label = "Unknown", name = "", configID = tonumber(configID) or 0 } }
    end

    local configInfo = type(C_Traits) == "table" and type(C_Traits.GetConfigInfo) == "function" and C_Traits.GetConfigInfo(configID) or nil
    local configName = trimString(configInfo and configInfo.name)

    return {
        talents = {
            status = "ready",
            label = "Configured",
            name = configName,
            configID = configID,
        },
    }
end

function service:GetWeeklyVaultReadiness()
    if type(C_WeeklyRewards) ~= "table" then
        return { weeklyVault = { status = "unknown", label = "Unknown", progress = 0, total = 0, available = false } }
    end

    local hasAvailableRewards = type(C_WeeklyRewards.HasAvailableRewards) == "function" and C_WeeklyRewards.HasAvailableRewards() or false
    local activities = type(C_WeeklyRewards.GetActivities) == "function" and C_WeeklyRewards.GetActivities() or nil

    if hasAvailableRewards then
        return { weeklyVault = { status = "ready", label = "Available", progress = 1, total = 1, available = true } }
    end

    if type(activities) ~= "table" or #activities == 0 then
        return { weeklyVault = { status = "unknown", label = "Unknown", progress = 0, total = 0, available = false } }
    end

    local completed = 0
    local started = 0

    for _, activity in ipairs(activities) do
        local progress = tonumber(activity and activity.progress) or 0
        local threshold = tonumber(activity and activity.threshold) or 0

        if progress > 0 then
            started = started + 1
        end

        if threshold > 0 and progress >= threshold then
            completed = completed + 1
        end
    end

    if completed > 0 then
        return {
            weeklyVault = {
                status = "ready",
                label = string.format("%d/%d Complete", completed, #activities),
                progress = completed,
                total = #activities,
                available = false,
            },
        }
    end

    if started > 0 then
        return {
            weeklyVault = {
                status = "attention",
                label = "In Progress",
                progress = started,
                total = #activities,
                available = false,
            },
        }
    end

    return {
        weeklyVault = {
            status = "unknown",
            label = "Not Started",
            progress = 0,
            total = #activities,
            available = false,
        },
    }
end

function service:BuildCurrentReadinessSnapshot()
    local readiness = self:GetDefaultReadiness()
    local gemsReadiness = self:GetGemReadiness()
    local enchantsReadiness = self:GetEnchantReadiness()
    local consumableReadiness = self:GetConsumableReadiness()
    local durabilityReadiness = self:GetDurabilityReadiness()
    local talentReadiness = self:GetTalentReadiness()
    local weeklyVaultReadiness = self:GetWeeklyVaultReadiness()

    mergeKnown(readiness, gemsReadiness)
    mergeKnown(readiness, enchantsReadiness)
    mergeKnown(readiness, consumableReadiness)
    mergeKnown(readiness, durabilityReadiness)
    mergeKnown(readiness, talentReadiness)
    mergeKnown(readiness, weeklyVaultReadiness)

    readiness = self:NormalizeReadiness(readiness)

    local percent = self:CalculatePercent(readiness)
    readiness.readinessPercent = percent or readiness.readinessPercent
    readiness.statusText = self:GetReadinessStatusText(readiness)
    readiness.updatedAt = type(time) == "function" and time() or 0
    readiness.source = "live-scan"

    return readiness
end

function service:NormalizeReadiness(readiness)
    return mergeKnown(self:GetDefaultReadiness(), deepCopy(readiness))
end

function service:CalculatePercent(readiness)
    local normalized = self:NormalizeReadiness(readiness)
    local checks = {
        normalizeStatus(normalized.gems.status),
        normalizeStatus(normalized.enchants.status),
        normalizeStatus(normalized.consumables.status),
        normalizeStatus(normalized.durability.status),
        normalizeFlag(normalized.buffed),
        normalizeFlag(normalized.fed),
        normalizeFlag(normalized.flask),
    }
    local known = 0
    local ready = 0

    for _, value in ipairs(checks) do
        if value ~= nil then
            known = known + 1
            ready = ready + value
        end
    end

    if known == 0 then
        return nil
    end

    return math.floor(((ready / known) * 100) + 0.5)
end

function service:GetStatusText(percent)
    if type(percent) ~= "number" then
        return "No Readiness Data"
    end

    if percent >= 100 then
        return "100% Ready"
    end

    if percent >= 75 then
        return "Good"
    end

    if percent >= 40 then
        return "Needs Attention"
    end

    return "Not Ready"
end

function service:GetReadinessStatusText(readiness)
    local normalized = self:NormalizeReadiness(readiness)
    local percent = self:CalculatePercent(normalized)

    if normalized.gems.status == "issues" then
        return (tonumber(normalized.gems.missing) or 0) == 1 and "Missing Gem" or "Needs Gems"
    end

    if normalized.enchants.status == "issues" then
        return (tonumber(normalized.enchants.missing) or 0) == 1 and "Missing Enchant" or "Missing Enchants"
    end

    if normalized.consumables.status == "issues" then
        if normalized.consumables.flask == false and normalized.consumables.food == false then
            return "No Consumables"
        end

        if normalized.consumables.flask == false then
            return "No Flask Active"
        end

        if normalized.consumables.food == false then
            return "Food Buff Missing"
        end

        return "Missing Consumables"
    end

    if normalized.durability.status == "issues" then
        return "Low Durability"
    end

    if type(percent) ~= "number" then
        return "No Readiness Data"
    end

    if percent >= 100 then
        return "Ready"
    end

    return self:GetStatusText(percent)
end

function service:BuildRosterReadinessSummary(readiness)
    local normalized = self:NormalizeReadiness(readiness)
    local percent = self:CalculatePercent(normalized)

    return {
        gems = normalized.gems.status,
        enchants = normalized.enchants.status,
        consumables = normalized.consumables.status,
        durability = (normalized.durability.status ~= "unknown" and normalized.durability.minPercent) or "unknown",
        talents = trimString(normalized.talents.label) ~= "" and normalized.talents.label or normalized.talents.status,
        weeklyVault = trimString(normalized.weeklyVault.label) ~= "" and normalized.weeklyVault.label or normalized.weeklyVault.status,
        buffed = normalized.buffed,
        fed = normalized.fed,
        flask = normalized.flask,
        readinessPercent = percent or 0,
        statusText = self:GetReadinessStatusText(normalized),
        updatedAt = normalized.updatedAt,
        source = "mytoons",
    }
end