BattlePetStatisticsDB = BattlePetStatisticsDB or nil

local frame = CreateFrame("Frame")
BattlePetStatistics_CoreFrame = frame

local currentOpponentKey         = nil
local currentOpponentDisplayName = nil
local battleIsPVP                = false
local battleResolved             = false
local playerDidForfeit           = false
local lastKnownEnemyDead         = false
local lastKnownAllyDead          = false
local debugMode                  = false

-- Tracks the opponent key for a battle that ended but whose resolution is
-- still pending the 1.5s server-sync delay (see ResolveAndApplyBattle below).
-- Used so a fast requeue can force-resolve it immediately instead of risking
-- a desync where the next battle starts before the previous one was recorded.
local pendingResolveOpponentKey = nil

-- Session stats: reset each login, NOT saved to DB
local sessionWins   = 0
local sessionLosses = 0
-- Session stats excluding Random opponents
local sessionNRWins   = 0
local sessionNRLosses = 0
-- Per-opponent session stats (wins/losses against each specific opponent key
-- since login). Reset each login, NOT saved to DB.
local sessionStatsByOpponent = {}

-- A "session" is counted once per login, but only once the player has
-- started at least 2 PvP pet battles during that login. sessionCounted
-- guards against incrementing more than once per login.
local pvpBattlesStartedThisLogin = 0
local sessionCounted             = false

function Debug(msg)
    if debugMode then
        print("|cff33ff99BPS|r: " .. tostring(msg))
    end
end

-- ===================== DB helpers =====================
function EnsureDB()
    if type(BattlePetStatisticsDB) ~= "table" then
        BattlePetStatisticsDB = {}
    end
    BattlePetStatisticsDB.language      = BattlePetStatisticsDB.language or "AUTO"
    BattlePetStatisticsDB.stats         = BattlePetStatisticsDB.stats or {}
    BattlePetStatisticsDB.minimap       = BattlePetStatisticsDB.minimap or { hide = false, angle = 200 }
    BattlePetStatisticsDB.tradeWinEnabled   = (BattlePetStatisticsDB.tradeWinEnabled ~= nil and BattlePetStatisticsDB.tradeWinEnabled) or false
    BattlePetStatisticsDB.alias         = BattlePetStatisticsDB.alias or ""
    -- When to show the TradeWin alias popup: "startup" / "after_first_battle" /
    -- "never" / "if_not_renamed" (only if the pets haven't been renamed yet).
    BattlePetStatisticsDB.showWindowMode = BattlePetStatisticsDB.showWindowMode or "after_first_battle"
    -- Whether to restore the player's pets to their default names on logout.
    if BattlePetStatisticsDB.restoreNamesOnLogout == nil then
        BattlePetStatisticsDB.restoreNamesOnLogout = false
    end
    -- Tracks whether the pets currently carry TradeWin names, so an abnormal
    -- exit (crash/force-quit/disconnect) that skipped the logout restore can
    -- be caught up on the next login instead of leaving names stuck forever.
    if BattlePetStatisticsDB.petsCurrentlyRenamed == nil then
        BattlePetStatisticsDB.petsCurrentlyRenamed = false
    end
    -- Set to true right when the player deliberately confirms a new/changed
    -- alias in the popup, so the next battle resolution always performs a
    -- real rename, bypassing the "assume already done" compatibility bridge.
    if BattlePetStatisticsDB.forceRenameNext == nil then
        BattlePetStatisticsDB.forceRenameNext = false
    end
    BattlePetStatisticsDB.currentGoalId = BattlePetStatisticsDB.currentGoalId or (BPS_GOALS and BPS_GOALS[1].id)
    BattlePetStatisticsDB.sessionCount  = BattlePetStatisticsDB.sessionCount or 0
    BattlePetStatisticsDB.display = BattlePetStatisticsDB.display or {}
    local d = BattlePetStatisticsDB.display
    if d.showOverall       == nil then d.showOverall       = true  end
    if d.showWinrate       == nil then d.showWinrate       = true  end
    if d.showGoal          == nil then d.showGoal          = true  end
    if d.showVs            == nil then d.showVs            = true  end
    if d.showVsStats       == nil then d.showVsStats       = true  end
    if d.showVsWinrate     == nil then d.showVsWinrate     = true  end
    if d.showPrediction    == nil then d.showPrediction    = true  end
    if d.showForfeitButton == nil then d.showForfeitButton = true  end
    if d.showNRStats       == nil then d.showNRStats       = true  end
    if d.showForfeitVsRandomWhenWinning == nil then d.showForfeitVsRandomWhenWinning = false end

    -- Automation options (stored at top-level, not inside display)
    if BattlePetStatisticsDB.autoRequeue      == nil then BattlePetStatisticsDB.autoRequeue      = false end
    if BattlePetStatisticsDB.autoRequeueDelay == nil then BattlePetStatisticsDB.autoRequeueDelay = 3     end
end

local function GetStatEntry(key)
    EnsureDB()
    if not BattlePetStatisticsDB.stats[key] then
        BattlePetStatisticsDB.stats[key] = { wins = 0, losses = 0 }
    end
    return BattlePetStatisticsDB.stats[key]
end

-- ===================== Public getters =====================
function BPS_GetCurrentOpponentDisplayName()
    return currentOpponentDisplayName
end

function BPS_GetStatsFor(key)
    if not key then return 0, 0 end
    local e = GetStatEntry(key)
    return e.wins, e.losses
end

-- Session totals (reset each login)
function BPS_GetSessionTotals()
    return sessionWins, sessionLosses
end

-- Session totals excluding Random opponents
function BPS_GetSessionNRTotals()
    return sessionNRWins, sessionNRLosses
end

-- Session wins/losses against a specific opponent key (reset each login)
function BPS_GetSessionStatsFor(key)
    if not key then return 0, 0 end
    local e = sessionStatsByOpponent[key]
    if not e then return 0, 0 end
    return e.wins, e.losses
end

function BPS_GetSessionCount()
    EnsureDB()
    return BattlePetStatisticsDB.sessionCount or 0
end

function BPS_GetAllStats()
    EnsureDB()
    return BattlePetStatisticsDB.stats
end

function BPS_ResetStats()
    EnsureDB()
    BattlePetStatisticsDB.stats = {}
    sessionWins, sessionLosses = 0, 0
    sessionNRWins, sessionNRLosses = 0, 0
    sessionStatsByOpponent = {}
end

function BPS_SetStatsFor(key, wins, losses)
    if not key then return end
    GetStatEntry(key)
    BattlePetStatisticsDB.stats[key].wins   = wins
    BattlePetStatisticsDB.stats[key].losses = losses
end

function BPS_DeleteStatsFor(key)
    if not key then return end
    EnsureDB()
    BattlePetStatisticsDB.stats[key] = nil
end

-- ===================== Win/loss recording =====================
local function IsRandomKey(key)
    if not key then return false end
    local lk = key:lower()
    return lk:find("^random%(") ~= nil or lk:find("^random ") ~= nil
end

local function RecordResult(key, won)
    if not key then return end
    local e = GetStatEntry(key)
    if won then
        e.wins = e.wins + 1
        sessionWins = sessionWins + 1
        if not IsRandomKey(key) then sessionNRWins = sessionNRWins + 1 end
    else
        e.losses = e.losses + 1
        sessionLosses = sessionLosses + 1
        if not IsRandomKey(key) then sessionNRLosses = sessionNRLosses + 1 end
    end

    local se = sessionStatsByOpponent[key]
    if not se then
        se = { wins = 0, losses = 0 }
        sessionStatsByOpponent[key] = se
    end
    if won then se.wins = se.wins + 1 else se.losses = se.losses + 1 end

    Debug(string.format("Recorded %s for %s (session %d/%d)", won and "WIN" or "LOSS", key, sessionWins, sessionLosses))
end

-- ===================== Forfeit hook =====================
if C_PetBattles and C_PetBattles.ForfeitGame then
    hooksecurefunc(C_PetBattles, "ForfeitGame", function()
        playerDidForfeit = true
        Debug("Player forfeited")
    end)
end

-- ===================== Death cache =====================
local function TeamFullyDead(teamId)
    local n = C_PetBattles.GetNumPets(teamId) or 0
    if n == 0 then return nil end
    for i = 1, n do
        if not C_PetBattles.IsPetDead(teamId, i) then return false end
    end
    return true
end

local function RefreshDeathCache()
    local ok1, ed = pcall(TeamFullyDead, Enum.BattlePetOwner.Enemy)
    if ok1 and ed ~= nil then lastKnownEnemyDead = ed end
    local ok2, ad = pcall(TeamFullyDead, Enum.BattlePetOwner.Ally)
    if ok2 and ad ~= nil then lastKnownAllyDead = ad end
end

-- ===================== PvP detection =====================
local function DetectPVP()
    local ok1, isPvp = pcall(function() return C_PetBattles.IsPVPMatch() end)
    if ok1 and isPvp then return true end

    local ok2, isWild = pcall(function() return C_PetBattles.IsWildBattle() end)
    if ok2 and isWild then return false end

    local ok3, isNPC = pcall(function() return C_PetBattles.IsPlayerNPC(Enum.BattlePetOwner.Enemy) end)
    if ok3 and not isNPC then return true end

    return false
end

-- ===================== Opponent identity =====================
local function IsEnemyPetRenamed(idx)
    local okS, speciesID = pcall(C_PetBattles.GetPetSpeciesID, Enum.BattlePetOwner.Enemy, idx)
    if not okS or not speciesID or speciesID == 0 then return false, nil end

    local okN, displayName = pcall(C_PetBattles.GetName, Enum.BattlePetOwner.Enemy, idx)
    if not okN or not displayName or displayName == "" then return false, nil end

    if C_PetJournal and C_PetJournal.GetPetInfoBySpeciesID then
        local defaultName = select(1, C_PetJournal.GetPetInfoBySpeciesID(speciesID))
        if defaultName and defaultName ~= displayName then
            return true, displayName
        end
    end
    return false, displayName
end

local function ResolveOpponent()
    local L = BPS_GetLocale()
    local renamed1, name1 = IsEnemyPetRenamed(1)
    if renamed1 and name1 then return name1, name1 end

    local renamed2 = select(1, IsEnemyPetRenamed(2))
    local renamed3 = select(1, IsEnemyPetRenamed(3))

    if not renamed1 and not renamed2 and not renamed3 then
        local abbrevs = {}
        for i = 1, 3 do
            local ok, n = pcall(C_PetBattles.GetName, Enum.BattlePetOwner.Enemy, i)
            if ok and n and n ~= "" then
                table.insert(abbrevs, n:sub(1, 3):upper())
            end
        end
        local label = string.format("%s (%s)", L.VS_RANDOM, table.concat(abbrevs, "/"))
        return label, label
    end

    local ok, n = pcall(C_PetBattles.GetName, Enum.BattlePetOwner.Enemy, 1)
    if ok and n then return n, n end
    return nil, nil
end

local function TryGrabOpponent()
    if not battleIsPVP then
        if DetectPVP() then battleIsPVP = true else return end
    end
    if currentOpponentKey then return end
    local key, display = ResolveOpponent()
    if key then
        currentOpponentKey = key
        currentOpponentDisplayName = display
        Debug("Opponent: " .. key)
        if BPS_ShowBattleFrame then BPS_ShowBattleFrame() end
    end
end

-- ===================== Win detection via achievements =====================
-- Primary method: compare achievement criteria count before and after battle.
-- Fallback: death state / forfeit (for battles where we're post-max-achievement).

local function ResolveWinLoss()
    -- 1) Try achievement snapshot comparison (most reliable)
    local achievementResult = BPS_DetectWinFromAchievements()
    if achievementResult ~= nil then
        Debug("Win detection via achievement snapshot: " .. tostring(achievementResult))
        return achievementResult
    end

    Debug("Achievement snapshot unchanged, falling back to death/forfeit detection")

    -- 2) Fallback: death state
    local ed = lastKnownEnemyDead
    local ad = lastKnownAllyDead

    if ed and not ad then return true  end
    if ad and not ed then return false end
    if ed and ad       then return nil  end  -- double KO

    -- 3) Forfeit fallback (nobody died)
    Debug("Forfeit fallback: playerForfeit=" .. tostring(playerDidForfeit))
    return not playerDidForfeit
end

-- Actually performs the win/loss resolution + recording for a battle. Called
-- either after the normal 1.5s server-sync delay, or immediately if the
-- player requeues before that delay has elapsed (see BPS_ForceResolvePendingBattle).
local function ResolveAndApplyBattle(opponentKeyAtResolution)
    if battleResolved then return end  -- guard against double-fire
    battleResolved = true
    pendingResolveOpponentKey = nil

    local won = ResolveWinLoss()
    if won ~= nil then
        RecordResult(opponentKeyAtResolution, won)
        local completedGoal = BPS_AutoAdvanceGoal()
        if completedGoal and BPS_OnGoalCompleted then
            BPS_OnGoalCompleted(completedGoal)
        end
    end

    -- Note: BPS_RenameOwnPetsForTradeWin is called after PET_BATTLE_CLOSE
    -- (with a 1s delay) so the Pet Journal is guaranteed to be available.

    -- "after_first_battle" mode: this fires once the very first
    -- PvP battle of the login session has just resolved.
    if pvpBattlesStartedThisLogin == 1 and BPS_MaybeShowAliasAfterBattle then
        BPS_MaybeShowAliasAfterBattle()
    end

    if BPS_UpdateBattleFrame  then BPS_UpdateBattleFrame()  end
    if BPS_ShowRequeueButton  then BPS_ShowRequeueButton()   end
end

-- If a battle just ended and is still waiting on its 1.5s resolution delay
-- (server-sync window for achievement criteria), force it to resolve right
-- now instead. Called right before requeuing/re-entering a battle, so two
-- players who requeue at different speeds don't end up with one side having
-- recorded the previous result and the other not yet — both addons resolve
-- the moment either player tries to queue again, instead of only after a
-- fixed delay that a fast click can outrun.
function BPS_ForceResolvePendingBattle()
    if pendingResolveOpponentKey and not battleResolved then
        ResolveAndApplyBattle(pendingResolveOpponentKey)
    end
end

-- ===================== Event handling =====================
frame:RegisterEvent("ADDON_LOADED")
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("PLAYER_LOGOUT")
frame:RegisterEvent("PET_BATTLE_OPENING_START")
frame:RegisterEvent("PET_BATTLE_OPENING_DONE")
frame:RegisterEvent("PET_BATTLE_HEALTH_CHANGED")
frame:RegisterEvent("PET_BATTLE_PET_ROUND_RESULTS")
frame:RegisterEvent("PET_BATTLE_OVER")
frame:RegisterEvent("PET_BATTLE_CLOSE")

frame:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" then
        if (...) == "BattlePetStatistics" then
            EnsureDB()
            print("|cff33ff99Battle Pet Statistics|r loaded. /bps to open options.")
        end

    elseif event == "PLAYER_ENTERING_WORLD" then
        EnsureDB()
        -- Safeguard: if last session ended abnormally (crash, force-quit,
        -- disconnect) and the pets were never restored on logout as
        -- requested, catch up now before anything else. Small delay so the
        -- Pet Journal data is actually available this early in login.
        if BPS_CatchUpPendingRestore then
            C_Timer.After(2, BPS_CatchUpPendingRestore)
        end
        if BPS_MaybeShowSetupPrompt then
            C_Timer.After(3, BPS_MaybeShowSetupPrompt)
        end

    elseif event == "PLAYER_LOGOUT" then
        -- Session totals are in-memory only; they reset automatically on next login.
        -- Nothing to do here — we explicitly do NOT save them to DB.
        Debug("Logout: session was " .. sessionWins .. "W / " .. sessionLosses .. "L")
        if BattlePetStatisticsDB and BattlePetStatisticsDB.restoreNamesOnLogout and BPS_RestoreOwnPetNames then
            BPS_RestoreOwnPetNames()
        end

    elseif event == "PET_BATTLE_OPENING_START" then
        -- Universal safeguard: a new battle is starting (no matter how it was
        -- triggered — addon button, keybind, or Blizzard's native "Find
        -- Battle" button). If the previous battle's resolution was still
        -- waiting on its 1.5s server-sync delay, force it to resolve right
        -- now, before resetting state for this new battle. This is what
        -- prevents two players who requeue at different speeds from ending
        -- up with one side having recorded the previous result and the
        -- other not yet.
        if BPS_ForceResolvePendingBattle then BPS_ForceResolvePendingBattle() end

        currentOpponentKey = nil
        currentOpponentDisplayName = nil
        battleIsPVP = false
        battleResolved = false
        playerDidForfeit = false
        lastKnownEnemyDead = false
        lastKnownAllyDead = false

        -- Snapshot achievements BEFORE the battle counts anything
        BPS_SnapshotAchievements()
        Debug("Achievement snapshot taken")

        if DetectPVP() then
            battleIsPVP = true
            TryGrabOpponent()

            -- A "session" only counts once the player has started at least 2
            -- PvP pet battles during this login.
            pvpBattlesStartedThisLogin = pvpBattlesStartedThisLogin + 1
            if pvpBattlesStartedThisLogin >= 2 and not sessionCounted then
                sessionCounted = true
                EnsureDB()
                BattlePetStatisticsDB.sessionCount = (BattlePetStatisticsDB.sessionCount or 0) + 1
            end
        end

    elseif event == "PET_BATTLE_OPENING_DONE" then
        TryGrabOpponent()
        RefreshDeathCache()

    elseif event == "PET_BATTLE_HEALTH_CHANGED" then
        if battleIsPVP then RefreshDeathCache() end

    elseif event == "PET_BATTLE_PET_ROUND_RESULTS" then
        if battleIsPVP then RefreshDeathCache() end

    elseif event == "PET_BATTLE_OVER" then
        if battleIsPVP and not currentOpponentKey then
            TryGrabOpponent()
        end
        RefreshDeathCache()

        if battleIsPVP and not battleResolved and currentOpponentKey then
            local opponentKeyAtResolution = currentOpponentKey
            pendingResolveOpponentKey = opponentKeyAtResolution
            C_Timer.After(1.5, function()
                ResolveAndApplyBattle(opponentKeyAtResolution)
            end)
        end

    elseif event == "PET_BATTLE_CLOSE" then
        local wasPVP    = battleIsPVP
        local doRequeue = wasPVP and BattlePetStatisticsDB.autoRequeue

        battleIsPVP = false
        currentOpponentKey = nil
        currentOpponentDisplayName = nil
        if BPS_HideBattleFrame then BPS_HideBattleFrame() end

        if doRequeue then
            -- Use the player's configured delay, but never less than 2s so
            -- the 1.5s resolution timer always has time to finish first.
            local delay = math.max(2.0, math.min(120,
                tonumber(BattlePetStatisticsDB.autoRequeueDelay) or 3))
            C_Timer.After(delay, function()
                EnsureDB()
                if not BattlePetStatisticsDB.autoRequeue then return end
                if BPS_ForceResolvePendingBattle then BPS_ForceResolvePendingBattle() end
                C_Timer.After(0.1, function()
                    if BattlePetStatisticsDB.autoRequeue then
                        pcall(C_PetBattles.StartPVPMatchmaking)
                        Debug("Auto-requeue fired after " .. delay .. "s")
                    end
                end)
            end)
        end

        -- TradeWin rename: the Pet Journal is not immediately available after
        -- PET_BATTLE_CLOSE (GetPetLoadOutInfo can fail while the battle scene
        -- is still unloading). Delay slightly so the journal data is ready.
        C_Timer.After(1.0, function()
            if BPS_RenameOwnPetsForTradeWin then BPS_RenameOwnPetsForTradeWin() end
        end)
    end
end)

-- ===================== Keybinding handlers =====================
function BPS_Binding_Forfeit()
    if C_PetBattles.IsInBattle and C_PetBattles.IsInBattle() then
        C_PetBattles.ForfeitGame()
    end
end
function BPS_Binding_Requeue()
    if BPS_ForceResolvePendingBattle then BPS_ForceResolvePendingBattle() end
    pcall(C_PetBattles.StartPVPMatchmaking)
end
function BPS_Binding_EnterBattle()
    if BPS_ForceResolvePendingBattle then BPS_ForceResolvePendingBattle() end
    pcall(C_PetBattles.AcceptQueuedPVPMatch)
end

-- ===================== Slash commands =====================
SLASH_BPS1 = "/bps"
SLASH_BPS2 = "/petstats"
SlashCmdList["BPS"] = function(msg)
    msg = msg and msg:lower():trim() or ""
    if msg == "debug" then
        debugMode = not debugMode
        print("|cff33ff99BPS|r debug: " .. (debugMode and "ON" or "OFF"))
    elseif msg == "setup" then
        if BPS_ForceShowAliasPopup then BPS_ForceShowAliasPopup() end
    elseif msg == "test" then
        if BPS_ShowBattleFrame then BPS_ShowBattleFrame() end
    elseif msg == "achcheck" then
        print("|cff33ff99BPS|r: Achievement diagnostic --")
        for _, goal in ipairs(BPS_GOALS) do
            local id, name, _, completed, _, _, _, _, _, _, _, wasEarnedByMe, earnedBy =
                GetAchievementInfo(goal.achID)
            local _, _, critCompleted, quantity, reqQuantity =
                GetAchievementCriteriaInfo(goal.achID, 1)
            print(string.format(
                "  [%d] %s | completed=%s wasEarnedByMe=%s earnedBy=%s | crit: completed=%s quantity=%s/%s",
                goal.achID, tostring(name), tostring(completed), tostring(wasEarnedByMe),
                tostring(earnedBy), tostring(critCompleted), tostring(quantity), tostring(reqQuantity)
            ))
        end
    else
        if BPS_ToggleOptions then BPS_ToggleOptions() end
    end
end
