// tools/repair-frontmatter-drift.ts — SPEC-698: Reconcile spec.md frontmatter with the
// data store. Treats the data store as the authoritative source (because update_status
// always writes there first); rewrites the on-disk frontmatter to match.
//
// Idempotent: running twice returns repairedCount: 0 on the second run.
import { z } from 'zod';
import { readFile, realpath } from 'node:fs/promises';
import { atomicWriteFile } from '../engine/safety/atomic-write-file.js';
import { resolve as pathResolve, isAbsolute, basename, sep as pathSep } from 'node:path';
import { safe } from './safe-handler.js';
import { listSpecs } from '../storage/spec-store.js';
import { hashProjectPath } from '../storage/base-store.js';
import { parseFrontmatter, updateFrontmatterField } from '../engine/frontmatter-parser.js';
import { verifyTerminalFrontmatter } from '../engine/frontmatter-sha/index.js';
import { appendTransitionEvent } from '../storage/transition-log.js';
// Source of truth for SpecStatus — kept in sync with src/types/common/primitives.ts
const VALID_SPEC_STATUSES = [
    'draft',
    'review',
    'approved',
    'implementing',
    'done',
    'discarded',
];
function isValidSpecStatus(value) {
    return typeof value === 'string' && VALID_SPEC_STATUSES.includes(value);
}
// ---------------------------------------------------------------------------
// Public API — pure(ish) function (filesystem I/O, no MCP plumbing)
// ---------------------------------------------------------------------------
/**
 * Repair spec.md frontmatter drift for a single project.
 *
 * For every spec in the data store with a non-empty `specPath`, read the
 * on-disk file, compare its `status:` frontmatter field to the data-store
 * status, and rewrite the file if they diverge. The data store is NEVER
 * modified — it is the source of truth.
 */
export async function repairFrontmatterDrift(args) {
    const projectId = resolveProjectId(args);
    const projectPath = args.projectPath;
    const specs = await listSpecs(projectId);
    const repairs = [];
    const errors = [];
    const corrupt = [];
    // Anchor for path-traversal containment check. If no projectPath is given, we
    // cannot safely validate that resolved spec paths stay inside planu/specs/, so
    // we refuse to write — the data-store specPath could be absolute and point
    // anywhere on disk.
    let canonicalSpecsRoot = null;
    if (projectPath) {
        try {
            canonicalSpecsRoot = await realpath(pathResolve(projectPath, 'planu', 'specs'));
        }
        catch {
            canonicalSpecsRoot = null;
        }
    }
    for (const spec of specs) {
        if (!spec.specPath) {
            continue;
        }
        if (!isValidSpecStatus(spec.status)) {
            errors.push({
                specId: spec.id,
                reason: `data-store status "${String(spec.status)}" is not a valid SpecStatus — refusing to write`,
            });
            continue;
        }
        if (!projectPath || !canonicalSpecsRoot) {
            errors.push({
                specId: spec.id,
                reason: 'projectPath is required to validate spec path containment — refusing to write',
            });
            continue;
        }
        const absSpecPath = isAbsolute(spec.specPath)
            ? spec.specPath
            : pathResolve(projectPath, spec.specPath);
        // Resolve symlinks to defeat path-traversal via crafted symlinks.
        let canonicalSpecPath;
        try {
            canonicalSpecPath = await realpath(absSpecPath);
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            errors.push({ specId: spec.id, reason: `realpath failed: ${msg}` });
            continue;
        }
        if (basename(canonicalSpecPath) !== 'spec.md') {
            errors.push({
                specId: spec.id,
                reason: `refused: target is not spec.md (basename="${basename(canonicalSpecPath)}")`,
            });
            continue;
        }
        if (canonicalSpecPath !== canonicalSpecsRoot &&
            !canonicalSpecPath.startsWith(canonicalSpecsRoot + pathSep)) {
            errors.push({
                specId: spec.id,
                reason: `refused: spec path "${canonicalSpecPath}" escapes planu/specs/ (root="${canonicalSpecsRoot}")`,
            });
            continue;
        }
        let content;
        try {
            content = await readFile(canonicalSpecPath, 'utf-8');
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            errors.push({ specId: spec.id, reason: `read failed: ${msg}` });
            continue;
        }
        const { metadata } = parseFrontmatter(content);
        const yamlStatus = typeof metadata.status === 'string' ? metadata.status : null;
        if (yamlStatus === spec.status) {
            continue;
        }
        // SPEC-723: Guard — if the data-store status is terminal (done/discarded), verify
        // that the on-disk frontmatter has not been tampered with before allowing a rewrite.
        if (spec.status === 'done' || spec.status === 'discarded') {
            const verification = await verifyTerminalFrontmatter({
                projectId,
                specId: spec.id,
                currentMetadata: metadata,
            }).catch(() => ({ ok: true }));
            if (!verification.ok) {
                // Tamper detected — do NOT rewrite. Record in audit trail and corrupt list.
                corrupt.push({
                    specId: spec.id,
                    reason: 'terminal_frontmatter_tamper_detected — SHA mismatch prevents rewrite',
                    expectedSha: verification.expectedSha,
                    actualSha: verification.actualSha,
                });
                void appendTransitionEvent({
                    projectId,
                    specId: spec.id,
                    eventType: 'terminal_drift_detected',
                    frontmatterSha: verification.actualSha,
                    actor: 'repair_frontmatter_drift',
                }).catch(() => {
                    /* best-effort */
                });
                continue;
            }
        }
        try {
            const updated = updateFrontmatterField(content, 'status', spec.status);
            if (updated === content) {
                // No-op write would mean the regex did not match — surface as error
                errors.push({
                    specId: spec.id,
                    reason: `frontmatter unchanged after update — file may be missing valid frontmatter`,
                });
                continue;
            }
            await atomicWriteFile(canonicalSpecPath, updated);
            repairs.push({
                specId: spec.id,
                oldStatus: yamlStatus,
                newStatus: spec.status,
                specPath: canonicalSpecPath,
            });
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            errors.push({ specId: spec.id, reason: `write failed: ${msg}` });
        }
    }
    return {
        repairedCount: repairs.length,
        repairs,
        errors,
        corrupt,
    };
}
function resolveProjectId(args) {
    if (args.projectId) {
        return args.projectId;
    }
    if (args.projectPath) {
        return hashProjectPath(args.projectPath);
    }
    throw new Error('repair_frontmatter_drift: projectPath or projectId is required');
}
// ---------------------------------------------------------------------------
// Formatting helper
// ---------------------------------------------------------------------------
function formatReport(report, projectLabel) {
    if (report.repairedCount === 0 && report.errors.length === 0) {
        return `No frontmatter drift detected — 0 specs repaired in ${projectLabel}`;
    }
    const lines = [`Repaired ${String(report.repairedCount)} spec(s) in ${projectLabel}`];
    if (report.repairs.length > 0) {
        lines.push('  Repairs:');
        for (const r of report.repairs) {
            const oldStr = r.oldStatus ?? '<missing>';
            lines.push(`    - ${r.specId}: ${oldStr} → ${r.newStatus}`);
        }
    }
    if (report.errors.length > 0) {
        lines.push('  Errors:');
        for (const e of report.errors) {
            lines.push(`    - ${e.specId}: ${e.reason}`);
        }
    }
    return lines.join('\n');
}
// ---------------------------------------------------------------------------
// Tool registration
// ---------------------------------------------------------------------------
export function registerRepairFrontmatterDriftTool(server) {
    server.registerTool('repair_frontmatter_drift', {
        description: 'SPEC-698: Reconcile on-disk planu/specs/*/spec.md frontmatter with the internal ' +
            'data store. The data store is the source of truth — any spec.md whose `status:` ' +
            'frontmatter differs from the data-store status is rewritten to match. Idempotent: ' +
            'running twice on the same project repairs 0 specs the second time. Use this when ' +
            'update_status reports a frontmatter sync warning.',
        inputSchema: {
            projectPath: z
                .string()
                .min(1)
                .max(4096)
                .optional()
                .describe('Absolute path to the project root.'),
            projectId: z
                .string()
                .min(1)
                .max(256)
                .optional()
                .describe('Project ID hash. Alternative to projectPath.'),
        },
        annotations: {
            title: 'Repair Frontmatter Drift',
            readOnlyHint: false,
            destructiveHint: false,
        },
    }, safe(async (args) => {
        const { projectPath, projectId } = args;
        if (!projectPath && !projectId) {
            return {
                content: [
                    {
                        type: 'text',
                        text: 'repair_frontmatter_drift: projectPath or projectId is required.',
                    },
                ],
                isError: true,
            };
        }
        const report = await repairFrontmatterDrift({ projectPath, projectId });
        const label = projectPath ?? projectId ?? '<unknown>';
        return {
            content: [{ type: 'text', text: formatReport(report, label) }],
            structuredContent: report,
        };
    }));
}
//# sourceMappingURL=repair-frontmatter-drift.js.map