--[[
    Commander 3.0 - Filesystem Commands

    Unix-like filesystem commands for the VFS.
]]

local ADDON_NAME, C3 = ...

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

-------------------------------------------------------------------------------
-- Intellisense Providers
-------------------------------------------------------------------------------

local DIR_CACHE = {
    abs = nil,
    tilde = nil,
    dirCount = nil,
    home = nil,
}

local function buildDirectoryCache(vfs)
    local driver = vfs and vfs.driver
    if not driver or not driver.meta then
        return nil, nil
    end

    local abs = {}
    for path, node in pairs(driver.meta) do
        if node and node.type == "dir" then
            abs[#abs + 1] = path
        end
    end
    table.sort(abs)

    local tilde = nil
    local home = vfs:GetHomePath()
    if home and home ~= "" then
        tilde = {}
        for _, path in ipairs(abs) do
            if path:sub(1, #home) == home then
                local suffix = path:sub(#home + 1)
                if suffix == "" then
                    tilde[#tilde + 1] = "~"
                else
                    tilde[#tilde + 1] = "~" .. suffix
                end
            end
        end
        table.sort(tilde)
    end

    return abs, tilde, home
end

local function getDirectorySuggestions()
    local vfs = C3:GetVFS()
    if not vfs then return {} end

    local stats = vfs:GetStats() or {}
    local dirCount = stats.directories
    local home = vfs:GetHomePath()
    if not DIR_CACHE.abs or DIR_CACHE.dirCount ~= dirCount or DIR_CACHE.home ~= home then
        local abs, tilde, cachedHome = buildDirectoryCache(vfs)
        DIR_CACHE.abs = abs
        DIR_CACHE.tilde = tilde
        DIR_CACHE.dirCount = dirCount
        DIR_CACHE.home = cachedHome
    end

    return DIR_CACHE.abs or {}, DIR_CACHE.tilde or {}, DIR_CACHE.home
end

local function registerDirectoryProvider()
    if not (C3.Intellisense and C3.Intellisense.RegisterProvider) then return end

    C3.Intellisense:RegisterProvider("Directories", function(partial, ctx)
        local vfs = C3:GetVFS()
        if not vfs then return {} end
        partial = partial or ""
        local cwd = vfs:GetCwd()

        if partial == "" then
            local home = vfs:GetHomePath()
            local defaults = {}
            if home and home ~= "" then
                defaults[#defaults + 1] = { text = "~", description = home }
            end
            defaults[#defaults + 1] = { text = "/", description = "root" }
            defaults[#defaults + 1] = { text = "/home", description = "dir" }
            defaults[#defaults + 1] = { text = "/sys", description = "dir" }
            defaults[#defaults + 1] = { text = "/tmp", description = "dir" }
            if cwd and cwd ~= "" then
                defaults[#defaults + 1] = { text = cwd, description = "cwd" }
            end
            return defaults
        end

        local abs, tilde, home = getDirectorySuggestions()
        local useTilde = partial:sub(1, 1) == "~"
        local list = useTilde and tilde or abs

        local items = {}
        if useTilde and home and home ~= "" then
            items[#items + 1] = { text = "~", description = home }
        end
        for _, path in ipairs(list or {}) do
            items[#items + 1] = { text = path, description = "dir" }
        end

        local limit = C3.Intellisense.MAX_SUGGESTIONS
        local results = C3.Intellisense.fuzzyFilter(items, partial, function(item)
            return item.text
        end, limit)

        if cwd and cwd ~= "" then
            for i, item in ipairs(results) do
                if item.text == cwd then
                    table.remove(results, i)
                    results[#results + 1] = item
                    break
                end
            end
        end

        return results
    end)
end

local function markFilesystemPrompt(ctx)
    if ctx and ctx.session and ctx.session.EnableCwdPrompt then
        ctx.session:EnableCwdPrompt()
    end
end

-------------------------------------------------------------------------------
-- cd - Change Directory
-------------------------------------------------------------------------------

local function cdCommand(ctx)
    markFilesystemPrompt(ctx)
    local path = ctx:GetArg(1) or "~"

    -- Handle home shortcut: ~ expands to character's home directory
    if path == "~" then
        path = C3.VFSSavedVarsDriver._getCharacterHome()
    elseif path:sub(1, 2) == "~/" then
        -- Handle ~/subpath
        path = C3.VFSSavedVarsDriver._getCharacterHome() .. path:sub(2)
    end

    local vfs = C3:GetVFS()
    local ok, err = vfs:SetCwd(path)

    if not ok then
        return ctx:Error(err)
    end

    return ctx:Text(vfs:GetCwd())
end

C3.Commands.cd = cdCommand

-------------------------------------------------------------------------------
-- Command Registry Definitions (for Intellisense)
-------------------------------------------------------------------------------

local function registerFilesystemCommands()
    if not (C3.Command and C3.Command.Define) then return end

    C3.Command:Define({
        name = "cd",
        description = "Change directory",
        category = "filesystem",
        acceptsInput = false,
        args = {
            { name = "path", required = false, description = "Directory path", provider = "Directories" },
        },
        execute = cdCommand
    })
end

if C3.OnInit then
    C3:OnInit(function()
        registerDirectoryProvider()
        registerFilesystemCommands()
    end)
else
    registerDirectoryProvider()
    registerFilesystemCommands()
end

-------------------------------------------------------------------------------
-- pwd - Print Working Directory
-------------------------------------------------------------------------------

C3.Commands.pwd = function(ctx)
    markFilesystemPrompt(ctx)
    local vfs = C3:GetVFS()
    return ctx:Text(vfs:GetCwd())
end

-------------------------------------------------------------------------------
-- ls - List Directory
-------------------------------------------------------------------------------

C3.Commands.ls = function(ctx)
    markFilesystemPrompt(ctx)
    local path = ctx:GetArg(1)
    local vfs = C3:GetVFS()

    local entries, err = vfs:List(path)
    if not entries then
        return ctx:Error(err)
    end

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

    -- Filter hidden files unless -a
    if not showAll then
        local filtered = {}
        for _, entry in ipairs(entries) do
            if entry.name:sub(1, 1) ~= "." then
                table.insert(filtered, entry)
            end
        end
        entries = filtered
    end

    if #entries == 0 then
        return ctx:Text("(empty)")
    end

    if longFormat then
        -- Long format with details
        return ctx:Produce(function(emit)
            for _, entry in ipairs(entries) do
                local typeChar = entry.type == "dir" and "d" or "-"
                local size = entry.size or 0
                local modified = entry.modified and date("%Y-%m-%d %H:%M", entry.modified) or "unknown"

                emit(C3.Types.Create("FileEntry", {
                    name = entry.name,
                    path = entry.path,
                    type = entry.type,
                    size = size,
                    modified = modified,
                    display = string.format("%s %6d  %s  %s",
                        typeChar, size, modified, entry.name)
                }))
            end
        end)
    else
        -- Simple format
        return ctx:Produce(function(emit)
            for _, entry in ipairs(entries) do
                local suffix = entry.type == "dir" and "/" or ""
                emit(C3.Types.Create("FileEntry", {
                    name = entry.name,
                    path = entry.path,
                    type = entry.type,
                    display = entry.name .. suffix
                }))
            end
        end)
    end
end

-------------------------------------------------------------------------------
-- cat - Concatenate/Display File
-------------------------------------------------------------------------------

C3.Commands.cat = function(ctx)
    markFilesystemPrompt(ctx)
    local path = ctx:GetArg(1)

    if not path then
        return ctx:Error("Usage: cat <file>")
    end

    local vfs = C3:GetVFS()
    local content, err = vfs:Read(path)

    if not content then
        return ctx:Error(err)
    end

    -- Return lines as separate objects for pipeline compatibility
    if ctx:HasFlag("n") or ctx:HasFlag("number") then
        local lineNum = 0
        return ctx:Produce(function(emit)
            for line in content:gmatch("[^\n]*") do
                lineNum = lineNum + 1
                emit(ctx:Text(string.format("%4d  %s", lineNum, line)))
            end
        end)
    else
        return ctx:Text(content)
    end
end

-------------------------------------------------------------------------------
-- write - Write to File (internal, usually used via redirection)
-------------------------------------------------------------------------------

C3.Commands.write = function(ctx)
    markFilesystemPrompt(ctx)
    local path = ctx:GetArg(1)

    if not path then
        return ctx:Error("Usage: write <file>")
    end

    local vfs = C3:GetVFS()

    -- Get content from pipeline or remaining args
    local content
    if ctx:HasInput() then
        local input = ctx:GetInput()
        if type(input) == "table" then
            if input._type == "Text" then
                content = input.content
            else
                -- Collect pipeline objects
                local parts = {}
                local collected = C3.Pipeline.Collect(input)
                for _, obj in ipairs(collected) do
                    if type(obj) == "table" and obj._type == "Text" then
                        table.insert(parts, obj.content)
                    else
                        table.insert(parts, tostring(obj))
                    end
                end
                content = table.concat(parts, "\n")
            end
        else
            content = tostring(input)
        end
    else
        local args = ctx:GetArgs()
        table.remove(args, 1) -- Remove filename
        content = table.concat(args, " ")
    end

    local append = ctx:HasFlag("a") or ctx:HasFlag("append")

    local ok, err
    if append then
        ok, err = vfs:Append(path, content .. "\n")
    else
        ok, err = vfs:Write(path, content)
    end

    if not ok then
        return ctx:Error(err)
    end

    return ctx:Text("Written to: " .. path)
end

-------------------------------------------------------------------------------
-- mkdir - Make Directory
-------------------------------------------------------------------------------

C3.Commands.mkdir = function(ctx)
    markFilesystemPrompt(ctx)
    local path = ctx:GetArg(1)

    if not path then
        return ctx:Error("Usage: mkdir <directory>")
    end

    local vfs = C3:GetVFS()
    local parents = ctx:HasFlag("p") or ctx:HasFlag("parents")

    local ok, err
    if parents then
        ok, err = vfs:MakeDirP(path)
    else
        ok, err = vfs:MakeDir(path)
    end

    if not ok then
        return ctx:Error(err)
    end

    return ctx:Text("Created: " .. vfs:ResolvePath(path))
end

-------------------------------------------------------------------------------
-- rm - Remove File or Directory
-------------------------------------------------------------------------------

C3.Commands.rm = function(ctx)
    markFilesystemPrompt(ctx)
    local path = ctx:GetArg(1)

    if not path then
        return ctx:Error("Usage: rm <path>")
    end

    local vfs = C3:GetVFS()
    local recursive = ctx:HasFlag("r") or ctx:HasFlag("recursive")
    local force = ctx:HasFlag("f") or ctx:HasFlag("force")

    if not vfs:Exists(path) then
        if force then
            return ctx:Text("(not found)")
        end
        return ctx:Error("Path not found: " .. path)
    end

    local ok, err
    if recursive then
        ok, err = vfs:DeleteRecursive(path)
    else
        ok, err = vfs:Delete(path)
    end

    if not ok then
        return ctx:Error(err)
    end

    return ctx:Text("Removed: " .. vfs:ResolvePath(path))
end

-------------------------------------------------------------------------------
-- mv - Move/Rename
-------------------------------------------------------------------------------

C3.Commands.mv = function(ctx)
    markFilesystemPrompt(ctx)
    local src = ctx:GetArg(1)
    local dest = ctx:GetArg(2)

    if not src or not dest then
        return ctx:Error("Usage: mv <source> <destination>")
    end

    local vfs = C3:GetVFS()
    local ok, err = vfs:Move(src, dest)

    if not ok then
        return ctx:Error(err)
    end

    return ctx:Text(string.format("Moved: %s -> %s",
        vfs:ResolvePath(src), vfs:ResolvePath(dest)))
end

-------------------------------------------------------------------------------
-- cp - Copy
-------------------------------------------------------------------------------

C3.Commands.cp = function(ctx)
    markFilesystemPrompt(ctx)
    local src = ctx:GetArg(1)
    local dest = ctx:GetArg(2)

    if not src or not dest then
        return ctx:Error("Usage: cp <source> <destination>")
    end

    local vfs = C3:GetVFS()
    local ok, err = vfs:Copy(src, dest)

    if not ok then
        return ctx:Error(err)
    end

    return ctx:Text(string.format("Copied: %s -> %s",
        vfs:ResolvePath(src), vfs:ResolvePath(dest)))
end

-------------------------------------------------------------------------------
-- touch - Create Empty File or Update Timestamp
-------------------------------------------------------------------------------

C3.Commands.touch = function(ctx)
    markFilesystemPrompt(ctx)
    local path = ctx:GetArg(1)

    if not path then
        return ctx:Error("Usage: touch <file>")
    end

    local vfs = C3:GetVFS()
    local ok, err = vfs:Touch(path)

    if not ok then
        return ctx:Error(err)
    end

    return ctx:Text("Touched: " .. vfs:ResolvePath(path))
end

-------------------------------------------------------------------------------
-- stat - File Information
-------------------------------------------------------------------------------

C3.Commands.stat = function(ctx)
    markFilesystemPrompt(ctx)
    local path = ctx:GetArg(1)

    if not path then
        return ctx:Error("Usage: stat <path>")
    end

    local vfs = C3:GetVFS()
    local info, err = vfs:Stat(path)

    if not info then
        return ctx:Error(err)
    end

    local results = {
        ctx:Text("  Path: " .. info.path),
        ctx:Text("  Type: " .. info.type),
        ctx:Text("  Size: " .. info.size .. " bytes"),
        ctx:Text("  Created: " .. (info.created and date("%Y-%m-%d %H:%M:%S", info.created) or "unknown")),
        ctx:Text("  Modified: " .. (info.modified and date("%Y-%m-%d %H:%M:%S", info.modified) or "unknown")),
    }

    return results
end

-------------------------------------------------------------------------------
-- tree - Directory Tree
-------------------------------------------------------------------------------

C3.Commands.tree = function(ctx)
    markFilesystemPrompt(ctx)
    local path = ctx:GetArg(1)
    local vfs = C3:GetVFS()
    local resolved = vfs:ResolvePath(path or vfs:GetCwd())

    local maxDepth = tonumber(ctx:GetFlag("d") or ctx:GetFlag("depth")) or 3

    local results = { ctx:Text(resolved) }
    local fileCount = 0
    local dirCount = 0

    local function buildTree(treePath, prefix, depth)
        if depth > maxDepth then return end

        local entries = vfs:List(treePath)
        if not entries then return end

        -- Use Formatter's prefix set (ASCII chars that render correctly in WoW)
        local prefixSet = C3.Formatter:GetPrefixSet()

        for i, entry in ipairs(entries) do
            local isLast = (i == #entries)
            local connector = isLast and prefixSet.LAST or prefixSet.ITEM
            local childPrefix = isLast and prefixSet.EMPTY or prefixSet.CHILD

            local suffix = entry.type == "dir" and "/" or ""
            table.insert(results, ctx:Text(prefix .. connector .. entry.name .. suffix))

            if entry.type == "dir" then
                dirCount = dirCount + 1
                buildTree(entry.path, prefix .. childPrefix, depth + 1)
            else
                fileCount = fileCount + 1
            end
        end
    end

    buildTree(resolved, "", 1)

    table.insert(results, ctx:Text(""))
    table.insert(results, ctx:Text(string.format("%d directories, %d files", dirCount, fileCount)))

    return results
end

-------------------------------------------------------------------------------
-- find - Find Files
-------------------------------------------------------------------------------

C3.Commands.find = function(ctx)
    markFilesystemPrompt(ctx)
    local pattern = ctx:GetArg(1)
    local startPath = ctx:GetArg(2)

    if not pattern then
        return ctx:Error("Usage: find <pattern> [path]")
    end

    local vfs = C3:GetVFS()

    -- Convert simple wildcards to glob pattern
    if not pattern:find("/") then
        pattern = "**/" .. pattern
    end

    local matches = vfs:Find(pattern, startPath)

    if #matches == 0 then
        return ctx:Text("(no matches)")
    end

    return ctx:Produce(function(emit)
        for _, match in ipairs(matches) do
            emit(C3.Types.Create("FileEntry", {
                name = match.name,
                path = match.path,
                type = match.type,
                display = match.path
            }))
        end
    end)
end

-------------------------------------------------------------------------------
-- du - Disk Usage (detailed stats)
-------------------------------------------------------------------------------

C3.Commands.du = function(ctx)
    markFilesystemPrompt(ctx)
    local vfs = C3:GetVFS()
    local stats = vfs:GetStats()

    local results = {
        ctx:Text("VFS Statistics:"),
        ctx:Text("  Files: " .. stats.files),
        ctx:Text("  Directories: " .. stats.directories),
        ctx:Text("  Total Size: " .. stats.totalSize .. " bytes"),
    }

    return results
end

-------------------------------------------------------------------------------
-- df - Disk Free (quota display)
-------------------------------------------------------------------------------

C3.Commands.df = function(ctx)
    markFilesystemPrompt(ctx)
    local vfs = C3:GetVFS()
    local driver = vfs.driver

    if not driver.GetTotalSize then
        return ctx:Error("Driver does not support quota stats")
    end

    local used = driver:GetTotalSize()
    local limit = driver.MAX_TOTAL_SIZE
    local pct = (used / limit) * 100

    -- Color based on usage
    local color = "00FF00"  -- Green
    if pct > 80 then color = "FFFF00" end  -- Yellow
    if pct > 95 then color = "FF0000" end  -- Red

    local results = {
        ctx:Text("VFS Quota:"),
        ctx:Text(string.format("  Used: |cFF%s%.2f KB|r / %.2f KB",
            color, used / 1024, limit / 1024)),
        ctx:Text(string.format("  Percentage: |cFF%s%.1f%%|r", color, pct)),
    }

    -- Show compression status if LibDeflate is available
    local LibDeflate = LibStub and LibStub("LibDeflate", true)
    if LibDeflate then
        table.insert(results, ctx:Text("  Compression: |cFF00FF00enabled|r"))
    else
        table.insert(results, ctx:Text("  Compression: |cFF888888disabled|r (install LibDeflate)"))
    end

    return results
end

-------------------------------------------------------------------------------
-- Register FileEntry type
-------------------------------------------------------------------------------

-------------------------------------------------------------------------------
-- fsck - Filesystem Check and Repair
-------------------------------------------------------------------------------

C3.Commands.fsck = function(ctx)
    markFilesystemPrompt(ctx)
    local vfs = C3:GetVFS()
    if not vfs.driver or not vfs.driver.Repair then
        return ctx:Error("VFS repair not available")
    end

    local report = vfs.driver:Repair()

    local results = {}
    local issueCount = #report.issues
    local fixCount = #report.fixes

    if issueCount == 0 then
        table.insert(results, ctx:Text(Colorize("Filesystem is healthy. No issues found.", "accent.success")))
    else
        table.insert(results, ctx:Text(
            Colorize(string.format("Found %d issues, applied %d fixes.", issueCount, fixCount), "accent.warning")
        ))

        -- List issues (max 10)
        for i, issue in ipairs(report.issues) do
            if i > 10 then
                table.insert(results, ctx:Text(
                    "  " .. Colorize(string.format("... and %d more issues", issueCount - 10), "status.dim")
                ))
                break
            end
            table.insert(results, ctx:Text(
                "  " .. Colorize(string.format("[%s]", issue.type), "accent.warning") .. " " .. (issue.path or issue.detail or "")
            ))
        end
    end

    -- Stats comparison
    local after = report.stats.after
    table.insert(results, ctx:Text(
        Colorize(string.format("Stats: %d files, %d dirs, %.1fKB",
            after.fileCount, after.dirCount, after.totalSize / 1024), "status.dim")
    ))

    return results
end

-------------------------------------------------------------------------------
-- Type Registration
-------------------------------------------------------------------------------

C3.Types:Register("FileEntry", {
    description = "File or directory entry",
    source = "ls",
    properties = {
        { "name", "File/directory name" },
        { "path", "Full path" },
        { "type", "Entry type: file, directory" },
        { "size", "File size in bytes" },
        { "modified", "Last modified timestamp" },
        { "display", "Formatted display string" },
    },
    format = function(self)
        return self.display or self.name
    end
})

return true
