local addonName, TOAST = ...

TOAST.commands = {}

-- Single source of truth for the addon version, read from the .toc so it
-- never drifts out of sync with what's actually shipped.
local getAddOnMetadata = C_AddOns and C_AddOns.GetAddOnMetadata or GetAddOnMetadata
TOAST.VERSION = (getAddOnMetadata and getAddOnMetadata(addonName, "Version")) or "?"

-- Lua's string.lower is byte-wise and locale-dependent, so it does not just
-- fail to fold non-ASCII text -- it can corrupt it. Folding "ВечнаяПесня"
-- with string.lower mangles the UTF-8 lead bytes (0xD0 -> 0xF0), producing a
-- string that is neither lowercase nor valid UTF-8.
--
-- That matters because identity keys are compared across two sources that
-- differ in case: the bot exports a lowercase realm_slug ("вечная-песня")
-- while the game reports a display realm ("Вечная Песня"). Folded with
-- string.lower those never match, so a Cyrillic-realm character silently fails
-- to resolve to its crew member.
--
-- This folds ASCII, Latin-1 Supplement (À-Þ) and Cyrillic (А-Я, Ё), which
-- covers every EU realm and character name. Bytes outside those ranges are
-- copied through untouched, so nothing can be corrupted.
local function LowerByte(b)
    if b >= 0x41 and b <= 0x5A then return b + 0x20 end
    return b
end

function TOAST.Utf8Lower(text)
    if type(text) ~= "string" then return text end
    local out, i, n = {}, 1, #text
    while i <= n do
        local b1 = text:byte(i)
        local b2 = text:byte(i + 1)
        if b1 < 0x80 then
            out[#out + 1] = string.char(LowerByte(b1))
            i = i + 1
        elseif b1 == 0xC3 and b2 and b2 >= 0x80 and b2 <= 0x9E and b2 ~= 0x97 then
            -- À-Þ -> à-þ (0x97 is the multiplication sign, not a letter)
            out[#out + 1] = string.char(0xC3, b2 + 0x20)
            i = i + 2
        elseif b1 == 0xD0 and b2 and b2 >= 0x90 and b2 <= 0x9F then
            out[#out + 1] = string.char(0xD0, b2 + 0x20)   -- А-П -> а-п
            i = i + 2
        elseif b1 == 0xD0 and b2 and b2 >= 0xA0 and b2 <= 0xAF then
            out[#out + 1] = string.char(0xD1, b2 - 0x20)   -- Р-Я -> р-я
            i = i + 2
        elseif b1 == 0xD0 and b2 == 0x81 then
            out[#out + 1] = string.char(0xD1, 0x91)        -- Ё -> ё
            i = i + 2
        else
            out[#out + 1] = string.char(b1)
            i = i + 1
        end
    end
    return table.concat(out)
end

-- Cross-realm targets ("Name-Realm") need Blizzard's *normalized realm name*,
-- which removes only spaces, hyphens and periods. Apostrophes, parentheses and
-- accents are preserved:
--
--     "Defias Brotherhood" -> "DefiasBrotherhood"
--     "Twilight's Hammer"  -> "Twilight'sHammer"   (apostrophe KEPT)
--     "Azjol-Nerub"        -> "AzjolNerub"
--     "Aggra (Português)"  -> "Aggra(Português)"   (parentheses KEPT)
--
-- https://warcraft.wiki.gg/wiki/API_GetNormalizedRealmName
--
-- This used to strip apostrophes too, which silently broke every whisper and
-- invite aimed at a realm like Twilight's Hammer.
--
-- The bot now exports the exact value as `realm_wow`, so the strip rule below
-- is only a fallback for data written before that existed.
-- Explicit ASCII whitespace, never %s: Lua's %s is locale-dependent and
-- matches byte 0xA0, which is the second byte of Cyrillic "Р" (U+0420), so %s
-- would rip a byte out of the middle of a UTF-8 realm name.
local ASCII_SPACE = " \t\r\n\f\v"

local function LegacyNormalizeRealm(realm)
    return (realm:gsub("[" .. ASCII_SPACE .. "%-%.]", ""))
end

--- Build an in-game "Name-Realm" target.
--
-- realmWow      the exported `realm_wow` (authoritative when present)
-- realmResolved the exported `realm_resolved` flag:
--                 false -> the bot could not resolve this realm at all
--                 nil   -> the field is absent, i.e. a pre-upgrade export
--
-- Fails closed: an explicitly unresolved record returns nil BEFORE any other
-- field is considered, even if a stale realm_wow or realm is also present.
-- Aiming an invite or a whisper at a guessed realm reaches a different player,
-- so callers must treat nil as "disable this action", never as "use the name".
function TOAST.FormatCharacterTarget(name, realm, realmWow, realmResolved)
    if type(name) ~= "string" or name == "" then return nil end

    -- Fail closed first. Order matters: this must precede realmWow.
    if realmResolved == false then return nil end

    if type(realmWow) == "string" and realmWow ~= "" then
        return name .. "-" .. realmWow
    end

    -- realmResolved == nil means pre-upgrade data, so the legacy rule applies.
    -- If it was true we should have had realmWow; treat a missing one as
    -- unusable rather than guessing.
    if realmResolved == true then return nil end

    if type(realm) ~= "string" or realm == "" then return name end
    local normalized = LegacyNormalizeRealm(realm)
    if normalized == "" then return name end
    return name .. "-" .. normalized
end

--- Convenience wrapper for an exported member/leaderboard row.
function TOAST.FormatMemberTarget(entry)
    if type(entry) ~= "table" then return nil end
    return TOAST.FormatCharacterTarget(
        entry.name, entry.realm, entry.realm_wow, entry.realm_resolved
    )
end

--- Canonical realm identity for an exported row.
--
-- Deliberately separate from FormatCharacterTarget: `realm_slug` and
-- `realm_wow` are different values with different rules, and the whisper/invite
-- formatter must never double as the identity formatter. Fails closed on an
-- explicitly unresolved record so it cannot be merged with a real realm.
function TOAST.RealmIdentity(entry)
    if type(entry) ~= "table" then return nil end
    if entry.realm_resolved == false then return nil end

    if type(entry.realm_slug) == "string" and entry.realm_slug ~= "" then
        return entry.realm_slug
    end
    if entry.realm_resolved == true then return nil end

    -- Pre-upgrade export: fold the display realm the old way. Uses the
    -- UTF-8-aware fold, because this produces an identity key and a Cyrillic
    -- realm folded with string.lower would neither match nor stay valid UTF-8.
    if type(entry.realm) ~= "string" or entry.realm == "" then return nil end
    local folded = TOAST.Utf8Lower(entry.realm:gsub("[" .. ASCII_SPACE .. "%-'%.]", ""))
    if folded == "" then return nil end
    return folded
end

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_LOGIN")
f:SetScript("OnEvent", function()
    print("|cff33ff99TOAST|r v" .. TOAST.VERSION .. " geladen.")
end)

-- Persists top-level window positions (leaderboard, dashboard, ...) in
-- TOAST_DB so dragging a window sticks across /reload instead of resetting
-- to CENTER every time. Called by each window's header OnDragStop.
function TOAST.SaveWindowPosition(frame, key)
    local point, _, relativePoint, x, y = frame:GetPoint()
    TOAST_DB.windowPos = TOAST_DB.windowPos or {}
    TOAST_DB.windowPos[key] = { point = point, relativePoint = relativePoint, x = x, y = y }
end

function TOAST.RestoreWindowPosition(frame, key)
    local pos = TOAST_DB.windowPos and TOAST_DB.windowPos[key]
    if pos then
        frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.x, pos.y)
    else
        frame:SetPoint("CENTER")
    end
end

--- Resolve a challenge-mode map ID for compact key presentation.
-- Generated data is authoritative. Unknown IDs fall back to Blizzard's full
-- name only outside combat; C_ChallengeMode values may be secret during
-- messaging lockdown, so the API is never touched in combat.
function TOAST.GetDungeonShort(mapID)
    mapID = tonumber(mapID)
    if not mapID or mapID <= 0 then return "" end

    local dungeon = type(TOAST.dungeons) == "table" and TOAST.dungeons[mapID] or nil
    if type(dungeon) == "table"
       and type(dungeon.dungeon_short) == "string"
       and dungeon.dungeon_short ~= "" then
        return dungeon.dungeon_short
    end

    if InCombatLockdown and InCombatLockdown() then return "" end
    if type(C_ChallengeMode) ~= "table"
       or type(C_ChallengeMode.GetMapUIInfo) ~= "function" then
        return ""
    end

    local ok, value = pcall(C_ChallengeMode.GetMapUIInfo, mapID)
    if not ok then return "" end
    if issecretvalue and issecretvalue(value) then return "" end
    return type(value) == "string" and value or ""
end

function TOAST.CanOpenMajorWindow()
    if TOAST.IsUpdateBlocked and TOAST.IsUpdateBlocked() then
        if TOAST.ShowUpdateRequired then TOAST.ShowUpdateRequired() end
        return false
    end
    if InCombatLockdown and InCombatLockdown() then
        print("|cff33ff99TOAST|r: Waters are rough... opening after combat.")
        return false
    end
    return true
end

TOAST.commands[""] = function()
    if TOAST.CheckReleaseStatus and not TOAST.CheckReleaseStatus() then return end
    if TOAST.CheckOnboardingProgress then
        TOAST.CheckOnboardingProgress("dashboard")
        return
    end

    if not TOAST.CanOpenMajorWindow() then return end
    if TOAST.IsInCommunity then
        TOAST.inCommunity = TOAST.IsInCommunity()
    end

    local dismissed = TOAST_DB.dismissedWelcome or {}
    local charName = UnitName("player")

    if not TOAST.inCommunity then
        -- Harde lock: geen community = geen TOAST. Bewust geen dismissed-check
        -- en geen fallback naar het dashboard, anders is de gate te omzeilen.
        if TOAST.ShowCommunityGate then
            TOAST.ShowCommunityGate()
        end
        return
    end

    -- Wel in community
    if TOAST.me then
        -- In roster, herkend → dashboard
        TOAST.commands["dashboard"]()
        return
    end

    if TOAST_DB.me then
        -- Niet in roster maar discord bekend (alt) → dashboard
        TOAST.commands["dashboard"]()
        return
    end

    -- In community maar discord onbekend → discord popup
    if dismissed[charName] ~= true then
        if TOAST.ShowWelcome then
            TOAST.ShowWelcome("discord")
        end
    else
        TOAST.commands["dashboard"]()
    end
end

SLASH_TOAST1 = "/toast"
SlashCmdList["TOAST"] = function(input)
    local command = strtrim(input or ""):lower()
    -- Dev remains reachable during an update block so its release previews
    -- can test that exact state. It is intentionally omitted from public help.
    local releaseCommand = command == "dev" or command == "changelog"
    if not releaseCommand and TOAST.IsUpdateBlocked and TOAST.IsUpdateBlocked() then
        if TOAST.ShowUpdateRequired then TOAST.ShowUpdateRequired() end
        return
    end
    if command == "" then
        TOAST.commands[""]()
    elseif TOAST.commands[command] then
        TOAST.commands[command]()
    else
        print("|cff33ff99TOAST|r: onbekend commando. Beschikbaar: /toast lb, /toast trends, /toast changelog")
    end
end
