/**
 * File System Surface — Sprint 3.4
 *
 * file.read, file.write, file.patch with path boundary enforcement
 * via PathGuard. Tracks operations for audit.
 */
import { readFile, writeFile, mkdir, stat, readdir, unlink, rename, rm, cp } from "node:fs/promises";
import { dirname, relative, join, basename } from "node:path";
import { PathGuard } from "../security/path-guard.js";
export class FileSystemSurface {
    id;
    guardOpts;
    type = "filesystem";
    _state = "idle";
    _sessionId = null;
    _startedAt = null;
    guard = null;
    _opCount = 0;
    /**
     * Backup map: absolute path → original file content.
     * Stored before the first write/patch so that undo can restore the original.
     * Only the *first* version is kept (subsequent edits don't overwrite the backup).
     */
    _backups = new Map();
    onOutput;
    onStateChange;
    constructor(id, guardOpts) {
        this.id = id;
        this.guardOpts = guardOpts;
    }
    get state() {
        return this._state;
    }
    get sessionId() {
        return this._sessionId;
    }
    async start(input) {
        this._sessionId = input.sessionId;
        this._startedAt = new Date().toISOString();
        this.guard = new PathGuard({
            workspaceRoot: input.workspaceRoot,
            ...this.guardOpts,
        });
        this._setState("running");
    }
    async stop(_input) {
        this._setState("stopped");
        this.guard = null;
    }
    snapshot() {
        return {
            id: this.id,
            type: this.type,
            state: this._state,
            sessionId: this._sessionId ?? "",
            startedAt: this._startedAt ?? undefined,
            metadata: {
                workspaceRoot: this.guard?.workspaceRoot ?? null,
                operationCount: this._opCount,
            },
        };
    }
    // ── File Operations ────────────────────────────────────────────
    async read(filePath) {
        this.ensureRunning();
        const abs = await this.guard.validateWithSymlinkCheck(filePath);
        this._opCount++;
        const content = await readFile(abs, "utf-8");
        this.onOutput?.(`read ${relative(this.guard.workspaceRoot, abs)} (${content.length} bytes)`);
        return content;
    }
    async write(filePath, content) {
        this.ensureRunning();
        const abs = await this.guard.validateWithSymlinkCheck(filePath);
        this._opCount++;
        // Save backup of original content (only on first modification)
        if (!this._backups.has(abs)) {
            try {
                const original = await readFile(abs, "utf-8");
                this._backups.set(abs, original);
            }
            catch {
                // File didn't exist before — backup is null (undo = delete)
                this._backups.set(abs, null);
            }
        }
        // Ensure parent directory exists
        await mkdir(dirname(abs), { recursive: true });
        await writeFile(abs, content, "utf-8");
        this.onOutput?.(`wrote ${relative(this.guard.workspaceRoot, abs)} (${content.length} bytes)`);
    }
    async patch(filePath, search, replace) {
        this.ensureRunning();
        const abs = await this.guard.validateWithSymlinkCheck(filePath);
        this._opCount++;
        const original = await readFile(abs, "utf-8");
        if (!original.includes(search)) {
            return { matched: false };
        }
        // Save backup of original content (only on first modification)
        if (!this._backups.has(abs)) {
            this._backups.set(abs, original);
        }
        const patched = original.replace(search, replace);
        await writeFile(abs, patched, "utf-8");
        this.onOutput?.(`patched ${relative(this.guard.workspaceRoot, abs)}`);
        return { matched: true };
    }
    async exists(filePath) {
        this.ensureRunning();
        const abs = this.guard.validate(filePath);
        try {
            await stat(abs);
            return true;
        }
        catch {
            return false;
        }
    }
    async list(dirPath) {
        this.ensureRunning();
        const abs = this.guard.validate(dirPath);
        this._opCount++;
        const entries = await readdir(abs, { withFileTypes: true });
        return entries.map((e) => (e.isDirectory() ? e.name + "/" : e.name));
    }
    async statFile(filePath) {
        this.ensureRunning();
        const abs = this.guard.validate(filePath);
        const s = await stat(abs);
        return {
            size: s.size,
            isDirectory: s.isDirectory(),
            modified: s.mtime.toISOString(),
        };
    }
    /** Check if a path is within workspace boundary */
    isPathAllowed(filePath) {
        return this.guard?.isAllowed(filePath) ?? false;
    }
    /**
     * Restore a file to its pre-modification state.
     * Returns true if a backup existed and was restored, false otherwise.
     */
    async restore(filePath) {
        this.ensureRunning();
        const abs = await this.guard.validateWithSymlinkCheck(filePath);
        if (!this._backups.has(abs))
            return false;
        const backup = this._backups.get(abs);
        if (backup === null) {
            // File was newly created — remove it
            const { unlink } = await import("node:fs/promises");
            try {
                await unlink(abs);
            }
            catch { /* already gone */ }
        }
        else {
            await writeFile(abs, backup, "utf-8");
        }
        this._backups.delete(abs);
        this.onOutput?.(`restored ${relative(this.guard.workspaceRoot, abs)}`);
        return true;
    }
    /** Check whether we have a backup for a given file path */
    hasBackup(filePath) {
        if (!this.guard)
            return false;
        try {
            const abs = this.guard.validate(filePath);
            return this._backups.has(abs);
        }
        catch {
            return false;
        }
    }
    /**
     * Get the backup (original) content for a file.
     * Returns the original string, null if the file was newly created, or undefined if no backup exists.
     */
    getBackup(filePath) {
        if (!this.guard)
            return undefined;
        try {
            const abs = this.guard.validate(filePath);
            if (!this._backups.has(abs))
                return undefined;
            return this._backups.get(abs) ?? null;
        }
        catch {
            return undefined;
        }
    }
    /** Clear the backup for a file (e.g. after user accepts changes). */
    clearBackup(filePath) {
        if (!this.guard)
            return;
        try {
            const abs = this.guard.validate(filePath);
            this._backups.delete(abs);
        }
        catch { /* ignore */ }
    }
    /**
     * Save a backup of the current file content for an external change.
     * Used when CLI providers (e.g. Codex) modify files directly, bypassing
     * the surface's own write/patch methods.
     * Only saves the *first* backup — won't overwrite existing entries.
     */
    async saveExternalBackup(filePath) {
        this.ensureRunning();
        const abs = await this.guard.validateWithSymlinkCheck(filePath);
        if (this._backups.has(abs))
            return;
        try {
            const original = await readFile(abs, "utf-8");
            this._backups.set(abs, original);
        }
        catch {
            // File doesn't exist yet — backup is null (undo = delete the new file)
            this._backups.set(abs, null);
        }
    }
    // ── File Management Operations ───────────────────────────────
    async deleteFile(filePath) {
        this.ensureRunning();
        const abs = await this.guard.validateWithSymlinkCheck(filePath);
        this._opCount++;
        await unlink(abs);
        this.onOutput?.(`deleted ${relative(this.guard.workspaceRoot, abs)}`);
    }
    async deleteDirectory(dirPath) {
        this.ensureRunning();
        const abs = await this.guard.validateWithSymlinkCheck(dirPath);
        this._opCount++;
        await rm(abs, { recursive: true });
        this.onOutput?.(`deleted directory ${relative(this.guard.workspaceRoot, abs)}`);
    }
    async renameFile(oldPath, newName) {
        this.ensureRunning();
        const absOld = await this.guard.validateWithSymlinkCheck(oldPath);
        const absNew = join(dirname(absOld), newName);
        // Validate the new path is also within workspace
        this.guard.validate(absNew);
        this._opCount++;
        await rename(absOld, absNew);
        const relNew = relative(this.guard.workspaceRoot, absNew);
        this.onOutput?.(`renamed ${relative(this.guard.workspaceRoot, absOld)} → ${relNew}`);
        return absNew;
    }
    async moveFile(srcPath, destDir) {
        this.ensureRunning();
        const absSrc = await this.guard.validateWithSymlinkCheck(srcPath);
        const absDestDir = await this.guard.validateWithSymlinkCheck(destDir);
        const fileName = basename(absSrc);
        const absDest = join(absDestDir, fileName);
        // Validate destination is also within workspace
        this.guard.validate(absDest);
        this._opCount++;
        await mkdir(absDestDir, { recursive: true });
        await rename(absSrc, absDest);
        const relDest = relative(this.guard.workspaceRoot, absDest);
        this.onOutput?.(`moved ${relative(this.guard.workspaceRoot, absSrc)} → ${relDest}`);
        return absDest;
    }
    async createDirectory(dirPath) {
        this.ensureRunning();
        const abs = this.guard.validate(dirPath);
        this._opCount++;
        await mkdir(abs, { recursive: true });
        this.onOutput?.(`created directory ${relative(this.guard.workspaceRoot, abs)}`);
    }
    async createFile(filePath, content = "") {
        this.ensureRunning();
        const abs = await this.guard.validateWithSymlinkCheck(filePath);
        this._opCount++;
        await mkdir(dirname(abs), { recursive: true });
        await writeFile(abs, content, "utf-8");
        this.onOutput?.(`created ${relative(this.guard.workspaceRoot, abs)}`);
    }
    async copyFile(srcPath, destPath) {
        this.ensureRunning();
        const absSrc = await this.guard.validateWithSymlinkCheck(srcPath);
        const absDest = this.guard.validate(destPath);
        this._opCount++;
        await mkdir(dirname(absDest), { recursive: true });
        const srcStat = await stat(absSrc);
        if (srcStat.isDirectory()) {
            await cp(absSrc, absDest, { recursive: true });
        }
        else {
            await cp(absSrc, absDest);
        }
        this.onOutput?.(`copied ${relative(this.guard.workspaceRoot, absSrc)} → ${relative(this.guard.workspaceRoot, absDest)}`);
    }
    ensureRunning() {
        if (this._state !== "running" || !this.guard) {
            throw new Error("FileSystem surface is not running");
        }
    }
    _setState(s) {
        this._state = s;
        this.onStateChange?.(s);
    }
}
export class FileSystemSurfaceFactory {
    guardOpts;
    type = "filesystem";
    constructor(guardOpts) {
        this.guardOpts = guardOpts;
    }
    create(id) {
        return new FileSystemSurface(id, this.guardOpts);
    }
}
//# sourceMappingURL=filesystem.js.map