-- Core.lua - namespace, constants, saved vars, shared helpers.
local ADDON, ns = ...

ns.ADDON = ADDON
ns.VERSION = "0.9.5"

-- Sub-tables populated by the other files.
ns.Cards  = ns.Cards  or {}
ns.Comm   = ns.Comm   or {}
ns.Engine = ns.Engine or {}
ns.UI     = ns.UI     or {}

-- ─── Tunables ──────────────────────────────────────────────────────────────
ns.MAX_SEATS      = 5
ns.NUM_DECKS      = 6        -- decks in the shoe
ns.RESHUFFLE_AT   = 0.25     -- reshuffle when this fraction of the shoe remains
ns.DEALER_HITS_SOFT_17 = false
ns.BLACKJACK_PAYOUT = 1.5    -- 3:2
ns.STARTING_BANKROLL = 1000  -- local flavour bankroll for solo / display
ns.MIN_BET = 1

ns.PHASE = {
    IDLE        = "idle",
    BETTING     = "betting",
    DEALING     = "dealing",   -- cards being handed out one at a time
    INSURANCE   = "insurance", -- dealer shows an Ace: insurance window open
    PLAYER_TURN = "player",
    DEALER_TURN = "dealer",
    PAYOUT      = "payout",
}

ns.DEAL_INTERVAL = 0.45        -- seconds between each dealt card
ns.SEED_ROLL_MAX = 1000000     -- /roll range whose result seeds the shared shoe

-- ─── Card model ────────────────────────────────────────────────────────────
ns.RANKS = { "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K" }
ns.SUITS = { "S", "H", "D", "C" }

-- Base value of a rank. Ace handled specially (1/11) by HandValue.
local RANK_VALUE = {
    A = 11, ["2"] = 2, ["3"] = 3, ["4"] = 4, ["5"] = 5, ["6"] = 6,
    ["7"] = 7, ["8"] = 8, ["9"] = 9, ["10"] = 10, J = 10, Q = 10, K = 10,
}

-- A card is a 2-3 char code: rank ("A".."10","J","Q","K") + suit letter.
function ns.CardRank(code)
    return code:sub(1, #code - 1)
end

function ns.CardSuit(code)
    return code:sub(#code)
end

local RANK_NAME = {
    A = "Ace", J = "Jack", Q = "Queen", K = "King",
    ["2"] = "2", ["3"] = "3", ["4"] = "4", ["5"] = "5", ["6"] = "6",
    ["7"] = "7", ["8"] = "8", ["9"] = "9", ["10"] = "10",
}
local SUIT_NAME = { S = "Spades", H = "Hearts", D = "Diamonds", C = "Clubs" }

-- Full readable card name, e.g. "King of Hearts", for chat announcements.
function ns.CardName(code)
    local r = RANK_NAME[ns.CardRank(code)] or "?"
    local s = SUIT_NAME[ns.CardSuit(code)] or "?"
    return r .. " of " .. s
end

-- Returns total, isSoft (an ace still counted as 11), isBlackjack.
function ns.HandValue(cards)
    local total, aces = 0, 0
    for _, code in ipairs(cards) do
        local r = ns.CardRank(code)
        total = total + RANK_VALUE[r]
        if r == "A" then aces = aces + 1 end
    end
    local soft = aces > 0
    while total > 21 and aces > 0 do
        total = total - 10   -- demote an ace from 11 to 1
        aces = aces - 1
        soft = aces > 0
    end
    local blackjack = (#cards == 2 and total == 21)
    return total, soft, blackjack
end

-- Human-readable total string, e.g. "Soft 17" or "20".
function ns.HandValueText(cards)
    if #cards == 0 then return "" end
    local total, soft, bj = ns.HandValue(cards)
    if bj then return "Blackjack" end
    if total > 21 then return total .. " (Bust)" end
    if soft then return "Soft " .. total end
    return tostring(total)
end

-- ─── Side bets (Perfect Pairs, 21+3) ──────────────────────────────────────
-- Payout multipliers expressed as X:1 (profit; the stake is returned on top).
ns.SIDE_PAYOUTS = {
    pp  = { perfect = 25, colored = 12, mixed = 6 },              -- Perfect Pairs
    ttt = { suited_trips = 100, straight_flush = 40,
            trips = 30, straight = 10, flush = 5 },               -- 21+3
}

ns.SIDE_LABEL = {
    perfect = "Perfect Pair", colored = "Coloured Pair", mixed = "Mixed Pair",
    suited_trips = "Suited Trips", straight_flush = "Straight Flush",
    trips = "Three of a Kind", straight = "Straight", flush = "Flush",
}

local SUIT_COLOR = { S = "b", C = "b", H = "r", D = "r" }

-- Perfect Pairs: the player's first two cards. Returns (category, multiplier) or
-- nil when the two cards are not a pair.
function ns.EvalPerfectPairs(c1, c2)
    if not (c1 and c2) then return nil end
    if ns.CardRank(c1) ~= ns.CardRank(c2) then return nil end
    local s1, s2 = ns.CardSuit(c1), ns.CardSuit(c2)
    local p = ns.SIDE_PAYOUTS.pp
    if s1 == s2 then return "perfect", p.perfect end             -- same rank AND suit
    if SUIT_COLOR[s1] == SUIT_COLOR[s2] then return "colored", p.colored end
    return "mixed", p.mixed
end

local RIDX = { A = 1, ["2"] = 2, ["3"] = 3, ["4"] = 4, ["5"] = 5, ["6"] = 6,
    ["7"] = 7, ["8"] = 8, ["9"] = 9, ["10"] = 10, J = 11, Q = 12, K = 13 }

-- Three ranks form a run? Tries both ace-low (A=1) and ace-high (A=14) so that
-- A-2-3 and Q-K-A both count.
local function isStraight3(r1, r2, r3)
    local function consec(v)
        table.sort(v)
        return v[2] == v[1] + 1 and v[3] == v[2] + 1
    end
    local a, b, c = RIDX[r1], RIDX[r2], RIDX[r3]
    if not (a and b and c) then return false end
    if consec({ a, b, c }) then return true end
    local function hi(x) return x == 1 and 14 or x end
    return consec({ hi(a), hi(b), hi(c) })
end

-- 21+3: the player's two cards plus the dealer up card make a 3-card poker hand.
-- Returns (category, multiplier) or nil for no win.
function ns.Eval21p3(c1, c2, dUp)
    if not (c1 and c2 and dUp) then return nil end
    local r1, r2, r3 = ns.CardRank(c1), ns.CardRank(c2), ns.CardRank(dUp)
    local s1, s2, s3 = ns.CardSuit(c1), ns.CardSuit(c2), ns.CardSuit(dUp)
    local flush = (s1 == s2 and s2 == s3)
    local trips = (r1 == r2 and r2 == r3)
    local straight = isStraight3(r1, r2, r3)
    local p = ns.SIDE_PAYOUTS.ttt
    if trips and flush then return "suited_trips", p.suited_trips end
    if straight and flush then return "straight_flush", p.straight_flush end
    if trips then return "trips", p.trips end
    if straight then return "straight", p.straight end
    if flush then return "flush", p.flush end
    return nil
end

-- ─── Shoe (multi-deck, shuffled) ──────────────────────────────────────────
local function FisherYates(t)
    for i = #t, 2, -1 do
        local j = math.random(i)
        t[i], t[j] = t[j], t[i]
    end
end

-- Canonical (unshuffled) multi-deck card list. Built in a FIXED order from the
-- same constants on every client, so two clients with the same seed shuffle an
-- identical starting deck.
local function CanonicalDeck(numDecks)
    local shoe = {}
    for _ = 1, (numDecks or ns.NUM_DECKS) do
        for _, s in ipairs(ns.SUITS) do
            for _, r in ipairs(ns.RANKS) do
                shoe[#shoe + 1] = r .. s
            end
        end
    end
    return shoe
end

function ns.NewShoe(numDecks)
    local shoe = CanonicalDeck(numDecks)
    FisherYates(shoe)
    return shoe
end

-- ─── Deterministic shared RNG (so every client builds the SAME shoe) ───────
-- Park–Miller "minimal standard" LCG via Schrage's method: pure integer math
-- that never exceeds 2^31, so it yields a BYTE-IDENTICAL sequence on every
-- client/platform. We deliberately do NOT use math.random for the shared shoe
-- (its sequence isn't guaranteed to match across machines). The generator is
-- seeded by a single /roll the whole group witnesses, so all clients agree.
local PM_M = 2147483647   -- 2^31 - 1 (modulus, prime)

function ns.MakeRNG(seed)
    local s = math.floor(tonumber(seed) or 1) % PM_M
    if s <= 0 then s = s + (PM_M - 1) end       -- keep state in [1, M-1]
    -- Returns a uniform-ish integer in [1, n] (or the raw state if n is nil).
    return function(n)
        local a, q, r = 16807, 127773, 2836     -- a*q ≈ 2^31, never overflows a double
        local hi = math.floor(s / q)
        local lo = s % q
        s = a * lo - r * hi
        if s <= 0 then s = s + PM_M end
        if not n or n < 1 then return s end
        return 1 + (s % n)
    end
end

-- Build a canonical multi-deck shoe and shuffle it DETERMINISTICALLY from a
-- shared seed (Fisher–Yates driven by MakeRNG). Same seed -> identical shoe on
-- every client, so card identities never have to cross the network.
function ns.NewShoeSeeded(seed, numDecks)
    local shoe = CanonicalDeck(numDecks)
    local rng = ns.MakeRNG(seed)
    for i = #shoe, 2, -1 do
        local j = rng(i)                         -- j in [1, i]
        shoe[i], shoe[j] = shoe[j], shoe[i]
    end
    return shoe
end

-- The fixed European deal order for a set of committed seat indices: one card to
-- each seat, then the dealer, then a second card to each seat. Host and client
-- call this SAME helper so a client can replay the deal off the shared shoe and
-- land on byte-identical hands (drives the client-side deal animation).
function ns.DealSteps(seats)
    local steps = {}
    for pass = 1, 2 do
        for _, i in ipairs(seats) do steps[#steps + 1] = { kind = "seat", seat = i } end
        if pass == 1 then steps[#steps + 1] = { kind = "dealer" } end
    end
    return steps
end

-- ─── Saved variables ───────────────────────────────────────────────────────
function ns.InitDB()
    BlackjackTableDB = BlackjackTableDB or {}
    local db = BlackjackTableDB
    if db.bankroll == nil then db.bankroll = ns.STARTING_BANKROLL end
    db.framePos = db.framePos or nil
    db.stats = db.stats or { wagered = 0, won = 0, lost = 0, hands = 0 }
    if db.sideBets == nil then db.sideBets = false end   -- dealer's last side-bet choice
    ns.db = db
    return db
end

-- ─── Misc helpers ──────────────────────────────────────────────────────────
function ns.Print(msg)
    DEFAULT_CHAT_FRAME:AddMessage("|cff33c0e5Blackjack|r: " .. tostring(msg))
end

function ns.Me()
    local name = UnitName("player")
    return name
end

-- Short class-coloured-ish name for display.
function ns.ShortName(full)
    if not full then return "?" end
    return (full:gsub("%-.*$", "")) -- strip realm
end

-- ─── Locale-aware /roll (RandomRoll) result parser ─────────────────────────
-- Read RandomRoll results from CHAT_MSG_SYSTEM on ANY client language. We build
-- the match pattern from Blizzard's own localized RANDOM_ROLL_RESULT string
-- (e.g. frFR "%s obtient %d (%d-%d)."), so a hardcoded English "rolls" can't
-- make card-seed rolls silently fail on a French/German/etc. client.
local rollPattern
local function buildRollPattern()
    local fmt = RANDOM_ROLL_RESULT or "%s rolls %d (%d-%d)"
    fmt = fmt:gsub("%%(%d+)%$([sd])", "%%%2")               -- "%1$s" -> "%s"
    fmt = fmt:gsub("%%s", "\1"):gsub("%%d", "\2")           -- protect specifiers
    fmt = fmt:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")  -- escape literal magic
    fmt = fmt:gsub("\1", "(%%S+)")                          -- sender capture
    fmt = fmt:gsub("\2", "(%%d+)")                          -- capture every number
    return fmt
end

-- Returns sender (bare name), value, low, high — or nil if not a roll result.
function ns.ParseRoll(message)
    if not message then return nil end
    rollPattern = rollPattern or buildRollPattern()
    local sender, value, low, high = message:match(rollPattern)
    if sender and value then
        return ns.ShortName(sender), tonumber(value), tonumber(low), tonumber(high)
    end
    return nil
end

-- Best available group chat channel for casino announcements, or nil if solo.
function ns.AnnounceChannel()
    if IsInRaid() then return "RAID"
    elseif IsInGroup() then return "PARTY" end
    return nil
end

-- Announce a casino event: always logged locally (addon log + chat frame),
-- and sent to the group chat so non-addon players at the table can see it too.
function ns.Announce(msg)
    ns.Print(msg)
    if ns.Engine and ns.Engine.state and ns.Engine.Log then
        ns.Engine.Log(">> " .. msg)
        if ns.UI and ns.UI.Render then ns.UI.Render(ns.Engine.state) end
    end
    local ch = ns.AnnounceChannel()
    if ch and SendChatMessage then
        SendChatMessage(msg, ch)
    end
end
