---@class AddonPrivate
local Private = select(2, ...)

---@class RewardPreview
local preview = {}
Private.RewardPreview = preview

local const = Private.constants

local PANEL_W = 230
local PANEL_H = 300

local FrameBackdrop = {
    bgFile   = "interface\\achievementframe\\ui-guildachievement-parchment",
    edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
    edgeSize = 16,
    insets   = { left = 4, right = 4, top = 4, bottom = 4 },
}

-- ---------------------------------------------------------------------------
-- Reward schema (add to an achievement in ExpansionData.lua):
--   reward  = { type = "mount",   id = 1234 }          -- C_MountJournal mountID
--   reward  = { type = "pet",     id = 3253 }          -- battle-pet speciesID
--   reward  = { type = "item",    id = 202223 }        -- transmog/weapon/armor itemID
--   reward  = { type = "creature",id = 56789 }         -- creatureID
--   reward  = { type = "display", id = 98765 }         -- raw creatureDisplayID (housing decor / anything)
--   reward  = { type = "model",   file = 1030000 }     -- explicit model file id/path (housing decor)
--   -- optional: name = "Reins of ...", to override the auto label.
-- Multiple rewards: use  rewards = { {..}, {..} }  and the panel shows arrows.
-- ---------------------------------------------------------------------------

-- Best-effort: resolve a reward to a renderable model on the widget.
local function applyReward(model, r)
    if not (model and r) then return false end
    model:ClearModel()
    local t, ok = r.type, false
    if t == "mount" and C_MountJournal and C_MountJournal.GetMountInfoExtraByID then
        local displayID = C_MountJournal.GetMountInfoExtraByID(r.id)
        if displayID and displayID > 0 then ok = pcall(model.SetDisplayInfo, model, displayID) end
    elseif t == "pet" and C_PetJournal and C_PetJournal.GetPetInfoBySpeciesID then
        local displayID = select(12, C_PetJournal.GetPetInfoBySpeciesID(r.id))
        if displayID and displayID > 0 then ok = pcall(model.SetDisplayInfo, model, displayID) end
    elseif t == "creature" then
        ok = pcall(model.SetCreature, model, r.id)
    elseif t == "display" then
        ok = pcall(model.SetDisplayInfo, model, r.id)
    elseif t == "model" then
        ok = pcall(model.SetModel, model, r.file or r.id)
    elseif t == "item" or t == "transmog" then
        pcall(model.SetUnit, model, "player")
        if model.Undress then pcall(model.Undress, model) end
        if model.TryOn then ok = pcall(model.TryOn, model, "item:" .. tostring(r.id))
        elseif model.SetItem then ok = pcall(model.SetItem, model, r.id) end
    elseif t == "unit" then
        ok = pcall(model.SetUnit, model, r.id or "player")   -- mainly for testing
    end
    return ok
end

-- Human label for the reward.
local function rewardName(r)
    if r.name then return r.name end
    local t = r.type
    if t == "mount" and C_MountJournal and C_MountJournal.GetMountInfoByID then
        local name = C_MountJournal.GetMountInfoByID(r.id)
        if name then return name end
    elseif t == "pet" and C_PetJournal and C_PetJournal.GetPetInfoBySpeciesID then
        local name = C_PetJournal.GetPetInfoBySpeciesID(r.id)
        if name then return name end
    elseif (t == "item" or t == "transmog") then
        local name = (C_Item and C_Item.GetItemNameByID and C_Item.GetItemNameByID(r.id)) or GetItemInfo(r.id)
        if name then return name end
    end
    local label = ({ mount = "Mount", pet = "Pet", item = "Item", transmog = "Transmog",
        creature = "Creature", display = "Decor", model = "Decor" })[t] or "Reward"
    return label
end

-- Normalise an achievement's reward field(s) to an array.
local function rewardList(ach)
    if not ach then return {} end
    if type(ach.rewards) == "table" and #ach.rewards > 0 then return ach.rewards end
    if type(ach.reward) == "table" then return { ach.reward } end
    return {}
end

-- ---------------------------------------------------------------------------
-- Auto-detect a MOUNT reward from the achievement's reward TEXT. The achievement
-- API gives us only a display string (no reward ID), but that string contains the
-- mount's name, and the Mount Journal maps every mount name -> mountID. So we can
-- recover the ID by matching. (Pets/transmog/decor have no clean full name index,
-- so those still need a manual `reward` entry.)
-- ---------------------------------------------------------------------------
local mountNameIndex          -- name -> mountID, built once
local autoCache = {}          -- achievementID -> resolved reward table or false

local petNameIndex            -- name -> speciesID, built once

local function buildMountIndex()
    mountNameIndex = {}
    if not (C_MountJournal and C_MountJournal.GetMountIDs) then return end
    for _, id in ipairs(C_MountJournal.GetMountIDs()) do
        local name = C_MountJournal.GetMountInfoByID(id)
        if name and name ~= "" then mountNameIndex[name] = id end
    end
end

-- Enumerate ALL battle pets (including uncollected reward pets) so their names
-- can be matched. This briefly widens the pet-journal collected/uncollected
-- filters, then restores them.
local function buildPetIndex()
    petNameIndex = {}
    local cj = C_PetJournal
    if not (cj and cj.GetNumPets and cj.GetPetInfoByIndex) then return end
    local savedCollected, savedUncollected
    pcall(function()
        if cj.IsFilterChecked then
            savedCollected = cj.IsFilterChecked(LE_PET_JOURNAL_FLAG_COLLECTED)
            savedUncollected = cj.IsFilterChecked(LE_PET_JOURNAL_FLAG_NOT_COLLECTED)
        end
        if cj.SetFilterChecked then
            cj.SetFilterChecked(LE_PET_JOURNAL_FLAG_COLLECTED, true)
            cj.SetFilterChecked(LE_PET_JOURNAL_FLAG_NOT_COLLECTED, true)
        end
    end)
    local num = cj.GetNumPets() or 0
    for i = 1, num do
        local _, speciesID, _, _, _, _, _, name = cj.GetPetInfoByIndex(i)
        if speciesID and name and name ~= "" then petNameIndex[name] = speciesID end
    end
    pcall(function()
        if cj.SetFilterChecked and savedCollected ~= nil then
            cj.SetFilterChecked(LE_PET_JOURNAL_FLAG_COLLECTED, savedCollected)
            cj.SetFilterChecked(LE_PET_JOURNAL_FLAG_NOT_COLLECTED, savedUncollected)
        end
    end)
end

-- Transmog appearance name -> itemID. Built lazily IN THE BACKGROUND (spread over
-- frames) because the collection is huge and each appearance needs a second call;
-- doing it all at once would hitch the game.
local transmogNameIndex
local transmogState           -- nil | "building" | "done"

local function finishTransmogBuild()
    transmogState = "done"
    wipe(autoCache)            -- allow appearance rewards to resolve now
    -- if the panel is waiting on an appearance for the achievement the player is
    -- still looking at, re-resolve and show it
    local last = preview.lastRequested
    local uiOpen = Private.UI and Private.UI.frame and Private.UI.frame.frame and Private.UI.frame.frame:IsShown()
    if last and uiOpen then preview:ShowForAchievement(last) end
end

local function startTransmogBuild()
    if transmogState then return end
    transmogState = "building"
    transmogNameIndex = {}
    local ctc = C_TransmogCollection
    if not (ctc and ctc.GetCategoryAppearances and ctc.GetAppearanceSources
        and Enum and Enum.TransmogCollectionType and C_Timer and C_Timer.NewTicker) then
        transmogState = "done"; return
    end
    -- flatten every appearance visualID across all categories
    local work = {}
    for _, catType in pairs(Enum.TransmogCollectionType) do
        local ok, apps = pcall(ctc.GetCategoryAppearances, catType)
        if ok and type(apps) == "table" then
            for _, a in ipairs(apps) do
                if a.visualID then work[#work + 1] = a.visualID end
            end
        end
    end
    -- resolve source names in chunks so no single frame stalls
    local i, ticker = 0, nil
    ticker = C_Timer.NewTicker(0, function()
        local processed = 0
        while i < #work and processed < 300 do
            i = i + 1
            processed = processed + 1
            local ok, sources = pcall(ctc.GetAppearanceSources, work[i])
            if ok and type(sources) == "table" then
                for _, s in ipairs(sources) do
                    if s.name and s.name ~= "" and s.itemID then
                        transmogNameIndex[s.name] = s.itemID
                    end
                end
            end
        end
        if i >= #work then
            ticker:Cancel()
            finishTransmogBuild()
        end
    end)
end

-- longest whole-name match of an index's names inside the reward text
local function bestNameMatch(index, rewardText)
    local bestName, bestVal
    for name, val in pairs(index) do
        if #name >= 5 and rewardText:find(name, 1, true) then
            if not bestName or #name > #bestName then bestName, bestVal = name, val end
        end
    end
    return bestName, bestVal
end

local function autoResolveReward(achievementID)
    local cached = autoCache[achievementID]
    if cached ~= nil then return cached or nil end
    local rewardText = select(11, GetAchievementInfo(achievementID))
    if not rewardText or rewardText == "" then
        autoCache[achievementID] = false
        return nil
    end
    if not mountNameIndex then buildMountIndex() end
    local name, id = bestNameMatch(mountNameIndex, rewardText)
    if id then
        local r = { type = "mount", id = id, name = name, auto = true }
        autoCache[achievementID] = r; return r
    end
    if not petNameIndex then buildPetIndex() end
    name, id = bestNameMatch(petNameIndex, rewardText)
    if id then
        local r = { type = "pet", id = id, name = name, auto = true }
        autoCache[achievementID] = r; return r
    end
    -- transmog appearance: index is built in the background; if it isn't ready
    -- yet, kick it off and return nil WITHOUT caching so it re-resolves later
    if transmogState ~= "done" then
        startTransmogBuild()
        return nil
    end
    name, id = bestNameMatch(transmogNameIndex, rewardText)
    if id then
        local r = { type = "item", id = id, name = name, auto = true }
        autoCache[achievementID] = r; return r
    end
    autoCache[achievementID] = false
    return nil
end

function preview:IsEnabled()
    local s = Private.Settings and Private.Settings:Preview()
    return not s or s.enabled ~= false
end

function preview:SetEnabled(enabled)
    local s = Private.Settings and Private.Settings:Preview()
    if s then s.enabled = enabled and true or false end
    if not enabled and self.frame then self.frame:Hide() end
end

-- Render the current reward index.
function preview:Render()
    local f = self.frame
    if not f or not self.rewards then return end
    local n = #self.rewards
    local r = self.rewards[self.index]
    if not r then f:Hide() return end

    self.model.facing = 0.5
    local ok = applyReward(self.model, r)
    if ok then
        pcall(self.model.SetFacing, self.model, 0.5)
        pcall(self.model.SetModelScale, self.model, 1)
    end
    local label = rewardName(r)
    if n > 1 then label = label .. string.format("  |cff888888(%d/%d)|r", self.index, n) end
    f.rewardName:SetText(label)
    f.prev:SetShown(n > 1)
    f.next:SetShown(n > 1)
    f.noModel:SetShown(not ok)
    self.model:SetShown(ok)
end

-- Show the panel for the given achievement, or hide it if there's nothing to show.
function preview:ShowForAchievement(achievementID)
    if not self:IsEnabled() then return end
    if not achievementID then return end
    self.lastRequested = achievementID
    local ach = Private.ExpansionUtil and Private.ExpansionUtil:GetAchievement(achievementID)
    local rewards = rewardList(ach)
    if #rewards == 0 then
        -- no hand-authored reward -> try to recover a mount from the reward text
        local auto = autoResolveReward(achievementID)
        if auto then rewards = { auto } end
    end
    if #rewards == 0 then
        if self.frame then self.frame:Hide() end
        return
    end
    if not self.frame then self:Initialize() end

    self.rewards = rewards
    self.index = 1
    self.frame.title:SetText((ach and ach.name) or GetAchievementInfo(achievementID) or "Reward")

    -- dock to the right edge of the main window if it's open, else screen centre
    local anchor = Private.UI and Private.UI.frame and Private.UI.frame.frame
    self.frame:ClearAllPoints()
    if anchor and anchor:IsVisible() then
        self.frame:SetPoint("TOPLEFT", anchor, "TOPRIGHT", 6, 0)
    else
        self.frame:SetPoint("CENTER", UIParent, "CENTER", 300, 0)
    end
    self.frame:Show()
    self:Render()
end

function preview:Toggle()
    self:SetEnabled(not self:IsEnabled())
end

-- Test the panel with no real data: renders your own character. Two entries so
-- the next/prev arrows are exercised too.
function preview:ShowDemo()
    if not self.frame then self:Initialize() end
    self.rewards = {
        { type = "unit", id = "player", name = "Demo: your character" },
        { type = "unit", id = "player", name = "Demo: entry 2 (arrows work)" },
    }
    self.index = 1
    self.frame.title:SetText("Reward preview (demo)")
    self.frame:ClearAllPoints()
    local anchor = Private.UI and Private.UI.frame and Private.UI.frame.frame
    if anchor and anchor:IsVisible() then
        self.frame:SetPoint("TOPLEFT", anchor, "TOPRIGHT", 6, 0)
    else
        self.frame:SetPoint("CENTER", UIParent, "CENTER", 300, 0)
    end
    self.frame:Show()
    self:Render()
end

function preview:Initialize()
    if self.frame then return end

    local f = CreateFrame("Frame", "MetaTrackerRewardPreview", UIParent, "BackdropTemplate")
    f:SetSize(PANEL_W, PANEL_H)
    f:SetBackdrop(FrameBackdrop)
    f:SetBackdropBorderColor(1, 1, 1, 1)
    f:SetFrameStrata("HIGH")
    f:SetClampedToScreen(true)
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    self.frame = f

    local titleBar = f:CreateTexture(nil, "ARTWORK")
    titleBar:SetTexture("Interface\\AchievementFrame\\UI-Achievement-Borders")
    titleBar:SetPoint("TOPLEFT", 10, -9)
    titleBar:SetPoint("TOPRIGHT", -10, -9)
    titleBar:SetHeight(22)
    titleBar:SetVertexColor(1, 1, 1, 0.8)
    titleBar:SetTexCoord(0, 0.9765625, 0.66015625, 0.73828125)

    f.title = f:CreateFontString(nil, "OVERLAY")
    f.title:SetFontObject(const.FONT_OBJECTS.HEADING)
    f.title:SetPoint("TOPLEFT", titleBar, "TOPLEFT", 4, -3)
    f.title:SetPoint("TOPRIGHT", titleBar, "TOPRIGHT", -20, -3)
    f.title:SetJustifyH("CENTER")
    f.title:SetWordWrap(false)

    local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", 3, 3)
    close:SetScale(0.8)

    -- 3D model
    local model = CreateFrame("DressUpModel", nil, f)
    model:SetPoint("TOPLEFT", 12, -34)
    model:SetPoint("BOTTOMRIGHT", -12, 52)
    model:EnableMouse(true)
    model:EnableMouseWheel(true)
    model:SetScript("OnMouseDown", function(m, btn)
        if btn == "LeftButton" then m.isRotating = true; m.rotCursorX = GetCursorPosition() end
    end)
    model:SetScript("OnMouseUp", function(m) m.isRotating = false end)
    model:SetScript("OnUpdate", function(m)
        if m.isRotating then
            local x = GetCursorPosition()
            local dx = x - (m.rotCursorX or x)
            m.rotCursorX = x
            m.facing = (m.facing or 0) + dx * 0.015
            pcall(m.SetFacing, m, m.facing)
        end
    end)
    model:SetScript("OnMouseWheel", function(m, delta)
        m.camScale = math.max(0.5, math.min(4, (m.camScale or 1) - delta * 0.15))
        pcall(m.SetCamDistanceScale, m, m.camScale)
    end)
    self.model = model

    -- fallback text when a reward can't be rendered as a model
    f.noModel = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    f.noModel:SetPoint("CENTER", model, "CENTER", 0, 0)
    f.noModel:SetText("No 3D preview for this reward.")
    f.noModel:Hide()

    -- reward name (page number is folded into this text when there are several)
    f.rewardName = f:CreateFontString(nil, "OVERLAY")
    f.rewardName:SetFontObject(const.FONT_OBJECTS.NORMAL)
    f.rewardName:SetPoint("BOTTOMLEFT", 12, 26)
    f.rewardName:SetPoint("BOTTOMRIGHT", -12, 26)
    f.rewardName:SetJustifyH("CENTER")
    f.rewardName:SetWordWrap(true)
    f.rewardName:SetMaxLines(2)

    f.prev = CreateFrame("Button", nil, f)
    f.prev:SetSize(24, 24)
    f.prev:SetPoint("BOTTOMLEFT", 10, 4)
    f.prev:SetNormalTexture("Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Up")
    f.prev:SetPushedTexture("Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Down")
    f.prev:SetScript("OnClick", function()
        self.index = (self.index - 2) % #self.rewards + 1
        self:Render()
    end)

    f.next = CreateFrame("Button", nil, f)
    f.next:SetSize(24, 24)
    f.next:SetPoint("BOTTOMRIGHT", -10, 4)
    f.next:SetNormalTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up")
    f.next:SetPushedTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Down")
    f.next:SetScript("OnClick", function()
        self.index = self.index % #self.rewards + 1
        self:Render()
    end)

    -- reset rotation/zoom
    local reset = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    reset:SetSize(50, 18)
    reset:SetPoint("BOTTOM", 0, 4)
    reset:SetText("Reset")
    reset:SetScript("OnClick", function()
        self.model.facing = 0.5
        self.model.camScale = 1
        pcall(self.model.SetFacing, self.model, 0.5)
        pcall(self.model.SetCamDistanceScale, self.model, 1)
    end)

    f:Hide()
end
