--[[
    Commander 3.0 - Achievements Data Source

    Provides access to player achievements.
    Commands: achievements, achievement

    Supports both Retail and Classic (where available)
]]

local ADDON_NAME, C3 = ...

-- Use shared utilities
local Colorize = function(...) return C3.Colors.Colorize(...) end

-------------------------------------------------------------------------------
-- Achievement Type Registration
-------------------------------------------------------------------------------

C3.Types:Register("Achievement", {
    description = "Achievement from achievement panel",
    source = "achievements",
    properties = {
        { "id", "Achievement ID number" },
        { "name", "Achievement name" },
        { "description", "Achievement description text" },
        { "icon", "Icon texture ID" },
        { "points", "Achievement points awarded" },
        { "completed", "true if completed" },
        { "completedDate", "Completion date string" },
        { "wasEarnedByMe", "true if earned by this character" },
        { "category", "Category ID" },
        { "categoryName", "Category name" },
        { "isGuild", "true if guild achievement" },
    },
    format = function(self)
        local name = self.name or "Unknown Achievement"
        local points = self.points or 0

        if self.completed then
            name = Colorize(name, "accent.success")
        else
            name = Colorize(name, "status.dim")
        end

        local pointStr = points > 0 and string.format(" [%d]", points) or ""
        return name .. pointStr
    end
})

-- Register formatter (no icons for performance)
C3.Formatter:Register("Achievement", {
    format = function(obj)
        local name = obj.name or "Unknown Achievement"
        local points = obj.points or 0

        if obj.completed then
            name = Colorize(name, "accent.success")
        else
            name = Colorize(name, "status.dim")
        end

        local pointStr = points > 0 and string.format(" [%d]", points) or ""
        return name .. pointStr
    end,
    list = function(obj)
        local status = obj.completed and "+" or "-"
        return string.format("[%s] %s", status, obj.name or "?")
    end,
    compact = function(obj)
        local status = obj.completed and "+" or "-"
        return string.format("[%s] %s", status, obj.name or "?")
    end
})

-------------------------------------------------------------------------------
-- Achievement Category Cache
-------------------------------------------------------------------------------

local categoryCache = {}

local function getCategoryName(categoryID)
    if not categoryID then return "Unknown" end

    if categoryCache[categoryID] then
        return categoryCache[categoryID]
    end

    local name = GetCategoryInfo(categoryID)
    categoryCache[categoryID] = name or "Unknown"
    return categoryCache[categoryID]
end

-------------------------------------------------------------------------------
-- achievements - List achievements
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "achievements",
    description = "List achievements (default limit 200, use --limit or filters)",
    category = "data",
    producesType = "Achievement",
    acceptsInput = false,
    flags = {
        completed = { short = "c", description = "Show only completed achievements" },
        incomplete = { short = "i", description = "Show only incomplete achievements" },
        recent = { short = "r", description = "Show recently completed (last 30 days)" },
        guild = { short = "g", description = "Show guild achievements" },
        limit = { short = "l", description = "Max results (default 200, 0 for unlimited)" },
    },
    args = {
        { name = "category", required = false, description = "Category ID or name to filter" }
    },
    execute = function(ctx)
        local completedOnly = ctx:HasFlag("completed") or ctx:HasFlag("c")
        local incompleteOnly = ctx:HasFlag("incomplete") or ctx:HasFlag("i")
        local recentOnly = ctx:HasFlag("recent") or ctx:HasFlag("r")
        local guildOnly = ctx:HasFlag("guild") or ctx:HasFlag("g")
        local limitFlag = ctx:GetFlag("limit") or ctx:GetFlag("l")
        local categoryFilter = ctx:GetArg(1)

        -- Guard: API availability
        if not GetAchievementInfo then
            return ctx:Error("Achievements not available in this game version")
        end

        local results = {}
        local processed = 0
        local YIELD_INTERVAL = 10
        local MAX_RESULTS = limitFlag and tonumber(limitFlag) or 200
        if MAX_RESULTS == 0 then MAX_RESULTS = math.huge end
        local hitLimit = false

        local categories = GetCategoryList()

        for _, categoryID in ipairs(categories) do repeat
            if hitLimit then break end

            local categoryName = getCategoryName(categoryID)

            -- Guard: Category filter
            if categoryFilter then
                local filterLower = categoryFilter:lower()
                local filterNum = tonumber(categoryFilter)
                if filterNum then
                    if categoryID ~= filterNum then break end
                else
                    if not (categoryName and categoryName:lower():find(filterLower, 1, true)) then break end
                end
            end

            local numAchievements = GetCategoryNumAchievements(categoryID, guildOnly)

            for i = 1, numAchievements do repeat
                if hitLimit then break end

                -- Yield periodically
                processed = processed + 1
                if processed % YIELD_INTERVAL == 0 then
                    coroutine.yield()
                end

                local achievementID = GetAchievementInfo(categoryID, i)

                -- Guard: Invalid achievement ID
                if not achievementID then break end

                local id, name, points, completed, month, day, year,
                      description, flags, icon, rewardText, isGuild,
                      wasEarnedByMe, earnedBy = GetAchievementInfo(achievementID)

                -- Guard: Invalid name
                if not name then break end

                -- Guard: Guild filter
                if guildOnly and not isGuild then break end

                -- Guard: Completed filter
                if completedOnly and not completed then break end

                -- Guard: Incomplete filter
                if incompleteOnly and completed then break end

                -- Calculate completion timestamp
                local completedDate = nil
                if completed and month and day and year then
                    completedDate = time({
                        year = 2000 + year,
                        month = month,
                        day = day,
                        hour = 0,
                        min = 0,
                        sec = 0
                    })
                end

                -- Guard: Recent filter
                if recentOnly then
                    if not completedDate then break end
                    local thirtyDaysAgo = time() - (30 * 24 * 60 * 60)
                    if completedDate < thirtyDaysAgo then break end
                end

                -- All guards passed - add result
                table.insert(results, C3.Types.Create("Achievement", {
                    id = id,
                    name = name,
                    description = description,
                    icon = icon,
                    points = points,
                    completed = completed,
                    completedDate = completedDate,
                    wasEarnedByMe = wasEarnedByMe,
                    category = categoryID,
                    categoryName = categoryName,
                    isGuild = isGuild,
                    rewardText = rewardText,
                }))

                if #results >= MAX_RESULTS then
                    hitLimit = true
                end
            until true end
        until true end

        if hitLimit then
            table.insert(results, C3.Types.Create("Text", {
                content = Colorize(string.format("... limited to %d results. Use --limit N or filters (-c, -i, -r, category)", MAX_RESULTS), "status.dim")
            }))
        end

        return results
    end
})

-------------------------------------------------------------------------------
-- Native Achievement Search Helper
-------------------------------------------------------------------------------

-- Cache for async search results
local searchCache = {
    query = nil,
    results = {},
    inFlight = false,
    lastUpdate = 0,
}

local function sliceResults(source, limit)
    if not source then return {} end
    if #source <= limit then return source end
    local out = {}
    for i = 1, limit do
        out[i] = source[i]
    end
    return out
end

-- Use WoW's native indexed search (SetAchievementSearchString)
-- Note: This API is async - fires ACHIEVEMENT_SEARCH_UPDATED when ready
local function searchAchievements(query, limit)
    limit = limit or 15
    local results = {}

    if not SetAchievementSearchString or not GetNumFilteredAchievements then
        return results
    end

    -- Avoid re-triggering the same search while results are cached or in flight.
    if query == searchCache.query then
        if searchCache.inFlight then
            return sliceResults(searchCache.results, limit)
        end
        if searchCache.results then
            return sliceResults(searchCache.results, limit)
        end
    end

    -- Store query BEFORE triggering search (needed for async handler)
    searchCache.query = query
    searchCache.inFlight = true

    -- Trigger WoW's native indexed search
    -- Returns true if search finished immediately, false if async
    local searchFinished = SetAchievementSearchString(query)

    -- If search didn't finish, return any cached results we have
    if not searchFinished then
        return sliceResults(searchCache.results, limit)
    end

    -- Search finished - get results
    local numResults = GetNumFilteredAchievements()

    for i = 1, math.min(numResults, limit) do
        local achievementID = GetFilteredAchievementID(i)
        if achievementID then
            local id, name, points, completed, month, day, year,
                  description, flags, icon = GetAchievementInfo(achievementID)
            if name then
                table.insert(results, {
                    id = id,
                    name = name,
                    nameLower = name:lower(),
                    points = points,
                    completed = completed,
                    icon = icon,
                    description = description,
                })
            end
        end
    end

    -- Cache results
    searchCache.query = query
    searchCache.results = results
    searchCache.inFlight = false
    searchCache.lastUpdate = GetTime()

    return results
end

-- Listen for async search completion
local searchEventFrame = CreateFrame("Frame")
searchEventFrame:RegisterEvent("ACHIEVEMENT_SEARCH_UPDATED")
searchEventFrame:SetScript("OnEvent", function()
    -- When search completes, update cache with current results
    if searchCache.query and searchCache.query ~= "" then
        local results = {}
        local numResults = GetNumFilteredAchievements()

        for i = 1, math.min(numResults, 15) do
            local achievementID = GetFilteredAchievementID(i)
            if achievementID then
                local id, name, points, completed, month, day, year,
                      description, flags, icon = GetAchievementInfo(achievementID)
                if name then
                    table.insert(results, {
                        id = id,
                        name = name,
                        nameLower = name:lower(),
                        points = points,
                        completed = completed,
                        icon = icon,
                        description = description,
                    })
                end
            end
        end

        searchCache.results = results
        searchCache.inFlight = false
        searchCache.lastUpdate = GetTime()

        -- Trigger intellisense refresh if available (use C3.Terminal, not C3.UI.TerminalWindow)
        local terminal = C3.Terminal
        if terminal and terminal.input then
            local input = terminal.input
            if input.onSuggestionsRequested and input.currentInput then
                input.onSuggestionsRequested(input.currentInput, input.editBox:GetCursorPosition())
            end
        end
    end
end)

-------------------------------------------------------------------------------
-- Intellisense Provider
-------------------------------------------------------------------------------

local MAX_SUGGESTIONS = 15

-- Fuzzy scoring for ranking results
local function calculateFuzzyScore(needle, haystack)
    if not needle or needle == "" then return 100 end
    if not haystack or haystack == "" then return 0 end

    local needleLower = needle:lower()
    local haystackLower = haystack:lower()

    -- Exact match
    if needleLower == haystackLower then
        return 100
    end

    -- Prefix match (starts with)
    if haystackLower:find(needleLower, 1, true) == 1 then
        return 90
    end

    -- Space-insensitive matching: "globalswarming" matches "Global Swarming"
    local haystackNoSpaces = haystackLower:gsub("%s+", "")
    local needleNoSpaces = needleLower:gsub("%s+", "")

    if needleNoSpaces == haystackNoSpaces then
        return 88  -- Almost as good as exact match
    end

    if haystackNoSpaces:find(needleNoSpaces, 1, true) == 1 then
        return 85  -- Space-insensitive prefix
    end

    -- Word boundary match (needle matches start of a word)
    if haystackLower:find("%s" .. needleLower, 1, true) or
       haystackLower:find("^" .. needleLower) then
        return 80
    end

    -- Contains match (with spaces)
    if haystackLower:find(needleLower, 1, true) then
        return 70
    end

    -- Contains match (without spaces)
    if haystackNoSpaces:find(needleNoSpaces, 1, true) then
        return 65
    end

    -- Fuzzy match - all characters appear in order
    local j = 1
    for i = 1, #needleLower do
        j = haystackLower:find(needleLower:sub(i, i), j, true)
        if not j then
            return 0
        end
        j = j + 1
    end

    return 50
end

C3.Intellisense:RegisterProvider("Achievements", function(partial, ctx)
    local results = {}

    -- Require at least 3 characters (matches WoW's default UI behavior)
    if not partial or #partial < 3 then
        return results
    end

    -- Collect achievements from multiple searches
    local seen = {}
    local allAchievements = {}

    local function addAchievements(achs)
        for _, ach in ipairs(achs) do
            if not seen[ach.id] then
                seen[ach.id] = true
                allAchievements[#allAchievements + 1] = ach
            end
        end
    end

    -- Search 1: Full query (catches metadata matches like "500 mounts" -> "Thanks for the carry!")
    addAchievements(searchAchievements(partial, MAX_SUGGESTIONS * 2))

    -- Search 2: Short prefix for fuzzy name matching ("globalswarming" -> "global" -> "Global Swarming")
    if #partial > 6 then
        addAchievements(searchAchievements(partial:sub(1, 6), MAX_SUGGESTIONS * 2))
    end

    -- Apply fuzzy scoring
    local scored = {}
    for _, ach in ipairs(allAchievements) do
        local score = calculateFuzzyScore(partial, ach.name)

        -- If name doesn't match at all (score 0), it's a metadata match - give it a low score
        if score == 0 then
            score = 25  -- Metadata match (description, reward text, etc.)
        end

        scored[#scored + 1] = { ach = ach, score = score }
    end

    -- Sort by score descending, then by name length (shorter = better)
    table.sort(scored, function(a, b)
        if a.score ~= b.score then
            return a.score > b.score
        end
        return #a.ach.name < #b.ach.name
    end)

    -- Build final results
    for i = 1, math.min(#scored, MAX_SUGGESTIONS) do
        local ach = scored[i].ach
        local status = ach.completed and Colorize("+", "accent.success") or Colorize("-", "status.dim")
        local pointsStr = ach.points > 0 and string.format(" [%d pts]", ach.points) or ""

        results[#results + 1] = {
            text = ach.name,
            description = status .. pointsStr,
            icon = ach.icon,
        }
    end

    return results
end)

-------------------------------------------------------------------------------
-- Criteria Formatting Helper (using Tree Formatter)
-------------------------------------------------------------------------------

local MAX_CRITERIA_DISPLAY = 8  -- Show first N criteria per level

-- Build criteria nodes for an achievement
local function buildCriteriaNodes(achievementID, depth)
    depth = depth or 0
    if depth > 2 then return {} end  -- Max 3 levels deep

    local nodes = {}
    local numCriteria = GetAchievementNumCriteria(achievementID) or 0

    for i = 1, math.min(numCriteria, MAX_CRITERIA_DISPLAY) do
        local criteriaString, criteriaType, completed, quantity,
              reqQuantity, _, _, assetID = GetAchievementCriteriaInfo(achievementID, i)

        if criteriaString and criteriaString ~= "" then
            local node = {
                text = criteriaString,
                completed = completed,
                quantity = quantity,
                reqQuantity = reqQuantity,
                criteriaType = criteriaType,
                assetID = assetID,
                depth = depth,
            }
            table.insert(nodes, node)
        end
    end

    -- Add "... and N more" indicator as a special node
    if numCriteria > MAX_CRITERIA_DISPLAY then
        table.insert(nodes, {
            text = string.format("... and %d more", numCriteria - MAX_CRITERIA_DISPLAY),
            isOverflow = true,
        })
    end

    return nodes
end

-- Get children for a criteria node (for nested achievements)
local function getCriteriaChildren(node)
    if node.isOverflow then return nil end
    -- Type 8 = achievement criteria
    if node.criteriaType == 8 and node.assetID then
        return buildCriteriaNodes(node.assetID, (node.depth or 0) + 1)
    end
    return nil
end

-- Format a criteria node's label
local function getCriteriaLabel(node)
    if node.isOverflow then
        return Colorize(node.text, "status.dim")
    end

    local status = node.completed and Colorize("+", "accent.success")
                                   or Colorize("-", "status.dim")

    local progress = ""
    if not node.completed and node.reqQuantity and node.reqQuantity > 1 then
        progress = Colorize(string.format(" (%d/%d)", node.quantity or 0, node.reqQuantity), "status.dim")
    end

    local text = node.completed and node.text or Colorize(node.text, "status.dim")

    return status .. " " .. text .. progress
end

-- Format criteria using the tree formatter
local function formatCriteria(achievementID)
    local nodes = buildCriteriaNodes(achievementID, 0)
    if #nodes == 0 then return {} end

    return C3.Formatter:FormatTree(nodes, getCriteriaChildren, getCriteriaLabel, {
        maxDepth = 3,
        maxItems = 50,
    })
end

-------------------------------------------------------------------------------
-- achievement - Get specific achievement info
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "achievement",
    description = "Get achievement info by name or ID",
    category = "data",
    acceptsInput = false,
    args = {
        { name = "query", required = true, description = "Achievement name or ID",
          provider = "Achievements" }
    },
    flags = {
        track = { short = "t", description = "Toggle tracking this achievement" },
        link = { short = "l", description = "Link achievement to chat" },
        open = { short = "o", description = "Open in achievement panel" },
    },
    execute = function(ctx)
        local query = table.concat(ctx:GetArgs(), " ")
        if not query or query == "" then
            return ctx:Error("Usage: achievement <name or id>")
        end

        if not GetAchievementInfo then
            return ctx:Error("Achievements not available in this game version")
        end

        -- Try as achievement ID first
        local achievementID = tonumber(query)
        local id, name, points, completed, month, day, year,
              description, achFlags, icon, rewardText, isGuild,
              wasEarnedByMe

        if achievementID then
            id, name, points, completed, month, day, year,
            description, achFlags, icon, rewardText, isGuild,
            wasEarnedByMe = GetAchievementInfo(achievementID)
        else
            -- Use native indexed search (instant)
            local results = searchAchievements(query, 1)
            if results[1] then
                achievementID = results[1].id
                id, name, points, completed, month, day, year,
                description, achFlags, icon, rewardText, isGuild,
                wasEarnedByMe = GetAchievementInfo(achievementID)
            end
        end

        if not name then
            return ctx:Error("Achievement not found: " .. query)
        end

        -- Handle --track flag: toggle tracking
        if ctx:HasFlag("track") or ctx:HasFlag("t") then
            local tracked = { GetTrackedAchievements() }
            local isTracked = false
            for _, tid in ipairs(tracked) do
                if tid == achievementID then
                    isTracked = true
                    break
                end
            end

            if isTracked then
                RemoveTrackedAchievement(achievementID)
                return C3.Types.Create("Text", {
                    content = Colorize("Untracked:", "accent.warning") .. " " .. name
                })
            else
                AddTrackedAchievement(achievementID)
                return C3.Types.Create("Text", {
                    content = Colorize("Now tracking:", "accent.success") .. " " .. name
                })
            end
        end

        -- Handle --link flag: insert into chat
        if ctx:HasFlag("link") or ctx:HasFlag("l") then
            local link = GetAchievementLink(achievementID)
            if link then
                local editBox = ChatFrame1EditBox
                if editBox and editBox:IsVisible() then
                    editBox:Insert(link)
                else
                    ChatFrame_OpenChat(link)
                end
                return C3.Types.Create("Text", {
                    content = "Linked: " .. link
                })
            else
                return ctx:Error("Could not get achievement link")
            end
        end

        -- Handle --open flag: open achievement panel
        if ctx:HasFlag("open") or ctx:HasFlag("o") then
            if OpenAchievementFrameToAchievement then
                OpenAchievementFrameToAchievement(achievementID)
                return C3.Types.Create("Text", {
                    content = "Opened achievement panel to: " .. name
                })
            else
                return ctx:Error("Achievement panel not available")
            end
        end

        -- Default: show detailed achievement info with criteria
        local lines = {}

        -- Header: Name [points] - Status (X/Y)
        local statusText = completed and Colorize("Completed", "accent.success")
                                     or Colorize("Incomplete", "status.dim")
        local numCriteria = GetAchievementNumCriteria(achievementID) or 0
        local numCompleted = 0
        if numCriteria > 0 then
            for i = 1, numCriteria do
                local _, _, criteriaCompleted = GetAchievementCriteriaInfo(achievementID, i)
                if criteriaCompleted then numCompleted = numCompleted + 1 end
            end
            statusText = statusText .. string.format(" (%d/%d)", numCompleted, numCriteria)
        end

        local header = string.format("%s [%d pts] - %s", name, points or 0, statusText)
        table.insert(lines, header)

        -- Description (dimmed)
        if description and description ~= "" then
            table.insert(lines, Colorize(description, "status.dim"))
        end

        -- Reward text if present
        if rewardText and rewardText ~= "" then
            table.insert(lines, Colorize("Reward: " .. rewardText, "status.gold"))
        end

        -- Criteria display
        if numCriteria > 0 then
            local criteriaLines = formatCriteria(achievementID, 0)
            for _, line in ipairs(criteriaLines) do
                table.insert(lines, line)
            end
        end

        return C3.Types.Create("Text", {
            content = table.concat(lines, "\n")
        })
    end
})

-------------------------------------------------------------------------------
-- achpoints - Show total achievement points
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "achpoints",
    description = "Show total achievement points",
    category = "data",
    acceptsInput = false,
    execute = function(ctx)
        if not GetTotalAchievementPoints then
            return ctx:Error("Achievements not available in this game version")
        end

        local totalPoints = GetTotalAchievementPoints()
        local guildPoints = GetTotalAchievementPoints(true) or 0

        return C3.Types.Create("Text", {
            content = string.format(
                "Achievement Points: %s  Guild: %s",
                Colorize(tostring(totalPoints), "status.gold"),
                Colorize(tostring(guildPoints), "status.gold")
            )
        })
    end
})

return true
