--[[
============================================================================
ERB/Core/CustomBiS.lua  (added in v15.7.0)
============================================================================

User-defined BiS lists. Lets a player create named ranked item lists per
class+spec that coexist with the curated WoWHead / WoWTBC sources.

Data flow:
    1. Player runs `/etb cbis create raid_bis` to make a new list
    2. Player runs `/etb cbis add raid_bis Head 96571` to add items
    3. CustomBiS writes to ETB_CustomBiS SavedVariables-per-character
    4. On next ERB browser open, custom lists appear in the source switcher
       alongside WoWHead and WoWTBC
    5. Picking a custom list flows through the same DataBridge:GetItemsForSlot
       path as the curated sources

SavedVariables schema (ETB_CustomBiS):

    ETB_CustomBiS = {
        [classID] = {
            [specIdx] = {
                [listName] = {
                    label    = "Display name shown in UI",
                    items    = {
                        [phase] = {        -- e.g. "P3" / "PR"
                            [slotIdx] = { itemID, itemID, ... },  -- ordered by rank
                        },
                    },
                    created  = epoch_seconds,
                    modified = epoch_seconds,
                },
            },
        },
    }

Why per-character: each character has different specs and gear targets.
Sharing across characters would require an explicit export/import flow,
which is fine to add later.

Why nest under classID+specIdx: lookup is O(1) at browser-open time, and
the DataBridge naturally indexes by class+spec already.

Public surface:
    CustomBiS:Initialize()
    CustomBiS:Create(name) -> ok, err
    CustomBiS:Delete(name) -> ok, err
    CustomBiS:Rename(oldName, newName) -> ok, err
    CustomBiS:AddItem(name, slotIdx, itemID, phase) -> ok, err
    CustomBiS:RemoveItem(name, slotIdx, rank, phase) -> ok, err
    CustomBiS:ReorderItem(name, slotIdx, fromRank, toRank, phase) -> ok, err
    CustomBiS:GetList(name) -> tbl or nil
    CustomBiS:ListAll() -> { name, name, ... } (sorted)
    CustomBiS:GetItemsForSlot(name, slotIdx, phase) -> { itemID, ... }
    CustomBiS:GetVirtualBiSData() -> table shaped like ERB.BiSData[source]

All operations scope automatically to the player's current class+spec via
ERB.DataBridge.playerClassIdx / playerSpecIdx.
============================================================================
]]--

local ADDON_NAME, NS = ...
local ETB = NS.ETB
if not ETB then return end
local ERB = ETB.ERB
if not ERB then return end

-- v15.6.1 pattern: preserve any submodule fields already attached to this
-- namespace before installing the orchestrator table.
ERB.CustomBiS = ERB.CustomBiS or {}
local CB = ERB.CustomBiS

CB.initialized = false

-- ============================================================================
-- VALIDATION
-- ============================================================================

local function validateName(name)
    if type(name) ~= "string" or name == "" then
        return false, "name must be a non-empty string"
    end
    if #name > 32 then
        return false, "name too long (max 32 chars)"
    end
    -- Disallow leading/trailing whitespace and control chars for SavedVariable safety
    if name:match("^%s") or name:match("%s$") then
        return false, "name must not start or end with whitespace"
    end
    return true
end

local function validateSlot(slotIdx)
    if type(slotIdx) ~= "number" then
        return false, "slot must be a number (1-13)"
    end
    if slotIdx < 1 or slotIdx > 13 then
        return false, "slot out of range (1-13)"
    end
    return true
end

local function validateItemID(itemID)
    if type(itemID) ~= "number" or itemID <= 0 then
        return false, "itemID must be a positive number"
    end
    return true
end

local function validatePhase(phase)
    -- Phases are short strings like "P3", "PR". Empty / nil falls back to default.
    if phase ~= nil and (type(phase) ~= "string" or phase == "") then
        return false, "phase must be a non-empty string or nil"
    end
    return true
end

-- ============================================================================
-- INTERNAL: resolve the current player's class+spec slot
-- ============================================================================
-- All operations implicitly target the player's active class+spec. This
-- matches how the rest of ERB works (DataBridge tracks playerClassIdx /
-- playerSpecIdx) and keeps the slash commands ergonomic — no need for the
-- player to type their classID every time.

local function getCurrentClassSpec()
    if not ERB.DataBridge then
        return nil, nil, "DataBridge not initialized"
    end
    local classIdx = ERB.DataBridge.playerClassIdx
    local specIdx  = ERB.DataBridge.playerSpecIdx
    if not classIdx or not specIdx then
        return nil, nil, "Player class/spec not detected yet — try again in a moment"
    end
    return classIdx, specIdx
end

-- Get-or-create the (classID, specIdx) container in SavedVariables. Returns
-- the container table directly; mutations on it persist.
local function getContainer()
    local classIdx, specIdx, err = getCurrentClassSpec()
    if not classIdx then return nil, err end
    ETB_CustomBiS = ETB_CustomBiS or {}
    ETB_CustomBiS[classIdx] = ETB_CustomBiS[classIdx] or {}
    ETB_CustomBiS[classIdx][specIdx] = ETB_CustomBiS[classIdx][specIdx] or {}
    return ETB_CustomBiS[classIdx][specIdx]
end

-- Read-only variant: doesn't create empty tables in SavedVariables. Returns
-- nil if the container hasn't been touched yet for this class+spec.
local function readContainer()
    local classIdx, specIdx, err = getCurrentClassSpec()
    if not classIdx then return nil, err end
    if not ETB_CustomBiS then return nil end
    local byClass = ETB_CustomBiS[classIdx]
    if not byClass then return nil end
    return byClass[specIdx]
end

local function now()
    return (time and time()) or 0  -- WoW exposes time(); fall back to 0 in tests
end

local function defaultPhase()
    if ERB.DataBridge and ERB.DataBridge.currentPhase then
        return ERB.DataBridge.currentPhase
    end
    return "P3"  -- fall back to MoP final phase
end

-- ============================================================================
-- CRUD: lists themselves
-- ============================================================================

function CB:Create(name)
    local ok, err = validateName(name)
    if not ok then return false, err end

    local container, cErr = getContainer()
    if not container then return false, cErr end

    if container[name] then
        return false, "list already exists"
    end

    container[name] = {
        label    = name,
        items    = {},
        created  = now(),
        modified = now(),
    }
    return true
end

function CB:Delete(name)
    local ok, err = validateName(name)
    if not ok then return false, err end

    local container, cErr = readContainer()
    if not container or not container[name] then
        return false, "list does not exist"
    end

    container[name] = nil
    return true
end

function CB:Rename(oldName, newName)
    local ok, err = validateName(oldName)
    if not ok then return false, "old name: " .. err end
    ok, err = validateName(newName)
    if not ok then return false, "new name: " .. err end
    if oldName == newName then return true end

    local container, cErr = readContainer()
    if not container or not container[oldName] then
        return false, "source list does not exist"
    end
    if container[newName] then
        return false, "target name already exists"
    end

    local entry = container[oldName]
    entry.label = newName
    entry.modified = now()
    container[newName] = entry
    container[oldName] = nil
    return true
end

function CB:GetList(name)
    local container = readContainer()
    if not container then return nil end
    return container[name]
end

function CB:ListAll()
    local container = readContainer()
    local out = {}
    if not container then return out end
    for k in pairs(container) do out[#out + 1] = k end
    table.sort(out)
    return out
end

-- ============================================================================
-- CRUD: items within a list
-- ============================================================================

-- Get-or-create the phase->slot->{} chain for one list. Returns the slot
-- array (a list of itemIDs) which the caller can mutate.
local function getSlotArray(list, slotIdx, phase)
    list.items = list.items or {}
    list.items[phase] = list.items[phase] or {}
    list.items[phase][slotIdx] = list.items[phase][slotIdx] or {}
    return list.items[phase][slotIdx]
end

function CB:AddItem(name, slotIdx, itemID, phase)
    local ok, err = validateName(name); if not ok then return false, err end
    ok, err = validateSlot(slotIdx);    if not ok then return false, err end
    ok, err = validateItemID(itemID);   if not ok then return false, err end
    ok, err = validatePhase(phase);     if not ok then return false, err end

    phase = phase or defaultPhase()

    local container, cErr = readContainer()
    if not container or not container[name] then
        return false, "list does not exist (use /etb cbis create first)"
    end
    local list = container[name]
    local slot = getSlotArray(list, slotIdx, phase)

    -- Reject duplicates within the same slot+phase: doesn't make sense to
    -- rank an item twice. Comparing as numbers because itemIDs are numeric.
    for _, existing in ipairs(slot) do
        if existing == itemID then
            return false, "item already in this slot"
        end
    end

    slot[#slot + 1] = itemID
    list.modified = now()
    return true, #slot  -- second return = the rank just assigned
end

function CB:RemoveItem(name, slotIdx, rank, phase)
    local ok, err = validateName(name); if not ok then return false, err end
    ok, err = validateSlot(slotIdx);    if not ok then return false, err end
    ok, err = validatePhase(phase);     if not ok then return false, err end
    if type(rank) ~= "number" or rank < 1 then
        return false, "rank must be a positive number"
    end

    phase = phase or defaultPhase()

    local container = readContainer()
    if not container or not container[name] then
        return false, "list does not exist"
    end
    local list = container[name]
    if not list.items or not list.items[phase] or not list.items[phase][slotIdx] then
        return false, "no items in this slot"
    end
    local slot = list.items[phase][slotIdx]
    if rank > #slot then
        return false, string.format("rank %d out of range (have %d)", rank, #slot)
    end

    local removed = table.remove(slot, rank)
    list.modified = now()
    -- Clean up empty slot/phase tables so SavedVariables stays compact
    if #slot == 0 then list.items[phase][slotIdx] = nil end
    return true, removed
end

function CB:ReorderItem(name, slotIdx, fromRank, toRank, phase)
    local ok, err = validateName(name); if not ok then return false, err end
    ok, err = validateSlot(slotIdx);    if not ok then return false, err end
    ok, err = validatePhase(phase);     if not ok then return false, err end
    if type(fromRank) ~= "number" or fromRank < 1 then
        return false, "fromRank must be a positive number"
    end
    if type(toRank) ~= "number" or toRank < 1 then
        return false, "toRank must be a positive number"
    end

    phase = phase or defaultPhase()

    local container = readContainer()
    if not container or not container[name] then
        return false, "list does not exist"
    end
    local list = container[name]
    if not list.items or not list.items[phase] or not list.items[phase][slotIdx] then
        return false, "no items in this slot"
    end
    local slot = list.items[phase][slotIdx]
    if fromRank > #slot or toRank > #slot then
        return false, string.format("rank out of range (have %d)", #slot)
    end
    if fromRank == toRank then return true end

    local item = table.remove(slot, fromRank)
    table.insert(slot, toRank, item)
    list.modified = now()
    return true
end

-- ============================================================================
-- READ ACCESS for DataBridge / UI
-- ============================================================================

function CB:GetItemsForSlot(name, slotIdx, phase)
    phase = phase or defaultPhase()
    local container = readContainer()
    if not container or not container[name] then return {} end
    local list = container[name]
    if not list.items or not list.items[phase] then return {} end
    return list.items[phase][slotIdx] or {}
end

-- Synthesize a virtual BiSData-shaped table for ALL custom lists belonging
-- to the current class+spec. Shape matches ERB.BiSData[source][classID][specIdx]
-- so DataBridge:GetSourceBiSData can return it unchanged.
--
-- Returns: { [classID] = { [specIdx] = { [phase] = { [slot] = {itemID, ...} } } } }
-- This is the per-source slice; the caller assigns it under a chosen key.
function CB:GetVirtualSpecSlice(name)
    local container = readContainer()
    if not container or not container[name] then return nil end
    local list = container[name]
    local classIdx, specIdx, err = getCurrentClassSpec()
    if not classIdx then return nil end

    local out = { [classIdx] = { [specIdx] = {} } }
    local specSlice = out[classIdx][specIdx]
    if list.items then
        for phase, slotData in pairs(list.items) do
            specSlice[phase] = {}
            for slotIdx, itemIDs in pairs(slotData) do
                -- Defensive copy so callers can't mutate SavedVariables by accident
                local copy = {}
                for i, id in ipairs(itemIDs) do copy[i] = id end
                specSlice[phase][slotIdx] = copy
            end
        end
    end
    return out
end

-- ============================================================================
-- INITIALIZE
-- ============================================================================
-- Called once after SavedVariables are loaded. Doesn't allocate anything
-- in ETB_CustomBiS — that happens lazily on first Create().

function CB:Initialize()
    if self.initialized then return end
    self.initialized = true
    -- Defensive: ensure the top-level SV exists so reads don't crash
    ETB_CustomBiS = ETB_CustomBiS or {}
end
