-- projectile.lua
-- ============================================================================
-- Project management and navigation package.
-- Similar to Emacs projectile package.
--
-- Features:
-- - Project root detection (via .git, .projectile, etc.)
-- - Project-scoped file finding
-- - Recent projects list
-- - Project-local commands
-- ============================================================================

local projectile = {}

-- Configuration
projectile.config = {
    -- Markers that indicate a project root
    project_markers = {
        ".git",
        ".projectile",
        ".project",
        "Makefile",
        "CMakeLists.txt",
        "package.json",
        "Cargo.toml",
        "go.mod",
        "pyproject.toml",
        "setup.py",
        ".hg",
        ".svn",
    },

    -- File patterns to ignore when listing project files
    ignore_patterns = {
        "^%.git/",
        "^%.hg/",
        "^%.svn/",
        "^node_modules/",
        "^build/",
        "^dist/",
        "^target/",
        "^%.cache/",
        "^__pycache__/",
        "%.pyc$",
        "%.o$",
        "%.a$",
        "%.so$",
        "%.dylib$",
    },

    -- Max files to scan in a project
    max_files = 5000,

    -- Max depth to search for project root
    max_depth = 10,

    -- Cache project file lists
    cache_enabled = true,
}

-- State
projectile.current_project = nil
projectile.recent_projects = {}
projectile.project_cache = {}  -- project_root -> {files, timestamp}

-- Helper: Check if a path exists
local function path_exists(path)
    local f = io.open(path, "r")
    if f then
        f:close()
        return true
    end
    return false
end

-- Helper: Check if path is a directory (heuristic)
local function is_directory(path)
    -- Try to open as directory by appending /. and checking
    local f = io.open(path .. "/.", "r")
    if f then
        f:close()
        return true
    end
    return false
end

-- Helper: Get parent directory
local function parent_dir(path)
    return path:match("(.+)/[^/]+$") or "/"
end

-- Helper: Get basename
local function basename(path)
    return path:match("[^/]+$") or path
end

-- Helper: Join paths
local function join_path(...)
    local parts = {...}
    local result = table.concat(parts, "/")
    -- Normalize double slashes
    result = result:gsub("//+", "/")
    return result
end

-- Find project root from a given path
function projectile.find_project_root(start_path)
    local path = start_path or os.getenv("PWD") or "."
    local depth = 0

    while path and path ~= "/" and depth < projectile.config.max_depth do
        for _, marker in ipairs(projectile.config.project_markers) do
            local marker_path = join_path(path, marker)
            if path_exists(marker_path) or is_directory(marker_path) then
                return path
            end
        end

        path = parent_dir(path)
        depth = depth + 1
    end

    return nil
end

-- Get current project root (from current buffer's file path)
function projectile.get_current_project()
    local buf = editor.buffer
    local filepath = buf:filepath()

    if filepath and filepath ~= "" then
        local dir = parent_dir(filepath)
        return projectile.find_project_root(dir)
    end

    -- Fallback to working directory
    return projectile.find_project_root()
end

-- Check if a file matches ignore patterns
local function should_ignore(relative_path)
    for _, pattern in ipairs(projectile.config.ignore_patterns) do
        if relative_path:match(pattern) then
            return true
        end
    end
    return false
end

-- List files in project using shell command
function projectile.list_project_files(project_root)
    if not project_root then
        return {}
    end

    -- Check cache
    if projectile.config.cache_enabled then
        local cached = projectile.project_cache[project_root]
        if cached then
            -- Use cache if less than 30 seconds old
            local age = os.time() - cached.timestamp
            if age < 30 then
                return cached.files
            end
        end
    end

    -- Use find command to list files
    local cmd = string.format(
        "find '%s' -type f -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/build/*' 2>/dev/null | head -%d",
        project_root,
        projectile.config.max_files
    )

    local handle = io.popen(cmd)
    if not handle then
        return {}
    end

    local files = {}
    local prefix_len = #project_root + 2  -- +2 for trailing / and 1-based index

    for line in handle:lines() do
        local relative = line:sub(prefix_len)
        if relative and relative ~= "" and not should_ignore(relative) then
            table.insert(files, relative)
        end
    end

    handle:close()

    -- Sort files
    table.sort(files)

    -- Cache results
    if projectile.config.cache_enabled then
        projectile.project_cache[project_root] = {
            files = files,
            timestamp = os.time()
        }
    end

    return files
end

-- Add project to recent list
function projectile.add_recent_project(project_root)
    if not project_root then return end

    -- Remove if already in list
    for i, p in ipairs(projectile.recent_projects) do
        if p == project_root then
            table.remove(projectile.recent_projects, i)
            break
        end
    end

    -- Add to front
    table.insert(projectile.recent_projects, 1, project_root)

    -- Keep only last 20
    while #projectile.recent_projects > 20 do
        table.remove(projectile.recent_projects)
    end
end

-- Switch to project
function projectile.switch_project(project_root)
    projectile.current_project = project_root
    projectile.add_recent_project(project_root)
    editor:message("Switched to project: " .. project_root)
end

-- Find file in project
function projectile.find_file_in_project(project_root, pattern)
    local files = projectile.list_project_files(project_root)
    local matches = {}

    pattern = pattern:lower()

    for _, file in ipairs(files) do
        if file:lower():find(pattern, 1, true) then
            table.insert(matches, file)
        end
    end

    return matches
end

-- Clear project cache
function projectile.invalidate_cache(project_root)
    if project_root then
        projectile.project_cache[project_root] = nil
    else
        projectile.project_cache = {}
    end
end

-- Get project name from root
function projectile.get_project_name(project_root)
    return basename(project_root)
end

-- ============================================================================
-- Commands
-- ============================================================================

-- Find file in current project
editor:register_command("projectile-find-file", "Find file in current project", function(args)
    local project_root = projectile.get_current_project()
    if not project_root then
        return {success = false, message = "Not in a project"}
    end

    projectile.switch_project(project_root)

    local files = projectile.list_project_files(project_root)
    if #files == 0 then
        return {success = false, message = "No files found in project"}
    end

    -- If pattern provided, filter immediately
    if #args > 0 then
        local matches = projectile.find_file_in_project(project_root, args[1])
        if #matches == 0 then
            return {success = false, message = "No matches for: " .. args[1]}
        elseif #matches == 1 then
            local filepath = join_path(project_root, matches[1])
            editor:load_file(filepath)
            return {success = true, message = "Opened: " .. matches[1]}
        else
            -- Show matches
            local display = {}
            for i, m in ipairs(matches) do
                if i > 10 then
                    table.insert(display, "... and " .. (#matches - 10) .. " more")
                    break
                end
                table.insert(display, m)
            end
            return {success = true, message = "Matches:\n" .. table.concat(display, "\n")}
        end
    end

    -- Show file count
    return {success = true, message = "Project: " .. projectile.get_project_name(project_root) .. " (" .. #files .. " files)"}
end, {"pf"}, true, "s")

-- Switch to known project
editor:register_command("projectile-switch-project", "Switch to a known project", function(args)
    if #projectile.recent_projects == 0 then
        return {success = false, message = "No recent projects"}
    end

    if #args > 0 then
        -- Find project matching pattern
        local pattern = args[1]:lower()
        for _, project in ipairs(projectile.recent_projects) do
            if project:lower():find(pattern, 1, true) then
                projectile.switch_project(project)
                return {success = true, message = "Switched to: " .. projectile.get_project_name(project)}
            end
        end
        return {success = false, message = "No project matching: " .. args[1]}
    end

    -- Show recent projects
    local display = {"Recent projects:"}
    for i, project in ipairs(projectile.recent_projects) do
        table.insert(display, i .. ". " .. projectile.get_project_name(project))
    end
    return {success = true, message = table.concat(display, "\n")}
end, {"pp"}, true, "s")

-- Show current project info
editor:register_command("projectile-project-info", "Show current project information", function(args)
    local project_root = projectile.get_current_project()
    if not project_root then
        return {success = false, message = "Not in a project"}
    end

    local files = projectile.list_project_files(project_root)
    local info = {
        "Project: " .. projectile.get_project_name(project_root),
        "Root: " .. project_root,
        "Files: " .. #files,
    }

    return {success = true, message = table.concat(info, "\n")}
end, {"pi"})

-- Invalidate project cache
editor:register_command("projectile-invalidate-cache", "Clear project file cache", function(args)
    local project_root = projectile.get_current_project()
    projectile.invalidate_cache(project_root)
    return {success = true, message = "Project cache cleared"}
end, {"pic"})

-- Add current directory as project
editor:register_command("projectile-add-known-project", "Add current directory as a known project", function(args)
    local project_root
    if #args > 0 then
        project_root = args[1]
    else
        project_root = projectile.get_current_project() or os.getenv("PWD")
    end

    if project_root then
        projectile.add_recent_project(project_root)
        return {success = true, message = "Added project: " .. project_root}
    else
        return {success = false, message = "Could not determine project root"}
    end
end, {"pak"}, true, "s")

-- Run command in project root
editor:register_command("projectile-run-command", "Run shell command in project root", function(args)
    local project_root = projectile.get_current_project()
    if not project_root then
        return {success = false, message = "Not in a project"}
    end

    if #args == 0 then
        return {success = false, message = "Usage: projectile-run-command <command>"}
    end

    local cmd = table.concat(args, " ")
    local full_cmd = string.format("cd '%s' && %s 2>&1", project_root, cmd)

    local handle = io.popen(full_cmd)
    if not handle then
        return {success = false, message = "Failed to run command"}
    end

    local output = handle:read("*a")
    handle:close()

    -- Truncate long output
    if #output > 500 then
        output = output:sub(1, 500) .. "\n... (truncated)"
    end

    return {success = true, message = output}
end, {"prc"}, true, "s")

-- Keybindings
editor:bind_key("C-c p f", "projectile-find-file", "Find file in project")
editor:bind_key("C-c p p", "projectile-switch-project", "Switch project")
editor:bind_key("C-c p i", "projectile-project-info", "Project info")

-- Store module
lumacs.projectile = projectile

return projectile
