/**
 * Diff Ownership Store — 持久化文件归属快照，绑定 stateDir。
 * CAP-011 实验性: 只做 advisory 冲突报告，不 hard block。
 *
 * 存储: JSON 文件，原子写入，路径绑定 stateDir。
 * 每个 task_id 下的快照按时间顺序追加。
 */
import fs from "node:fs";
import path from "node:path";
import { debug } from "../core/logger.js";
import { captureBaseline, recordTaskWrite, casCheck, findLatestSnapshot, summarizeOwnership, } from "./diff_ownership.js";
import { errorMessage, safeJsonParse } from "../core/helpers.js";
const DEFAULT_MAX_FILES = 500;
const DEFAULT_MAX_TOTAL_BYTES = 50 * 1024 * 1024; // 50MB
const EXCLUDED_DIRS = new Set([
    ".git", "node_modules", "dist", "build", "coverage",
    ".next", ".nuxt", ".cache", ".turbo", "out", "target",
    "vendor", "__pycache__", ".venv", "venv",
]);
const EXCLUDED_EXTENSIONS = new Set([
    ".map", ".lock", ".woff", ".woff2", ".ttf", ".eot",
    ".ico", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg",
    ".mp4", ".mp3", ".wav", ".avi", ".mov",
    ".zip", ".tar", ".gz", ".rar", ".7z",
    ".wasm", ".exe", ".dll", ".so", ".dylib",
]);
const EXCLUDED_FILES = new Set([
    "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
    "Gemfile.lock", "composer.lock", "Cargo.lock",
    "go.sum", "poetry.lock",
]);
function isExcluded(filePath) {
    const basename = path.basename(filePath);
    if (EXCLUDED_FILES.has(basename))
        return true;
    const ext = path.extname(filePath).toLowerCase();
    if (EXCLUDED_EXTENSIONS.has(ext))
        return true;
    // 排除隐藏文件（点文件）
    if (basename.startsWith("."))
        return true;
    return false;
}
export class DiffOwnershipStore {
    filePath;
    states = new Map();
    loaded = false;
    constructor(stateDir) {
        this.filePath = path.join(stateDir, "diff-ownership.json");
        if (!fs.existsSync(stateDir)) {
            // 无操作
        }
    }
    ensureLoaded() {
        if (this.loaded)
            return;
        this.loaded = true;
        try {
            const data = fs.readFileSync(this.filePath, "utf-8");
            const entries = safeJsonParse(data) ?? [];
            for (const e of entries) {
                this.states.set(e.task_id, e);
            }
        }
        catch (e) {
            debug("差异所有权存储", "加载状态文件失败(预期行为: 文件不存在)，从空状态重新开始 —", errorMessage(e));
            this.states = new Map();
        }
    }
    persist() {
        const entries = [...this.states.values()];
        const tmpPath = `${this.filePath}.tmp`;
        fs.writeFileSync(tmpPath, JSON.stringify(entries, null, 2));
        fs.renameSync(tmpPath, this.filePath);
    }
    /**
     * 为任务捕获基线快照。在任务开始执行时调用一次。
     *
     * @param taskId - 任务 ID
     * @param filePaths - 待捕获的文件路径列表
     * @returns 文件快照数组
     */
    captureBaseline(taskId, filePaths) {
        this.ensureLoaded();
        const snapshots = captureBaseline(filePaths, taskId);
        let state = this.states.get(taskId);
        if (!state) {
            state = { task_id: taskId, baseline_captured_at: null, snapshots: [] };
            this.states.set(taskId, state);
        }
        state.baseline_captured_at = snapshots[0]?.captured_at ?? new Date().toISOString();
        state.snapshots.push(...snapshots);
        this.persist();
        return snapshots;
    }
    /**
     * 从作用域路径捕获基线，将目录展开为其包含的文件。
     * 应用排除规则和大小限制。超出限制时返回建议性摘要。
     * 不做硬性阻止 — 调用方收到摘要后自行决定。
     */
    captureBaselineFromScope(taskId, scopePaths, options) {
        const maxFiles = options?.maxFiles ?? DEFAULT_MAX_FILES;
        const maxTotalBytes = options?.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
        const allFiles = [];
        const excluded = [];
        for (const p of scopePaths) {
            const resolved = path.resolve(p);
            let stat = null;
            try {
                stat = fs.statSync(resolved);
            }
            catch {
                debug("差异所有权存储", "文件不存在(预期行为) —", resolved);
            }
            if (stat?.isDirectory()) {
                const files = listFilesRecursive(resolved);
                allFiles.push(...files);
            }
            else {
                allFiles.push(resolved);
            }
        }
        // 应用排除规则
        const eligible = [];
        for (const f of allFiles) {
            if (isExcluded(f)) {
                excluded.push(f);
            }
            else {
                eligible.push(f);
            }
        }
        // 检查大小限制
        let totalBytes = 0;
        const withinLimit = [];
        let exceededLimit = false;
        for (const f of eligible) {
            let fileSize = 0;
            try {
                fileSize = fs.statSync(f).size;
            }
            catch {
                debug("差异所有权存储", "文件不存在(预期行为) —", f);
            }
            if (withinLimit.length >= maxFiles || totalBytes + fileSize > maxTotalBytes) {
                exceededLimit = true;
                debug("归属存储", "基线捕获超出限制，已截断文件列表");
                break;
            }
            totalBytes += fileSize;
            withinLimit.push(f);
        }
        // 保留原始 scopePaths 中不存在的路径（作为新文件占位符）
        for (const p of scopePaths) {
            const resolved = path.resolve(p);
            try {
                fs.statSync(resolved);
            }
            catch {
                debug("差异所有权存储", "文件不存在(预期行为) —", resolved);
                if (!withinLimit.includes(resolved)) {
                    withinLimit.push(resolved);
                }
            }
        }
        let snapshots = [];
        if (withinLimit.length > 0) {
            snapshots = this.captureBaseline(taskId, withinLimit);
        }
        const advisorySummary = exceededLimit
            ? `baseline scope exceeded limits: ${eligible.length} eligible files found, captured ${withinLimit.length} (${maxFiles} files / ${Math.round(maxTotalBytes / 1024 / 1024)}MB limit). ${excluded.length} files excluded by rules.`
            : excluded.length > 0
                ? `baseline captured ${withinLimit.length} files, ${excluded.length} excluded by rules`
                : null;
        return {
            snapshots,
            advisory_summary: advisorySummary,
            total_files_found: allFiles.length,
            files_excluded: excluded.length,
            files_captured: snapshots.length,
            exceeded_limit: exceededLimit,
        };
    }
    /**
     * 记录任务对文件的写入。
     *
     * @param taskId - 任务 ID
     * @param filePath - 写入的文件路径
     * @returns 文件快照
     */
    recordWrite(taskId, filePath) {
        this.ensureLoaded();
        let state = this.states.get(taskId);
        if (!state) {
            state = { task_id: taskId, baseline_captured_at: null, snapshots: [] };
            this.states.set(taskId, state);
        }
        const snap = recordTaskWrite(filePath, taskId);
        state.snapshots.push(snap);
        this.persist();
        return snap;
    }
    /**
     * 写前执行 CAS 检查。检查失败时返回建议性冲突报告。
     * 不阻止写入 — 调用方自行决定处理方式。
     */
    preWriteCheck(taskId, filePath) {
        this.ensureLoaded();
        const state = this.states.get(taskId);
        const snapshots = state?.snapshots ?? [];
        const latest = findLatestSnapshot(filePath, snapshots);
        const expectedHash = latest?.content_hash ?? null;
        const cas = casCheck(filePath, expectedHash, taskId, snapshots);
        if (!cas.cas_ok) {
            debug("归属存储", "CAS 冲突检测", filePath, "原因:", cas.conflict_reason);
        }
        if (cas.cas_ok) {
            return { cas, conflict: null };
        }
        return {
            cas,
            conflict: {
                file_path: cas.file_path,
                expected_hash: cas.expected_hash,
                current_hash: cas.current_hash,
                inferred_ownership: cas.inferred_ownership,
                severity: "advisory",
                message: `建议性: ${cas.conflict_reason}`,
            },
        };
    }
    /**
     * 获取任务的所有快照。
     *
     * @param taskId - 任务 ID
     * @returns 文件快照数组
     */
    getSnapshots(taskId) {
        this.ensureLoaded();
        const state = this.states.get(taskId);
        return state ? [...state.snapshots] : [];
    }
    /**
     * 获取任务的归属摘要。
     *
     * @param taskId - 任务 ID
     * @returns 按归属类型分组的文件计数
     */
    getOwnershipSummary(taskId) {
        return summarizeOwnership(this.getSnapshots(taskId));
    }
    /**
     * 清理已完成/失败任务的状态。
     *
     * @param taskId - 任务 ID
     */
    clearTask(taskId) {
        this.ensureLoaded();
        this.states.delete(taskId);
        this.persist();
    }
    clear() {
        this.states.clear();
        this.loaded = true;
        this.persist();
    }
}
function listFilesRecursive(dir) {
    const result = [];
    function walk(d) {
        let entries;
        try {
            entries = fs.readdirSync(d, { withFileTypes: true });
        }
        catch {
            debug("差异所有权存储", "读取目录失败(预期行为: 目录不存在) —", d);
            return;
        }
        for (const e of entries) {
            if (e.isDirectory()) {
                if (!EXCLUDED_DIRS.has(e.name) && !e.name.startsWith(".")) {
                    walk(path.join(d, e.name));
                }
            }
            else if (e.isFile()) {
                result.push(path.join(d, e.name));
            }
        }
    }
    walk(dir);
    return result;
}
//# sourceMappingURL=diff_ownership_store.js.map