--------------------------------------------------------------------------------
-- S-Frames: Offline DB for Bag Module (Bags/Offline.lua)
-- Tracks inventory across characters and realms
-- NOTE: This file defines SFrames.Bags first - must be loaded before other Bag files
--------------------------------------------------------------------------------

SFrames.Bags = SFrames.Bags or {}
SFrames.Bags.Offline = {}

local offlineFrame = CreateFrame("Frame")
offlineFrame:RegisterEvent("PLAYER_LOGIN")
offlineFrame:RegisterEvent("BAG_UPDATE")
offlineFrame:RegisterEvent("BANKFRAME_OPENED")
offlineFrame:RegisterEvent("BANKFRAME_CLOSED")
offlineFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED")
offlineFrame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED")

local realmName = ""
local playerName = ""
local isBankOpen = false

local function QueueBankRefreshScans()
    local elapsed = 0
    local index = 1
    local points = { 0.08, 0.25, 0.55, 0.95 }
    local t = CreateFrame("Frame")
    t:SetScript("OnUpdate", function()
        elapsed = elapsed + (arg1 or 0)
        if index <= table.getn(points) and elapsed >= points[index] then
            if isBankOpen then
                SFrames.Bags.Offline:ScanBags()
            end
            index = index + 1
        end
        if index > table.getn(points) then
            this:SetScript("OnUpdate", nil)
        end
    end)
end

local function InitDB()
    if not SFramesGlobalDB then SFramesGlobalDB = {} end
    if not SFramesGlobalDB[realmName] then SFramesGlobalDB[realmName] = {} end
    if not SFramesGlobalDB[realmName][playerName] then
        SFramesGlobalDB[realmName][playerName] = {
            bags = {},
            bank = {},
            bankSlots = 0,
            bankBags = {},
            equippedBags = {},
            money = 0
        }
    end
end

local function IsBankBagInvSlot(slotID)
    return type(slotID) == "number" and slotID > 23
end

local function IsBagItemLink(link)
    if type(link) ~= "string" or link == "" then
        return false
    end
    local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link)
    return equipLoc == "INVTYPE_BAG"
end

local function GetBagLinkState(link)
    if type(link) ~= "string" or link == "" then
        return "invalid"
    end
    local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link)
    if equipLoc == "INVTYPE_BAG" then
        return "bag"
    end
    if equipLoc == nil or equipLoc == "" then
        return "unknown"
    end
    return "invalid"
end

local function SafeGetInventorySlotByName(slotName)
    if not GetInventorySlotInfo then return nil end
    local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end)
    if ok and IsBankBagInvSlot(slotID) then
        return slotID
    end
    return nil
end

local function ResolvePlayerBagInvSlot(index)
    if type(index) ~= "number" or index < 1 or index > 4 then
        return nil
    end

    local liveBtn = _G["CharacterBag" .. (index - 1) .. "Slot"]
    if liveBtn and liveBtn.GetID then
        local id = liveBtn:GetID()
        if type(id) == "number" and id > 0 then
            return id
        end
    end

    if ContainerIDToInventoryID then
        local ok, slotID = pcall(function() return ContainerIDToInventoryID(index) end)
        if ok and type(slotID) == "number" and slotID > 0 then
            return slotID
        end
    end

    local slotNames = {
        "Bag" .. (index - 1) .. "Slot",
        "Bag" .. index .. "Slot",
        "CharacterBag" .. (index - 1) .. "Slot",
    }
    for _, slotName in ipairs(slotNames) do
        local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end)
        if ok and type(slotID) == "number" and slotID > 0 then
            return slotID
        end
    end

    -- Vanilla fallback.
    local fallback = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 }
    return fallback[index]
end

local function CaptureEquippedBagMeta(db)
    if not db then return end

    db.equippedBags = {}
    for index = 1, 4 do
        local invSlot = ResolvePlayerBagInvSlot(index)
        local link = nil
        local texture = nil
        if invSlot then
            link = GetInventoryItemLink("player", invSlot)
            texture = GetInventoryItemTexture("player", invSlot)
        end

        local size = GetContainerNumSlots(index) or 0
        db.equippedBags[index] = {
            link = link,
            texture = texture,
            size = tonumber(size) or 0,
        }
    end
end

local function ResolveBankBagInvSlot(index)
    if type(index) ~= "number" or index <= 0 then return nil end
    local bagID = index + 4
    local function TryBankButtonID(buttonID, isBank)
        if not BankButtonIDToInvSlotID then return nil end
        local ok, slotID = pcall(function() return BankButtonIDToInvSlotID(buttonID, isBank) end)
        if ok and IsBankBagInvSlot(slotID) then
            return slotID
        end
        return nil
    end

    local function AcceptIfBag(slotID)
        if not IsBankBagInvSlot(slotID) then return nil end
        local link = GetInventoryItemLink("player", slotID)
        if link and IsBagItemLink(link) then
            return slotID
        end
        return nil
    end

    -- Guda-style primary mapping
    local slot = TryBankButtonID(bagID, 1)
    if slot then return slot end
    slot = TryBankButtonID(index, 1)
    if slot then return slot end

    -- Fallbacks: only when they are confirmed bag items
    slot = TryBankButtonID(bagID, nil)
    slot = AcceptIfBag(slot)
    if slot then return slot end

    slot = TryBankButtonID(index, nil)
    slot = AcceptIfBag(slot)
    if slot then return slot end

    if ContainerIDToInventoryID then
        local ok, s = pcall(function() return ContainerIDToInventoryID(bagID) end)
        if ok then
            slot = AcceptIfBag(s)
            if slot then return slot end
        end
    end

    slot = AcceptIfBag(SafeGetInventorySlotByName("BankBagSlot" .. index))
    if slot then return slot end
    slot = AcceptIfBag(SafeGetInventorySlotByName("BankSlot" .. index))
    if slot then return slot end

    local liveBtn = _G["BankFrameBag" .. index] or _G["BankFrameBag" .. index .. "Slot"]
    if liveBtn and liveBtn.GetID then
        slot = AcceptIfBag(liveBtn:GetID())
        if slot then return slot end
    end

    return nil
end

local function CaptureBankBagMeta(db)
    if not db then return end

    local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0
    db.bankSlots = tonumber(purchased) or 0
    db.bankBags = {}

    for index = 1, 7 do
        local bagID = index + 4
        local size = GetContainerNumSlots(bagID) or 0
        local info = {
            unlocked = (index <= db.bankSlots),
            size = tonumber(size) or 0,
            link = nil,
            texture = nil,
        }

        if info.unlocked then
            local invSlot = ResolveBankBagInvSlot(index)
            if invSlot then
                local link = GetInventoryItemLink("player", invSlot)
                local state = GetBagLinkState(link)
                if (state == "bag") or (state == "unknown" and info.size > 0) then
                    info.link = link
                    info.texture = GetInventoryItemTexture("player", invSlot)
                end
            end
        end

        db.bankBags[index] = info
    end
end

local function ScanContainer(containerID, isBank)
    if not SFramesGlobalDB or realmName == "" or playerName == "" then return end
    local db = SFramesGlobalDB[realmName][playerName]
    local targetDB = isBank and db.bank or db.bags
    local size = GetContainerNumSlots(containerID)
    targetDB[containerID] = { size = size, items = {} }
    for slot = 1, size do
        local link = GetContainerItemLink(containerID, slot)
        if link then
            local texture, itemCount, locked, quality = GetContainerItemInfo(containerID, slot)
            local _, _, idStr = string.find(link, "item:(%d+):")
            local itemID = idStr and tonumber(idStr) or nil
            targetDB[containerID].items[slot] = { id = itemID,
                link = link,
                count = itemCount,
                texture = texture,
                quality = quality
            }
        end
    end
end

function SFrames.Bags.Offline:ScanBags()
    if realmName == "" or playerName == "" then return end
    InitDB()
    local db = SFramesGlobalDB[realmName] and SFramesGlobalDB[realmName][playerName]
    if not db then return end

    for bag = 0, 4 do
        ScanContainer(bag, false)
    end
    CaptureEquippedBagMeta(db)
    if isBankOpen then
        ScanContainer(-1, true)
        for bag = 5, 11 do
            ScanContainer(bag, true)
        end
        CaptureBankBagMeta(db)
    end
    db.money = GetMoney()
end

local scanPending = false
local scanTimer = CreateFrame("Frame")
scanTimer:Hide()
local function RequestBagScan()
    if scanPending then return end
    scanPending = true
    scanTimer.elapsed = 0
    scanTimer:Show()
    scanTimer:SetScript("OnUpdate", function()
        this.elapsed = this.elapsed + arg1
        if this.elapsed > 0.5 then
            this:SetScript("OnUpdate", nil)
            this:Hide()
            scanPending = false
            SFrames.Bags.Offline:ScanBags()
        end
    end)
end

offlineFrame:SetScript("OnEvent", function()
    if event == "PLAYER_LOGIN" then
        realmName = GetRealmName() or ""
        playerName = UnitName("player") or ""
        InitDB()
        SFrames.Bags.Offline:ScanBags()
    elseif event == "BAG_UPDATE" or event == "PLAYERBANKSLOTS_CHANGED" or event == "PLAYERBANKBAGSLOTS_CHANGED" then
        RequestBagScan()
    elseif event == "BANKFRAME_OPENED" then
        isBankOpen = true
        RequestBagScan()
        QueueBankRefreshScans()
    elseif event == "BANKFRAME_CLOSED" then
        isBankOpen = false
    end
end)

function SFrames.Bags.Offline:GetCharacterList()
    if not SFramesGlobalDB or realmName == "" then return {} end
    local chars = {}
    if SFramesGlobalDB[realmName] then
        for name in pairs(SFramesGlobalDB[realmName]) do
            table.insert(chars, name)
        end
    end
    return chars
end

function SFrames.Bags.Offline:GetCharacterData(name)
    if not SFramesGlobalDB or realmName == "" then return nil end
    return SFramesGlobalDB[realmName] and SFramesGlobalDB[realmName][name]
end

function SFrames.Bags.Offline:GetCurrentPlayerName()
    return playerName
end
