// engine/spec-versioning/bump-version.ts — Pure SemVer bump + spec frontmatter writer (SPEC-717)
import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
import { existsSync } from 'node:fs';
import { detectSpecFormatVersion, migrateFrontmatterToCurrentFormat } from './migrate-format.js';
import { renderHistoryYaml } from './render-history.js';
// ---------------------------------------------------------------------------
// Pure SemVer bump
// ---------------------------------------------------------------------------
/**
 * Compute the next SemVer string from a current version and a bump kind.
 *
 * @throws if `current` is not a valid "MAJOR.MINOR.PATCH" string.
 */
export function bumpSemver(current, kind) {
    const parts = current.split('.');
    if (parts.length !== 3) {
        throw new Error(`bumpSemver: invalid SemVer "${current}" — expected MAJOR.MINOR.PATCH`);
    }
    const [majorStr, minorStr, patchStr] = parts;
    const major = parseInt(majorStr ?? '0', 10);
    const minor = parseInt(minorStr ?? '0', 10);
    const patch = parseInt(patchStr ?? '0', 10);
    if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
        throw new Error(`bumpSemver: non-numeric segment in "${current}"`);
    }
    switch (kind) {
        case 'major':
            return `${major + 1}.0.0`;
        case 'minor':
            return `${major}.${minor + 1}.0`;
        case 'patch':
            return `${major}.${minor}.${patch + 1}`;
    }
}
// ---------------------------------------------------------------------------
// Frontmatter-level bump on spec.md
// ---------------------------------------------------------------------------
/**
 * Read the spec.md at `specPath`, bump `spec_version`, append a history entry,
 * and write the file atomically via a .tmp sibling.
 *
 * @throws if the spec is in format 0.x (must be migrated first via a write operation).
 */
export async function bumpSpecVersion(input) {
    const { specId, kind, reason, by, specPath } = input;
    if (reason.trim().length < 5) {
        throw new Error('bumpSpecVersion: reason must be at least 5 characters');
    }
    const raw = await readFile(specPath, 'utf-8');
    // Parse frontmatter block
    const fm = extractFrontmatterBlock(raw);
    if (!fm) {
        throw new Error(`bumpSpecVersion: no frontmatter found in ${specPath}`);
    }
    // Refuse to bump 0.x specs — migration must happen on the next real write
    const formatVersion = detectSpecFormatVersion(parseFrontmatterKV(fm.yamlBlock));
    if (formatVersion === '0.x') {
        throw new Error(`bumpSpecVersion: spec ${specId} is in format 0.x — write to it once via any tool to auto-migrate first`);
    }
    const currentVersion = extractField(fm.yamlBlock, 'spec_version') ?? '1.0.0';
    const nextVersion = bumpSemver(currentVersion, kind);
    const entry = {
        at: new Date().toISOString(),
        kind: 'version-bumped',
        spec_version_from: currentVersion,
        spec_version_to: nextVersion,
        by: by ?? deriveSessionId(),
        reason,
    };
    // Re-render the frontmatter with updated spec_version + appended history
    const updatedContent = applyVersionBump(raw, fm, currentVersion, nextVersion, entry);
    // Write atomically via .tmp sibling
    await atomicWriteText(specPath, updatedContent);
    return { specId, previous: currentVersion, next: nextVersion, entry };
}
function extractFrontmatterBlock(content) {
    const re = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
    const m = re.exec(content);
    if (!m?.[1] || m[2] === undefined) {
        return null;
    }
    return { yamlBlock: m[1], body: m[2] };
}
function parseFrontmatterKV(yamlBlock) {
    const result = {};
    for (const line of yamlBlock.split('\n')) {
        const m = /^(\w[\w_-]*):\s*(.+)$/.exec(line.trim());
        if (m?.[1] && m[2] !== undefined) {
            result[m[1]] = m[2].replace(/^["']|["']$/g, '');
        }
    }
    return result;
}
function extractField(yamlBlock, key) {
    // Only top-level (non-indented) keys
    const re = new RegExp(`^${key}:\\s*(.+)$`, 'm');
    const m = re.exec(yamlBlock);
    if (!m?.[1]) {
        return null;
    }
    return m[1].trim().replace(/^["']|["']$/g, '');
}
/** Replace or append `spec_version` in the YAML block. */
function setField(yamlBlock, key, value) {
    const re = new RegExp(`^(${key}:\\s*).*$`, 'm');
    if (re.test(yamlBlock)) {
        return yamlBlock.replace(re, `${key}: ${value}`);
    }
    return `${yamlBlock}\n${key}: ${value}`;
}
/** Parse existing history array from the YAML block. */
function extractExistingHistory(yamlBlock) {
    // history: is a YAML sequence between the key and the next top-level key or end
    const histRe = /^history:\s*\n((?: {2}[\s\S]*?\n?)*?)(?=^\w|$)/m;
    const m = histRe.exec(yamlBlock);
    if (!m?.[1] || m[1].trim() === '' || m[1].trim() === '[]') {
        return [];
    }
    const entries = [];
    // Each entry starts with "  - at:"
    const entryBlocks = m[1].split(/(?= {2}- at:)/);
    for (const block of entryBlocks) {
        if (!block.trim()) {
            continue;
        }
        const entry = parseHistoryEntry(block);
        if (entry) {
            entries.push(entry);
        }
    }
    return entries;
}
function parseHistoryEntry(block) {
    const get = (k) => {
        const r = new RegExp(`^\\s+${k}:\\s*(.+)$`, 'm');
        const m = r.exec(block);
        return m?.[1]?.replace(/^["']|["']$/g, '') ?? null;
    };
    const at = get('at');
    const kind = get('kind');
    const spec_version_to = get('spec_version_to');
    const by = get('by');
    const reason = get('reason');
    if (!at || !kind || !spec_version_to || !by || !reason) {
        return null;
    }
    const from = get('spec_version_from');
    return {
        at,
        kind,
        spec_version_from: from === 'null' ? null : from,
        spec_version_to,
        by,
        reason,
    };
}
function applyVersionBump(_raw, fm, _currentVersion, nextVersion, entry) {
    let yaml = fm.yamlBlock;
    // Update spec_version
    yaml = setField(yaml, 'spec_version', `"${nextVersion}"`);
    // Append history entry
    const existing = extractExistingHistory(yaml);
    const allEntries = [...existing, entry];
    const historyYaml = renderHistoryYaml(allEntries);
    // Replace the history block (or append)
    const historyBlockRe = /^history:.*?(?=^\w|$)/ms;
    if (historyBlockRe.test(yaml)) {
        yaml = yaml.replace(historyBlockRe, historyYaml + '\n');
    }
    else {
        yaml = `${yaml}\n${historyYaml}`;
    }
    return `---\n${yaml}\n---\n${fm.body}`;
}
async function atomicWriteText(filePath, content) {
    const tmpPath = `${filePath}.tmp`;
    await mkdir(dirname(filePath), { recursive: true });
    try {
        await writeFile(tmpPath, content, 'utf-8');
        await rename(tmpPath, filePath);
    }
    catch (err) {
        if (existsSync(tmpPath)) {
            try {
                const { unlink } = await import('node:fs/promises');
                await unlink(tmpPath);
            }
            catch {
                // ignore cleanup errors
            }
        }
        throw err;
    }
}
function deriveSessionId() {
    return `session:${Date.now().toString(36)}`;
}
export { migrateFrontmatterToCurrentFormat };
//# sourceMappingURL=bump-version.js.map