import { calculateAccuracy } from '../../engine/estimator.js';
import { metricsStore } from '../../storage/index.js';
import { addLesson, getLessons } from '../../storage/lessons-store.js';
import { dispatchFeedbackEvent } from '../learn.js';
import { generateProgressUpdate } from '../../engine/progress-writer.js';
import { autoReconcileOnDone } from '../update-status-reconcile.js';
import { readFile, writeFile } from 'node:fs/promises';
import { replaceSectionInSpec } from '../../engine/spec-format/replace-section.js';
import { parseFrontmatter } from '../../engine/frontmatter-parser.js';
import { computeFrontmatterSha } from '../../engine/frontmatter-sha/index.js';
import { appendTransitionEvent } from '../../storage/transition-log.js';
export async function recordDoneMetrics(projectId, specId, spec, actuals, projectPath) {
    const accuracyScore = calculateAccuracy(spec.estimation, actuals.devHours + actuals.reviewHours);
    const metricEntry = {
        specId,
        difficulty: spec.difficulty,
        estimated: spec.estimation,
        actual: actuals,
        accuracyScore,
    };
    await metricsStore.addMetricEntry(projectId, metricEntry);
    if (accuracyScore < 0.7 || accuracyScore > 1.3) {
        void dispatchFeedbackEvent(projectId, {
            type: 'validate',
            specId,
            input: { specType: spec.type, scope: spec.scope, difficulty: spec.difficulty },
            outcome: {
                estimationAccuracy: accuracyScore,
                estimatedHours: spec.estimation.devHours + spec.estimation.reviewHours,
                actualHours: actuals.devHours + actuals.reviewHours,
            },
            delta: { accuracyDrift: Math.abs(1 - accuracyScore) },
            timestamp: new Date().toISOString(),
        }).catch((err) => {
            console.error('[planu] dispatchFeedbackEvent failed:', err);
        });
    }
    if (accuracyScore < 0.7 && projectPath) {
        void autoCaptureLessonEstimation(spec, actuals, accuracyScore, projectId, projectPath);
    }
}
async function autoCaptureLessonEstimation(spec, actuals, accuracy, projectId, projectPath) {
    try {
        const existing = await getLessons(projectPath, {
            specId: spec.id,
            category: 'estimation',
            tags: ['auto-captured'],
        });
        if (existing.length > 0) {
            return;
        }
        const estimatedHours = spec.estimation.devHours + spec.estimation.reviewHours;
        const actualHours = actuals.devHours + actuals.reviewHours;
        const severity = accuracy < 0.6 ? 'high' : 'medium';
        await addLesson(projectPath, {
            projectId,
            specId: spec.id,
            category: 'estimation',
            severity,
            title: `Estimation miss on ${spec.title}`,
            description: `Spec took ${String(actualHours)}h actual vs ${String(estimatedHours)}h estimated (${String(Math.round(accuracy * 100))}% accuracy)`,
            prevention: 'Review complexity and scope assumptions before estimating similar specs',
            tags: [...spec.tags, 'auto-captured', 'estimation'],
        });
    }
    catch {
        /* best-effort — never throw */
    }
}
const SPEC_BODY_SHRINK_ABSOLUTE_THRESHOLD = 200;
const SPEC_BODY_SHRINK_RATIO_THRESHOLD = 0.05;
function bodyWithoutFrontmatter(content) {
    return parseFrontmatter(content).body;
}
function hasExplicitTruncationMarker(content) {
    return /\btruncated\b|SPEC_INTEGRITY_ALLOWED_REDUCTION/i.test(content);
}
function assertSpecBodyIntegrity(args) {
    if (args.newStatus === 'done') {
        return;
    }
    if (hasExplicitTruncationMarker(args.updated)) {
        return;
    }
    const originalBodyLength = bodyWithoutFrontmatter(args.original).length;
    const updatedBodyLength = bodyWithoutFrontmatter(args.updated).length;
    const lost = originalBodyLength - updatedBodyLength;
    const ratio = originalBodyLength === 0 ? 0 : lost / originalBodyLength;
    if (lost > SPEC_BODY_SHRINK_ABSOLUTE_THRESHOLD && ratio > SPEC_BODY_SHRINK_RATIO_THRESHOLD) {
        throw new Error(`SPEC_INTEGRITY_WRITE_FAILED: refusing to write ${args.specId} because update_status(${args.newStatus}) would shrink spec.md body by ${String(lost)} characters without an explicit migration marker.`);
    }
}
export async function syncSpecFiles(updatedSpec, currentStatus, newStatus, projectPath) {
    let warning;
    if (updatedSpec.specPath) {
        try {
            const specContent = await readFile(updatedSpec.specPath, 'utf-8');
            const { updateFrontmatterField } = await import('../../engine/frontmatter-parser.js');
            let updatedContent = updateFrontmatterField(specContent, 'status', newStatus);
            if (newStatus === 'done') {
                updatedContent = markAllCriteriaDone(updatedContent);
                if (isLeanSpecContent(updatedContent)) {
                    updatedContent = stripDoneCriteria(updatedContent);
                }
            }
            assertSpecBodyIntegrity({
                original: specContent,
                updated: updatedContent,
                newStatus,
                specId: updatedSpec.id,
            });
            await writeFile(updatedSpec.specPath, updatedContent, 'utf-8');
            // SPEC-544: Auto-stage spec.md after writing so it's included in the user's next commit
            // SPEC-575: Auto-commit staged planu/ docs (idempotent, safe-fail)
            // Commit message must reflect the actual transition — 'mark-done' is only
            // correct when newStatus === 'done'; any other transition uses 'status-update'
            // to avoid misleading "mark as done" messages on approved/implementing/etc.
            if (projectPath &&
                process.env.PLANU_ENABLE_AUTOCOMMIT === 'true' &&
                process.env.PLANU_SUPPRESS_AUTOCOMMIT !== 'true') {
                try {
                    const { git: gitCmd } = await import('../git/git-helpers.js');
                    await gitCmd(projectPath, ['add', '--', updatedSpec.specPath]);
                    const { planuAutoCommit } = await import('../../engine/git/planu-autocommit.js');
                    void planuAutoCommit({
                        projectPath,
                        specId: updatedSpec.id,
                        reason: newStatus === 'done' ? 'mark-done' : 'status-update',
                    });
                }
                catch {
                    // best-effort — never blocks the transition
                }
            }
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            // SPEC-698: log AND surface the warning so the user sees it in the tool response
            console.warn(`[Planu] update_status: failed to sync spec.md for ${updatedSpec.id} at ${updatedSpec.specPath}: ${msg}`);
            warning = { specId: updatedSpec.id, specPath: updatedSpec.specPath, reason: msg };
        }
    }
    else {
        const reason = 'specPath not set on spec — spec.md not updated on disk. Run reconcile_spec to re-link this spec to its file.';
        console.warn(`[Planu] update_status: ${reason} (${updatedSpec.id})`);
        warning = { specId: updatedSpec.id, specPath: null, reason };
    }
    // SPEC-1010 PR-C: Write progress into the unified spec.md ## Progress section
    // instead of a standalone progress.md file. We do this only when specPath is
    // known (which it always is for specs that reach syncSpecFiles).
    if (updatedSpec.specPath) {
        try {
            const existing = await readFile(updatedSpec.specPath, 'utf-8');
            // Extract current ## Progress body (may be empty for new lean specs)
            const progressBody = extractProgressBody(existing);
            const updatedProgress = generateProgressUpdate(updatedSpec, currentStatus, progressBody);
            await replaceSectionInSpec(updatedSpec.specPath, 'Progress', updatedProgress);
        }
        catch {
            // best-effort — never blocks the transition
        }
    }
    return warning ? { warning } : {};
}
/** Flip all `    done: false` lines to `    done: true` in spec content.
 * Safe: in lean YAML spec, `done: false` only appears inside criteria items. */
function markAllCriteriaDone(content) {
    return content.replace(/^(\s+done:\s*)false$/gm, '$1true');
}
/** True when content is a lean-format spec (has a `criteria:` key in the frontmatter). */
function isLeanSpecContent(content) {
    return /^criteria:/m.test(content);
}
/**
 * SPEC-630: Remove done criteria entries from lean spec and annotate the count on the criteria key.
 * Only call this after markAllCriteriaDone — assumes all completed criteria have `done: true`.
 */
function stripDoneCriteria(content) {
    let doneCount = 0;
    const stripped = content.replace(/^(\s+- text:.*\n\s+done: true\n?)/gm, () => {
        doneCount++;
        return '';
    });
    return stripped.replace(/^(criteria:)(\s*#.*)?$/m, `$1 # ${String(doneCount)} done`);
}
/**
 * Extract the body of the `## Progress` section from spec.md content.
 * Returns "" when the section is absent so callers get a clean empty string.
 * Mirrors the extractSectionBody logic without importing from spec-format to
 * keep this module's dependency chain flat.
 */
function extractProgressBody(specContent) {
    const normalized = specContent.replace(/\r\n/g, '\n');
    // Mask fenced code blocks so inner markdown headings are not matched
    const masked = normalized.replace(/(```|~~~)[\s\S]*?\1/g, (block) => block.replace(/[^\n]/g, ' '));
    const headingMatch = /^##\s+Progress\b.*$/m.exec(masked);
    if (!headingMatch) {
        return '';
    }
    const afterHeading = headingMatch.index + headingMatch[0].length;
    const nextRe = /^##\s+\S/gm;
    nextRe.lastIndex = afterHeading;
    const next = nextRe.exec(masked);
    const end = next ? next.index : normalized.length;
    return normalized.slice(afterHeading, end).trim();
}
export async function tryReconcile(newStatus, specId, projectId) {
    if (newStatus !== 'done') {
        return null;
    }
    return autoReconcileOnDone(specId, projectId).catch(() => null);
}
/**
 * SPEC-723: Record a terminal transition event in the hash-chained log.
 * SPEC-734: Now threads sessionId, modelId, and gateResults through to the log entry.
 *
 * Called AFTER the spec.md has been written to disk so the SHA reflects the
 * final on-disk state. Best-effort — never throws.
 */
export async function recordTerminalTransitionEvent(args) {
    try {
        let frontmatterSha;
        if (args.specPath) {
            try {
                const content = await readFile(args.specPath, 'utf-8');
                const { metadata } = parseFrontmatter(content);
                frontmatterSha = computeFrontmatterSha(metadata);
            }
            catch {
                // If we can't read the spec.md, compute from minimal metadata
                frontmatterSha = undefined;
            }
        }
        await appendTransitionEvent({
            projectId: args.projectId,
            specId: args.specId,
            eventType: 'transitioned_to_terminal',
            frontmatterSha,
            from: args.from,
            to: args.newStatus,
            actor: args.actor,
            sessionId: args.sessionId,
            modelId: args.modelId,
            gateResults: args.gateResults,
            meta: args.meta,
        });
    }
    catch {
        // Best-effort — never throw from file-sync
    }
}
//# sourceMappingURL=file-sync.js.map