--[[
    Commander 3.0 - Equipment Sets Data Source

    Provides access to player equipment sets.
    Commands: sets, equip

    Uses C_EquipmentSet API (Retail)
]]

local ADDON_NAME, C3 = ...

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

-------------------------------------------------------------------------------
-- EquipmentSet Type Registration
-------------------------------------------------------------------------------

C3.Types:Register("EquipmentSet", {
    description = "Saved equipment set",
    source = "sets",
    properties = {
        { "id", "Set ID number" },
        { "name", "Set name" },
        { "icon", "Icon texture ID" },
        { "numItems", "Total items in set" },
        { "numEquipped", "Items currently equipped" },
        { "numMissing", "Items missing from bags" },
        { "isEquipped", "true if fully equipped" },
        { "specID", "Linked spec ID (if any)" },
        { "specName", "Linked spec name" },
    },
})

C3.Formatter:Register("EquipmentSet", {
    format = function(obj)
        local icon = obj.icon and (C3.Formatter:Icon(obj.icon) .. " ") or ""
        local name = obj.name or "Unknown"

        -- Status indicator
        local status = ""
        if obj.isEquipped then
            status = Colorize("*", "accent.success") .. " "
        elseif obj.numMissing and obj.numMissing > 0 then
            status = Colorize(string.format("[%d missing] ", obj.numMissing), "accent.warning")
        end

        -- Spec binding
        local specStr = ""
        if obj.specName and obj.specName ~= "" then
            specStr = " " .. Colorize("(" .. obj.specName .. ")", "status.dim")
        end

        return status .. icon .. name .. specStr
    end,
    list = function(obj)
        local equipped = obj.isEquipped and "*" or " "
        return string.format("%s %s (%d/%d)", equipped, obj.name or "?",
            obj.numEquipped or 0, obj.numItems or 0)
    end,
    compact = function(obj)
        return obj.name or "?"
    end
})

-------------------------------------------------------------------------------
-- Helper Functions
-------------------------------------------------------------------------------

---Find equipment set by exact name (case-insensitive)
---@param query string Set name to find
---@return number|nil setID
---@return string|nil setName
local function findEquipmentSet(query)
    if not C_EquipmentSet then
        return nil, nil
    end

    local queryLower = query:lower()
    local setIDs = C_EquipmentSet.GetEquipmentSetIDs()

    for _, setID in ipairs(setIDs) do
        local name = C_EquipmentSet.GetEquipmentSetInfo(setID)
        if name and name:lower() == queryLower then
            return setID, name
        end
    end

    return nil, nil
end

---Equip an equipment set by ID
---@param setID number
---@param setName string
---@param ctx table Context
---@return table Result
local function equipSet(setID, setName, ctx)
    -- Combat check
    if InCombatLockdown() then
        return ctx:Error("Cannot change equipment during combat")
    end

    -- Use the equipment set
    local success = C_EquipmentSet.UseEquipmentSet(setID)

    if success then
        return C3.Types.Create("Text", {
            content = "Equipped set: " .. (setName or "Unknown")
        })
    else
        return ctx:Error("Failed to equip set: " .. (setName or "Unknown"))
    end
end

-------------------------------------------------------------------------------
-- EquipmentSets Intellisense Provider
-------------------------------------------------------------------------------

local MAX_SUGGESTIONS = C3.Constants.MAX_SUGGESTIONS

-- Helper: Check if string needs quoting for intellisense
local function intellisenseNeedsQuoting(str)
    return str and str:find("[%s;|&]")
end

-- Helper: Quote a string for intellisense if needed
local function intellisenseMaybeQuote(str)
    if intellisenseNeedsQuoting(str) then
        local escaped = str:gsub('"', '\\"')
        return '"' .. escaped .. '"'
    end
    return str
end

C3.Intellisense:RegisterProvider("EquipmentSets", function(partial, ctx)
    if not C_EquipmentSet then
        return {}
    end

    -- Strip leading/trailing quotes from partial (user may have typed them)
    local cleanPartial = partial and partial:gsub('^"', ''):gsub('"$', '') or ""

    local setIDs = C_EquipmentSet.GetEquipmentSetIDs()

    local sets = {}
    for _, setID in ipairs(setIDs) do
        local name, icon, _, isEquipped = C_EquipmentSet.GetEquipmentSetInfo(setID)
        if name and fuzzyMatch(cleanPartial, name) then
            sets[#sets + 1] = {
                name = name,
                icon = icon,
                isEquipped = isEquipped,
            }
            if #sets >= MAX_SUGGESTIONS then break end
        end
    end

    local results = {}
    for _, s in ipairs(sets) do
        local quotedName = intellisenseMaybeQuote(s.name)
        results[#results + 1] = {
            text = quotedName,  -- Quoted for safe insertion
            displayText = s.name,  -- Unquoted for display
            description = s.isEquipped and "Equipped" or "Equipment Set",
            icon = s.icon,
        }
    end
    return results
end)

-------------------------------------------------------------------------------
-- sets - List equipment sets
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "sets",
    description = "List equipment sets",
    category = "data",
    acceptsInput = false,
    flags = {
        all = { short = "a", description = "Show all sets (including incomplete)" },
    },
    execute = function(ctx)
        if not C_EquipmentSet then
            return ctx:Error("Equipment sets not available in this game version")
        end

        local showAll = ctx:HasFlag("all") or ctx:HasFlag("a")

        return ctx:Produce(function(emit)
            local setIDs = C_EquipmentSet.GetEquipmentSetIDs()

            for _, setID in ipairs(setIDs) do
                local name, icon, _, isEquipped, numItems, numEquipped,
                      numMissing, _, numIgnored = C_EquipmentSet.GetEquipmentSetInfo(setID)

                if name then
                    -- Get bound spec if any
                    local specID, specName = nil, nil
                    local assignedSpec = C_EquipmentSet.GetEquipmentSetAssignedSpec(setID)
                    if assignedSpec and assignedSpec > 0 then
                        specID = assignedSpec
                        local _, specNameResult = GetSpecializationInfoByID(specID)
                        specName = specNameResult
                    end

                    local include = showAll or (numMissing == 0)

                    if include then
                        emit(C3.Types.Create("EquipmentSet", {
                            id = setID,
                            name = name,
                            icon = icon,
                            numItems = numItems or 0,
                            numEquipped = numEquipped or 0,
                            numMissing = numMissing or 0,
                            isEquipped = isEquipped or false,
                            specID = specID,
                            specName = specName,
                        }))
                    end
                end
            end
        end)
    end
})

-------------------------------------------------------------------------------
-- equip - Equip an equipment set
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "equip",
    description = "Equip an equipment set",
    category = "action",
    args = {
        { name = "name", required = false, description = "Equipment set name (fuzzy match)",
          provider = "EquipmentSets" }
    },
    execute = function(ctx)
        if not C_EquipmentSet then
            return ctx:Error("Equipment sets not available in this game version")
        end

        -- Check for pipeline input first
        if ctx:HasInput() then
            local input = ctx:GetInput()
            -- Pipeline mode: equip from piped set
            local items = {}

            -- Handle both iterator (function) and array (table) input
            if type(input) == "function" then
                for obj in input do
                    items[#items + 1] = obj
                end
            elseif type(input) == "table" then
                items = input._type and { input } or input
            end

            if #items > 0 then
                local setObj = items[1]
                if setObj._type == "EquipmentSet" then
                    return equipSet(setObj.id, setObj.name, ctx)
                end
            end
        end

        -- Argument mode
        local query = ctx:GetArg(1)
        if not query or query == "" then
            return ctx:Error("Usage: equip <set name> or sets | where ... | equip")
        end

        -- Find set by name (fuzzy)
        local setID, setName = findEquipmentSet(query)
        if not setID then
            return ctx:Error("Equipment set not found: " .. query)
        end

        return equipSet(setID, setName, ctx)
    end
})

-------------------------------------------------------------------------------
-- set - Equipment set management with CRUD operations
-------------------------------------------------------------------------------

-- Helper: Get set ID by name
local function getSetIDByName(name)
    local setIDs = C_EquipmentSet.GetEquipmentSetIDs()
    local nameLower = name:lower()
    for _, setID in ipairs(setIDs) do
        local setName = C_EquipmentSet.GetEquipmentSetInfo(setID)
        if setName and setName:lower() == nameLower then
            return setID, setName
        end
    end
    return nil, nil
end

-- Helper: Find spec by name or number
local function findSpec(query)
    local specNum = tonumber(query)
    if specNum then
        if specNum >= 1 and specNum <= GetNumSpecializations() then
            local specID = GetSpecializationInfo(specNum)
            local _, specName = GetSpecializationInfoByID(specID)
            return specID, specName
        end
        return nil, nil
    end

    -- Search by name
    local queryLower = query:lower()
    for i = 1, GetNumSpecializations() do
        local specID, specName = GetSpecializationInfo(i)
        if specName and specName:lower():find(queryLower, 1, true) then
            return specID, specName
        end
    end
    return nil, nil
end

-- Helper: List equipment sets
local function listEquipmentSets()
    local results = {}
    local setIDs = C_EquipmentSet.GetEquipmentSetIDs()

    for _, setID in ipairs(setIDs) do
        local name, icon, _, isEquipped, numItems, numEquipped,
              numMissing = C_EquipmentSet.GetEquipmentSetInfo(setID)

        if name then
            local specID, specName = nil, nil
            local assignedSpec = C_EquipmentSet.GetEquipmentSetAssignedSpec(setID)
            if assignedSpec and assignedSpec > 0 then
                specID = assignedSpec
                local _, specNameResult = GetSpecializationInfoByID(specID)
                specName = specNameResult
            end

            table.insert(results, C3.Types.Create("EquipmentSet", {
                id = setID,
                name = name,
                icon = icon,
                numItems = numItems or 0,
                numEquipped = numEquipped or 0,
                numMissing = numMissing or 0,
                isEquipped = isEquipped or false,
                specID = specID,
                specName = specName,
            }))
        end
    end

    return results
end

-- Helper: Get spec suggestions
local function getSpecSuggestions(partial)
    local suggestions = {}
    local partialLower = partial and partial:lower() or ""

    for i = 1, GetNumSpecializations() do
        local specID, specName, _, icon = GetSpecializationInfo(i)
        if specName then
            if partialLower == "" or specName:lower():find(partialLower, 1, true) then
                suggestions[#suggestions + 1] = {
                    text = specName,
                    description = "Specialization",
                    icon = icon,
                }
            end
        end
    end

    -- Also allow numeric input
    if partialLower == "" then
        for i = 1, GetNumSpecializations() do
            suggestions[#suggestions + 1] = {
                text = tostring(i),
                description = "Spec #" .. i,
            }
        end
    end

    return suggestions
end

-- Helper: Collect all args starting from an index
local function collectArgs(ctx, startIndex)
    local args = {}
    local i = startIndex
    while ctx:GetArg(i) do
        args[#args + 1] = ctx:GetArg(i)
        i = i + 1
    end
    return args
end

-- Helper: Parse tokens respecting quoted strings
local function parseTokens(input)
    local tokens = {}
    local i = 1
    local len = #input

    while i <= len do
        -- Skip whitespace
        while i <= len and input:sub(i, i):match("%s") do
            i = i + 1
        end
        if i > len then break end

        local char = input:sub(i, i)
        if char == '"' then
            -- Quoted string
            i = i + 1  -- skip opening quote
            local start = i
            while i <= len and input:sub(i, i) ~= '"' do
                if input:sub(i, i) == '\\' then
                    i = i + 2  -- skip escape sequence
                else
                    i = i + 1
                end
            end
            tokens[#tokens + 1] = input:sub(start, i - 1):gsub('\\"', '"')
            i = i + 1  -- skip closing quote
        else
            -- Unquoted token
            local start = i
            while i <= len and not input:sub(i, i):match("%s") do
                i = i + 1
            end
            tokens[#tokens + 1] = input:sub(start, i - 1)
        end
    end

    return tokens
end

C3.Command:Define({
    name = "set",
    description = "Equipment set management",
    category = "action",
    acceptsInput = false,
    args = {
        { name = "action_or_name", required = false, description = "Action or set name",
          provider = function(partial, ctx)
              -- Parse tokens respecting quotes (for set names with spaces)
              local tokens = parseTokens(partial or "")
              local numTokens = #tokens

              -- Check if input ends with space (partial has spaces stripped by intellisense)
              local beforeCursor = ctx and ctx.beforeCursor or ""
              local endsWithSpace = beforeCursor:match("%s$") ~= nil

              -- Helper to get equipment sets with optional prefix for insertion
              local function getSets(filter, prefix)
                  local provider = C3.Intellisense and C3.Intellisense:GetProvider("EquipmentSets")
                  local sets = provider and provider(filter or "", ctx) or {}
                  for _, s in ipairs(sets) do
                      -- Base provider already sets:
                      --   s.text = quoted name (e.g., '"My Set"' or 'simple')
                      --   s.displayText = unquoted name for display
                      s.highlightPartial = filter  -- Highlight based on what user typed
                      if prefix and prefix ~= "" then
                          s.text = prefix .. " " .. s.text  -- Insert with prefix + already-quoted name
                      end
                      -- If no prefix, s.text is already correctly set by base provider
                  end
                  return sets
              end

              -- Helper to get specs with prefix for insertion
              local function getSpecs(filter, prefix)
                  local specs = getSpecSuggestions(filter)
                  if prefix and prefix ~= "" then
                      for _, s in ipairs(specs) do
                          s.displayText = s.text  -- Show just the spec name
                          s.highlightPartial = filter  -- Highlight based on what user typed for spec name
                          s.text = prefix .. " " .. s.text  -- Insert with prefix
                      end
                  end
                  return specs
              end

              -- No tokens or typing first token: suggest sets first, then subcommands
              if numTokens == 0 or (numTokens == 1 and not endsWithSpace) then
                  local typingPartial = (tokens[1] or ""):lower()
                  local suggestions = {}

                  -- Sets first (for direct equip)
                  for _, s in ipairs(getSets(typingPartial, nil)) do
                      suggestions[#suggestions + 1] = s
                  end

                  -- Then subcommands
                  local subcommands = {
                      { text = "list", description = "List all equipment sets" },
                      { text = "save", description = "Save current gear (creates or updates)" },
                      { text = "delete", description = "Delete an equipment set" },
                      { text = "rename", description = "Rename an equipment set" },
                      { text = "assign", description = "Assign set to a specialization" },
                      { text = "unassign", description = "Remove spec assignment from set" },
                  }

                  for _, cmd in ipairs(subcommands) do
                      if typingPartial == "" or cmd.text:find(typingPartial, 1, true) == 1 then
                          suggestions[#suggestions + 1] = cmd
                      end
                  end

                  return suggestions
              end

              local subcommand = tokens[1]:lower()

              -- "list", "save" -> no further suggestions
              if subcommand == "list" or subcommand == "save" then return {} end

              -- Commands that take a set name as second arg
              local WANTS_SET = { delete = true, rename = true, assign = true, unassign = true }
              if not WANTS_SET[subcommand] then return {} end

              -- "subcommand " -> suggest sets (prefix with subcommand)
              if numTokens == 1 and endsWithSpace then return getSets("", subcommand) end

              -- "subcommand partial" -> suggest matching sets (prefix with subcommand)
              if numTokens == 2 and not endsWithSpace then return getSets(tokens[2], subcommand) end

              -- After set name selected...
              local setName = tokens[2]
              local quotedSetName = intellisenseMaybeQuote(setName)
              local assignPrefix = subcommand .. " " .. quotedSetName

              -- "rename setname " -> new name is freeform
              if subcommand == "rename" then return {} end

              -- "update/delete/unassign setname " -> done
              if subcommand ~= "assign" then return {} end

              -- "assign setname " -> suggest specs (prefix with "assign setname")
              if numTokens == 2 and endsWithSpace then return getSpecs("", assignPrefix) end

              -- "assign setname partial" -> suggest matching specs
              if numTokens == 3 and not endsWithSpace then return getSpecs(tokens[3], assignPrefix) end

              return {}
          end },
        { name = "arg2", required = false, description = "Set name or additional argument" },
        { name = "arg3", required = false, description = "Additional argument (icon, new name, spec)" },
    },
    execute = function(ctx)
        if not C_EquipmentSet then
            return ctx:Error("Equipment sets not available in this game version")
        end

        local action = ctx:GetArg(1)
        local arg2 = ctx:GetArg(2)
        local arg3 = ctx:GetArg(3)

        -- No argument: list sets
        if not action or action == "" or action == "list" then
            return listEquipmentSets()
        end

        -- SAVE: Create or update equipment set
        if action == "save" then
            local parts = collectArgs(ctx, 2)
            if #parts == 0 then return ctx:Error("Usage: set save <name> [icon]") end

            -- Extract icon if last arg is numeric or item link
            local iconID = 134400  -- Default: question mark
            local lastArg = parts[#parts]
            local numIcon = tonumber(lastArg)
            local itemID = not numIcon and lastArg:match("^item:(%d+)")

            if numIcon then
                iconID = numIcon
                table.remove(parts)
            elseif itemID then
                iconID = C_Item.GetItemIconByID(tonumber(itemID)) or iconID
                table.remove(parts)
            end

            if #parts == 0 then return ctx:Error("Usage: set save <name> [icon]") end

            local setName = table.concat(parts, " ")

            -- Update if exists
            local existingID, existingName = getSetIDByName(setName)
            if existingID then
                local _, currentIcon = C_EquipmentSet.GetEquipmentSetInfo(existingID)
                C_EquipmentSet.SaveEquipmentSet(existingID, currentIcon)
                return ctx:Text(Colorize("Updated:", "accent.success") .. " " .. existingName)
            end

            -- Create new
            C_EquipmentSet.CreateEquipmentSet(setName, iconID)
            if getSetIDByName(setName) then
                return ctx:Text(Colorize("Created:", "accent.success") .. " " .. setName)
            end
            return ctx:Error("Failed to create equipment set: " .. setName)
        end

        -- DELETE: Remove equipment set
        if action == "delete" then
            if not arg2 or arg2 == "" then return ctx:Error("Usage: set delete <name>") end

            local setID, setName = findEquipmentSet(arg2)
            if not setID then return ctx:Error("Equipment set not found: " .. arg2) end

            C_EquipmentSet.DeleteEquipmentSet(setID)
            return ctx:Text(Colorize("Deleted:", "accent.warning") .. " " .. setName)
        end

        -- RENAME: Rename equipment set
        if action == "rename" then
            local parts = collectArgs(ctx, 2)
            if #parts < 2 then return ctx:Error("Usage: set rename <old name> <new name>") end

            -- Parse old/new names - if last part has space it was quoted (= new name)
            -- Otherwise first part is old, rest is new
            local oldName, newName
            if #parts == 2 then
                oldName, newName = parts[1], parts[2]
            elseif parts[#parts]:find(" ") then
                newName = table.remove(parts)
                oldName = table.concat(parts, " ")
            else
                oldName = table.remove(parts, 1)
                newName = table.concat(parts, " ")
            end

            if oldName == "" or newName == "" then return ctx:Error("Usage: set rename <old name> <new name>") end

            local setID, setName = findEquipmentSet(oldName)
            if not setID then return ctx:Error("Equipment set not found: " .. oldName) end
            if getSetIDByName(newName) then return ctx:Error("Equipment set already exists: " .. newName) end

            -- Try RenameEquipmentSet first, fall back to ModifyEquipmentSet
            if C_EquipmentSet.RenameEquipmentSet then
                C_EquipmentSet.RenameEquipmentSet(setID, newName)
            elseif C_EquipmentSet.ModifyEquipmentSet then
                local _, icon = C_EquipmentSet.GetEquipmentSetInfo(setID)
                C_EquipmentSet.ModifyEquipmentSet(setID, newName, icon)
            else
                return ctx:Error("Rename not supported in this WoW version")
            end
            return ctx:Text(Colorize("Renamed:", "accent.success") .. " " .. setName .. " → " .. newName)
        end

        -- ASSIGN: Link set to specialization
        if action == "assign" then
            if not arg2 or arg2 == "" or not arg3 or arg3 == "" then
                return ctx:Error("Usage: set assign <set name> <spec name or number>")
            end

            local setID, setName = findEquipmentSet(arg2)
            if not setID then return ctx:Error("Equipment set not found: " .. arg2) end

            local specID, specName = findSpec(arg3)
            if not specID then return ctx:Error("Specialization not found: " .. arg3) end

            C_EquipmentSet.AssignSpecToEquipmentSet(setID, specID)
            return ctx:Text(Colorize("Assigned:", "accent.success") .. " " .. setName .. " → " .. specName)
        end

        -- UNASSIGN: Remove spec assignment
        if action == "unassign" then
            if not arg2 or arg2 == "" then return ctx:Error("Usage: set unassign <name>") end

            local setID, setName = findEquipmentSet(arg2)
            if not setID then return ctx:Error("Equipment set not found: " .. arg2) end

            if C_EquipmentSet.UnassignSpecFromEquipmentSet then
                C_EquipmentSet.UnassignSpecFromEquipmentSet(setID)
            elseif C_EquipmentSet.AssignSpecToEquipmentSet then
                C_EquipmentSet.AssignSpecToEquipmentSet(setID, 0)
            else
                return ctx:Error("Unassign not supported in this WoW version")
            end
            return ctx:Text(Colorize("Unassigned:", "accent.warning") .. " " .. setName .. " (no longer linked to a spec)")
        end

        -- Otherwise treat as set name to equip
        local setID, setName = findEquipmentSet(action)
        if not setID then
            return ctx:Error("Equipment set not found: " .. action .. "\nSubcommands: list, save, delete, rename, assign, unassign")
        end

        return equipSet(setID, setName, ctx)
    end
})

return true
