// tools/sync-spec-state-handler.ts — SPEC-334: Sync spec state between YAML frontmatter
// and the data store. Tool layer: coordinates storage I/O + engine pure logic.
// SPEC-720: File→store status drift is REJECTED. Only store→file sync is allowed for status.
import { z } from 'zod';
import { safe } from './safe-handler.js';
import { detectSpecStateDivergences } from '../engine/spec-state-syncer.js';
import { listSpecs } from '../storage/spec-store.js';
import { getProjects } from '../storage/global-projects-store.js';
import { hashProjectPath } from '../storage/base-store.js';
import { handleUpdateStatus } from './update-status/index.js';
import { parseFrontmatter } from '../engine/frontmatter-parser.js';
import { verifyTerminalFrontmatter } from '../engine/frontmatter-sha/index.js';
import { appendTransitionEvent } from '../storage/transition-log.js';
import { cleanEphemeralArtifacts } from '../engine/housekeeping/index.js';
import { readFile } from 'node:fs/promises';
// ---------------------------------------------------------------------------
// Sync helpers (exported for startup use)
// ---------------------------------------------------------------------------
/**
 * Sync spec state for a single project.
 * Reads YAML frontmatter from spec.md files and reconciles divergences into the store.
 */
export async function syncSpecState(projectPath, projectId) {
    let dataEntries;
    try {
        const specs = await listSpecs(projectId);
        dataEntries = specs.map((s) => ({ id: s.id, status: s.status, specPath: s.specPath }));
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        return {
            synced: 0,
            divergences: [],
            errors: [`Failed to load specs for ${projectId}: ${msg}`],
            rejected: [],
        };
    }
    const { updates, report } = await detectSpecStateDivergences(projectPath, dataEntries);
    // SyncReport.rejected is always defined (SPEC-720)
    const TERMINAL_STATUSES = ['done', 'discarded'];
    for (const { specId, newStatus } of updates) {
        const dataEntry = dataEntries.find((e) => e.id === specId);
        const currentStoreStatus = dataEntry?.status;
        // SPEC-723: If data-store status is terminal, verify SHA before any sync action
        if (dataEntry && currentStoreStatus && TERMINAL_STATUSES.includes(currentStoreStatus)) {
            const specPath = dataEntry.specPath;
            if (specPath) {
                try {
                    const content = await readFile(specPath, 'utf-8');
                    const { metadata } = parseFrontmatter(content);
                    const verification = await verifyTerminalFrontmatter({
                        projectId,
                        specId,
                        currentMetadata: metadata,
                    }).catch(() => ({ ok: true }));
                    if (!verification.ok) {
                        report.rejected.push({
                            specId,
                            reason: `terminal_drift — expectedSha=${verification.expectedSha} actualSha=${verification.actualSha}`,
                        });
                        void appendTransitionEvent({
                            projectId,
                            specId,
                            eventType: 'terminal_drift_detected',
                            frontmatterSha: verification.actualSha,
                            actor: 'sync_spec_state',
                        }).catch(() => {
                            /* best-effort */
                        });
                        const idx = report.divergences.findIndex((d) => d.specId === specId);
                        if (idx !== -1) {
                            report.divergences.splice(idx, 1);
                        }
                        continue;
                    }
                }
                catch {
                    // Unreadable file — fall through to SPEC-720 terminal rejection below
                }
            }
        }
        // SPEC-720: File→store status writes are FORBIDDEN.
        // 1. Terminal drift (passed SHA): file still tries to change a terminal spec → reject.
        // 2. Non-terminal drift: route through handleUpdateStatus so gates run.
        const refusalReason = `SPEC-${specId}: on-disk status '${newStatus}' diverges from store '${currentStoreStatus ?? 'unknown'}'. ` +
            `Direct file→store status sync is disabled. ` +
            `Run update_status(${specId}, ${newStatus}) to attempt the transition through full gates.`;
        // Terminal drift: data store already has terminal status — never overwrite
        if (currentStoreStatus && TERMINAL_STATUSES.includes(currentStoreStatus)) {
            report.rejected.push({
                specId,
                reason: `file→store status drift on ${specId} (terminal) — rejected. Store status '${currentStoreStatus}' is authoritative.`,
            });
            const idx = report.divergences.findIndex((d) => d.specId === specId);
            if (idx !== -1) {
                report.divergences.splice(idx, 1);
            }
            continue;
        }
        // Non-terminal drift: attempt via handleUpdateStatus (runs all gates)
        if (newStatus === currentStoreStatus) {
            // No actual change needed
            report.synced += 1;
            continue;
        }
        try {
            const result = await handleUpdateStatus({
                specId,
                status: newStatus,
                projectId,
                projectPath,
                trigger: 'sync-spec-state',
                viaSync: true,
            });
            if (result.isError) {
                // Gate blocked the transition
                const errText = result.content[0]?.type === 'text' ? result.content[0].text : 'gate blocked';
                report.rejected.push({ specId, reason: refusalReason + ` Gate blocked: ${errText}` });
                const idx = report.divergences.findIndex((d) => d.specId === specId);
                if (idx !== -1) {
                    report.divergences.splice(idx, 1);
                }
            }
            else {
                report.synced += 1;
            }
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            report.errors.push(`Failed to route ${specId} through update_status: ${msg}`);
            const idx = report.divergences.findIndex((d) => d.specId === specId);
            if (idx !== -1) {
                report.divergences.splice(idx, 1);
            }
        }
    }
    return report;
}
/**
 * Startup sync: iterates all registered projects and calls syncSpecState for each.
 * Non-blocking — errors are logged but not propagated.
 */
export async function startupSync() {
    let projects;
    try {
        projects = await getProjects();
    }
    catch (err) {
        console.error('[Planu] startupSync: failed to load projects:', err);
        return;
    }
    await Promise.all(projects.map(async (project) => {
        try {
            await syncSpecState(project.path, project.hash);
        }
        catch (err) {
            console.error(`[Planu] startupSync: failed to sync project ${project.path}:`, err);
        }
        try {
            await cleanEphemeralArtifacts({ projectPath: project.path, dryRun: false });
        }
        catch (err) {
            console.error(`[Planu] startupSync: failed to clean spec artifacts ${project.path}:`, err);
        }
    }));
}
// ---------------------------------------------------------------------------
// Formatting helper
// ---------------------------------------------------------------------------
function formatReport(report, label) {
    const lines = [`Sync complete for ${label}`];
    lines.push(`  Updated: ${report.synced}`);
    if (report.divergences.length > 0) {
        lines.push('  Divergences resolved:');
        for (const d of report.divergences) {
            lines.push(`    - ${d.specId}: ${d.dataStatus} → ${d.yamlStatus}`);
        }
    }
    // SPEC-720: Report rejected file→store status writes
    if (report.rejected.length > 0) {
        lines.push('  Rejected (file→store status drift — use update_status to transition):');
        for (const r of report.rejected) {
            lines.push(`    - ${r.specId}: ${r.reason}`);
        }
    }
    if (report.errors.length > 0) {
        lines.push('  Errors:');
        for (const e of report.errors) {
            lines.push(`    - ${e}`);
        }
    }
    return lines.join('\n');
}
// ---------------------------------------------------------------------------
// Tool registration
// ---------------------------------------------------------------------------
/**
 * Registers the sync_spec_state tool on the MCP server (SPEC-334).
 * Always-on (free) tool — registered directly without safeLicensed.
 */
export function registerSyncSpecStateTool(server) {
    server.registerTool('sync_spec_state', {
        description: 'Synchronise spec status between planu/specs/*/spec.md YAML frontmatter and the ' +
            'internal data store. When a spec.md is edited directly, its status may drift from ' +
            'the store — this tool reconciles them. Without arguments it syncs all registered ' +
            'projects; pass projectPath (or projectId) to limit the sync to one project.',
        inputSchema: {
            projectPath: z
                .string()
                .min(1)
                .max(4096)
                .optional()
                .describe('Absolute path to the project root. Syncs only this project when provided.'),
            projectId: z
                .string()
                .min(1)
                .max(256)
                .optional()
                .describe('Project ID hash. Alternative to projectPath — use whichever is available.'),
        },
        annotations: { title: 'Sync Spec State', readOnlyHint: false },
    }, safe(async (args) => {
        const { projectPath, projectId } = args;
        if (projectPath !== undefined || projectId !== undefined) {
            return handleSingleProject(projectPath, projectId);
        }
        return handleAllProjects();
    }));
}
async function handleSingleProject(projectPath, projectId) {
    let resolvedPath;
    let resolvedId;
    if (projectPath !== undefined) {
        resolvedPath = projectPath;
        resolvedId = projectId ?? hashProjectPath(projectPath);
    }
    else {
        const projects = await getProjects();
        const found = projects.find((p) => p.hash === projectId);
        if (found === undefined) {
            return {
                content: [
                    {
                        type: 'text',
                        text: `Project "${projectId}" not found in registry. Use register_project_path first.`,
                    },
                ],
            };
        }
        resolvedPath = found.path;
        resolvedId = found.hash;
    }
    const report = await syncSpecState(resolvedPath, resolvedId);
    return {
        content: [{ type: 'text', text: formatReport(report, resolvedPath) }],
    };
}
async function handleAllProjects() {
    const projects = await getProjects();
    if (projects.length === 0) {
        return {
            content: [
                {
                    type: 'text',
                    text: 'No registered projects found. Use register_project_path to add projects.',
                },
            ],
        };
    }
    const reportLines = [];
    let totalSynced = 0;
    for (const project of projects) {
        const report = await syncSpecState(project.path, project.hash);
        totalSynced += report.synced;
        reportLines.push(formatReport(report, project.path));
    }
    const summary = `Synced ${totalSynced} spec(s) across ${projects.length} project(s).\n\n`;
    return {
        content: [{ type: 'text', text: summary + reportLines.join('\n\n') }],
    };
}
//# sourceMappingURL=sync-spec-state-handler.js.map