/**
 * Spec Application Logic
 *
 * Extracted from ArchiveCommand to enable standalone spec application.
 * Applies delta specs from a change to main specs without archiving.
 */
import { promises as fs } from 'fs';
import path from 'path';
import chalk from 'chalk';
import { extractRequirementsSection, parseDeltaSpec, normalizeRequirementName, } from './parsers/requirement-blocks.js';
import { Validator } from './validation/validator.js';
// -----------------------------------------------------------------------------
// Public API
// -----------------------------------------------------------------------------
/**
 * Find all delta spec files that need to be applied from a change.
 */
export async function findSpecUpdates(changeDir, mainSpecsDir) {
    const updates = [];
    const changeSpecsDir = path.join(changeDir, 'specs');
    try {
        const entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
        for (const entry of entries) {
            if (entry.isDirectory()) {
                const specFile = path.join(changeSpecsDir, entry.name, 'spec.md');
                const targetFile = path.join(mainSpecsDir, entry.name, 'spec.md');
                try {
                    await fs.access(specFile);
                    // Check if target exists
                    let exists = false;
                    try {
                        await fs.access(targetFile);
                        exists = true;
                    }
                    catch {
                        exists = false;
                    }
                    updates.push({
                        source: specFile,
                        target: targetFile,
                        exists,
                    });
                }
                catch {
                    // Source spec doesn't exist, skip
                }
            }
        }
    }
    catch {
        // No specs directory in change
    }
    return updates;
}
/**
 * Build an updated spec by applying delta operations.
 * Returns the rebuilt content and counts of operations.
 */
export async function buildUpdatedSpec(update, changeName) {
    // Read change spec content (delta-format expected)
    const changeContent = await fs.readFile(update.source, 'utf-8');
    // Parse deltas from the change spec file
    const plan = parseDeltaSpec(changeContent);
    const specName = path.basename(path.dirname(update.target));
    // Pre-validate duplicates within sections
    const addedNames = new Set();
    for (const add of plan.added) {
        const name = normalizeRequirementName(add.name);
        if (addedNames.has(name)) {
            throw new Error(`${specName} validation failed - duplicate requirement in ADDED for header "### Requirement: ${add.name}"`);
        }
        addedNames.add(name);
    }
    const modifiedNames = new Set();
    for (const mod of plan.modified) {
        const name = normalizeRequirementName(mod.name);
        if (modifiedNames.has(name)) {
            throw new Error(`${specName} validation failed - duplicate requirement in MODIFIED for header "### Requirement: ${mod.name}"`);
        }
        modifiedNames.add(name);
    }
    const removedNamesSet = new Set();
    for (const rem of plan.removed) {
        const name = normalizeRequirementName(rem);
        if (removedNamesSet.has(name)) {
            throw new Error(`${specName} validation failed - duplicate requirement in REMOVED for header "### Requirement: ${rem}"`);
        }
        removedNamesSet.add(name);
    }
    const renamedFromSet = new Set();
    const renamedToSet = new Set();
    for (const { from, to } of plan.renamed) {
        const fromNorm = normalizeRequirementName(from);
        const toNorm = normalizeRequirementName(to);
        if (renamedFromSet.has(fromNorm)) {
            throw new Error(`${specName} validation failed - duplicate FROM in RENAMED for header "### Requirement: ${from}"`);
        }
        if (renamedToSet.has(toNorm)) {
            throw new Error(`${specName} validation failed - duplicate TO in RENAMED for header "### Requirement: ${to}"`);
        }
        renamedFromSet.add(fromNorm);
        renamedToSet.add(toNorm);
    }
    // Pre-validate cross-section conflicts
    const conflicts = [];
    for (const n of modifiedNames) {
        if (removedNamesSet.has(n))
            conflicts.push({ name: n, a: 'MODIFIED', b: 'REMOVED' });
        if (addedNames.has(n))
            conflicts.push({ name: n, a: 'MODIFIED', b: 'ADDED' });
    }
    for (const n of addedNames) {
        if (removedNamesSet.has(n))
            conflicts.push({ name: n, a: 'ADDED', b: 'REMOVED' });
    }
    // Renamed interplay: MODIFIED must reference the NEW header, not FROM
    for (const { from, to } of plan.renamed) {
        const fromNorm = normalizeRequirementName(from);
        const toNorm = normalizeRequirementName(to);
        if (modifiedNames.has(fromNorm)) {
            throw new Error(`${specName} validation failed - when a rename exists, MODIFIED must reference the NEW header "### Requirement: ${to}"`);
        }
        // Detect ADDED colliding with a RENAMED TO
        if (addedNames.has(toNorm)) {
            throw new Error(`${specName} validation failed - RENAMED TO header collides with ADDED for "### Requirement: ${to}"`);
        }
    }
    if (conflicts.length > 0) {
        const c = conflicts[0];
        throw new Error(`${specName} validation failed - requirement present in multiple sections (${c.a} and ${c.b}) for header "### Requirement: ${c.name}"`);
    }
    const hasAnyDelta = plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length > 0;
    if (!hasAnyDelta) {
        throw new Error(`Delta parsing found no operations for ${path.basename(path.dirname(update.source))}. ` +
            `Provide ADDED/MODIFIED/REMOVED/RENAMED sections in change spec.`);
    }
    // Load or create base target content
    let targetContent;
    let isNewSpec = false;
    try {
        targetContent = await fs.readFile(update.target, 'utf-8');
    }
    catch {
        // Target spec does not exist; MODIFIED and RENAMED are not allowed for new specs
        // REMOVED will be ignored with a warning since there's nothing to remove
        if (plan.modified.length > 0 || plan.renamed.length > 0) {
            throw new Error(`${specName}: target spec does not exist; only ADDED requirements are allowed for new specs. MODIFIED and RENAMED operations require an existing spec.`);
        }
        // Warn about REMOVED requirements being ignored for new specs
        if (plan.removed.length > 0) {
            console.log(chalk.yellow(`⚠️  Warning: ${specName} - ${plan.removed.length} REMOVED requirement(s) ignored for new spec (nothing to remove).`));
        }
        isNewSpec = true;
        targetContent = buildSpecSkeleton(specName, changeName);
    }
    // Extract requirements section and build name->block map
    const parts = extractRequirementsSection(targetContent);
    const nameToBlock = new Map();
    for (const block of parts.bodyBlocks) {
        nameToBlock.set(normalizeRequirementName(block.name), block);
    }
    // Apply operations in order: RENAMED → REMOVED → MODIFIED → ADDED
    // RENAMED
    for (const r of plan.renamed) {
        const from = normalizeRequirementName(r.from);
        const to = normalizeRequirementName(r.to);
        if (!nameToBlock.has(from)) {
            throw new Error(`${specName} RENAMED failed for header "### Requirement: ${r.from}" - source not found`);
        }
        if (nameToBlock.has(to)) {
            throw new Error(`${specName} RENAMED failed for header "### Requirement: ${r.to}" - target already exists`);
        }
        const block = nameToBlock.get(from);
        const newHeader = `### Requirement: ${to}`;
        const rawLines = block.raw.split('\n');
        rawLines[0] = newHeader;
        const renamedBlock = {
            headerLine: newHeader,
            name: to,
            raw: rawLines.join('\n'),
        };
        nameToBlock.delete(from);
        nameToBlock.set(to, renamedBlock);
    }
    // REMOVED
    for (const name of plan.removed) {
        const key = normalizeRequirementName(name);
        if (!nameToBlock.has(key)) {
            // For new specs, REMOVED requirements are already warned about and ignored
            // For existing specs, missing requirements are an error
            if (!isNewSpec) {
                throw new Error(`${specName} REMOVED failed for header "### Requirement: ${name}" - not found`);
            }
            // Skip removal for new specs (already warned above)
            continue;
        }
        nameToBlock.delete(key);
    }
    // MODIFIED
    for (const mod of plan.modified) {
        const key = normalizeRequirementName(mod.name);
        if (!nameToBlock.has(key)) {
            throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - not found`);
        }
        // Replace block with provided raw (ensure header line matches key)
        const modHeaderMatch = mod.raw.split('\n')[0].match(/^###\s*Requirement:\s*(.+)\s*$/);
        if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
            throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - header mismatch in content`);
        }
        nameToBlock.set(key, mod);
    }
    // ADDED
    for (const add of plan.added) {
        const key = normalizeRequirementName(add.name);
        if (nameToBlock.has(key)) {
            throw new Error(`${specName} ADDED failed for header "### Requirement: ${add.name}" - already exists`);
        }
        nameToBlock.set(key, add);
    }
    // Duplicates within resulting map are implicitly prevented by key uniqueness.
    // Recompose requirements section preserving original ordering where possible
    const keptOrder = [];
    const seen = new Set();
    for (const block of parts.bodyBlocks) {
        const key = normalizeRequirementName(block.name);
        const replacement = nameToBlock.get(key);
        if (replacement) {
            keptOrder.push(replacement);
            seen.add(key);
        }
    }
    // Append any newly added that were not in original order
    for (const [key, block] of nameToBlock.entries()) {
        if (!seen.has(key)) {
            keptOrder.push(block);
        }
    }
    const reqBody = [parts.preamble && parts.preamble.trim() ? parts.preamble.trimEnd() : '']
        .filter(Boolean)
        .concat(keptOrder.map((b) => b.raw))
        .join('\n\n')
        .trimEnd();
    const rebuilt = [parts.before.trimEnd(), parts.headerLine, reqBody, parts.after]
        .filter((s, idx) => !(idx === 0 && s === ''))
        .join('\n')
        .replace(/\n{3,}/g, '\n\n');
    return {
        rebuilt,
        counts: {
            added: plan.added.length,
            modified: plan.modified.length,
            removed: plan.removed.length,
            renamed: plan.renamed.length,
        },
    };
}
/**
 * Write an updated spec to disk.
 */
export async function writeUpdatedSpec(update, rebuilt, counts) {
    // Create target directory if needed
    const targetDir = path.dirname(update.target);
    await fs.mkdir(targetDir, { recursive: true });
    await fs.writeFile(update.target, rebuilt);
    const specName = path.basename(path.dirname(update.target));
    console.log(`Applying changes to openspec/specs/${specName}/spec.md:`);
    if (counts.added)
        console.log(`  + ${counts.added} added`);
    if (counts.modified)
        console.log(`  ~ ${counts.modified} modified`);
    if (counts.removed)
        console.log(`  - ${counts.removed} removed`);
    if (counts.renamed)
        console.log(`  → ${counts.renamed} renamed`);
}
/**
 * Build a skeleton spec for new capabilities.
 */
export function buildSpecSkeleton(specFolderName, changeName) {
    const titleBase = specFolderName;
    return `# ${titleBase} Specification\n\n## Purpose\nTBD - created by archiving change ${changeName}. Update Purpose after archive.\n\n## Requirements\n`;
}
/**
 * Apply all delta specs from a change to main specs.
 *
 * @param projectRoot - The project root directory
 * @param changeName - The name of the change to apply
 * @param options - Options for the operation
 * @returns Result of the operation with counts
 */
export async function applySpecs(projectRoot, changeName, options = {}) {
    const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
    const mainSpecsDir = path.join(projectRoot, 'openspec', 'specs');
    // Verify change exists
    try {
        const stat = await fs.stat(changeDir);
        if (!stat.isDirectory()) {
            throw new Error(`Change '${changeName}' not found.`);
        }
    }
    catch {
        throw new Error(`Change '${changeName}' not found.`);
    }
    // Find specs to update
    const specUpdates = await findSpecUpdates(changeDir, mainSpecsDir);
    if (specUpdates.length === 0) {
        return {
            changeName,
            capabilities: [],
            totals: { added: 0, modified: 0, removed: 0, renamed: 0 },
            noChanges: true,
        };
    }
    // Prepare all updates first (validation pass, no writes)
    const prepared = [];
    for (const update of specUpdates) {
        const built = await buildUpdatedSpec(update, changeName);
        prepared.push({ update, rebuilt: built.rebuilt, counts: built.counts });
    }
    // Validate rebuilt specs unless validation is skipped
    if (!options.skipValidation) {
        const validator = new Validator();
        for (const p of prepared) {
            const specName = path.basename(path.dirname(p.update.target));
            const report = await validator.validateSpecContent(specName, p.rebuilt);
            if (!report.valid) {
                const errors = report.issues
                    .filter((i) => i.level === 'ERROR')
                    .map((i) => `  ✗ ${i.message}`)
                    .join('\n');
                throw new Error(`Validation errors in rebuilt spec for ${specName}:\n${errors}`);
            }
        }
    }
    // Build results
    const capabilities = [];
    const totals = { added: 0, modified: 0, removed: 0, renamed: 0 };
    for (const p of prepared) {
        const capability = path.basename(path.dirname(p.update.target));
        if (!options.dryRun) {
            // Write the updated spec
            const targetDir = path.dirname(p.update.target);
            await fs.mkdir(targetDir, { recursive: true });
            await fs.writeFile(p.update.target, p.rebuilt);
            if (!options.silent) {
                console.log(`Applying changes to openspec/specs/${capability}/spec.md:`);
                if (p.counts.added)
                    console.log(`  + ${p.counts.added} added`);
                if (p.counts.modified)
                    console.log(`  ~ ${p.counts.modified} modified`);
                if (p.counts.removed)
                    console.log(`  - ${p.counts.removed} removed`);
                if (p.counts.renamed)
                    console.log(`  → ${p.counts.renamed} renamed`);
            }
        }
        else if (!options.silent) {
            console.log(`Would apply changes to openspec/specs/${capability}/spec.md:`);
            if (p.counts.added)
                console.log(`  + ${p.counts.added} added`);
            if (p.counts.modified)
                console.log(`  ~ ${p.counts.modified} modified`);
            if (p.counts.removed)
                console.log(`  - ${p.counts.removed} removed`);
            if (p.counts.renamed)
                console.log(`  → ${p.counts.renamed} renamed`);
        }
        capabilities.push({
            capability,
            ...p.counts,
        });
        totals.added += p.counts.added;
        totals.modified += p.counts.modified;
        totals.removed += p.counts.removed;
        totals.renamed += p.counts.renamed;
    }
    return {
        changeName,
        capabilities,
        totals,
        noChanges: false,
    };
}
//# sourceMappingURL=specs-apply.js.map