--[[ UI/DetailPane.lua - right pane (spec F3): title/author/desc, budgets, code
     copy box, ONE-CLICK IMPORT (spike-confirmed), chat link, verify, manifest
     with own/missing colors, score summary, favorite toggle. ]]

local _, ns = ...
local S = ns.Style
local W = ns.Widgets
ns.UI = ns.UI or {}
local UI = ns.UI

local D = {}      -- widget refs
local current     -- selected entry
local MANIFEST_LINES = 12

function UI.BuildDetail(panel)
  local pad = S.pad

  -- Hero image: the first thing you should see about a build is the build.
  -- It lives in a holder whose height collapses to nothing when there is no
  -- screenshot, and everything below flows from the holder's bottom edge.
  D.heroHolder = CreateFrame("Frame", nil, panel)
  D.heroHolder:SetPoint("TOPLEFT", pad, -pad)
  D.heroHolder:SetPoint("TOPRIGHT", -pad, -pad)
  D.heroHolder:SetHeight(1)
  D.hero = D.heroHolder:CreateTexture(nil, "ARTWORK")
  D.hero:SetAllPoints()
  D.hero:Hide()

  -- Some builds are too big for one picture (the AT-AT started this): a build
  -- can carry several screenshots, and arrows page through them.
  local function updateHero()
    local thumbs = current and current.thumbs
    if not (thumbs and #thumbs > 0) then
      D.hero:Hide()
      D.heroHolder:SetHeight(1)
      D.heroPrev:Hide(); D.heroNext:Hide(); D.heroCount:Hide()
      return
    end
    D.heroIndex = math.max(1, math.min(D.heroIndex or 1, #thumbs))
    D.hero:SetTexture("Interface\\AddOns\\BlueprintBazaar\\Media\\"
      .. thumbs[D.heroIndex]:gsub("%.tga$", ""))
    D.hero:Show()
    D.heroHolder:SetHeight(163)
    if #thumbs > 1 then
      D.heroPrev:Show(); D.heroNext:Show()
      D.heroCount:SetText(D.heroIndex .. " / " .. #thumbs)
      D.heroCount:Show()
    else
      D.heroPrev:Hide(); D.heroNext:Hide(); D.heroCount:Hide()
    end
  end
  D.UpdateHero = updateHero

  local function page(step)
    local thumbs = current and current.thumbs
    if not (thumbs and #thumbs > 1) then return end
    D.heroIndex = ((D.heroIndex - 1 + step) % #thumbs) + 1
    updateHero()
  end

  D.heroPrev = W.Button(D.heroHolder, "<", function() page(-1) end)
  D.heroPrev:SetPoint("LEFT", 4, 0)
  D.heroPrev:SetWidth(22)
  D.heroPrev:SetFrameLevel(D.heroHolder:GetFrameLevel() + 2)
  D.heroPrev:Hide()

  D.heroNext = W.Button(D.heroHolder, ">", function() page(1) end)
  D.heroNext:SetPoint("RIGHT", -4, 0)
  D.heroNext:SetWidth(22)
  D.heroNext:SetFrameLevel(D.heroHolder:GetFrameLevel() + 2)
  D.heroNext:Hide()

  D.heroCount = W.Text(D.heroHolder, S.size.tiny, S.text)
  D.heroCount:SetPoint("BOTTOMRIGHT", -8, 6)
  D.heroCount:Hide()

  D.title = W.Text(panel, S.size.title, S.gold)
  D.title:SetPoint("TOPLEFT", D.heroHolder, "BOTTOMLEFT", 0, -6)
  D.title:SetPoint("RIGHT", -40, 0)
  D.title:SetWordWrap(false)

  D.fav = W.Button(panel, "*", function()
    if not current then return end
    ns.db.favorites[current.id] = not ns.db.favorites[current.id] or nil
    D.fav.text:SetTextColor(ns.db.favorites[current.id] and S.amber[1] or S.textDim[1],
      ns.db.favorites[current.id] and S.amber[2] or S.textDim[2],
      ns.db.favorites[current.id] and S.amber[3] or S.textDim[3])
    UI.RefreshGrid(true)
  end)
  D.fav:SetPoint("TOPRIGHT", D.title, "TOPRIGHT", 30, 2)
  D.fav:SetWidth(24)

  D.meta = W.Text(panel, S.size.small, S.textDim)
  D.meta:SetPoint("TOPLEFT", D.title, "BOTTOMLEFT", 0, -6)
  D.meta:SetPoint("RIGHT", -pad, 0)

  D.score = W.Text(panel, S.size.body)
  D.score:SetPoint("TOPLEFT", D.meta, "BOTTOMLEFT", 0, -6)
  D.score:SetPoint("RIGHT", -pad, 0)

  D.desc = W.Text(panel, S.size.small)
  D.desc:SetPoint("TOPLEFT", D.score, "BOTTOMLEFT", 0, -8)
  D.desc:SetPoint("RIGHT", -pad, 0)
  D.desc:SetHeight(40)
  D.desc:SetJustifyV("TOP")
  D.desc:SetWordWrap(true)

  local codeHead = W.Text(panel, S.size.small, S.gold)
  codeHead:SetPoint("TOPLEFT", D.desc, "BOTTOMLEFT", 0, -8)
  codeHead:SetText("SHARE CODE  (Ctrl+C)")

  D.code = W.CopyBox(panel, S.detail - 2 * pad)
  D.code:SetPoint("TOPLEFT", codeHead, "BOTTOMLEFT", 0, -4)

  D.import = W.Button(panel, "Import", function()
    if not current then return end
    if not ns.Guard.Has("import") then
      D.status:SetText("|cffff5050One-click import isn't available in this game " ..
        "build. Copy the code above and use the Blueprints tab instead.|r")
      return
    end
    -- Importing replaces the player's current layout. With the 12.1 "lost
    -- build" bug still unconfirmed-fixed, one stray click must never be able
    -- to do that: first click arms, second click within 8s imports.
    local now = GetTime()
    if not (D.armedAt and D.armedFor == current.id and (now - D.armedAt) < 8) then
      D.armedAt = now
      D.armedFor = current.id
      -- Partial-import wipe bug: PTR-fixed by Blizzard 2026-08-08. The red
      -- warning only returns if ns.BUG_PARTIAL_WIPE is flipped (bug alive on
      -- live). The two-click confirm stays regardless - replacing a layout
      -- should never be one stray click.
      local t = current.type
      if ns.BUG_PARTIAL_WIPE and (t == "exterior" or t == "interior") then
        local opposite = (t == "exterior") and "interior" or "exterior"
        D.status:SetText("|cffff5050Known 12.1 bug: after importing a " .. t ..
          "-only blueprint, opening Edit Mode on your " .. opposite .. " can " ..
          "wipe its decor. Save a FULL blueprint of your house first - it's " ..
          "the only way to restore. Click Import again to continue.|r")
      else
        D.status:SetText("|cffffcc00This replaces your current layout. Blizzard " ..
          "keeps an auto-save backup, but consider saving a manual blueprint " ..
          "first. Click Import again to continue.|r")
      end
      return
    end
    D.armedAt = nil
    D.status:SetText("Importing...")
    ns.API.Import(current.code, function(ok, err)
      if ok then
        local who = (current.author and current.author.name)
          and (" by " .. current.author.name) or ""
        D.status:SetText("|cff40ff40Imported '" .. (current.title or "build") .. "'" .. who .. "!|r")
        print("|cffc9a227BlueprintBazaar|r imported \"" .. (current.title or "?") .. "\"" .. who .. " - enjoy!")
      else
        D.status:SetText("|cffff5050Import failed (" .. tostring(err) ..
          ") - make sure you're at your house. If you are, the code may " ..
          "no longer be valid.|r")
      end
    end)
  end)
  D.import:SetPoint("TOPLEFT", D.code, "BOTTOMLEFT", 0, -8)

  D.chatLink = W.Button(panel, "Link in Chat", function()
    if not current then return end
    -- credit travels with every share: "[Housing Blueprint] - build by Name"
    local msg = ns.API.Hyperlink(current.code)
    if current.author and current.author.name then
      msg = msg .. " - build by " .. current.author.name
    end
    ChatFrame_OpenChat(msg)
  end)
  D.chatLink:SetPoint("LEFT", D.import, "RIGHT", 6, 0)

  D.verify = W.Button(panel, "Verify", function()
    if not current then return end
    if not ns.Guard.Has("contents") then
      D.status:SetText("|cffff5050Live blueprint lookups aren't available in this " ..
        "game build.|r")
      return
    end
    D.status:SetText("Checking code with server...")
    ns.API.Request(current.code, function(ok, res)
      if ok then
        D.status:SetText("|cff40ff40Code is live. Score updated.|r")
        ns.bus:Fire("SCORE_INVALIDATE", current.id)
        UI.ShowDetail(current)
      else
        D.status:SetText("|cffff5050This code appears to be no longer valid; it " ..
          "may have been retired by the author.|r")
      end
    end, true)
  end)
  D.verify:SetPoint("LEFT", D.chatLink, "RIGHT", 6, 0)

  -- Status text varies from empty to three wrapped lines (the import warning),
  -- so everything below it FLOWS from its bottom edge instead of sitting at a
  -- fixed offset. Fixed offsets are what let the warning eat the item list.
  D.status = W.Text(panel, S.size.small, S.textDim)
  D.status:SetPoint("TOPLEFT", D.import, "BOTTOMLEFT", 0, -8)
  D.status:SetPoint("RIGHT", -pad, 0)
  D.status:SetJustifyV("TOP")
  D.status:SetWordWrap(true)

  local manHead = W.Text(panel, S.size.small, S.gold)
  manHead:SetPoint("TOPLEFT", D.status, "BOTTOMLEFT", 0, -12)
  manHead:SetText("ITEMS")

  -- Each item row is a frame so it can carry a tooltip. C_HousingCatalog has
  -- no vendor or source data (verified 14 Aug), but every decor piece maps to
  -- a real item, and the item's own tooltip is the closest thing to "what is
  -- this and where might I have seen it".
  D.lines = {}
  local anchor, gap = manHead, -8
  for i = 1, MANIFEST_LINES do
    local row = CreateFrame("Frame", nil, panel)
    row:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, gap)
    row:SetPoint("RIGHT", -pad, 0)
    row:SetHeight(14)           -- fixed: empty rows keep the rhythm
    row:EnableMouse(true)

    local line = W.Text(row, S.size.small)
    line:SetAllPoints()
    line:SetWordWrap(false)
    row.text = line

    row:SetScript("OnEnter", function(self)
      if not self.itemID then return end
      GameTooltip:SetOwner(self, "ANCHOR_LEFT")
      GameTooltip:SetItemByID(self.itemID)
      GameTooltip:AddLine(" ")
      GameTooltip:AddLine("Click: Wowhead link  -  Shift-click: link in chat", 0.6, 0.58, 0.52)
      GameTooltip:AddLine("The Decor Catalog (press H) lists this item's vendor and zone.", 0.6, 0.58, 0.52)
      GameTooltip:Show()
    end)
    row:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- The API has no source data, but the answer lives two places already:
    -- Wowhead (per-item page, crowd-sourced sources) and Blizzard's own Decor
    -- Catalog. Click hands the player a copyable Wowhead URL; shift-click
    -- links the item in chat so they can ask a guildie.
    row:EnableMouse(true)
    row:SetScript("OnMouseUp", function(self)
      if not self.itemID then return end
      if IsShiftKeyDown() then
        local link = select(2, C_Item.GetItemInfo(self.itemID))
        if link then ChatFrame_OpenChat(link) end
        return
      end
      if D.ShowItemURL then D.ShowItemURL(self.itemID) end
    end)

    D.lines[i] = row
    anchor, gap = row, -4
  end

  D.more = W.Text(panel, S.size.tiny, S.textDim)
  D.more:SetPoint("TOPLEFT", D.lines[MANIFEST_LINES], "BOTTOMLEFT", 0, -8)

  -- artist credit block (bottom of pane): the name is the product's promise
  D.credit = W.Text(panel, S.size.small, S.gold)
  D.credit:SetPoint("BOTTOMLEFT", pad, pad + 14)
  D.creditLink = W.Text(panel, S.size.tiny, S.textDim)
  D.creditLink:SetPoint("BOTTOMLEFT", pad, pad)
  D.creditLink:SetPoint("RIGHT", -pad, 0)
  D.creditLink:SetWordWrap(false)

  -- Popup with a copyable Wowhead address for one item. WoW cannot open
  -- links, so a copy box is as close as an addon gets to "show me where this
  -- comes from".
  local urlBox
  function D.ShowItemURL(itemID)
    if not urlBox then
      -- Parented to UIParent, not the library window: you may want to keep
      -- this open beside the game while you look something up.
      urlBox = W.Panel(UIParent)
      urlBox:SetSize(380, 124)   -- room for the button to clear the field
      local saved = ns.db.settings.itemBoxPos
      if saved then
        urlBox:SetPoint(saved.point, UIParent, saved.point, saved.x, saved.y)
      else
        urlBox:SetPoint("CENTER")
      end
      urlBox:SetFrameStrata("DIALOG")
      urlBox:EnableMouse(true)
      urlBox:SetMovable(true)
      urlBox:SetClampedToScreen(true)
      urlBox:RegisterForDrag("LeftButton")
      urlBox:SetScript("OnDragStart", function(self) self:StartMoving() end)
      urlBox:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, _, x, y = self:GetPoint()
        ns.db.settings.itemBoxPos = { point = point, x = x, y = y }
      end)

      urlBox.title = W.Text(urlBox, S.size.body, S.gold)
      urlBox.title:SetPoint("TOPLEFT", pad, -pad)
      urlBox.title:SetPoint("RIGHT", -pad, 0)
      urlBox.title:SetWordWrap(false)

      urlBox.hint = W.Text(urlBox, S.size.tiny, S.textDim)
      urlBox.hint:SetPoint("TOPLEFT", pad, -30)
      urlBox.hint:SetPoint("RIGHT", -pad, 0)
      urlBox.hint:SetText("Ctrl+C to copy, or type your own search. Drag to "
        .. "move. The Decor Catalog (H) lists vendor and zone in game.")
      urlBox.hint:SetHeight(28)
      urlBox.hint:SetWordWrap(true)

      -- A real edit box, not a locked copy field: people want to tweak the
      -- search, paste something else in, or clear it entirely.
      urlBox.box = W.CopyBox(urlBox, 380 - 2 * pad)
      urlBox.box:SetPoint("TOPLEFT", pad, -62)
      urlBox.box:SetScript("OnCursorChanged", nil)   -- stop forcing selection
      urlBox.box:SetScript("OnTextChanged", nil)     -- let it be typed in
      urlBox.box:SetScript("OnEscapePressed", function(self)
        self:ClearFocus(); urlBox:Hide()
      end)

      local close = W.Button(urlBox, "Close", function() urlBox:Hide() end)
      close:SetPoint("TOPRIGHT", urlBox.box, "BOTTOMRIGHT", 0, -8)
      urlBox:Hide()
    end
    local name = C_Item and C_Item.GetItemNameByID and C_Item.GetItemNameByID(itemID)
    urlBox.title:SetText(name or ("Item " .. itemID))
    urlBox.box:SetValue("wowhead.com/item=" .. itemID)
    urlBox:Show()
    urlBox.box:SetFocus()
    urlBox.box:HighlightText()   -- selected on open, but free to edit after
  end

  D.empty = W.Text(panel, S.size.body, S.textDim)
  D.empty:SetPoint("CENTER")
  D.empty:SetText("Select a blueprint")
end

--- Re-run the current build's layout (called after a resize)
function UI.RelayoutDetail()
  if current and D.title then UI.ShowDetail(current) end
end

function UI.ShowDetail(entry)
  current = entry
  if not D.title then return end
  D.empty:Hide()

  D.heroIndex = 1
  D.UpdateHero()

  D.title:SetText(entry.title or "?")
  -- Build the meta line from what we actually know. An unknown type on an
  -- ad-hoc code should say nothing, not "?" - the type enum shifted between
  -- PTR and live once already.
  local bits = {}
  if entry.type then bits[#bits + 1] = W.TypeLabel(entry.type) end
  if entry.author and entry.author.name then
    bits[#bits + 1] = "by " .. entry.author.name
  end
  if entry.budget and entry.budget.interior and entry.budget.interior > 0 then
    bits[#bits + 1] = "budget " .. entry.budget.interior
  end
  D.meta:SetText(table.concat(bits, "  -  "))
  D.desc:SetText(entry.description or "")
  D.code:SetValue(entry.code or "")
  local aname = entry.author and entry.author.name
  D.credit:SetText(aname and ("Build by " .. aname) or "")
  local alink = entry.author and entry.author.link
  D.creditLink:SetText((alink and alink ~= "") and alink or "")
  D.fav.text:SetTextColor(ns.db.favorites[entry.id] and S.amber[1] or S.textDim[1],
    ns.db.favorites[entry.id] and S.amber[2] or S.textDim[2],
    ns.db.favorites[entry.id] and S.amber[3] or S.textDim[3])
  D.status:SetText("")

  local sc = UI.ScoreFor(entry)
  if sc.pct == nil then
    D.score:SetText("|cff888888Buildability unknown - click Verify for a live check.|r")
  else
    local r, g, b = ns.Score.RingColor(sc.pct)
    local pctText = math.floor(sc.pct * 100 + 0.5) .. "% buildable"
    local extra = ""
    if #sc.missing > 0 then
      extra = "  -  missing " .. #sc.missing .. " items"
      if sc.estGoldTotal > 0 then extra = extra .. ", ~" .. sc.estGoldTotal .. "g" end
      if sc.unobtainableCount > 0 then
        extra = extra .. ", " .. sc.unobtainableCount .. " unobtainable"
      end
    end
    D.score:SetText(("|cff%02x%02x%02x%s|r%s"):format(r * 255, g * 255, b * 255, pctText, extra))
  end

  -- manifest lines: live payload wins, else baked manifest
  local live = ns.API.Cached(entry.code)
  local rows = {}
  if live then
    for _, e in ipairs(live.entries) do
      rows[#rows + 1] = { name = e.name or ("record " .. tostring(e.recordID)),
        qty = e.qty, missing = e.missing, recordID = e.recordID }
    end
  else
    for _, m in ipairs(entry.manifest or {}) do
      local rid = m.recordID or m.entryID
      local owned = ns.Catalog.OwnedByRecord(rid)
      rows[#rows + 1] = { name = m.name or ("record " .. tostring(rid)),
        qty = m.qty or 1, missing = owned and math.max(0, (m.qty or 1) - owned) or nil,
        recordID = rid }
    end
  end
  for i = 1, MANIFEST_LINES do
    local row = rows[i]
    if row then
      local color = "|cff40ff40"
      local suffix = ""
      if row.missing == nil then color = "|cff999999"
      elseif row.missing > 0 then
        color = "|cffff5050"
        suffix = "  (missing " .. row.missing .. ")"
      end
      D.lines[i].text:SetText(color .. row.qty .. "x " .. row.name .. suffix .. "|r")
      D.lines[i].itemID = row.recordID and ns.Catalog.ItemIDByRecord(row.recordID) or nil
      D.lines[i]:Show()
    else
      D.lines[i].text:SetText("")
      D.lines[i].itemID = nil
      D.lines[i]:Hide()
    end
  end

  -- Only show as many item lines as actually fit above the credit block. The
  -- pane is resizable, and a short window used to run the list straight
  -- through the credit and out of the frame.
  local shown = math.min(#rows, MANIFEST_LINES)
  local floorY = D.credit:GetTop()
  if floorY then
    for i = 1, shown do
      local line = D.lines[i]
      local bottom = line:GetBottom()
      if bottom and bottom < floorY + 14 then
        line:Hide()
        shown = math.min(shown, i - 1)
      end
    end
  end

  local hidden = #rows - shown
  D.more:SetText(hidden > 0 and ("+" .. hidden .. " more items") or "")
  -- the "+N more" line needs the same headroom as the rows it summarises
  if floorY then
    local mb = D.more:GetBottom()
    if mb and mb < floorY + 14 then D.more:SetText("") end
  end
end
