--[[ Core/Init.lua — addon table, hand-rolled event dispatcher (spec §5.1: no Ace),
     slash commands. Everything hangs off the single global `BlueprintBazaar`. ]]--

local ADDON, ns = ...
BlueprintBazaar = ns          -- the only global besides BlueprintBazaarDB (spec §8.9)
ns.name = ADDON
ns.version = C_AddOns and C_AddOns.GetAddOnMetadata(ADDON, "Version") or "dev"

-- The 12.1 partial-import wipe bug: Blizzard applied a PTR fix 2026-08-08
-- (Kaivax). Flip to true ONLY if the bug turns out to be alive on live -
-- DetailPane then shows the harsh red warning again instead of the soft one.
ns.BUG_PARTIAL_WIPE = false

-- Where players are pointed to submit their own builds. Kept in one place so
-- it can move without hunting through the UI. A Discord invite only goes here
-- once that server's owner has said yes to the traffic.
-- Housing of Argent Dawn's #blueprints channel. Linked with the server
-- owner's explicit permission (12 Aug). If it ever needs to move, this is the
-- only line to change.
ns.SUBMIT_URL = "discord.gg/rwEVcXXZXZ"

------------------------------------------------------------------------
-- Tiny event dispatcher (~40 lines). ns.events:Register(event, fn) -> handle
------------------------------------------------------------------------
local dispatcher = { frame = CreateFrame("Frame"), handlers = {} }
ns.events = dispatcher

function dispatcher:Register(event, fn)
  if not self.handlers[event] then
    self.handlers[event] = {}
    -- Some events won't exist on all client builds; never hard-error
    pcall(self.frame.RegisterEvent, self.frame, event)
  end
  table.insert(self.handlers[event], fn)
  return fn
end

function dispatcher:Unregister(event, fn)
  local list = self.handlers[event]
  if not list then return end
  for i = #list, 1, -1 do
    if list[i] == fn then table.remove(list, i) end
  end
end

dispatcher.frame:SetScript("OnEvent", function(_, event, ...)
  local list = dispatcher.handlers[event]
  if not list then return end
  for i = 1, #list do
    local ok, err = pcall(list[i], event, ...)
    if not ok then
      geterrorhandler()(("BlueprintBazaar handler error (%s): %s"):format(event, tostring(err)))
    end
  end
end)

-- Internal message bus (same shape, no Blizzard events)
local bus = { handlers = {} }
ns.bus = bus
function bus:On(msg, fn)
  self.handlers[msg] = self.handlers[msg] or {}
  table.insert(self.handlers[msg], fn)
end
function bus:Fire(msg, ...)
  local list = self.handlers[msg]
  if not list then return end
  for i = 1, #list do pcall(list[i], ...) end
end

------------------------------------------------------------------------
-- Slash commands
------------------------------------------------------------------------
SLASH_BLUEPRINTBAZAAR1 = "/bb"
SLASH_BLUEPRINTBAZAAR2 = "/bazaar"
SlashCmdList.BLUEPRINTBAZAAR = function(msg)
  msg = (msg or ""):gsub("^%s+", ""):gsub("%s+$", "")
  local cmd, arg = msg:match("^(%S*)%s*(.-)$")
  cmd = cmd:lower()
  if cmd == "" then
    if ns.UI and ns.UI.Toggle then ns.UI.Toggle() else
      print("|cffc9a227BlueprintBazaar|r UI not built yet (M2). Core is loaded.")
    end
  elseif cmd == "bake" and ns.dev then
    -- §6.1 curation: dump a code's manifest into SavedVariables for the JSON pipeline
    ns.API.Request(arg, function(ok, payload)
      BlueprintBazaarDB.bake = BlueprintBazaarDB.bake or {}
      BlueprintBazaarDB.bake[arg] = { ok = ok, payload = payload, at = date("%c") }
      print("|cffc9a227BlueprintBazaar|r baked " .. arg .. " -> SavedVariables (ok=" .. tostring(ok) .. "). /reload to flush.")
    end)
  elseif cmd == "bakeall" then
    -- Curation harvest: request every library code through the polite queue
    -- and store each server reply in SavedVariables. The replies carry the
    -- full manifest and the real budget costs - everything the JSON entries
    -- are missing. /reload afterwards to flush to disk.
    local entries = (ns.Library and ns.Library.entries) or {}
    if #entries == 0 then
      print("|cffc9a227BlueprintBazaar|r library is empty, nothing to bake.")
      return
    end
    BlueprintBazaarDB.bake = BlueprintBazaarDB.bake or {}
    local done, total = 0, #entries
    print("|cffc9a227BlueprintBazaar|r baking " .. total .. " builds...")
    local function bakeNext(i)
      local e = entries[i]
      if not e then
        print("|cffc9a227BlueprintBazaar|r bake complete: " .. done .. "/" ..
          total .. " ok. /reload to flush to disk.")
        return
      end
      ns.API.Request(e.code, function(ok, payload)
        BlueprintBazaarDB.bake[e.id] = { ok = ok, code = e.code,
          payload = ok and payload or tostring(payload), at = date("%c") }
        if ok then done = done + 1 end
        print("  " .. e.id .. " " .. (ok and "|cff40ff40ok|r" or "|cffff5050failed|r")
          .. "  (" .. i .. "/" .. total .. ")")
        bakeNext(i + 1)
      end, true)
    end
    bakeNext(1)
  elseif cmd == "dev" then
    ns.dev = not ns.dev
    print("|cffc9a227BlueprintBazaar|r dev mode: " .. tostring(ns.dev))
  elseif cmd == "mock" and arg ~= "off" then
    -- M2 acceptance test: 500 generated entries, smooth scroll expected
    local n = tonumber(arg) or 500
    if ns._realLibrary == nil then ns._realLibrary = ns.Library end
    local types = { "house", "interior", "exterior", "room" }
    local entries = {}
    for i = 1, n do
      entries[i] = {
        id = "mock-" .. i,
        title = "Mock Blueprint #" .. i,
        author = { name = "Tester" .. (i % 25) },
        addedIn = ("2026-%02d-%02d"):format((i % 12) + 1, (i % 28) + 1),
        type = types[(i % 4) + 1],
        code = ("MOCKCODE%020d=="):format(i),
        tags = { "mock" },
        budget = { interior = (i * 7) % 400, exterior = 0 },
        description = "Generated perf-test entry.",
        mockPct = ((i - 1) % 101) / 100,
        manifest = {},
      }
    end
    ns.Library = { datasetVersion = 0, entries = entries }
    print("|cffc9a227BlueprintBazaar|r mock library: " .. n .. " entries. /bb mock off to restore.")
    if ns.UI and ns.UI.RefreshGrid then ns.UI.RefreshGrid() end
  elseif cmd == "unmock" or (cmd == "mock" and arg == "off") then
    if ns._realLibrary then ns.Library = ns._realLibrary end
    if ns.UI and ns.UI.RefreshGrid then ns.UI.RefreshGrid() end
    print("|cffc9a227BlueprintBazaar|r real library restored.")
  elseif cmd == "code" then
    if ns.UI and ns.UI.InspectCode then ns.UI.InspectCode(arg) end
  elseif cmd == "minimap" then
    if ns.UI and ns.UI.ToggleMinimapButton then ns.UI.ToggleMinimapButton() end
  else
    print("|cffc9a227BlueprintBazaar|r /bb (toggle) | /bb code <sharecode> | /bb minimap | /bb dev | /bb mock [n] | /bb unmock | /bb bake <code> (dev)")
  end
end

------------------------------------------------------------------------
-- Lifecycle
------------------------------------------------------------------------
ns.events:Register("ADDON_LOADED", function(_, name)
  if name ~= ADDON then return end
  ns.DB.Init()
  if ns.UI and ns.UI.BuildMinimapButton then
    local ok, err = pcall(ns.UI.BuildMinimapButton)
    if not ok then
      -- a broken minimap button must never take the addon down with it
      geterrorhandler()("BlueprintBazaar minimap button: " .. tostring(err))
    end
  end
  -- API drift check runs before we say anything, so the login line is honest
  local warning = ns.Guard and ns.Guard.CheckAndDescribe()
  if warning then
    print("|cffc9a227BlueprintBazaar|r " .. warning .. " (/bb still opens the library)")
  else
    print("|cffc9a227BlueprintBazaar|r loaded - /bb")   -- spec §7: one login line max
  end
end)
