local _, ns = ...
local MythicTools = ns.MythicTools
local Capture = ns.Capture

local BuildCompletionMembersFromActiveRun = Capture.BuildCompletionMembersFromActiveRun
local CopyCompletionInfo = Capture.CopyCompletionInfo
local CreateActiveRunSnapshot = Capture.CreateActiveRunSnapshot
local DEBUG_EVENT_BUFFER_SIZE = Capture.DEBUG_EVENT_BUFFER_SIZE
local GetChallengeCompletionInfoCompat = Capture.GetChallengeCompletionInfoCompat
local GetMapInfo = Capture.GetMapInfo
local INACTIVE_RESOLUTION_DELAY = Capture.INACTIVE_RESOLUTION_DELAY
local IsValidCompletionInfo = Capture.IsValidCompletionInfo
local LOOT_TRACKING_TIMEOUT = Capture.LOOT_TRACKING_TIMEOUT
local SetCachedCompletionInfo = Capture.SetCachedCompletionInfo

function MythicTools:PushRuntimeDebug(message)
    if not (self.runtime and message) then
        return
    end

    local buffer = self.runtime.debugLog
    if type(buffer) ~= "table" then
        buffer = {}
        self.runtime.debugLog = buffer
    end

    buffer[#buffer + 1] = ("%s %s"):format(date("%H:%M:%S"), tostring(message))
    while #buffer > DEBUG_EVENT_BUFFER_SIZE do
        table.remove(buffer, 1)
    end
end

function MythicTools:UpdateActiveRunSnapshot(activeRun)
    if not (self.runtime and type(activeRun) == "table") then
        return
    end

    self.runtime.lastActiveRunSnapshot = CreateActiveRunSnapshot(self, activeRun)
end

function MythicTools:QueueActiveRunSnapshot(activeRun, delay)
    if not (self.runtime and type(activeRun) == "table") then
        return
    end

    self._queuedActiveRunSnapshot = activeRun
    if self._activeRunSnapshotQueued then
        return
    end

    self._activeRunSnapshotQueued = true
    self:Schedule(delay or 0.25, function()
        local queuedRun = self._queuedActiveRunSnapshot
        self._queuedActiveRunSnapshot = nil
        self._activeRunSnapshotQueued = false
        if self.runtime and type(queuedRun) == "table" and self.runtime.activeRun == queuedRun then
            self:UpdateActiveRunSnapshot(queuedRun)
        end
    end)
end

function MythicTools:RememberPendingCompletionSnapshot(activeRun, reason)
    if not (self.runtime and type(activeRun) == "table") then
        return nil
    end

    local snapshot = CreateActiveRunSnapshot(self, activeRun)
    if type(snapshot) ~= "table" then
        return nil
    end

    snapshot.completionPending = true
    snapshot.allowCompletionRecovery = true
    snapshot.pendingSnapshotReason = reason or "unknown"

    local pending = self.runtime.pendingCompletionSnapshots
    if type(pending) ~= "table" then
        pending = {}
        self.runtime.pendingCompletionSnapshots = pending
    end

    for index, existing in ipairs(pending) do
        if type(existing) == "table" and existing.runId == snapshot.runId then
            pending[index] = snapshot
            self:PushRuntimeDebug(("refresh pending snapshot run %s from %s"):format(tostring(snapshot.runId), tostring(reason or "unknown")))
            return snapshot
        end
    end

    pending[#pending + 1] = snapshot
    self:PushRuntimeDebug(("remember pending snapshot run %s from %s"):format(tostring(snapshot.runId), tostring(reason or "unknown")))
    return snapshot
end

function MythicTools:ForgetRecoveryState(runId)
    runId = tonumber(runId)
    if not (self.runtime and runId) then
        return
    end

    if type(self.runtime.lastActiveRunSnapshot) == "table" and tonumber(self.runtime.lastActiveRunSnapshot.runId) == runId then
        self.runtime.lastActiveRunSnapshot = nil
    end

    local pending = self.runtime.pendingCompletionSnapshots
    if type(pending) ~= "table" then
        return
    end

    for index = #pending, 1, -1 do
        local snapshot = pending[index]
        if type(snapshot) == "table" and tonumber(snapshot.runId) == runId then
            table.remove(pending, index)
        end
    end
end

function MythicTools:GetRecoverableCompletionSnapshot(mapChallengeModeID, level)
    if not self.runtime then
        return nil
    end

    local expectedMapID = tonumber(mapChallengeModeID) or 0
    local expectedLevel = tonumber(level) or 0

    local function SnapshotMatches(snapshot)
        if type(snapshot) ~= "table" then
            return false
        end

        local snapshotMapID = tonumber(snapshot.mapChallengeModeID) or 0
        local snapshotLevel = tonumber(snapshot.keyLevel) or 0
        local mapMatches = expectedMapID <= 0 or snapshotMapID <= 0 or snapshotMapID == expectedMapID
        local levelMatches = expectedLevel <= 0 or snapshotLevel <= 0 or snapshotLevel == expectedLevel
        return mapMatches and levelMatches
    end

    local pending = self.runtime.pendingCompletionSnapshots
    if type(pending) == "table" then
        for index = #pending, 1, -1 do
            if SnapshotMatches(pending[index]) then
                return pending[index]
            end
        end
    end

    local snapshot = self.runtime.lastActiveRunSnapshot
    if SnapshotMatches(snapshot) then
        return snapshot
    end

    return nil
end

function MythicTools:RestoreActiveRunFromSnapshot(snapshot, reason, completionInfo)
    if type(snapshot) ~= "table" then
        return nil
    end

    local restoredRun = CreateActiveRunSnapshot(self, snapshot)
    if type(restoredRun) ~= "table" then
        return nil
    end

    if IsValidCompletionInfo(completionInfo) then
        restoredRun.pendingCompletionInfo = CopyCompletionInfo(completionInfo)
        restoredRun.pendingCompletionSource = restoredRun.pendingCompletionSource or reason or "recovered"
    end

    restoredRun.restoredFromReload = true
    restoredRun.allowCompletionRecovery = true
    restoredRun.finalizationInProgress = false
    restoredRun.inactiveResolutionQueued = false
    restoredRun.state = restoredRun.state or "running"

    self.runtime.activeRun = restoredRun
    self.EventFrame:RegisterEvent("UNIT_HEALTH")
    self:UpdateActiveRunSnapshot(restoredRun)
    self:PushRuntimeDebug(("restore active run %s from %s"):format(tostring(restoredRun.runId), tostring(reason or "snapshot")))
    return restoredRun
end

function MythicTools:QueuePendingCompletionProcessing(trigger)
    if not (self.runtime and not self.runtime.pendingCompletionProcessingQueued) then
        return
    end

    local pending = self.runtime.pendingCompletionSnapshots
    if type(pending) ~= "table" or #pending == 0 then
        return
    end

    self.runtime.pendingCompletionProcessingQueued = true
    self:Schedule(INACTIVE_RESOLUTION_DELAY, function()
        if not self.runtime then
            return
        end

        self.runtime.pendingCompletionProcessingQueued = false
        if C_ChallengeMode.IsChallengeModeActive and C_ChallengeMode.IsChallengeModeActive() then
            return
        end

        if self.ProcessPendingCompletionSnapshots then
            self:ProcessPendingCompletionSnapshots(trigger or "PLAYER_ENTERING_WORLD", true)
        end
    end)
end

function MythicTools:SetActiveRunState(activeRun, newState)
    if type(activeRun) ~= "table" or activeRun.state == newState then
        return
    end

    activeRun.state = newState
    self:PushRuntimeDebug(("run %s state=%s"):format(tostring(activeRun.runId), tostring(newState)))
end

function MythicTools:MarkCompletionSignal(activeRun, signal)
    if type(activeRun) ~= "table" or type(signal) ~= "string" or signal == "" then
        return
    end

    local signals = type(activeRun.completionSignals) == "table" and activeRun.completionSignals or {}
    activeRun.completionSignals = signals
    signals[signal] = signals[signal] or time()
    self:PushRuntimeDebug(("run %s signal=%s"):format(tostring(activeRun.runId), signal))
end

function MythicTools:HasCompletionSignals(activeRun)
    if type(activeRun) ~= "table" then
        return false
    end

    for _ in pairs(activeRun.completionSignals or {}) do
        return true
    end

    return false
end

function MythicTools:HasCompletionSignal(activeRun, signal)
    return type(activeRun) == "table"
        and type(activeRun.completionSignals) == "table"
        and activeRun.completionSignals[signal] ~= nil
end

function MythicTools:ShouldUseEarlyCompletionRecovery(activeRun, attempt)
    if type(activeRun) ~= "table" then
        return false
    end

    attempt = tonumber(attempt) or 0
    if attempt < 3 then
        return false
    end

    if self.runtime and self.runtime.lastCompletionRewards ~= nil then
        return true
    end

    if self:HasCompletionSignal(activeRun, "SCENARIO_COMPLETED") and self:HasCompletionSignal(activeRun, "CHALLENGE_MODE_COMPLETED") then
        return true
    end

    if self:HasCompletionSignal(activeRun, "CHALLENGE_MODE_COMPLETED") and attempt >= 5 then
        return true
    end

    if not (C_ChallengeMode.IsChallengeModeActive and C_ChallengeMode.IsChallengeModeActive()) then
        return attempt >= 10 and self:HasCompletionSignals(activeRun)
    end

    return false
end

function MythicTools:QueueInactiveRunResolution(activeRun, trigger)
    if type(activeRun) ~= "table" or activeRun.inactiveResolutionQueued then
        return
    end

    activeRun.inactiveResolutionQueued = true
    self:PushRuntimeDebug(("run %s queue inactive resolution from %s"):format(tostring(activeRun.runId), tostring(trigger or "unknown")))
    self:Schedule(INACTIVE_RESOLUTION_DELAY, function()
        if not self.runtime or self.runtime.activeRun ~= activeRun then
            return
        end

        activeRun.inactiveResolutionQueued = false
        if C_ChallengeMode.IsChallengeModeActive and C_ChallengeMode.IsChallengeModeActive() then
            return
        end

        self:HandleChallengeModeInactive(trigger or "PLAYER_ENTERING_WORLD")
    end)
end

function MythicTools:ScheduleLootTrackingTimeout(tracking)
    if type(tracking) ~= "table" then
        return
    end

    self:Schedule(LOOT_TRACKING_TIMEOUT, function()
        if not (self.runtime and self.runtime.lootTracking == tracking) then
            return
        end

        local deadline = tonumber(tracking.lootFinalizeDeadline) or 0
        if deadline > 0 and time() < deadline then
            self:ScheduleLootTrackingTimeout(tracking)
            return
        end

        self:PushRuntimeDebug(("loot tracking timeout run %s"):format(tostring(tracking.runId)))
        self:StopLootTracking()
    end)
end

function MythicTools:RequestMythicPlusData()
    if not C_MythicPlus then
        return
    end

    if C_MythicPlus.RequestMapInfo then
        pcall(C_MythicPlus.RequestMapInfo)
    end

    if C_MythicPlus.RequestRewards then
        pcall(C_MythicPlus.RequestRewards)
    end
end

function MythicTools:GetMythicPlusRunHistory()
    if not (C_MythicPlus and C_MythicPlus.GetRunHistory) then
        return nil
    end

    if self.RequestMythicPlusData then
        self:RequestMythicPlusData()
    end

    local ok, runs = pcall(C_MythicPlus.GetRunHistory, true, true, true)
    if ok and type(runs) == "table" then
        return runs
    end

    ok, runs = pcall(C_MythicPlus.GetRunHistory, true, true)
    if ok and type(runs) == "table" then
        return runs
    end

    ok, runs = pcall(C_MythicPlus.GetRunHistory)
    if ok and type(runs) == "table" then
        return runs
    end

    return nil
end

function MythicTools:GetRunHistoryCompletionInfo(activeRun)
    local runs = self:GetMythicPlusRunHistory()
    if type(runs) ~= "table" or #runs == 0 then
        return nil
    end

    local expectedMapID = tonumber(activeRun and activeRun.mapChallengeModeID) or 0
    local expectedLevel = tonumber(activeRun and activeRun.keyLevel) or 0
    local expectedCompletedAt = tonumber(activeRun and activeRun.completedAt) or time()
    local bestRun, bestScore

    local function NormalizeHistoryTimestamp(value)
        value = tonumber(value) or 0
        if value <= 0 then
            return 0
        end
        if value > 1000000000000 then
            value = value / 1000
        end
        return value
    end

    for index, runInfo in ipairs(runs) do
        if type(runInfo) == "table" then
            local mapChallengeModeID = tonumber(runInfo.mapChallengeModeID or runInfo.challengeModeMapID or runInfo.mapID) or 0
            local level = tonumber(runInfo.level or runInfo.keystoneLevel) or 0
            local durationSeconds = tonumber(runInfo.durationSec or runInfo.time or runInfo.duration) or 0
            local completedAt = NormalizeHistoryTimestamp(runInfo.completedDate or runInfo.completedAt or runInfo.completedTime or runInfo.timestamp or runInfo.endTime)
            local completed = runInfo.completed
            local score = (#runs - index)
            local mapMatches = expectedMapID <= 0 or mapChallengeModeID == expectedMapID
            local levelMatches = expectedLevel <= 0 or level == expectedLevel
            local timeMatches = completedAt <= 0 or expectedCompletedAt <= 0 or math.abs(completedAt - expectedCompletedAt) <= 1800

            if completed ~= false and mapMatches and levelMatches and timeMatches and mapChallengeModeID > 0 and level > 0 and durationSeconds > 0 then
                if expectedMapID > 0 and mapChallengeModeID == expectedMapID then
                    score = score + 10
                end
                if expectedLevel > 0 and level == expectedLevel then
                    score = score + 6
                end
                if completedAt > 0 and expectedCompletedAt > 0 then
                    score = score + math.max(0, 4 - math.floor(math.abs(completedAt - expectedCompletedAt) / 300))
                end

                if not bestScore or score > bestScore then
                    bestRun = runInfo
                    bestScore = score
                end
            end
        end
    end

    if type(bestRun) ~= "table" then
        return nil
    end

    local mapChallengeModeID = tonumber(bestRun.mapChallengeModeID or bestRun.challengeModeMapID or bestRun.mapID) or 0
    local level = tonumber(bestRun.level or bestRun.keystoneLevel) or 0
    local durationSeconds = tonumber(bestRun.durationSec or bestRun.time or bestRun.duration) or 0
    local mapInfo = GetMapInfo(mapChallengeModeID)
    local timeLimitSeconds = tonumber(mapInfo and mapInfo.timeLimit) or tonumber(activeRun and activeRun.timeLimitSeconds) or 0

    if mapChallengeModeID <= 0 or level <= 0 or durationSeconds <= 0 then
        return nil
    end

    return {
        mapChallengeModeID = mapChallengeModeID,
        level = level,
        time = math.floor((durationSeconds * 1000) + 0.5),
        onTime = timeLimitSeconds > 0 and durationSeconds <= timeLimitSeconds or nil,
        keystoneUpgradeLevels = 0,
        practiceRun = false,
        oldOverallDungeonScore = nil,
        newOverallDungeonScore = nil,
        isMapRecord = nil,
        isAffixRecord = nil,
        primaryAffix = nil,
        isEligibleForScore = nil,
        members = BuildCompletionMembersFromActiveRun(activeRun),
    }
end

function MythicTools:GetResolvedChallengeCompletionInfo(activeRun, allowEstimated)
    local pendingInfo = type(activeRun) == "table" and activeRun.pendingCompletionInfo or nil
    if IsValidCompletionInfo(pendingInfo) then
        return pendingInfo
    end

    local completionInfo = GetChallengeCompletionInfoCompat()
    if IsValidCompletionInfo(completionInfo) then
        return SetCachedCompletionInfo(activeRun, completionInfo, "native")
    end

    local rewardsInfo = self.runtime and self.runtime.lastCompletionRewards or nil
    local mapChallengeModeID = tonumber(rewardsInfo and rewardsInfo.mapChallengeModeID) or tonumber(activeRun and activeRun.mapChallengeModeID) or 0
    local level = tonumber(activeRun and activeRun.keyLevel) or 0
    local mapInfo = GetMapInfo(mapChallengeModeID)
    local timeLimitSeconds = tonumber(mapInfo and mapInfo.timeLimit) or tonumber(activeRun and activeRun.timeLimitSeconds) or 0
    local endTimestamp = tonumber(activeRun and activeRun.completedAt) or tonumber(rewardsInfo and rewardsInfo.receivedAt) or time()
    local startedAt = tonumber(activeRun and activeRun.startedAt) or endTimestamp
    local rewardTimeMS = tonumber(rewardsInfo and rewardsInfo.timeMS) or 0

    if mapChallengeModeID > 0 and level > 0 and rewardTimeMS > 0 then
        local fallbackInfo = {
            mapChallengeModeID = mapChallengeModeID,
            level = level,
            time = rewardTimeMS,
            onTime = timeLimitSeconds > 0 and rewardTimeMS <= (timeLimitSeconds * 1000) or nil,
            keystoneUpgradeLevels = 0,
            practiceRun = false,
            oldOverallDungeonScore = nil,
            newOverallDungeonScore = nil,
            isMapRecord = nil,
            isAffixRecord = nil,
            primaryAffix = nil,
            isEligibleForScore = true,
            members = BuildCompletionMembersFromActiveRun(activeRun),
        }
        return SetCachedCompletionInfo(activeRun, fallbackInfo, "rewards")
    end

    local allowRecovery = allowEstimated and type(activeRun) == "table" and (activeRun.restoredFromReload or activeRun.allowCompletionRecovery)
    if allowRecovery then
        local historyInfo = self:GetRunHistoryCompletionInfo(activeRun)
        if IsValidCompletionInfo(historyInfo) then
            return SetCachedCompletionInfo(activeRun, historyInfo, "history")
        end
    end

    return nil
end

