-- ETB Integrations Framework (v15.165.0)
-- ===========================================================================
-- Optional third-party adapter framework. This file is the CONTRACT only — it
-- defines how adapters plug in, how ETB detects them, how ETB falls back to
-- native behaviour, and how adapters shut down. Individual adapters (TomTom,
-- HandyNotes, etc.) are separate files registered against this framework; none
-- ship in this file by design.
--
-- NON-NEGOTIABLE PRINCIPLES (enforced by this framework):
--   1. INDEPENDENCE. Every capability ETB exposes has a native fallback, so ETB
--      is fully functional with zero adapters installed. Get() returning nil is
--      a normal, expected path — callers must handle it.
--   2. AUTHORITY. ETB data and Blizzard API results are canonical. Adapters
--      ENRICH only; Enrich() never lets external data overwrite a newer ETB or
--      Blizzard fact. Freshness/source travel with every enriched value.
--   3. CAPABILITY DETECTION, not name matching. IsAvailable() probes the actual
--      functions an adapter needs, so a renamed/updated addon still works and a
--      half-loaded one is correctly treated as unavailable.
--   4. SAFE FAILURE + ISOLATION. Every adapter call is pcall-guarded; one
--      misbehaving adapter can never break an ETB feature.
--   5. CLEAN SHUTDOWN. Disabling an adapter calls Shutdown(), which must release
--      every event, callback, timer, map pin, and reference it created.
--   6. LAZY. Adapter Initialize() runs on first use, not at login.
-- ===========================================================================

local ADDON_NAME, NS = ...
local ETB = NS.ETB

ETB.Integrations = ETB.Integrations or {}
local I = ETB.Integrations

I.adapters = {}     -- id -> adapter table
I._active  = {}     -- id -> true once Initialize() has succeeded
I._order   = {}     -- registration order (stable iteration)

-- Required contract methods every adapter table MUST provide.
local REQUIRED = { "IsAvailable", "GetCapabilities", "Initialize", "Shutdown" }

-- Per-adapter enabled state lives in settings so users can toggle each one.
local function enabledStore()
    if ETB.Settings and ETB.Settings.GetIntegrationEnabled then return ETB.Settings end
    return nil
end

-- A guarded call: never let an adapter error escape into ETB.
local function safe(fn, ...)
    if type(fn) ~= "function" then return false end
    local ok, a, b, c = pcall(fn, ...)
    if not ok then
        if ETB.Log then ETB:Log("Integrations", "adapter call failed: " .. tostring(a)) end
        return false
    end
    return true, a, b, c
end

-- ---------------------------------------------------------------------------
-- Registration
-- ---------------------------------------------------------------------------
-- adapter = { id, addon, IsAvailable, GetCapabilities, Initialize, Shutdown, ... }
function I:Register(adapter)
    if type(adapter) ~= "table" or not adapter.id then return false end
    for _, m in ipairs(REQUIRED) do
        if type(adapter[m]) ~= "function" then
            if ETB.Log then ETB:Log("Integrations", adapter.id .. " missing contract method: " .. m) end
            return false
        end
    end
    if not self.adapters[adapter.id] then self._order[#self._order + 1] = adapter.id end
    self.adapters[adapter.id] = adapter
    return true
end

-- ---------------------------------------------------------------------------
-- Enable / disable (user toggle). Disabling shuts the adapter down.
-- ---------------------------------------------------------------------------
function I:IsEnabled(id)
    local s = enabledStore()
    if s then return s:GetIntegrationEnabled(id) ~= false end
    return true  -- default on; absence of a store means "not explicitly disabled"
end

function I:SetEnabled(id, on)
    local s = enabledStore()
    if s and s.SetIntegrationEnabled then s:SetIntegrationEnabled(id, on and true or false) end
    if not on then self:Shutdown(id) end
end

-- ---------------------------------------------------------------------------
-- Resolution: returns the adapter ONLY if it is present, enabled, available,
-- and successfully initialised. Returns nil otherwise — callers fall back to
-- native behaviour on nil. Initialisation is lazy (first Get()).
-- ---------------------------------------------------------------------------
function I:Get(id)
    local a = self.adapters[id]
    if not a then return nil end
    if not self:IsEnabled(id) then return nil end

    -- capability detection (probe functions, not just addon name)
    local ok, avail = safe(a.IsAvailable, a)
    if not ok or not avail then return nil end

    if not self._active[id] then
        local okInit, result = safe(a.Initialize, a)
        if not okInit or result == false then return nil end
        self._active[id] = true
    end
    return a
end

function I:HasCapability(id, cap)
    local a = self:Get(id)
    if not a then return false end
    local ok, caps = safe(a.GetCapabilities, a)
    return (ok and type(caps) == "table" and caps[cap]) and true or false
end

-- ---------------------------------------------------------------------------
-- Shutdown. Must be called on disable, /reload teardown, and PLAYER_LOGOUT.
-- ---------------------------------------------------------------------------
function I:Shutdown(id)
    local a = self.adapters[id]
    if a and self._active[id] then
        safe(a.Shutdown, a)
        self._active[id] = nil
    end
end

function I:ShutdownAll()
    for _, id in ipairs(self._order) do self:Shutdown(id) end
end

-- ---------------------------------------------------------------------------
-- AUTHORITY / ENRICHMENT
-- ETB and Blizzard facts are canonical. Enrich() returns the canonical value
-- whenever it exists and is at least as fresh as the external one; otherwise it
-- returns the external value tagged with its source and timestamp. External
-- data can NEVER silently replace a newer canonical fact.
--   canonical = { value=?, ts=epoch }  (ETB/Blizzard; ts optional)
--   external  = { value=?, ts=epoch, source="Auctionator" }
-- Returns: value, sourceTag, ts
-- ---------------------------------------------------------------------------
function I:Enrich(canonical, external)
    local cv = canonical and canonical.value
    local cts = (canonical and canonical.ts) or nil
    if cv ~= nil then
        -- Canonical present. Only defer to external if external is strictly newer
        -- AND canonical carries a timestamp to compare against.
        if external and external.value ~= nil and external.ts and cts and external.ts > cts then
            return external.value, (external.source or "external"), external.ts
        end
        return cv, "ETB", cts
    end
    if external and external.value ~= nil then
        return external.value, (external.source or "external"), external.ts
    end
    return nil, nil, nil
end

-- ---------------------------------------------------------------------------
-- COMPATIBILITY SELF-TEST
-- Verifies: ETB-alone integrity (no adapter required), each adapter's contract
-- compliance, that IsAvailable/GetCapabilities don't error, and that Shutdown
-- is callable. Returns a results table; also prints when verbose.
-- ---------------------------------------------------------------------------
function I:RunSelfTest(verbose)
    local results = { passed = 0, failed = 0, lines = {} }
    local function record(ok, msg)
        if ok then results.passed = results.passed + 1
        else results.failed = results.failed + 1 end
        results.lines[#results.lines + 1] = (ok and "PASS: " or "FAIL: ") .. msg
    end

    -- 1. ETB-alone: framework must function with no adapters resolved.
    record(type(self.adapters) == "table", "framework present")

    -- 2. Each registered adapter: contract + non-erroring detection.
    for _, id in ipairs(self._order) do
        local a = self.adapters[id]
        local contractOK = true
        for _, m in ipairs(REQUIRED) do
            if type(a[m]) ~= "function" then contractOK = false end
        end
        record(contractOK, id .. ": implements full contract")

        local okAvail = safe(a.IsAvailable, a)
        record(okAvail, id .. ": IsAvailable() runs without error")

        local okCaps, caps = safe(a.GetCapabilities, a)
        record(okCaps and type(caps) == "table", id .. ": GetCapabilities() returns a table")
    end

    -- 3. Authority rule: external never overrides newer canonical.
    local v = self:Enrich({ value = "etb", ts = 100 }, { value = "ext", ts = 50, source = "X" })
    record(v == "etb", "authority: newer ETB value wins over older external")
    local v2 = self:Enrich({ value = nil }, { value = "ext", ts = 50, source = "X" })
    record(v2 == "ext", "fallback: external fills gap when ETB has no value")

    if verbose and ETB.Print then
        ETB:Print(string.format("|cffffd100ETB Integrations self-test:|r %d passed, %d failed", results.passed, results.failed))
        for _, line in ipairs(results.lines) do ETB:Print("  " .. line) end
    end
    return results
end

-- Ensure adapters release everything on logout / reload.
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_LOGOUT")
f:SetScript("OnEvent", function() I:ShutdownAll() end)
