--[[ Core/Score.lua — Can-I-Build-It (pure Lua, unit-testable outside WoW).

     TWO inputs, one output shape:
     1) Score.FromLive(normalized)  — a BlueprintAPI normalized payload where the
        server already computed per-entry `missing` (spike run 2). AUTHORITATIVE.
     2) Score.Compute(manifest, ownership, sources) — baked manifest + injected
        ownership fn, used before any live request has run.

     Output: { pct, missing = { {recordID|entryID, need, have, name?, source, estGold,
               obtainable} }, estGoldTotal, unobtainableCount, craftableCount, ahPricedCount } ]]--

local _, ns = ...
local Score = {}
if ns then ns.Score = Score end

local function newResult()
  return {
    pct = 1, missing = {},
    estGoldTotal = 0, unobtainableCount = 0, craftableCount = 0, ahPricedCount = 0,
  }
end

local function applySource(r, m, src, deficit)
  if not src then return end
  m.source = src
  if src.obtainable == false or src.type == "promo" then
    m.obtainable = false
    r.unobtainableCount = r.unobtainableCount + 1
  elseif src.type == "vendor" and src.cost and (src.currency == "gold" or src.currency == nil) then
    m.estGold = src.cost * deficit
    r.estGoldTotal = r.estGoldTotal + m.estGold
  elseif src.type == "profession" then
    r.craftableCount = r.craftableCount + 1
  elseif src.type == "ah" then
    r.ahPricedCount = r.ahPricedCount + 1
  end
end

--- Authoritative: from a live normalized payload (server-computed missing)
function Score.FromLive(normalized, sources)
  local r = newResult()
  local owned, total = 0, 0
  for _, e in ipairs(normalized.entries or {}) do
    local qty = e.qty or 1
    total = total + qty
    local missing = math.min(e.missing or 0, qty)
    owned = owned + (qty - missing)
    if missing > 0 then
      local m = {
        recordID = e.recordID, name = e.name,
        need = missing, have = qty - missing, obtainable = true,
      }
      applySource(r, m, sources and sources(e.recordID) or nil, missing)
      r.missing[#r.missing + 1] = m
    end
  end
  r.pct = (total > 0) and (owned / total) or 1
  return r
end

--- Fallback: from a baked manifest + ownership fn (nil ownership -> nil pct = "unknown")
function Score.Compute(manifest, ownership, sources)
  local r = newResult()
  local owned, total = 0, 0
  local anyKnown = false
  for _, item in ipairs(manifest or {}) do
    local need = item.qty or 1
    total = total + need
    local have = ownership and ownership(item.recordID or item.entryID)
    if have ~= nil then anyKnown = true else have = 0 end
    owned = owned + math.min(have, need)
    if have < need then
      local m = {
        recordID = item.recordID, entryID = item.entryID, name = item.name,
        need = need - have, have = have, obtainable = true,
      }
      applySource(r, m, sources and sources(item.recordID or item.entryID) or nil, need - have)
      r.missing[#r.missing + 1] = m
    end
  end
  -- An empty manifest means we have not baked this build's contents yet, NOT
  -- that it contains nothing. Claiming 100% there would tell every player they
  -- can build anything. Unknown must stay unknown - the grid then asks the
  -- server for the real score.
  if total == 0 or not anyKnown then
    r.pct = nil
  else
    r.pct = owned / total
  end
  return r
end

--- Ring colour (spec §7): red <40, amber 40-79, green >=80, star @100, grey unknown
function Score.RingColor(pct)
  if pct == nil then return 0.5, 0.5, 0.5, "unknown"
  elseif pct >= 1 then return 1.0, 0.85, 0.2, "star"
  elseif pct >= 0.8 then return 0.25, 0.8, 0.25, "green"
  elseif pct >= 0.4 then return 0.9, 0.65, 0.15, "amber"
  else return 0.85, 0.25, 0.25, "red" end
end

if not ns then return Score end
