--[[ Core/BlueprintAPI.lua — async wrapper over C_HousingBlueprint.
     SPIKE-VERIFIED (M0): events echo the shareCode, so requests are correlated
     PER CODE (no single-in-flight bottleneck). Payload shape (run 2):
       { shareCode, targetHouseGUID, unmetRequirementFlags, blockingRequirementFlags,
         contentGroups = { { contentType, entries = {
             { recordID, total, name, invalid, contentType, numMissing } } } },
         budgetInfo = { interiorBudgets = {...}, exteriorBudgets = {...} } }
     numMissing is computed SERVER-SIDE for the requesting player.

     API.Request(code, cb)  -> cb(ok, normalized | reason)
     normalized = { code, entries = { {recordID, name, qty, missing, invalid} },
                    budgets, flags = {unmet, blocking}, raw } ]]--

local _, ns = ...
local API = {}
ns.API = API

local BP = C_HousingBlueprint
local TIMEOUT = 10

local pending = {}   -- [code] = { callbacks = {fn...}, timer = timerObj }
local cache = {}     -- [code] = normalized (session cache; contents rarely change mid-session)

local function normalize(payload)
  local out = {
    code = payload.shareCode,
    entries = {},
    budgets = payload.budgetInfo,
    flags = {
      unmet = payload.unmetRequirementFlags or 0,
      blocking = payload.blockingRequirementFlags or 0,
    },
    raw = payload,
  }
  for _, group in ipairs(payload.contentGroups or {}) do
    for _, e in ipairs(group.entries or {}) do
      out.entries[#out.entries + 1] = {
        recordID = e.recordID,
        name = e.name,
        qty = e.total or 1,
        missing = e.numMissing or 0,
        invalid = e.invalid or false,
        contentType = e.contentType,
      }
    end
  end
  return out
end

local function settle(code, ok, result)
  local p = pending[code]
  if not p then return end
  pending[code] = nil
  if p.timer then p.timer:Cancel() end
  if ok then
    cache[code] = result
    -- Every reply carries the player's own budget ceilings. Learn them here
    -- so "needs house level 7" can also say "your house holds 200", and
    -- redraw the grid if they moved - a level-up changes every verdict.
    if ns.HouseLevel and ns.HouseLevel.Record(result.budgets) then
      ns.bus:Fire("SCORE_INVALIDATE")
    end
  end
  for _, cb in ipairs(p.callbacks) do pcall(cb, ok, result) end
end

--- Request blueprint contents for a share code (deduped + session-cached).
function API.Request(code, callback, forceRefresh)
  if type(code) ~= "string" or code == "" then
    if callback then callback(false, "bad-code") end
    return
  end
  if cache[code] and not forceRefresh then
    if callback then callback(true, cache[code]) end
    return
  end
  if pending[code] then
    table.insert(pending[code].callbacks, callback)
    return
  end
  if not (BP and BP.RequestBlueprintContents) then
    if callback then callback(false, "api-missing") end
    return
  end
  pending[code] = { callbacks = { callback } }
  pending[code].timer = C_Timer.NewTimer(TIMEOUT, function()
    settle(code, false, "timeout")
  end)
  local ok, err = pcall(BP.RequestBlueprintContents, code)
  if not ok then
    settle(code, false, "call-error: " .. tostring(err))
  end
end

function API.Cached(code) return cache[code] end

--- Local validation (spike: ~0.1ms, no round-trip — safe per keystroke)
function API.IsValid(code)
  if not (BP and BP.IsShareCodeValid) then return nil end
  local ok, res = pcall(BP.IsShareCodeValid, code)
  return ok and res or false
end

function API.TypeOf(code)
  if not (BP and BP.GetBlueprintTypeForCode) then return nil end
  local ok, res = pcall(BP.GetBlueprintTypeForCode, code)
  return ok and res or nil
end

--- Hyperlink (spike: |Hhousingblueprint:CODE|h[Housing Blueprint]|h)
function API.Hyperlink(code)
  if BP and BP.GetBlueprintHyperlink then
    local ok, link = pcall(BP.GetBlueprintHyperlink, code)
    if ok and link then return link end
  end
  return ("|cff66bbff|Hhousingblueprint:%s|h[Housing Blueprint]|h|r"):format(code)
end

--- Can the player import ANYTHING right now? Returns (ok, reasonText).
--- The client answers this without a code and without a request, so it is the
--- cheap check to make before offering the button - being in the wrong place
--- is by far the most common reason an import fails, and finding that out
--- afterwards is what made a player delete the addon rather than walk home.
function API.CanImport()
  if not (BP and BP.GetImportAvailability) then
    return true, nil          -- older client: let the attempt answer instead
  end
  local ok, result = pcall(BP.GetImportAvailability)
  if not ok then return true, nil end
  if ns.Result.IsSuccess(result) then return true, nil end
  return false, ns.Result.Explain(result)
end

--- One-click import (spike-CONFIRMED insecurely callable). cb(ok, errEnum)
function API.Import(code, callback)
  if not (BP and BP.ImportBlueprint) then
    if callback then callback(false, "api-missing") end
    return
  end
  API._importCb = callback
  local ok, err = pcall(BP.ImportBlueprint, code)
  if not ok and callback then
    API._importCb = nil
    callback(false, tostring(err))
  end
end

------------------------------------------------------------------------
-- Events (payloads verified in SPIKE_RESULTS.md run 2)
------------------------------------------------------------------------
ns.events:Register("HOUSING_BLUEPRINT_CONTENTS_RECEIVED", function(_, payload)
  if type(payload) ~= "table" or not payload.shareCode then return end
  settle(payload.shareCode, true, normalize(payload))
end)
ns.events:Register("HOUSING_BLUEPRINT_CONTENTS_FAILURE", function(_, code, errEnum)
  if type(code) == "string" then
    settle(code, false, "server-error:" .. tostring(errEnum))
  end
end)
ns.events:Register("HOUSING_BLUEPRINT_IMPORT_SUCCESS", function()
  local cb = API._importCb
  API._importCb = nil
  if cb then pcall(cb, true) end
  ns.bus:Fire("IMPORT_DONE", true)
end)
ns.events:Register("HOUSING_BLUEPRINT_IMPORT_FAILURE", function(_, errEnum)
  local cb = API._importCb
  API._importCb = nil
  if cb then pcall(cb, false, errEnum) end
  ns.bus:Fire("IMPORT_DONE", false, errEnum)
end)
