/**
 * Git service for executing git commands within project directories.
 * Parses git porcelain output into structured data.
 */
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import * as pty from "node-pty";
import { detectCredentialPrompt, requestCredential } from "../handlers/git/credentials.js";
import { logger } from "../utils/logger.js";
const execFileAsync = promisify(execFile);
export class GitNotInitializedError extends Error {
    constructor(path) {
        super(`Not a git repository: ${path}`);
        this.name = "GitNotInitializedError";
    }
}
/**
 * Parse a porcelain v2 status code to a human-readable status string.
 */
function parseStatusCode(code) {
    switch (code) {
        case "M":
            return "modified";
        case "A":
            return "added";
        case "D":
            return "deleted";
        case "R":
            return "renamed";
        case "C":
            return "copied";
        case "?":
            return "untracked";
        case "!":
            return "ignored";
        case "U":
            return "unmerged";
        default:
            return "unknown";
    }
}
/**
 * Get the git status for a project directory.
 * Uses `git status --porcelain=v1 --branch` for structured output.
 */
export async function getGitStatus(projectPath) {
    try {
        const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1", "--branch"], {
            cwd: projectPath,
        });
        const lines = stdout.split("\n").filter((line) => line.length > 0);
        let branch = "";
        const staged = [];
        const unstaged = [];
        const conflicts = [];
        for (const line of lines) {
            // Branch line: ## branch...tracking
            if (line.startsWith("## ")) {
                const branchInfo = line.slice(3);
                // Extract branch name (before "..." if tracking info exists)
                const dotIndex = branchInfo.indexOf("...");
                branch = dotIndex >= 0 ? branchInfo.slice(0, dotIndex) : branchInfo;
                // Handle detached HEAD
                if (branch.startsWith("HEAD (no branch)") || branch.startsWith("No commits yet on ")) {
                    branch = branch.replace("No commits yet on ", "");
                }
                continue;
            }
            // File status lines: XY path
            const indexStatus = line[0];
            const workTreeStatus = line[1];
            const filePath = line.slice(3);
            // Check for conflicts (both modified, added by both, etc.)
            if (indexStatus === "U" ||
                workTreeStatus === "U" ||
                (indexStatus === "A" && workTreeStatus === "A") ||
                (indexStatus === "D" && workTreeStatus === "D")) {
                conflicts.push({ path: filePath, status: "conflict" });
                continue;
            }
            // Untracked files
            if (indexStatus === "?" && workTreeStatus === "?") {
                unstaged.push({ path: filePath, status: "untracked" });
                continue;
            }
            // Staged changes (index status is not empty and not '?')
            if (indexStatus !== " " && indexStatus !== "?") {
                // For renames, extract the new path (format: "old -> new")
                const renamedPath = indexStatus === "R" ? filePath.split(" -> ")[1] || filePath : filePath;
                staged.push({ path: renamedPath, status: parseStatusCode(indexStatus) });
            }
            // Unstaged changes (work tree status is not empty)
            if (workTreeStatus !== " " && workTreeStatus !== "?") {
                unstaged.push({ path: filePath, status: parseStatusCode(workTreeStatus) });
            }
        }
        const clean = staged.length === 0 && unstaged.length === 0 && conflicts.length === 0;
        return { branch, clean, staged, unstaged, conflicts };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        if (message.includes("not a git repository") ||
            message.includes("fatal: not a git repository")) {
            throw new GitNotInitializedError(projectPath);
        }
        throw err;
    }
}
/**
 * Create a new git branch.
 * @param projectPath The path to the project
 * @param name The branch name to create
 * @param startPoint The commit/ref to base the branch on (default: HEAD)
 */
export async function createGitBranch(projectPath, name, startPoint) {
    try {
        const args = ["branch", name];
        if (startPoint) {
            args.push(startPoint);
        }
        await execFileAsync("git", args, { cwd: projectPath });
        return { name, created: true };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        if (message.includes("not a git repository") ||
            message.includes("fatal: not a git repository")) {
            throw new GitNotInitializedError(projectPath);
        }
        throw err;
    }
}
/**
 * Checkout a git ref (branch, tag, or commit).
 */
export async function checkoutGitRef(projectPath, ref) {
    try {
        await execFileAsync("git", ["checkout", ref], { cwd: projectPath });
        return { ref, switched: true };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        if (message.includes("not a git repository") ||
            message.includes("fatal: not a git repository")) {
            throw new GitNotInitializedError(projectPath);
        }
        throw err;
    }
}
/**
 * Stage files in the git index.
 * @param projectPath The path to the project
 * @param paths Files to stage (use ['.'] to stage all)
 * @returns List of staged file paths
 */
export async function stageFiles(projectPath, paths) {
    try {
        await execFileAsync("git", ["add", ...paths], { cwd: projectPath });
        // Return the list of currently staged files
        const { stdout } = await execFileAsync("git", ["diff", "--cached", "--name-only"], {
            cwd: projectPath,
        });
        return stdout.split("\n").filter((line) => line.length > 0);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        if (message.includes("not a git repository") ||
            message.includes("fatal: not a git repository")) {
            throw new GitNotInitializedError(projectPath);
        }
        throw err;
    }
}
/**
 * Unstage files from the git index.
 * @param projectPath The path to the project
 * @param paths Files to unstage (use ['.'] to unstage all)
 * @returns List of unstaged file paths
 */
export async function unstageFiles(projectPath, paths) {
    try {
        if (paths.length === 1 && paths[0] === ".") {
            await execFileAsync("git", ["reset", "HEAD"], { cwd: projectPath });
        }
        else {
            await execFileAsync("git", ["reset", "HEAD", "--", ...paths], { cwd: projectPath });
        }
        return paths;
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        if (message.includes("not a git repository") ||
            message.includes("fatal: not a git repository")) {
            throw new GitNotInitializedError(projectPath);
        }
        throw err;
    }
}
/**
 * Create a git commit with the given message.
 * @param projectPath The path to the project
 * @param message The commit message
 * @returns The commit sha and message
 */
export async function createGitCommit(projectPath, message) {
    try {
        await execFileAsync("git", ["commit", "-m", message], { cwd: projectPath });
        // Get the sha of the newly created commit
        const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], {
            cwd: projectPath,
        });
        return { sha: stdout.trim(), message };
    }
    catch (err) {
        const message2 = err instanceof Error ? err.message : String(err);
        if (message2.includes("not a git repository") ||
            message2.includes("fatal: not a git repository")) {
            throw new GitNotInitializedError(projectPath);
        }
        throw err;
    }
}
/**
 * Push commits to a remote repository via PTY for credential prompt support.
 * @param projectPath The path to the project
 * @param remote Remote name (default: origin)
 * @param branch Branch to push (default: current branch)
 * @param force Whether to force push (default: false)
 * @param requestId Request ID for credential prompt correlation
 * @param send Function to send messages to the client (for credential prompts)
 */
export async function gitPush(projectPath, remote = "origin", branch, force = false, requestId, send) {
    // If no branch specified, use current branch
    let targetBranch = branch;
    if (!targetBranch) {
        try {
            const { stdout } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
                cwd: projectPath,
            });
            targetBranch = stdout.trim();
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            if (message.includes("not a git repository") ||
                message.includes("fatal: not a git repository")) {
                throw new GitNotInitializedError(projectPath);
            }
            throw err;
        }
    }
    const args = ["push", remote, targetBranch];
    if (force) {
        args.splice(1, 0, "--force");
    }
    return runGitViaPty(projectPath, args, targetBranch, remote, requestId, send);
}
/**
 * Run a git command via PTY with credential prompt handling.
 * Used for push and pull operations that may require authentication.
 */
export function runGitViaPty(projectPath, args, branch, remote, requestId, send) {
    return new Promise((resolve, reject) => {
        let output = "";
        const ptyProcess = pty.spawn("git", args, {
            cwd: projectPath,
            env: process.env,
            cols: 80,
            rows: 24,
        });
        ptyProcess.onData((data) => {
            output += data;
            // Check for credential prompts
            if (requestId && send) {
                const credPrompt = detectCredentialPrompt(data);
                if (credPrompt) {
                    logger.info(`Credential prompt detected during git ${args[0]}: ${credPrompt.type}`);
                    requestCredential(requestId, credPrompt.type, credPrompt.prompt, send).then((response) => {
                        if (response.cancelled) {
                            ptyProcess.kill();
                        }
                        else {
                            ptyProcess.write(response.value + "\n");
                        }
                    });
                }
            }
        });
        ptyProcess.onExit(({ exitCode }) => {
            if (exitCode === 0) {
                resolve({ remote, branch, pushed: true });
            }
            else {
                // Check for non-git-repo errors
                if (output.includes("not a git repository") || output.includes("Not a git repository")) {
                    reject(new GitNotInitializedError(projectPath));
                    return;
                }
                reject(new Error(output.trim()));
            }
        });
    });
}
/**
 * Pull from a remote repository via PTY for credential prompt support.
 * Detects merge conflicts and returns conflict file list.
 * @param projectPath The path to the project
 * @param remote Remote name (default: origin)
 * @param branch Branch to pull (default: current branch)
 * @param requestId Request ID for credential prompt correlation
 * @param send Function to send messages to the client (for credential prompts)
 */
export async function gitPull(projectPath, remote = "origin", branch, requestId, send) {
    // If no branch specified, use current branch
    let targetBranch = branch;
    if (!targetBranch) {
        try {
            const { stdout } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
                cwd: projectPath,
            });
            targetBranch = stdout.trim();
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            if (message.includes("not a git repository") ||
                message.includes("fatal: not a git repository")) {
                throw new GitNotInitializedError(projectPath);
            }
            throw err;
        }
    }
    const args = ["pull", remote, targetBranch];
    return new Promise((resolve, reject) => {
        let output = "";
        const ptyProcess = pty.spawn("git", args, {
            cwd: projectPath,
            env: process.env,
            cols: 80,
            rows: 24,
        });
        ptyProcess.onData((data) => {
            output += data;
            // Check for credential prompts
            if (requestId && send) {
                const credPrompt = detectCredentialPrompt(data);
                if (credPrompt) {
                    logger.info(`Credential prompt detected during git pull: ${credPrompt.type}`);
                    requestCredential(requestId, credPrompt.type, credPrompt.prompt, send).then((response) => {
                        if (response.cancelled) {
                            ptyProcess.kill();
                        }
                        else {
                            ptyProcess.write(response.value + "\n");
                        }
                    });
                }
            }
        });
        ptyProcess.onExit(({ exitCode }) => {
            if (exitCode === 0) {
                resolve({ remote, branch: targetBranch, pulled: true });
            }
            else {
                // Check for non-git-repo errors
                if (output.includes("not a git repository") || output.includes("Not a git repository")) {
                    reject(new GitNotInitializedError(projectPath));
                    return;
                }
                // Check for merge conflicts
                if (output.includes("CONFLICT") ||
                    output.includes("Automatic merge failed") ||
                    output.includes("fix conflicts")) {
                    // Extract conflict file paths from output
                    const conflicts = [];
                    const conflictRegex = /CONFLICT \([^)]+\): (?:Merge conflict in |)(.+)/g;
                    let match;
                    while ((match = conflictRegex.exec(output)) !== null) {
                        conflicts.push(match[1].trim());
                    }
                    resolve({
                        remote,
                        branch: targetBranch,
                        pulled: false,
                        conflicts,
                    });
                    return;
                }
                reject(new Error(output.trim()));
            }
        });
    });
}
/**
 * Get the content of a file at a given git ref (default HEAD).
 * Returns null content for files that don't exist at the ref (new files).
 */
export async function getFileAtRef(projectPath, filePath, ref = "HEAD") {
    try {
        const { stdout } = await execFileAsync("git", ["show", `${ref}:${filePath}`], {
            cwd: projectPath,
            maxBuffer: 10 * 1024 * 1024,
        });
        return { path: filePath, ref, content: stdout };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        if (message.includes("not a git repository") ||
            message.includes("fatal: not a git repository")) {
            throw new GitNotInitializedError(projectPath);
        }
        // File doesn't exist at ref → new file
        if (message.includes("does not exist in") ||
            message.includes("exists on disk, but not in") ||
            message.includes("fatal: path")) {
            return { path: filePath, ref, content: null };
        }
        throw err;
    }
}
/**
 * Get the list of local branches with remote tracking info.
 * Uses `git branch -vv --no-color` for structured output.
 */
export async function getGitBranches(projectPath) {
    try {
        // Get current branch name
        const { stdout: currentBranch } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: projectPath });
        const current = currentBranch.trim();
        // Get all local branches with verbose tracking info
        // Format: `* main   abc1234 [origin/main: ahead 2, behind 1] commit message`
        //         `  feat   def5678 [origin/feat] commit message`
        //         `  local  ghi9012 commit message`
        const { stdout } = await execFileAsync("git", [
            "for-each-ref",
            "--format=%(refname:short)%09%(upstream:short)%09%(upstream:track)",
            "refs/heads/",
        ], { cwd: projectPath });
        const branches = [];
        const lines = stdout.split("\n").filter((line) => line.length > 0);
        for (const line of lines) {
            const [name, remote, trackInfo] = line.split("\t");
            let ahead = 0;
            let behind = 0;
            if (trackInfo) {
                const aheadMatch = trackInfo.match(/ahead (\d+)/);
                const behindMatch = trackInfo.match(/behind (\d+)/);
                if (aheadMatch)
                    ahead = parseInt(aheadMatch[1], 10);
                if (behindMatch)
                    behind = parseInt(behindMatch[1], 10);
            }
            branches.push({
                name,
                remote: remote || null,
                ahead,
                behind,
            });
        }
        return { current, branches };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        if (message.includes("not a git repository") ||
            message.includes("fatal: not a git repository")) {
            throw new GitNotInitializedError(projectPath);
        }
        throw err;
    }
}
/**
 * Get diff for files in a project directory.
 * @param projectPath The path to the project
 * @param path Optional specific file path to diff
 * @param staged Whether to diff staged changes (--cached)
 */
export async function getDiff(projectPath, path, staged) {
    try {
        // Build args for diff with numstat (additions/deletions counts)
        const numstatArgs = ["diff", "--numstat"];
        if (staged)
            numstatArgs.push("--cached");
        if (path)
            numstatArgs.push("--", path);
        const { stdout: numstatOutput } = await execFileAsync("git", numstatArgs, {
            cwd: projectPath,
        });
        // Build args for full diff output
        const diffArgs = ["diff"];
        if (staged)
            diffArgs.push("--cached");
        if (path)
            diffArgs.push("--", path);
        const { stdout: diffOutput } = await execFileAsync("git", diffArgs, {
            cwd: projectPath,
            maxBuffer: 10 * 1024 * 1024,
        });
        // Parse numstat for additions/deletions per file
        const numstatMap = new Map();
        for (const line of numstatOutput.split("\n").filter((l) => l.length > 0)) {
            const [add, del, filePath] = line.split("\t");
            numstatMap.set(filePath, {
                additions: add === "-" ? 0 : parseInt(add, 10),
                deletions: del === "-" ? 0 : parseInt(del, 10),
            });
        }
        // Split full diff into per-file diffs
        const files = [];
        const fileDiffs = diffOutput.split(/^diff --git /m).filter((s) => s.length > 0);
        for (const fileDiff of fileDiffs) {
            const fullDiff = "diff --git " + fileDiff;
            // Extract file path from "diff --git a/path b/path"
            const headerMatch = fileDiff.match(/^a\/(.+?) b\/(.+)/);
            if (!headerMatch)
                continue;
            const filePath = headerMatch[2];
            // Determine status from diff content
            let status = "modified";
            if (fullDiff.includes("new file mode")) {
                status = "added";
            }
            else if (fullDiff.includes("deleted file mode")) {
                status = "deleted";
            }
            const stats = numstatMap.get(filePath) || { additions: 0, deletions: 0 };
            files.push({
                path: filePath,
                status,
                additions: stats.additions,
                deletions: stats.deletions,
                diff: fullDiff,
            });
        }
        return { files };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        if (message.includes("not a git repository") || message.includes("Not a git repository")) {
            throw new GitNotInitializedError(projectPath);
        }
        throw err;
    }
}
/**
 * Discard uncommitted changes.
 * For tracked files: uses `git checkout HEAD -- <paths>`.
 * If all=true: also removes untracked files with `git clean -fd`.
 * @param projectPath The path to the project
 * @param paths Specific files to discard (optional)
 * @param all Whether to discard all changes including untracked files
 */
export async function discardChanges(projectPath, paths, all) {
    try {
        // Get status before to count changed files
        const { stdout: statusOutput } = await execFileAsync("git", ["status", "--porcelain"], {
            cwd: projectPath,
        });
        const statusLines = statusOutput.split("\n").filter((l) => l.length > 0);
        if (paths && paths.length > 0) {
            // Discard specific files
            await execFileAsync("git", ["checkout", "HEAD", "--", ...paths], {
                cwd: projectPath,
            });
            return { discarded: paths.length };
        }
        // Discard all tracked changes
        const trackedChanges = statusLines.filter((l) => !l.startsWith("??"));
        if (trackedChanges.length > 0) {
            await execFileAsync("git", ["checkout", "HEAD", "--", "."], { cwd: projectPath });
        }
        let discarded = trackedChanges.length;
        if (all) {
            // Also remove untracked files
            const untrackedCount = statusLines.filter((l) => l.startsWith("??")).length;
            if (untrackedCount > 0) {
                await execFileAsync("git", ["clean", "-fd"], { cwd: projectPath });
                discarded += untrackedCount;
            }
        }
        return { discarded };
    }
    catch (err) {
        const errMessage = err instanceof Error ? err.message : String(err);
        if (errMessage.includes("not a git repository") ||
            errMessage.includes("fatal: not a git repository")) {
            throw new GitNotInitializedError(projectPath);
        }
        throw err;
    }
}
//# sourceMappingURL=git.js.map