import { elicitOrFallback, buildEnumSchema } from '../engine/elicitation/elicit-helper.js';
import { ti, t } from '../i18n/index.js';
import { formatSuccess, addNextSteps } from './response-helpers.js';
import { specStore, knowledgeStore } from '../storage/index.js';
import { createVersion } from '../engine/spec-versioner.js';
import { readFile } from 'node:fs/promises';
import { atomicWriteFile } from '../engine/safety/atomic-write-file.js';
import { dirname } from 'node:path';
import { replaceSectionInSpec } from '../engine/spec-format/replace-section.js';
import { extractSectionBody } from '../engine/spec-format/read-technical-section.js';
import { appendChangelogEntry } from '../engine/spec-changelog/index.js';
import { stripFrontmatter } from '../engine/frontmatter-parser.js';
import { analyzeLivingSpec } from '../engine/living-spec-analyzer.js';
import { notifyStoreChange } from '../engine/doc-generator/portal/regen-hook.js';
// SPEC-1011 Bug D: Post-write verification + apply text changes
import { applyChangesToSpec } from '../engine/reconcile/apply-changes.js';
import { verifyWriteSucceeded } from '../engine/reconcile/verify-write.js';
/**
 * Auto-detect changes by comparing spec metadata with current state.
 */
async function autoDetectChanges(spec, _knowledge) {
    const changes = [];
    // Check if estimation was significantly off (only if actuals exist)
    if (spec.actuals) {
        const estimatedTotal = spec.estimation.devHours + spec.estimation.reviewHours;
        const actualTotal = spec.actuals.devHours + spec.actuals.reviewHours;
        const ratio = actualTotal / estimatedTotal;
        if (ratio > 1.3 || ratio < 0.7) {
            changes.push({
                section: 'estimation',
                originalValue: `${estimatedTotal}h estimated`,
                newValue: `${actualTotal}h actual (${Math.round(ratio * 100)}% of estimate)`,
                reason: ratio > 1.3
                    ? 'Implementation took significantly longer than estimated'
                    : 'Implementation was completed faster than estimated',
                approved: true,
            });
        }
        // Cost discrepancy
        if (Math.abs(spec.estimation.totalCostUsd - spec.actuals.totalCostUsd) > 10) {
            changes.push({
                section: 'cost',
                originalValue: `$${spec.estimation.totalCostUsd}`,
                newValue: `$${spec.actuals.totalCostUsd}`,
                reason: 'Actual cost differs from estimate',
                approved: true,
            });
        }
    }
    // Check if status should be updated
    if (spec.actuals && spec.status !== 'done') {
        changes.push({
            section: 'status',
            originalValue: spec.status,
            newValue: 'done',
            reason: 'Actuals are recorded but status is not done',
            approved: false,
        });
    }
    // Detect if spec files have been manually modified
    if (!spec.specPath) {
        return changes;
    }
    try {
        const rawHuContent = await readFile(spec.specPath, 'utf-8');
        const huContent = stripFrontmatter(rawHuContent);
        if (huContent.includes('MODIFIED') || huContent.includes('UPDATED')) {
            changes.push({
                section: 'hu-manual-edits',
                originalValue: 'Original spec content',
                newValue: 'Manual edits detected in spec.md',
                reason: 'spec.md appears to have been manually modified after creation',
                approved: true,
            });
        }
    }
    catch {
        changes.push({
            section: 'hu-file',
            originalValue: spec.specPath,
            newValue: 'File not found',
            reason: 'spec.md file is missing or was moved',
            approved: false,
        });
    }
    try {
        await readFile(spec.technicalPath, 'utf-8');
    }
    catch {
        changes.push({
            section: 'ficha-file',
            originalValue: spec.technicalPath,
            newValue: 'File not found',
            reason: 'spec.md inline Technical/Files section is missing or incomplete',
            approved: false,
        });
    }
    return changes;
}
/**
 * Apply approved changes to the spec files.
 */
async function applyChangesToSpecFiles(spec, changes) {
    const approvedChanges = changes.filter((c) => c.approved);
    if (approvedChanges.length === 0) {
        return;
    }
    // Build reconciliation log entry
    const reconcileRows = approvedChanges.map((c) => `| ${c.section} | ${c.originalValue} | ${c.newValue} | ${c.reason} |`);
    const reconcileSection = [
        `> Reconciled at: ${new Date().toISOString()}`,
        '',
        '| Section | Original | Updated | Reason |',
        '|---------|----------|---------|--------|',
        ...reconcileRows,
    ].join('\n');
    // Write reconciliation log into the ## Technical section of spec.md
    if (spec.specPath) {
        try {
            // Read current Technical section body and append reconciliation log to it
            const specContent = await readFile(spec.specPath, 'utf-8');
            const existingTechnical = extractSectionBody(specContent, 'Technical');
            let updatedTechnical;
            if (existingTechnical.includes('### Reconciliation Log')) {
                // Append inside existing Reconciliation Log subsection. The Log uses
                // h3 (### ) so it nests inside ## Technical without the body parser
                // truncating the parent section. Terminate the inner block at the
                // next h3, h2, hr, or EOF. (SPEC-1010 PR-C dual-Opus review.)
                updatedTechnical = existingTechnical.replace(/(### Reconciliation Log[\s\S]*?)(\n### |\n## |\n$|$)/, `$1\n${reconcileSection}\n$2`);
            }
            else {
                updatedTechnical =
                    existingTechnical.length > 0
                        ? `${existingTechnical}\n\n### Reconciliation Log\n\n${reconcileSection}`
                        : `### Reconciliation Log\n\n${reconcileSection}`;
            }
            await replaceSectionInSpec(spec.specPath, 'Technical', updatedTechnical);
        }
        catch {
            // spec.md unreadable — skip
        }
    }
    // Update post-implementation actuals inside the ## Technical section
    if (spec.actuals && spec.specPath) {
        try {
            const specContent = await readFile(spec.specPath, 'utf-8');
            let fichaContent = extractSectionBody(specContent, 'Technical');
            fichaContent = fichaContent.replace(/(\| Dev Hours \| [^|]+ \|) — \|/, `$1 ${spec.actuals.devHours}h |`);
            fichaContent = fichaContent.replace(/(\| Review Hours \| [^|]+ \|) — \|/, `$1 ${spec.actuals.reviewHours}h |`);
            fichaContent = fichaContent.replace(/(\| API Cost \| [^|]+ \|) — \|/, `$1 $${spec.actuals.apiCostUsd} |`);
            fichaContent = fichaContent.replace(/(\| Total Cost \| [^|]+ \|) — \|/, `$1 $${spec.actuals.totalCostUsd} |`);
            await replaceSectionInSpec(spec.specPath, 'Technical', fichaContent);
        }
        catch {
            // spec.md unreadable — skip
        }
    }
}
/**
 * Append a reconciliation note to PLAN.md when changes are applied.
 */
async function appendPlanReconcileNote(planPath, changes) {
    try {
        const existing = await readFile(planPath, 'utf-8');
        const note = [
            '',
            '---',
            '',
            `## Reconciliation Note — ${new Date().toISOString()}`,
            '',
            `> ${changes.length} change(s) applied during reconciliation:`,
            '',
            ...changes.map((c) => `- **${c.section}**: ${c.reason}`),
            '',
        ].join('\n');
        await atomicWriteFile(planPath, existing + note);
    }
    catch {
        // Non-fatal: if PLAN.md can't be updated, reconcile still succeeds
    }
}
export async function handleReconcileSpec(params, server) {
    const { specId, projectId, autoDetect = true, livingSpec = false, changes: manualChanges, } = params;
    try {
        // Get the spec
        const spec = await specStore.getSpec(projectId, specId);
        if (!spec) {
            return {
                content: [{ type: 'text', text: ti('spec.notFound', { id: specId }) }],
                isError: true,
            };
        }
        // Load project knowledge
        const knowledge = await knowledgeStore.getKnowledge(projectId);
        // Collect all changes
        const allChanges = [];
        // Auto-detect changes
        if (autoDetect) {
            const autoChanges = await autoDetectChanges(spec, knowledge);
            allChanges.push(...autoChanges);
        }
        // Add manual changes
        if (manualChanges && manualChanges.length > 0) {
            allChanges.push(...manualChanges);
        }
        // SPEC-595: Elicit conflict resolution strategy when there are pending (non-approved) changes
        const pendingChanges = allChanges.filter((c) => !c.approved);
        if (server !== undefined && pendingChanges.length > 0) {
            const { field, property } = buildEnumSchema('resolution', ['keep-mine', 'use-spec', 'merge-interactive', 'cancel'], [
                'Keep mine — discard spec suggestions',
                'Use spec — apply all suggested changes',
                'Merge interactively — review each change',
                'Cancel — do not apply anything',
            ], `${String(pendingChanges.length)} conflict(s) detected. How to resolve?`, 'keep-mine');
            const schema = { type: 'object', properties: { [field]: property } };
            const fallback = {
                header: 'Conflict Resolution',
                question: `${String(pendingChanges.length)} pending change(s) detected. How do you want to resolve conflicts?`,
                multiSelect: false,
                options: [
                    {
                        label: 'keep-mine',
                        description: 'Keep current state — discard spec suggestions (safe default)',
                    },
                    { label: 'use-spec', description: 'Apply all suggested changes from spec' },
                    { label: 'merge-interactive', description: 'Review each change interactively' },
                    { label: 'cancel', description: 'Cancel — do not apply anything' },
                ],
            };
            const outcome = await elicitOrFallback(server, `${String(pendingChanges.length)} conflict(s) detected. How to resolve?`, schema, [fallback]);
            if (outcome.mode === 'fallback') {
                return {
                    content: [
                        {
                            type: 'text',
                            text: `⚡ INTERACTIVE — call AskUserQuestion now with the interactiveQuestions from structuredContent`,
                        },
                    ],
                    structuredContent: {
                        specId,
                        pendingChanges: pendingChanges.length,
                        interactiveQuestions: outcome.interactiveQuestions,
                    },
                };
            }
            const resolution = outcome.action === 'accept'
                ? (outcome.content?.resolution ?? 'keep-mine')
                : 'cancel';
            if (resolution === 'cancel') {
                return { content: [{ type: 'text', text: `Reconcile ${specId} cancelled.` }] };
            }
            if (resolution === 'keep-mine') {
                // Remove pending changes — only keep already-approved ones
                allChanges.splice(0, allChanges.length, ...allChanges.filter((c) => c.approved));
            }
            else if (resolution === 'use-spec') {
                // Approve all pending changes
                for (const change of pendingChanges) {
                    change.approved = true;
                }
            }
            // 'merge-interactive': proceed with whatever is currently approved (LLM handles the rest)
        }
        // Apply approved changes to spec files
        await applyChangesToSpecFiles(spec, allChanges);
        // SPEC-1011 Bug D: Apply text changes to spec.md body and run post-write verification.
        // This is the actual write path for manual changes with section content updates.
        const approvedChanges = allChanges.filter((c) => c.approved);
        const preWriteVersion = approvedChanges.length > 0 ? await specStore.getLatestVersion(projectId, specId) : null;
        let specBodyWritten = false;
        let bodySkippedChanges = [];
        if (spec.specPath && approvedChanges.length > 0) {
            const applyResult = await applyChangesToSpec(spec.specPath, approvedChanges);
            specBodyWritten = applyResult.fileWritten;
            bodySkippedChanges = applyResult.skipped;
            if (applyResult.fileWritten) {
                // SPEC-1011 Bug D — Post-write integrity check: re-read and verify each newValue
                // is present. If verification fails, the file was written but the content does not
                // reflect the requested changes (e.g., atomic-write race or replace silently no-oped).
                // In that case we surface a hard error and do NOT create a version, since reporting
                // specUpdated:true with versionCreated:N would lie about disk state — exactly the bug.
                //
                // Verify only changes that applyChangesToSpec actually applied. Skipped changes
                // (originalValue not found in section, ambiguous, etc.) are documented as
                // non-fatal further down — including them here would falsely fail verification on
                // legitimate conceptual/metadata-only changes. (SPEC-1010 PR-C dual-Opus review.)
                const skippedKey = (s) => `${s.section}::${s.originalValue}`;
                const skippedKeys = new Set(applyResult.skipped.map(skippedKey));
                const appliedChanges = approvedChanges.filter((c) => !skippedKeys.has(skippedKey(c)));
                const verification = await verifyWriteSucceeded(spec.specPath, appliedChanges);
                if (!verification.verified) {
                    return {
                        content: [
                            {
                                type: 'text',
                                text: `reconcile_spec post-write verification failed: ${verification.missingChanges.map((m) => `[${m.section}] ${m.reason}`).join('; ')}`,
                            },
                        ],
                        isError: true,
                        structuredContent: {
                            specUpdated: false,
                            postWriteVerificationFailed: true,
                            missingChanges: verification.missingChanges,
                            // The file was already written before verification ran; "version-rolled-back"
                            // means we refused to record a SpecVersion entry, not that disk was reverted.
                            versionRolledBack: true,
                            preWriteVersion,
                        },
                    };
                }
            }
            // applyResult.skipped (originalValue not found in spec.md body) is non-fatal:
            // many manual changes are conceptual (e.g., scope expansion notes) and only update
            // metadata/version, not the spec body. Surfaced via structuredContent below.
        }
        // Update spec metadata if needed
        const hasStatusChange = approvedChanges.find((c) => c.section === 'status');
        if (hasStatusChange) {
            // SPEC-720: Use __internalSetStatus for reconcile (tool-layer maintenance write,
            // not a user-initiated transition — no gates needed here as this is driven by an
            // approved reconcile review with explicit change approval).
            await specStore.__internalSetStatus(projectId, specId, hasStatusChange.newValue);
        }
        // Update PLAN.md if spec has one and there were approved changes
        if (spec.planPath && approvedChanges.length > 0) {
            await appendPlanReconcileNote(spec.planPath, approvedChanges);
        }
        // Create a new SpecVersion only when there were approved changes that actually mutated state.
        // (specBodyWritten OR metadata-only changes that are still valid)
        let createdVersion = null;
        if (approvedChanges.length > 0) {
            const currentVersion = await specStore.getLatestVersion(projectId, specId);
            const version = createVersion(specId, currentVersion, approvedChanges, `Reconcile: ${approvedChanges.length} change(s) applied`);
            await specStore.saveVersion(projectId, specId, version);
            createdVersion = version.version;
        }
        // Mark spec as reconciled by updating the timestamp
        await specStore.updateSpec(projectId, specId, {
            updatedAt: new Date().toISOString(),
        });
        const report = {
            specId,
            changes: allChanges,
            specUpdated: approvedChanges.length > 0,
            reason: approvedChanges.length > 0
                ? `Applied ${approvedChanges.length} change(s) to spec`
                : 'No changes to apply',
            reconciledAt: new Date().toISOString(),
        };
        // Generate changelog entry if there were approved changes (AC-02, AC-08)
        let changelogUpdated = false;
        if (approvedChanges.length > 0 && spec.specPath) {
            const specDir = dirname(spec.specPath);
            changelogUpdated = await appendChangelogEntry({
                specDir,
                specId,
                entry: {
                    timestamp: new Date().toISOString(),
                    type: 'reconciled',
                    description: `Se reconcilio la spec. ${approvedChanges.length} cambio(s) aplicado(s): ${approvedChanges.map((c) => c.section).join(', ')}.`,
                    triggeredBy: 'reconcile_spec',
                    before: { criteriaCount: allChanges.length, completedCount: 0, status: spec.status },
                    after: {
                        criteriaCount: allChanges.length,
                        completedCount: 0,
                        status: hasStatusChange ? hasStatusChange.newValue : spec.status,
                    },
                },
            });
        }
        // Living spec analysis: compare criteria against codebase
        let livingSpecReport = null;
        if (livingSpec) {
            const projectKnowledge = await knowledgeStore.getKnowledge(projectId);
            if (projectKnowledge?.projectPath) {
                livingSpecReport = await analyzeLivingSpec(spec, projectKnowledge.projectPath);
            }
        }
        const result = {
            ...report,
            versionCreated: createdVersion,
            changelogUpdated,
            specBodyWritten,
            ...(bodySkippedChanges.length > 0 ? { skippedTextChanges: bodySkippedChanges } : {}),
            ...(livingSpecReport ? { livingSpec: livingSpecReport } : {}),
            summary: {
                totalChanges: allChanges.length,
                approvedChanges: approvedChanges.length,
                pendingChanges: allChanges.filter((c) => !c.approved).length,
                autoDetected: autoDetect
                    ? allChanges.filter((c) => !manualChanges?.some((mc) => mc.section === c.section)).length
                    : 0,
                manual: manualChanges?.length ?? 0,
            },
            message: ti('tools.reconcile_spec.success', {
                specId,
                count: String(allChanges.length),
            }),
        };
        // Notify portal regeneration (fire-and-forget)
        if (knowledge?.projectPath) {
            notifyStoreChange(knowledge.projectPath, 'specs');
        }
        const summaryLines = [
            `**${t('tools.reconcileSpec.specId')}:** ${result.specId}`,
            `**${t('tools.reconcileSpec.totalChanges')}:** ${result.summary.totalChanges}`,
            `**${t('tools.reconcileSpec.approvedChanges')}:** ${result.summary.approvedChanges}`,
            `**${t('tools.reconcileSpec.pendingChanges')}:** ${result.summary.pendingChanges}`,
        ];
        if (result.versionCreated !== null) {
            summaryLines.push(`**${t('tools.reconcileSpec.versionCreated')}:** v${String(result.versionCreated)}`);
        }
        if (result.changelogUpdated) {
            summaryLines.push(`**${t('tools.reconcileSpec.changelogUpdated')}:** ${t('tools.reconcileSpec.yes')}`);
        }
        if (result.livingSpec) {
            summaryLines.push(`**${t('tools.reconcileSpec.livingSpec')}:** ${String(result.livingSpec.completionPercent)}% ${t('tools.reconcileSpec.coverage')}`);
        }
        const body = summaryLines.join('\n');
        const nextSteps = [t('tools.reconcileSpec.nextValidate'), t('tools.reconcileSpec.nextDrift')];
        const humanText = addNextSteps(formatSuccess(t('tools.response.reconcileTitle'), body), nextSteps);
        return {
            content: [{ type: 'text', text: humanText }],
            structuredContent: result,
        };
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        return {
            content: [{ type: 'text', text: ti('errors.internalError', { message }) }],
            isError: true,
        };
    }
}
//# sourceMappingURL=reconcile-spec.js.map