--[[
    Commander 3.0 - Virtual Filesystem

    High-level VFS API that ties together:
    - Paths (path manipulation)
    - Nodes (file/directory objects)
    - Drivers (storage backends)
]]

local ADDON_NAME, C3 = ...

local VFS = C3.class()
C3.VirtualFilesystem = VFS

-------------------------------------------------------------------------------
-- Initialization
-------------------------------------------------------------------------------

---Create a new VFS instance
---@param driver table Storage driver (e.g., SavedVarsDriver)
---@return table
function VFS:init(driver)
    self.driver = driver
    -- Default cwd to character's home directory
    self.cwd = C3.VFSSavedVarsDriver._getCharacterHome()

    -- Ensure cwd tracks the active character once player info is available.
    self:_syncCwdToCharacter()
end

---Sync cwd to the current character's home directory on login
function VFS:_syncCwdToCharacter()
    local function setHome()
        local home = C3.VFSSavedVarsDriver._getCharacterHome()
        if home and home ~= "" then
            self.cwd = home
        end

        if self.driver and self.driver.TagCharacterHome then
            self.driver:TagCharacterHome()
        end
    end

    local eventFrame = CreateFrame("Frame")
    eventFrame:RegisterEvent("PLAYER_LOGIN")
    eventFrame:SetScript("OnEvent", function()
        setHome()
        eventFrame:UnregisterEvent("PLAYER_LOGIN")
    end)

    -- If player info is already available, sync immediately.
    if UnitName("player") then
        setHome()
    end
end

---Get the current character's home directory path
---@return string e.g. "/home/Argent Dawn/Ludwig"
function VFS:GetHomePath()
    return C3.VFSSavedVarsDriver._getCharacterHome()
end

-------------------------------------------------------------------------------
-- Path Resolution
-------------------------------------------------------------------------------

---Resolve a path relative to cwd
---@param path string
---@return string
function VFS:ResolvePath(path)
    if not path or path == "" then
        return self.cwd
    end

    if C3.Paths.IsAbsolute(path) then
        return C3.Paths.Normalize(path)
    end

    return C3.Paths.Join(self.cwd, path)
end

---Resolve a path by matching case-insensitively against existing entries
---@param path string Absolute path to resolve
---@return string? resolved
---@return string? error
function VFS:_resolveCaseInsensitive(path)
    if not path or path == "" then
        return nil, "Empty path"
    end

    local normalized = C3.Paths.Normalize(path)
    if normalized == "/" then
        return "/"
    end

    local current = "/"
    for segment in C3.Paths.Segments(normalized) do
        local entries = self.driver:List(current)
        if not entries then
            return nil, "Not a directory: " .. current
        end

        local exactMatch = nil
        local ciMatch = nil
        local ciCount = 0
        local target = segment:lower()

        for _, entry in ipairs(entries) do
            if entry.name == segment then
                exactMatch = entry.name
                break
            end
            if entry.name:lower() == target then
                ciMatch = entry.name
                ciCount = ciCount + 1
            end
        end

        local nextName = exactMatch or ciMatch
        if not nextName or (not exactMatch and ciCount > 1) then
            return nil, "Path not found: " .. normalized
        end

        current = C3.Paths.Join(current, nextName)
    end

    return current
end

---Get current working directory
---@return string
function VFS:GetCwd()
    return self.cwd
end

---Set current working directory
---@param path string
---@return boolean success
---@return string? error
function VFS:SetCwd(path)
    local resolved = self:ResolvePath(path)

    if not self.driver:Exists(resolved) then
        local ciResolved = self:_resolveCaseInsensitive(resolved)
        if ciResolved then
            resolved = ciResolved
        else
            return false, "Directory not found: " .. resolved
        end
    end

    if not self.driver:IsDirectory(resolved) then
        return false, "Not a directory: " .. resolved
    end

    self.cwd = resolved
    return true
end

-------------------------------------------------------------------------------
-- File Operations
-------------------------------------------------------------------------------

---Check if a path exists
---@param path string
---@return boolean
function VFS:Exists(path)
    local resolved = self:ResolvePath(path)
    return self.driver:Exists(resolved)
end

---Check if path is a file
---@param path string
---@return boolean
function VFS:IsFile(path)
    local resolved = self:ResolvePath(path)
    return self.driver:IsFile(resolved)
end

---Check if path is a directory
---@param path string
---@return boolean
function VFS:IsDirectory(path)
    local resolved = self:ResolvePath(path)
    return self.driver:IsDirectory(resolved)
end

---Read file contents
---@param path string
---@return string? content
---@return string? error
function VFS:Read(path)
    local resolved = self:ResolvePath(path)
    return self.driver:Read(resolved)
end

---Write file contents
---@param path string
---@param content string
---@return boolean success
---@return string? error
function VFS:Write(path, content)
    local resolved = self:ResolvePath(path)
    return self.driver:Write(resolved, content)
end

---Append to file
---@param path string
---@param content string
---@return boolean success
---@return string? error
function VFS:Append(path, content)
    local resolved = self:ResolvePath(path)
    return self.driver:Append(resolved, content)
end

---Delete file or empty directory
---@param path string
---@return boolean success
---@return string? error
function VFS:Delete(path)
    local resolved = self:ResolvePath(path)
    return self.driver:Delete(resolved)
end

---Delete recursively
---@param path string
---@return boolean success
---@return string? error
function VFS:DeleteRecursive(path)
    local resolved = self:ResolvePath(path)
    return self.driver:DeleteRecursive(resolved)
end

-------------------------------------------------------------------------------
-- Directory Operations
-------------------------------------------------------------------------------

---List directory contents
---@param path? string
---@return table? entries
---@return string? error
function VFS:List(path)
    local resolved = self:ResolvePath(path or self.cwd)
    return self.driver:List(resolved)
end

---Create directory
---@param path string
---@return boolean success
---@return string? error
function VFS:MakeDir(path)
    local resolved = self:ResolvePath(path)
    return self.driver:MakeDir(resolved)
end

---Create directory with parents (mkdir -p)
---@param path string
---@return boolean success
---@return string? error
function VFS:MakeDirP(path)
    local resolved = self:ResolvePath(path)
    return self.driver:MakeDirP(resolved)
end

-------------------------------------------------------------------------------
-- Move/Copy Operations
-------------------------------------------------------------------------------

---Move/rename a file or directory
---@param oldPath string
---@param newPath string
---@return boolean success
---@return string? error
function VFS:Move(oldPath, newPath)
    local resolvedOld = self:ResolvePath(oldPath)
    local resolvedNew = self:ResolvePath(newPath)
    return self.driver:Move(resolvedOld, resolvedNew)
end

---Copy a file
---@param srcPath string
---@param destPath string
---@return boolean success
---@return string? error
function VFS:Copy(srcPath, destPath)
    local resolvedSrc = self:ResolvePath(srcPath)
    local resolvedDest = self:ResolvePath(destPath)
    return self.driver:Copy(resolvedSrc, resolvedDest)
end

-------------------------------------------------------------------------------
-- File Info
-------------------------------------------------------------------------------

---Get file/directory info
---@param path string
---@return table? info
---@return string? error
function VFS:Stat(path)
    local resolved = self:ResolvePath(path)
    local node = self.driver:GetNode(resolved)

    if not node then
        return nil, "Path not found: " .. resolved
    end

    return {
        name = node.name,
        path = resolved,
        type = node.type,
        size = node.size or 0,
        created = node.created,
        modified = node.modified,
        metadata = node.metadata,
    }
end

---Get filesystem stats
---@return table
function VFS:GetStats()
    return self.driver:GetStats()
end

-------------------------------------------------------------------------------
-- Convenience Methods
-------------------------------------------------------------------------------

---Touch a file (create if not exists, update modified time)
---@param path string
---@return boolean success
---@return string? error
function VFS:Touch(path)
    local resolved = self:ResolvePath(path)

    if self.driver:Exists(resolved) then
        -- Update modified time by reading and writing
        local content = self.driver:Read(resolved)
        return self.driver:Write(resolved, content or "")
    else
        -- Create empty file
        return self.driver:Write(resolved, "")
    end
end

---Read file as lines
---@param path string
---@return table? lines
---@return string? error
function VFS:ReadLines(path)
    local content, err = self:Read(path)
    if not content then
        return nil, err
    end

    local lines = {}
    for line in content:gmatch("[^\n]+") do
        table.insert(lines, line)
    end

    -- Handle trailing newline edge case
    if content:sub(-1) == "\n" then
        -- Content ended with newline, that's fine
    end

    return lines
end

---Write lines to file
---@param path string
---@param lines table
---@return boolean success
---@return string? error
function VFS:WriteLines(path, lines)
    local content = table.concat(lines, "\n")
    if #lines > 0 then
        content = content .. "\n"
    end
    return self:Write(path, content)
end

---Find files matching a pattern
---@param pattern string Glob pattern
---@param startPath? string Starting directory
---@return table matches
function VFS:Find(pattern, startPath)
    local start = self:ResolvePath(startPath or "/")
    local matches = {}

    local function search(path)
        local entries = self.driver:List(path)
        if not entries then return end

        for _, entry in ipairs(entries) do
            if C3.Paths.Match(entry.path, pattern) then
                table.insert(matches, entry)
            end

            if entry.type == "dir" then
                search(entry.path)
            end
        end
    end

    search(start)
    return matches
end

---Walk directory tree
---@param path? string Starting path
---@param callback function(path, entry)
---@param depth? number Max depth (nil = unlimited)
function VFS:Walk(path, callback, depth)
    local start = self:ResolvePath(path or "/")
    local currentDepth = 0

    local function walk(walkPath, d)
        if depth and d > depth then return end

        local entries = self.driver:List(walkPath)
        if not entries then return end

        for _, entry in ipairs(entries) do
            callback(entry.path, entry)

            if entry.type == "dir" then
                walk(entry.path, d + 1)
            end
        end
    end

    walk(start, 0)
end

-------------------------------------------------------------------------------
-- Script Execution
-------------------------------------------------------------------------------

---Check if file is executable (has .cmd or .lua extension)
---@param path string
---@return boolean
function VFS:IsExecutable(path)
    local ext = C3.Paths.Extension(path)
    return ext == "cmd" or ext == "lua"
end

---Execute a script file
---@param path string
---@param session? table
---@return any result
---@return string? error
function VFS:Execute(path, session)
    local resolved = self:ResolvePath(path)
    local content, err = self.driver:Read(resolved)

    if not content then
        return nil, err
    end

    local ext = C3.Paths.Extension(resolved)
    local allowYield = coroutine.running() ~= nil

    if ext == "cmd" then
        -- Execute as Commander script (line by line)
        local results = {}
        for line in content:gmatch("[^\n]+") do
            line = line:match("^%s*(.-)%s*$") -- Trim
            if line ~= "" and line:sub(1, 1) ~= "#" then
                local result
                if allowYield then
                    local ast = C3.Parser.ParseString(line)
                    if not ast then
                        error("Failed to parse: " .. line)
                    end
                    result = C3.Pipeline:Execute(ast, {
                        session = session,
                        input = line,
                    })
                else
                    result = C3:Query(line, session)
                end
                table.insert(results, result)
            end
        end
        return results
    elseif ext == "lua" then
        -- Execute as Lua (sandboxed)
        local fn, loadErr = loadstring(content)
        if not fn then
            return nil, "Lua syntax error: " .. loadErr
        end

        -- Create restricted C3 proxy (moderate restriction)
        -- Excludes: VFS, Kernel, Terminal, Session, Execute (could be malicious)
        local safeC3 = {
            -- Query API (read-only data access)
            Query = function(...) return C3:Query(...) end,

            -- Output functions
            Print = function(...) return C3:Print(...) end,
            Debug = function(...) return C3:Debug(...) end,

            -- Read-only getters for common data
            GetPlayerName = function() return UnitName("player") end,
            GetPlayerClass = function() return select(2, UnitClass("player")) end,
            GetPlayerLevel = function() return UnitLevel("player") end,
            GetZone = function() return GetZoneText() end,
            GetSubZone = function() return GetSubZoneText() end,
            GetGold = function() return GetMoney() end,

            -- Version info (read-only)
            Version = C3.Version,
        }

        -- Create sandboxed environment
        local sandbox = {
            print = function(...) C3:Debug(...) end,
            C3 = safeC3,  -- Restricted proxy instead of full C3
            pairs = pairs,
            ipairs = ipairs,
            type = type,
            tostring = tostring,
            tonumber = tonumber,
            string = string,
            table = table,
            math = math,
        }
        setfenv(fn, sandbox)

        local ok, result = pcall(fn)
        if not ok then
            return nil, "Lua runtime error: " .. tostring(result)
        end
        return result
    else
        return nil, "Unknown script type: " .. (ext or "none")
    end
end

-------------------------------------------------------------------------------
-- Singleton Instance
-------------------------------------------------------------------------------

-- The global VFS instance will be created during addon initialization
-- after SavedVariables are loaded

function C3:InitVFS()
    if self._vfs then return end

    -- Ensure we have a database reference
    if not Commander3DB then
        Commander3DB = {}
    end

    local driver = C3.VFSSavedVarsDriver:new(Commander3DB)
    self._vfs = VFS:new(driver)

    C3:Debug("VFS: Initialized")
end

---Get the VFS instance
---@return table
function C3:GetVFS()
    if not self._vfs then
        self:InitVFS()
    end
    return self._vfs
end

-- Convenience property
C3.VFS = setmetatable({}, {
    __index = function(_, key)
        local vfs = C3:GetVFS()
        return vfs[key]
    end,
    __newindex = function(_, key, value)
        local vfs = C3:GetVFS()
        vfs[key] = value
    end,
})

return VFS
