import fs from "node:fs/promises";
import path from "node:path";
import crypto from "node:crypto";
import { debug } from "./logger.js";
import { safeJsonParse } from "./helpers.js";
/** 默认锁超时: 2 小时 */
const DEFAULT_LOCK_TTL_MS = 2 * 60 * 60 * 1000;
/**
 * 工作区脏数据异常 — 检测到外部修改时抛出。
 * 调用方必须捕获此异常并启动 Diff 合并流程。
 */
export class DirtyWorkspaceError extends Error {
    /** 被修改的文件列表 */
    dirtyFiles;
    /** 锁文件中的原始快照 */
    originalSnapshots;
    /** 任务 ID */
    taskId;
    constructor(dirtyFiles, snapshots, taskId) {
        const fileList = dirtyFiles.length <= 5
            ? dirtyFiles.join(", ")
            : `${dirtyFiles.slice(0, 5).join(", ")} 等 ${dirtyFiles.length} 个文件`;
        super(`⚠ 工作区已被外部修改: ${fileList}。禁止静默覆盖，必须启动 Diff 合并流程`);
        this.name = "DirtyWorkspaceError";
        this.dirtyFiles = dirtyFiles;
        this.originalSnapshots = snapshots;
        this.taskId = taskId;
    }
}
// ── 核心实现 ──
export class IOController {
    projectPath;
    lockPath;
    constructor(projectPath) {
        this.projectPath = projectPath;
        this.lockPath = path.join(projectPath, ".soloforge.lock");
    }
    /**
     * 获取工作区锁。
     * 扫描目标文件并生成快照，写入锁文件。
     * @param taskId - 任务 ID
     * @param targetFiles - 目标文件列表（将要修改的文件）
     * @param reason - 加锁原因
     */
    async acquire(taskId, targetFiles, reason) {
        debug("IOController", "获取工作区锁", { taskId, reason, targetFileCount: targetFiles.length });
        // 原子获取锁：直接尝试创建锁文件（O_EXCL），避免 TOCTOU 竞态
        // 先准备锁数据
        const gitHash = await this.computeGitTreeHash();
        const snapshots = await this.snapshotFiles(targetFiles);
        const lock = {
            task_id: taskId,
            acquired_at: new Date().toISOString(),
            expires_at: new Date(Date.now() + DEFAULT_LOCK_TTL_MS).toISOString(),
            reason,
            git_tree_hash: gitHash,
            file_snapshots: snapshots,
        };
        try {
            // O_EXCL (wx): 原子创建锁文件 — 文件已存在时抛出 EEXIST，消除 check-then-create 竞态窗口
            const handle = await fs.open(this.lockPath, "wx");
            await handle.writeFile(JSON.stringify(lock, null, 2));
            await handle.close();
        }
        catch (e) {
            if (e instanceof Error && "code" in e && e.code === "EEXIST") {
                // 锁文件已存在 — 读取现有锁信息给出友好提示
                const existing = await this.readLock();
                debug("IOController", "工作区已被锁定", { existingTaskId: existing?.task_id });
                throw new Error(`工作区已被任务 ${existing?.task_id} 锁定（原因: ${existing?.reason}，时间: ${existing?.acquired_at}）。请等待完成或手动删除 ${this.lockPath}`);
            }
            throw e;
        }
    }
    /**
     * 校验工作区完整性 — 写入前必须调用。
     * @returns 校验结果
     * @throws DirtyWorkspaceError 当检测到外部修改时
     */
    async verify() {
        debug("IOController", "校验工作区完整性");
        const lock = await this.readLock();
        if (!lock) {
            return { clean: true, dirty_files: [], git_changed: false, message: "无活跃锁" };
        }
        // 锁过期检查: 过期锁视为无效，直接返回 clean 并清理
        if (lock.expires_at && Date.now() > new Date(lock.expires_at).getTime()) {
            debug("IOController", "verify(): 锁已过期，自动释放并视为 clean", { task_id: lock.task_id, expires_at: lock.expires_at });
            await this.release();
            return { clean: true, dirty_files: [], git_changed: false, message: "锁已过期，自动释放" };
        }
        const dirtyFiles = [];
        // 校验每个文件快照
        for (const [filePath, snapshot] of Object.entries(lock.file_snapshots)) {
            const fullPath = path.join(this.projectPath, filePath);
            try {
                const stat = await fs.stat(fullPath);
                const content = await fs.readFile(fullPath);
                const hash = computeHash(content);
                if (stat.mtimeMs !== snapshot.mtime_ms || hash !== snapshot.hash) {
                    dirtyFiles.push(filePath);
                }
            }
            catch {
                // 文件被删除也算脏
                dirtyFiles.push(filePath);
            }
        }
        // 校验 Git 工作树
        const currentGitHash = await this.computeGitTreeHash();
        const gitChanged = currentGitHash !== lock.git_tree_hash;
        const clean = dirtyFiles.length === 0 && !gitChanged;
        let message;
        if (clean) {
            message = "工作区一致，可安全写入";
        }
        else {
            const parts = [];
            if (dirtyFiles.length > 0) {
                parts.push(`${dirtyFiles.length} 个文件被外部修改`);
            }
            if (gitChanged) {
                parts.push("Git 工作树发生变化");
            }
            message = `⚠ 工作区不一致: ${parts.join("，")}。禁止覆盖，需 Diff 合并`;
        }
        return { clean, dirty_files: dirtyFiles, git_changed: gitChanged, message };
    }
    /**
     * 写入前强制校验 — 不通过则抛出 DirtyWorkspaceError。
     * @throws DirtyWorkspaceError
     */
    async assertClean() {
        debug("IOController", "写入前强制校验工作区");
        const result = await this.verify();
        if (!result.clean) {
            debug("IOController", "工作区不干净，抛出 DirtyWorkspaceError", { dirtyFiles: result.dirty_files });
            const lock = await this.readLock();
            throw new DirtyWorkspaceError(result.dirty_files, lock?.file_snapshots ?? {}, lock?.task_id ?? "unknown");
        }
    }
    /**
     * 释放工作区锁 — 任务完成或取消后调用。
     */
    async release() {
        debug("IOController", "释放工作区锁");
        try {
            await fs.unlink(this.lockPath);
        }
        catch {
            // 锁文件不存在，忽略
        }
    }
    /**
     * 检查工作区是否被锁定。
     */
    async isLocked() {
        debug("IOController", "检查工作区锁定状态");
        const lock = await this.readLock();
        if (!lock)
            return { locked: false };
        // 租约超时检查: 过期锁自动视为无效
        if (lock.expires_at && Date.now() > new Date(lock.expires_at).getTime()) {
            debug("IOController", "锁已过期，自动释放", { task_id: lock.task_id, expires_at: lock.expires_at });
            await this.release();
            return { locked: false, expired: true };
        }
        return { locked: true, task_id: lock.task_id, reason: lock.reason };
    }
    // ── 内部方法 ──
    async readLock() {
        try {
            const data = await fs.readFile(this.lockPath, "utf-8");
            return safeJsonParse(data) ?? null;
        }
        catch {
            return null;
        }
    }
    async snapshotFiles(files) {
        const snapshots = {};
        for (const filePath of files) {
            const fullPath = path.join(this.projectPath, filePath);
            try {
                const stat = await fs.stat(fullPath);
                const content = await fs.readFile(fullPath);
                snapshots[filePath] = {
                    mtime_ms: stat.mtimeMs,
                    size: stat.size,
                    hash: computeHash(content),
                };
            }
            catch {
                // 文件不存在，跳过（新建文件的场景）
            }
        }
        return snapshots;
    }
    async computeGitTreeHash() {
        try {
            const { execFileSync } = await import("node:child_process");
            const diff = execFileSync("git", ["diff", "HEAD"], {
                cwd: this.projectPath,
                encoding: "utf-8",
                timeout: 5000,
                stdio: ["pipe", "pipe", "pipe"],
            });
            return computeHash(Buffer.from(diff));
        }
        catch {
            // 非 Git 仓库或 git 不可用 — 用时间戳作为哈希
            return computeHash(Buffer.from(new Date().toISOString().split("T")[0]));
        }
    }
}
function computeHash(data) {
    return crypto.createHash("sha256").update(data).digest("hex").slice(0, 16);
}
//# sourceMappingURL=io_controller.js.map