"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkspaceManager = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const os_1 = require("os");
const crypto_1 = require("crypto");
const child_process_1 = require("child_process");
const util_1 = require("util");
const exec = (0, util_1.promisify)(child_process_1.exec);
class WorkspaceManager {
    workspaces = new Map();
    exclude;
    constructor(options) {
        this.exclude = new Set(options.exclude);
    }
    async create(projectPath, workflowName) {
        const hasGit = (0, fs_1.existsSync)((0, path_1.join)(projectPath, '.git'));
        if (hasGit) {
            return this.createWorktree(projectPath, workflowName);
        }
        return this.createEphemeral(projectPath, workflowName);
    }
    async createWorktree(projectPath, workflowName) {
        const id = `run-${(0, crypto_1.randomBytes)(6).toString('hex')}`;
        const branchName = `vsaf/${workflowName}/${id}`;
        const worktreePath = (0, path_1.join)(projectPath, '.vsaf', 'worktrees', id);
        (0, fs_1.mkdirSync)((0, path_1.join)(projectPath, '.vsaf', 'worktrees'), { recursive: true });
        await exec(`git worktree add -b "${branchName}" "${worktreePath}"`, {
            cwd: projectPath,
        });
        const artifactsPath = (0, path_1.join)(worktreePath, '.vsaf', 'artifacts');
        (0, fs_1.mkdirSync)(artifactsPath, { recursive: true });
        const workspace = {
            id,
            sourcePath: worktreePath,
            branchName,
            artifactsPath,
            projectPath,
            type: 'worktree',
        };
        this.workspaces.set(id, workspace);
        return workspace;
    }
    createEphemeral(projectPath, _workflowName) {
        const id = `run-${(0, crypto_1.randomBytes)(6).toString('hex')}`;
        const rootPath = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), `vsaf-${id}-`));
        const sourcePath = (0, path_1.join)(rootPath, 'source');
        const artifactsPath = (0, path_1.join)(rootPath, 'artifacts');
        (0, fs_1.mkdirSync)(sourcePath, { recursive: true });
        (0, fs_1.mkdirSync)(artifactsPath, { recursive: true });
        (0, fs_1.cpSync)(projectPath, sourcePath, {
            recursive: true,
            filter: (src) => {
                const rel = (0, path_1.relative)(projectPath, src);
                if (rel === '')
                    return true;
                const topLevel = rel.split(path_1.sep)[0];
                return !this.exclude.has(topLevel);
            },
        });
        const workspace = {
            id,
            sourcePath,
            artifactsPath,
            projectPath,
            type: 'ephemeral',
        };
        this.workspaces.set(id, workspace);
        return workspace;
    }
    adopt(id, sourcePath, artifactsPath, projectPath, type, branchName) {
        this.workspaces.set(id, { id, sourcePath, branchName, artifactsPath, projectPath, type });
    }
    async diff(workspaceId) {
        const ws = this.getWorkspace(workspaceId);
        if (ws.type === 'worktree') {
            return this.diffWorktree(ws);
        }
        return this.diffEphemeral(ws);
    }
    async diffWorktree(ws) {
        // Find the commit that was HEAD in the parent project when the worktree was created.
        // The worktree branch was forked from that commit, so diffing base..HEAD gives us
        // only the changes made inside this worktree (not the full parent history).
        const { stdout: baseCommit } = await exec(`git -C "${ws.projectPath}" rev-parse HEAD`, { cwd: ws.sourcePath }).catch(() => ({ stdout: '' }));
        const base = baseCommit.trim();
        if (!base) {
            return { modified: [], added: [], deleted: [] };
        }
        const { stdout } = await exec(`git diff --name-status ${base}..HEAD`, { cwd: ws.sourcePath }).catch(() => ({ stdout: '' }));
        return this.parseDiffOutput(stdout);
    }
    diffEphemeral(ws) {
        const modified = [];
        const added = [];
        const deleted = [];
        this.walkDir(ws.sourcePath, ws.sourcePath, (relPath, wsFilePath) => {
            const projFilePath = (0, path_1.join)(ws.projectPath, relPath);
            if (!(0, fs_1.existsSync)(projFilePath)) {
                added.push(relPath);
            }
            else {
                const wsContent = (0, fs_1.readFileSync)(wsFilePath);
                const projContent = (0, fs_1.readFileSync)(projFilePath);
                if (!wsContent.equals(projContent)) {
                    modified.push(relPath);
                }
            }
        });
        // Reverse walk: project files → check if still in workspace
        this.walkDir(ws.projectPath, ws.projectPath, (relPath) => {
            const topLevel = relPath.split(path_1.sep)[0];
            if (this.exclude.has(topLevel))
                return;
            const wsFilePath = (0, path_1.join)(ws.sourcePath, relPath);
            if (!(0, fs_1.existsSync)(wsFilePath)) {
                deleted.push(relPath);
            }
        });
        return { modified, added, deleted };
    }
    parseDiffOutput(stdout) {
        const modified = [];
        const added = [];
        const deleted = [];
        for (const line of stdout.trim().split('\n')) {
            if (!line)
                continue;
            const parts = line.split('\t');
            const status = parts[0];
            if (status === 'M')
                modified.push(parts[1]);
            else if (status === 'A')
                added.push(parts[1]);
            else if (status === 'D')
                deleted.push(parts[1]);
            else if (status.startsWith('R') || status.startsWith('C')) {
                // Rename/Copy: R100\told-name\tnew-name — treat old as deleted, new as added
                if (parts[1])
                    deleted.push(parts[1]);
                if (parts[2])
                    added.push(parts[2]);
            }
        }
        return { modified, added, deleted };
    }
    async applyChanges(workspaceId) {
        const ws = this.getWorkspace(workspaceId);
        if (ws.type === 'worktree') {
            return this.applyWorktreeChanges(ws);
        }
        return this.applyEphemeralChanges(ws);
    }
    async applyWorktreeChanges(ws) {
        const diff = await this.diff(ws.id);
        await exec(`git merge "${ws.branchName}" --no-edit`, {
            cwd: ws.projectPath,
        });
        return diff;
    }
    applyEphemeralChanges(ws) {
        const modified = [];
        const added = [];
        const deleted = [];
        this.walkDir(ws.sourcePath, ws.sourcePath, (relPath, wsFilePath) => {
            const projFilePath = (0, path_1.join)(ws.projectPath, relPath);
            if (!(0, fs_1.existsSync)(projFilePath)) {
                added.push(relPath);
            }
            else {
                const wsContent = (0, fs_1.readFileSync)(wsFilePath);
                const projContent = (0, fs_1.readFileSync)(projFilePath);
                if (!wsContent.equals(projContent)) {
                    modified.push(relPath);
                }
            }
            (0, fs_1.mkdirSync)((0, path_1.join)(projFilePath, '..'), { recursive: true });
            (0, fs_1.copyFileSync)(wsFilePath, projFilePath);
        });
        // Delete files that exist in project but not in workspace
        this.walkDir(ws.projectPath, ws.projectPath, (relPath) => {
            const topLevel = relPath.split(path_1.sep)[0];
            if (this.exclude.has(topLevel))
                return;
            if (!(0, fs_1.existsSync)((0, path_1.join)(ws.sourcePath, relPath))) {
                deleted.push(relPath);
                (0, fs_1.rmSync)((0, path_1.join)(ws.projectPath, relPath));
            }
        });
        return { modified, added, deleted };
    }
    async destroy(workspaceId) {
        const ws = this.getWorkspace(workspaceId);
        if (ws.type === 'worktree') {
            await exec(`git worktree remove "${ws.sourcePath}" --force`, {
                cwd: ws.projectPath,
            });
            await exec(`git branch -D "${ws.branchName}"`, {
                cwd: ws.projectPath,
            });
        }
        else {
            const rootPath = (0, path_1.join)(ws.sourcePath, '..');
            (0, fs_1.rmSync)(rootPath, { recursive: true, force: true });
        }
        this.workspaces.delete(workspaceId);
    }
    async pruneOrphans(projectPath, storageDir) {
        const pruned = [];
        // Get active runIds from storage
        const activeRuns = new Set();
        if ((0, fs_1.existsSync)(storageDir)) {
            for (const dir of (0, fs_1.readdirSync)(storageDir)) {
                if (dir.startsWith('run-'))
                    activeRuns.add(dir);
            }
        }
        // Clean .vsaf/worktrees/ entries with no matching run
        const wtDir = (0, path_1.join)(projectPath, '.vsaf', 'worktrees');
        if ((0, fs_1.existsSync)(wtDir)) {
            for (const dir of (0, fs_1.readdirSync)(wtDir)) {
                if (!activeRuns.has(dir)) {
                    try {
                        await exec(`git worktree remove "${(0, path_1.join)(wtDir, dir)}" --force`, { cwd: projectPath });
                    }
                    catch { /* best-effort */ }
                    try {
                        (0, fs_1.rmSync)((0, path_1.join)(wtDir, dir), { recursive: true, force: true });
                    }
                    catch { /* best-effort */ }
                    pruned.push(dir);
                }
            }
        }
        // git worktree prune — cleans stale refs
        try {
            await exec('git worktree prune', { cwd: projectPath });
        }
        catch { /* best-effort */ }
        return pruned;
    }
    getWorkspace(id) {
        const ws = this.workspaces.get(id);
        if (!ws)
            throw new Error(`Workspace not found: ${id}`);
        return ws;
    }
    walkDir(basePath, currentPath, callback) {
        for (const entry of (0, fs_1.readdirSync)(currentPath)) {
            const fullPath = (0, path_1.join)(currentPath, entry);
            const stat = (0, fs_1.statSync)(fullPath);
            if (stat.isDirectory()) {
                this.walkDir(basePath, fullPath, callback);
            }
            else {
                callback((0, path_1.relative)(basePath, fullPath), fullPath);
            }
        }
    }
}
exports.WorkspaceManager = WorkspaceManager;
//# sourceMappingURL=workspace-manager.js.map