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

local DATA = ns.Data or {}
local MIDNIGHT_RARE_ZONES = DATA.MIDNIGHT_RARE_ZONES or {}
local MIDNIGHT_RARE_STATE_SCHEMA_VERSION = 1
local MIDNIGHT_RARE_QUEST_LOOKUP = {}
local MIDNIGHT_RARE_MAP_LOOKUP = {}
local localizedRareNameCache = {}
local localizedMapNameCache = {}
local rareQuestCompletionCache = {}
local rareQuestCompletionCacheReady = false
local questLogProbePending = false
local QUEST_LOG_PROBE_DELAY = 0.55

for _, zone in ipairs(MIDNIGHT_RARE_ZONES) do
    local zoneMapID = tonumber(zone and zone.mapID)
    if zoneMapID then
        MIDNIGHT_RARE_MAP_LOOKUP[zoneMapID] = true
    end
    for _, rare in ipairs(zone.rares or {}) do
        local questID = tonumber(rare and rare.questID)
        if questID then
            MIDNIGHT_RARE_QUEST_LOOKUP[questID] = true
        end
        local rareMapID = tonumber(rare and rare.mapID)
        if rareMapID then
            MIDNIGHT_RARE_MAP_LOOKUP[rareMapID] = true
        end
    end
end

local Quest = ns.Quest or {}
local isQuestCompleted = Quest.IsQuestCompleted or function(questID)
    if C_QuestLog and type(C_QuestLog.IsQuestFlaggedCompleted) == "function" then
        local ok, completed = pcall(C_QuestLog.IsQuestFlaggedCompleted, questID)
        return ok and completed == true
    end
    return false
end

local function getNow()
    return type(time) == "function" and time() or 0
end

local function getDailyResetAt()
    local seconds = ns.GetDailyResetSeconds and ns:GetDailyResetSeconds() or 0
    seconds = math.max(0, tonumber(seconds) or 0)
    return seconds > 0 and (getNow() + seconds) or nil
end

local function isMidnightRareMapID(mapID)
    mapID = tonumber(mapID)
    local depth = 0
    while mapID and depth < 8 do
        if MIDNIGHT_RARE_MAP_LOOKUP[mapID] then
            return true
        end
        if not C_Map or type(C_Map.GetMapInfo) ~= "function" then
            break
        end

        local ok, info = pcall(C_Map.GetMapInfo, mapID)
        mapID = ok and type(info) == "table" and tonumber(info.parentMapID) or nil
        depth = depth + 1
    end

    return false
end

local function isPlayerOnMidnightRareMap()
    if not C_Map or type(C_Map.GetBestMapForUnit) ~= "function" then
        return false
    end

    local ok, mapID = pcall(C_Map.GetBestMapForUnit, "player")
    return ok and isMidnightRareMapID(mapID) or false
end

local function rememberRareQuestCompletion(questID, completed)
    questID = tonumber(questID)
    if questID then
        rareQuestCompletionCache[questID] = completed == true
    end
end

local function scanRareQuestCompletionChanges()
    local changed = false
    local hadQuest = false
    for questID in pairs(MIDNIGHT_RARE_QUEST_LOOKUP) do
        hadQuest = true
        local completed = isQuestCompleted(questID) == true
        local previous = rareQuestCompletionCache[questID]
        if rareQuestCompletionCacheReady and completed and previous == false then
            changed = true
        end
        rememberRareQuestCompletion(questID, completed)
    end

    if hadQuest then
        rareQuestCompletionCacheReady = true
    end

    return changed
end

local function triggerMidnightRareQuestLogRefresh()
    questLogProbePending = false
    if not isPlayerOnMidnightRareMap() or not scanRareQuestCompletionChanges() then
        return
    end
    if ns.ScheduleLiveDataRefresh and ns.BuildLiveRefreshSpec then
        ns:ScheduleLiveDataRefresh(0.05, ns:BuildLiveRefreshSpec("midnightRares", "renownForce"))
    end
end

function ns:PrimeMidnightRareQuestCompletionCache()
    scanRareQuestCompletionChanges()
end

local function getMountInfoFromItem(itemID)
    itemID = tonumber(itemID)
    if not itemID then
        return nil, false
    end

    if C_Item and type(C_Item.RequestLoadItemDataByID) == "function" then
        pcall(C_Item.RequestLoadItemDataByID, itemID)
    end

    local icon
    if C_Item and type(C_Item.GetItemIconByID) == "function" then
        local okIcon, itemIcon = pcall(C_Item.GetItemIconByID, itemID)
        icon = okIcon and itemIcon or nil
    end

    if not C_MountJournal or type(C_MountJournal.GetMountFromItem) ~= "function" then
        return icon, false
    end

    local okMount, mountID = pcall(C_MountJournal.GetMountFromItem, itemID)
    mountID = okMount and tonumber(mountID) or nil
    if not mountID or type(C_MountJournal.GetMountInfoByID) ~= "function" then
        return icon, false
    end

    local okInfo, _, _, _, _, _, _, _, _, _, _, collected = pcall(C_MountJournal.GetMountInfoByID, mountID)
    return icon, okInfo and collected == true
end

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

    local okEmpty, isEmpty = pcall(function()
        return text == ""
    end)
    if not okEmpty or isEmpty then
        return true
    end

    local placeholders = {
        UNKNOWN,
        UNKNOWNOBJECT,
        RETRIEVING_DATA,
    }
    for _, placeholder in ipairs(placeholders) do
        local okMatch, isMatch = pcall(function()
            return type(placeholder) == "string" and text == placeholder
        end)
        if not okMatch or isMatch then
            return true
        end
    end

    return false
end

local function getTooltipInfoName(link)
    if not C_TooltipInfo or type(C_TooltipInfo.GetHyperlink) ~= "function" then
        return nil
    end

    local ok, info = pcall(C_TooltipInfo.GetHyperlink, link)
    if not ok or type(info) ~= "table" or type(info.lines) ~= "table" then
        return nil
    end

    for _, line in ipairs(info.lines) do
        local text = line and line.leftText
        if not isPlaceholderName(text) then
            return text
        end
    end

    return nil
end

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

    local ok, tooltip = pcall(CreateFrame, "GameTooltip", "VaultloomMidnightRareScanTooltip", nil, "GameTooltipTemplate")
    if not ok or not tooltip then
        return nil
    end
    if tooltip.SetOwner then
        tooltip:SetOwner(WorldFrame or UIParent, "ANCHOR_NONE")
    end

    scanTooltip = tooltip
    return scanTooltip
end

local function getScanTooltipName(link)
    local tooltip = getScanTooltip()
    if not tooltip or type(tooltip.SetHyperlink) ~= "function" then
        return nil
    end

    if tooltip.ClearLines then
        tooltip:ClearLines()
    end

    local ok = pcall(tooltip.SetHyperlink, tooltip, link)
    if not ok then
        return nil
    end

    local tooltipName = tooltip.GetName and tooltip:GetName() or nil
    local nameLine = tooltipName and _G and _G[tooltipName .. "TextLeft1"] or nil
    local text = nameLine and nameLine.GetText and nameLine:GetText() or nil
    if tooltip.ClearLines then
        tooltip:ClearLines()
    end

    return not isPlaceholderName(text) and text or nil
end

local function getLocalizedRareName(npcID, fallback)
    fallback = tostring(fallback or "")
    npcID = tonumber(npcID)
    if not npcID then
        return fallback
    end

    local cached = localizedRareNameCache[npcID]
    if cached ~= nil then
        return cached or fallback
    end

    local link = string.format("unit:Creature-0-0-0-0-%d-0000000000", npcID)
    local localizedName = getTooltipInfoName(link) or getScanTooltipName(link)
    if isPlaceholderName(localizedName) then
        localizedName = false
    end

    localizedRareNameCache[npcID] = localizedName
    return localizedName or fallback
end

function ns:GetLocalizedMidnightRareName(npcID, fallback)
    return getLocalizedRareName(npcID, fallback)
end

local function getLocalizedMapName(mapID, fallback)
    fallback = tostring(fallback or "")
    mapID = tonumber(mapID)
    if not mapID then
        return fallback
    end

    local cached = localizedMapNameCache[mapID]
    if cached ~= nil then
        return cached or fallback
    end

    local localizedName
    if C_Map and type(C_Map.GetMapInfo) == "function" then
        local ok, info = pcall(C_Map.GetMapInfo, mapID)
        if ok and type(info) == "table" and not isPlaceholderName(info.name) then
            localizedName = info.name
        end
    end

    if isPlaceholderName(localizedName) then
        localizedName = false
    end

    localizedMapNameCache[mapID] = localizedName
    return localizedName or fallback
end

function ns:GetLocalizedMidnightRareZoneName(mapID, fallback)
    return getLocalizedMapName(mapID, fallback)
end

local function copyMountState(mount)
    local itemID = tonumber(mount and mount.itemID)
    local icon, collected = getMountInfoFromItem(itemID)

    return {
        itemID = itemID,
        name = mount and mount.name or "",
        icon = icon,
        collected = collected == true,
        status = collected and "complete" or "missing",
        text = collected and (L.PVE_RARES_MOUNT_COLLECTED or "Collected") or (L.PVE_RARES_MOUNT_MISSING or "Missing"),
    }
end

local function addIndexedState(lookup, prefix, key, value)
    if type(lookup) ~= "table" or key == nil or type(value) ~= "table" then
        return
    end

    local indexKey = prefix .. tostring(key)
    if lookup[indexKey] == nil then
        lookup[indexKey] = value
    end
end

local function buildExistingRareLookup(existingState)
    local rares = {}
    local mounts = {}
    if type(existingState) ~= "table" then
        return rares, mounts
    end

    for _, zone in ipairs(existingState.zones or {}) do
        for _, rare in ipairs(type(zone) == "table" and zone.rares or {}) do
            if type(rare) == "table" then
                addIndexedState(rares, "key:", rare.key, rare)
                addIndexedState(rares, "quest:", rare.questID, rare)
                addIndexedState(rares, "npc:", rare.npcID, rare)
            end
        end

        for _, mount in ipairs(type(zone) == "table" and zone.mounts or {}) do
            if type(mount) == "table" then
                addIndexedState(mounts, "item:", mount.itemID, mount)
                addIndexedState(mounts, "name:", mount.name, mount)
            end
        end
    end

    return rares, mounts
end

local function getIndexedRareState(lookup, rare)
    if type(lookup) ~= "table" or type(rare) ~= "table" then
        return nil
    end

    return lookup["quest:" .. tostring(rare.questID)]
        or lookup["key:" .. tostring(rare.key)]
        or lookup["npc:" .. tostring(rare.npcID)]
end

local function getIndexedMountState(lookup, mount)
    if type(lookup) ~= "table" or type(mount) ~= "table" then
        return nil
    end

    return lookup["item:" .. tostring(mount.itemID)]
        or lookup["name:" .. tostring(mount.name)]
end

local function mergeMidnightRareStateWithExisting(state, existingState)
    if type(state) ~= "table" or type(existingState) ~= "table" then
        return state
    end

    local rareLookup, mountLookup = buildExistingRareLookup(existingState)
    local completedTotal = 0
    local rareTotal = 0
    local collectedMounts = 0
    local mountTotal = 0

    for _, zone in ipairs(state.zones or {}) do
        if type(zone) == "table" then
            local zoneCompleted = 0
            local zoneTotal = 0

            for _, rare in ipairs(zone.rares or {}) do
                if type(rare) == "table" then
                    local existingRare = getIndexedRareState(rareLookup, rare)
                    if rare.completed ~= true and type(existingRare) == "table" and existingRare.completed == true then
                        rare.status = "complete"
                        rare.text = L.PVE_RARES_DONE or L.STATUS_DONE or "Done"
                        rare.completed = true
                        rare.seen = true
                    end

                    zoneTotal = zoneTotal + 1
                    if rare.completed == true then
                        zoneCompleted = zoneCompleted + 1
                    end
                end
            end

            for _, mount in ipairs(zone.mounts or {}) do
                if type(mount) == "table" then
                    local existingMount = getIndexedMountState(mountLookup, mount)
                    if mount.collected ~= true and type(existingMount) == "table" and existingMount.collected == true then
                        mount.collected = true
                        mount.status = "complete"
                        mount.text = L.PVE_RARES_MOUNT_COLLECTED or "Collected"
                    end

                    mountTotal = mountTotal + 1
                    if mount.collected == true then
                        collectedMounts = collectedMounts + 1
                    end
                end
            end

            zone.completed = zoneCompleted
            zone.total = zoneTotal
            zone.text = string.format("%d/%d", zoneCompleted, zoneTotal)
            completedTotal = completedTotal + zoneCompleted
            rareTotal = rareTotal + zoneTotal
        end
    end

    state.completed = completedTotal
    state.total = rareTotal
    state.mountCollected = collectedMounts
    state.mountTotal = mountTotal
    state.text = string.format("%d/%d", completedTotal, rareTotal)
    state.mountText = string.format("%d/%d", collectedMounts, mountTotal)
    return state
end

local function buildRareState(zone, rare)
    local questID = tonumber(rare and rare.questID)
    local completed = questID and isQuestCompleted(questID) or false
    rememberRareQuestCompletion(questID, completed)
    local mapID = tonumber(rare and rare.mapID) or tonumber(zone and zone.mapID)
    local x = tonumber(rare and rare.x) or 0
    local y = tonumber(rare and rare.y) or 0
    local name = getLocalizedRareName(rare and rare.npcID, rare and rare.name)

    return {
        key = rare and rare.key or tostring(questID or name),
        name = name,
        npcID = tonumber(rare and rare.npcID),
        questID = questID,
        mapID = mapID,
        x = x,
        y = y,
        label = name,
        status = completed and "complete" or "open",
        text = completed and (L.PVE_RARES_DONE or L.STATUS_DONE or "Done") or (L.PVE_RARES_OPEN or L.STATUS_OPEN or "Open"),
        completed = completed == true,
        seen = true,
        tooltipTitle = name,
        tooltipLines = {
            string.format(L.PVE_RARES_COORDS_FORMAT or "Coordinates: %.1f, %.1f", x, y),
            L.PVE_RARES_WAYPOINT_HINT or "Left-click to set a Blizzard waypoint.",
        },
    }
end

local function buildZoneState(zone)
    local mapID = tonumber(zone and zone.mapID)
    local label = getLocalizedMapName(mapID, zone and zone.label)
    local zoneState = {
        key = zone.key,
        label = label,
        mapID = mapID,
        rares = {},
        mounts = {},
        completed = 0,
        total = 0,
    }

    for _, mount in ipairs(zone.mounts or {}) do
        zoneState.mounts[#zoneState.mounts + 1] = copyMountState(mount)
    end

    for _, rare in ipairs(zone.rares or {}) do
        local rareState = buildRareState(zone, rare)
        zoneState.rares[#zoneState.rares + 1] = rareState
        zoneState.total = zoneState.total + 1
        if rareState.completed then
            zoneState.completed = zoneState.completed + 1
        end
    end

    zoneState.text = string.format("%d/%d", zoneState.completed, zoneState.total)
    return zoneState
end

function ns:GetMidnightRareZones()
    return MIDNIGHT_RARE_ZONES
end

function ns:IsMidnightRareQuestID(questID)
    questID = tonumber(questID)
    return questID and MIDNIGHT_RARE_QUEST_LOOKUP[questID] == true or false
end

function ns:IsMidnightRareCompleted(rareOrQuestID)
    local questID = type(rareOrQuestID) == "table" and rareOrQuestID.questID or rareOrQuestID
    questID = tonumber(questID)
    return questID and isQuestCompleted(questID) == true or false
end

function ns:IsMidnightRareMapID(mapID)
    return isMidnightRareMapID(mapID)
end

function ns:QueueMidnightRareQuestLogProbe()
    if questLogProbePending or not isPlayerOnMidnightRareMap() then
        return false
    end

    questLogProbePending = true
    if C_Timer and type(C_Timer.After) == "function" then
        C_Timer.After(QUEST_LOG_PROBE_DELAY, triggerMidnightRareQuestLogRefresh)
    else
        triggerMidnightRareQuestLogRefresh()
    end

    return true
end

function ns:GetSelectedMidnightRareZoneKey()
    local selected = self.db and self.db.ui and self.db.ui.selectedMidnightRareZoneKey or nil
    for _, zone in ipairs(MIDNIGHT_RARE_ZONES) do
        if zone.key == selected then
            return selected
        end
    end

    return MIDNIGHT_RARE_ZONES[1] and MIDNIGHT_RARE_ZONES[1].key or nil
end

function ns:SetSelectedMidnightRareZoneKey(zoneKey)
    if not self.db or not self.db.ui then
        return false
    end

    for _, zone in ipairs(MIDNIGHT_RARE_ZONES) do
        if zone.key == zoneKey then
            self.db.ui.selectedMidnightRareZoneKey = zoneKey
            return true
        end
    end

    return false
end

function ns:BuildCurrentMidnightRareState()
    local state = {
        schemaVersion = MIDNIGHT_RARE_STATE_SCHEMA_VERSION,
        generatedAt = getNow(),
        resetAt = getDailyResetAt(),
        zones = {},
        completed = 0,
        total = 0,
        mountCollected = 0,
        mountTotal = 0,
    }

    for _, zone in ipairs(MIDNIGHT_RARE_ZONES) do
        local zoneState = buildZoneState(zone)
        state.zones[#state.zones + 1] = zoneState
        state.completed = state.completed + zoneState.completed
        state.total = state.total + zoneState.total
        for _, mount in ipairs(zoneState.mounts or {}) do
            state.mountTotal = state.mountTotal + 1
            if mount.collected then
                state.mountCollected = state.mountCollected + 1
            end
        end
    end

    state.text = string.format("%d/%d", state.completed, state.total)
    state.mountText = string.format("%d/%d", state.mountCollected, state.mountTotal)
    return state
end

function ns:RefreshCurrentCharacterMidnightRareData()
    local record = self:RefreshCurrentCharacterRecord()
    if self.ClearExpiredMidnightRareState then
        self:ClearExpiredMidnightRareState(record)
    end
    local existingState = type(record.midnightRares) == "table" and record.midnightRares.state or nil

    local state = self:BuildCurrentMidnightRareState()
    if not state then
        return nil
    end
    state = mergeMidnightRareStateWithExisting(state, existingState)

    record.midnightRares = {
        schemaVersion = MIDNIGHT_RARE_STATE_SCHEMA_VERSION,
        resetAt = state.resetAt,
        state = state,
    }

    return state
end

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

    if self:IsCurrentCharacterKey(key) then
        local storedState = self.GetStoredMidnightRareState and self:GetStoredMidnightRareState(key)
        local liveState = self.BuildCurrentMidnightRareState and self:BuildCurrentMidnightRareState()
        if liveState then
            return mergeMidnightRareStateWithExisting(liveState, storedState)
        end

        if storedState then
            return storedState
        end
    end

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

    return fallbackState
end

function ns:SetMidnightRareWaypoint(rare)
    if type(rare) ~= "table" then
        return false
    end

    local mapID = tonumber(rare.mapID)
    local x = tonumber(rare.x)
    local y = tonumber(rare.y)
    local name = getLocalizedRareName(rare.npcID, rare.name or rare.label or L.PVE_RARES_WAYPOINT or "Rare")
    if not mapID or not x or not y then
        return false
    end

    if not C_Map
        or type(C_Map.SetUserWaypoint) ~= "function"
        or not UiMapPoint
        or type(UiMapPoint.CreateFromCoordinates) ~= "function"
    then
        if self.Print then
            self:Print(L.PVE_RARES_WAYPOINT_UNAVAILABLE or "Blizzard waypoint API is not available.")
        end
        return false
    end

    local okPoint, point = pcall(UiMapPoint.CreateFromCoordinates, mapID, x / 100, y / 100)
    if not okPoint or not point then
        return false
    end

    local okWaypoint = pcall(C_Map.SetUserWaypoint, point)
    if okWaypoint and C_SuperTrack and type(C_SuperTrack.SetSuperTrackedUserWaypoint) == "function" then
        pcall(C_SuperTrack.SetSuperTrackedUserWaypoint, true)
    end
    if okWaypoint and self.Print then
        self:Print(string.format(L.PVE_RARES_WAYPOINT_SET or "Waypoint set: %s", name))
    end

    return okWaypoint == true
end

ns.MIDNIGHT_RARE_STATE_SCHEMA_VERSION = MIDNIGHT_RARE_STATE_SCHEMA_VERSION
