// spec-migrator/lean-migration.ts — Migrate old verbose specs to lean format (SPEC-461 Phase 2)
// Detects old-format specs, extracts useful info, generates lean files, deletes obsolete files.
import { readFile, readdir, stat } from 'node:fs/promises';
import { atomicWriteFile } from '../safety/atomic-write-file.js';
import { join } from 'node:path';
import { safeUnlink } from './git-aware-fs.js';
import { parseFrontmatterLocal } from './frontmatter-parser.js';
import { generateLeanSpecContent } from '../spec-format/lean-spec-generator.js';
import { generateLeanTechnicalContent } from '../spec-format/lean-technical-generator.js';
import { buildUnifiedSpecContent } from '../spec-format/unified-spec-builder.js';
/** Files to delete from old-format specs. */
const OBSOLETE_FILES = [
    'technical.md',
    'progress.md',
    'executive-report.html',
    'technical-report.html',
    'CHANGELOG.md',
];
/** Check if a spec directory is in old (verbose) format. */
export async function isOldFormat(specDir) {
    try {
        const files = await readdir(specDir);
        // Old format has progress.md or HTML reports
        const hasObsoleteFiles = files.some((f) => OBSOLETE_FILES.includes(f));
        if (hasObsoleteFiles) {
            return true;
        }
        // Old format spec.md is >100 lines (lean is ~30-50)
        const specPath = join(specDir, 'spec.md');
        const content = await readFile(specPath, 'utf-8');
        const lineCount = content.split('\n').length;
        return lineCount > 100;
    }
    catch {
        return false;
    }
}
/** Check if a spec is already in lean format (has estimation in YAML frontmatter). */
function isLeanFormat(content) {
    const fmMatch = /^---\n([\s\S]*?)\n---/m.exec(content);
    if (!fmMatch?.[1]) {
        return false;
    }
    return fmMatch[1].includes('estimation:') && fmMatch[1].includes('criteria:');
}
/** Extract the user description from old spec.md (skip generic sections, keep real content). */
function extractDescription(content) {
    // Remove frontmatter
    const withoutFm = content.replace(/^---\n[\s\S]*?\n---\n/, '').trim();
    // Remove the heading line (# SPEC-XXX: Title)
    const lines = withoutFm.split('\n');
    const startIdx = lines.findIndex((l) => /^#\s+SPEC-\d+/.test(l));
    const afterHeading = startIdx >= 0 ? lines.slice(startIdx + 1) : lines;
    // Generic sections to SKIP entirely (including their content until next ##)
    const genericHeaders = [
        /^#{2,3}\s+(Acceptance Criteria|Quality Guidelines|Testing Strategy|Impact Analysis)/,
        /^#{2,3}\s+(Seguridad|Cumplimiento|Security|Privacy|Resilience)/,
        /^#{2,3}\s+(Out of Scope|Dependencies|Tags|Dependency Warnings)/,
        /^#{2,3}\s+Estado de Implementaci[oó]n/,
        /^#{2,3}\s+Criterios OWASP/i,
        /^#{2,3}\s+An[aá]lisis STRIDE/,
        /^#{2,3}\s+Cumplimiento normativo/,
        /^#{2,3}\s+Criterios (Completados|Pendientes)/,
        /^#{2,3}\s+Verification Criteria/,
        /^#{2,3}\s+SOLID Checklist/,
        /^#{2,3}\s+Security Requirements/,
    ];
    // Collect non-generic content sections
    const kept = [];
    let skipping = false;
    for (const line of afterHeading) {
        // Check if this is a section header
        if (/^##\s/.test(line) || /^###\s/.test(line)) {
            skipping = genericHeaders.some((re) => re.test(line));
            if (!skipping) {
                kept.push(line);
            }
            continue;
        }
        if (!skipping) {
            kept.push(line);
        }
    }
    // Clean up: remove empty lines at start/end, collapse multiple blank lines
    const description = kept
        .join('\n')
        .replace(/\n{3,}/g, '\n\n')
        .trim();
    return description.length > 0 ? description : 'No description available.';
}
/** Extract acceptance criteria from old spec.md checkbox patterns. */
function extractOldCriteria(content) {
    const criteria = [];
    const re = /^[-*]\s*\[([ xX])\]\s*(.+)$/gm;
    let match = re.exec(content);
    while (match) {
        const text = match[2]?.trim() ?? '';
        // Skip generic criteria (OWASP, STRIDE, etc.)
        if (!isGenericCriterion(text)) {
            criteria.push({ text, done: match[1]?.toLowerCase() === 'x' });
        }
        match = re.exec(content);
    }
    if (criteria.length === 0) {
        criteria.push({ text: 'Implementation complete and tested', done: false });
    }
    return criteria;
}
/** Check if a criterion is generic boilerplate (not specific to the feature). */
function isGenericCriterion(text) {
    const genericPatterns = [
        /naming conventions/i,
        /import patterns comply/i,
        /file and function sizes/i,
        /code quality rules/i,
        /testing conventions/i,
        /module boundary/i,
        /feature flag/i,
        /unit tests cover.*happy path/i,
        /SRP.*single reason/i,
        /OCP.*extend behavior/i,
        /DIP.*abstractions/i,
        /all inputs validated/i,
        /no hardcoded secrets/i,
        /parameterized queries/i,
        /core business logic covered/i,
        /happy path.*failure modes/i,
        /end-to-end flow/i,
        /API contract tests/i,
        /existing consumers/i,
        /no breaking changes/i,
        /define rollback/i,
        /sentencias parametrizadas/i,
        /input.*validado.*sanitizado/i,
        /permisos m[ií]nimos/i,
        /datos sensibles.*encriptados/i,
        /archivos subidos validados/i,
        /Content-Security-Policy/i,
        /PCI-DSS/i,
        /TLS 1\.2/i,
        /rotaci[oó]n de claves/i,
        /OWASP/i,
        /STRIDE/i,
    ];
    return genericPatterns.some((p) => p.test(text));
}
/** Extract file lists from old technical.md. */
function extractFilesFromTechnical(content) {
    const result = { create: [], modify: [], test: [] };
    let currentSection = null;
    for (const line of content.split('\n')) {
        // Detect section headers (various formats used in old specs)
        if (/^#{2,3}\s*(To\s+)?[Cc]reate/i.test(line) || /^#{2,3}\s*Files?\s+to\s+create/i.test(line)) {
            currentSection = 'create';
        }
        else if (/^#{2,3}\s*(To\s+)?[Mm]odif/i.test(line) ||
            /^#{2,3}\s*Files?\s+to\s+(create\/)?modify/i.test(line)) {
            currentSection = 'modify';
        }
        else if (/^#{2,3}\s*[Tt]est/i.test(line)) {
            currentSection = 'test';
        }
        else if (/^#{2,3}\s/.test(line) && !/^#{2,3}\s*Files?\b/i.test(line)) {
            currentSection = null;
        }
        else if (currentSection && /^\s*[-*]\s+/.test(line)) {
            const filePath = extractFilePath(line);
            if (filePath) {
                result[currentSection].push(filePath);
            }
        }
    }
    // Fallback: scan for file paths in the entire content if nothing found
    if (result.create.length === 0 && result.modify.length === 0 && result.test.length === 0) {
        const filePathRe = /[-*]\s+`?((?:src|tests|lib|app|pages|components|api)\/[\w/.@-]+\.\w+)`?/g;
        let match = filePathRe.exec(content);
        while (match) {
            const fp = match[1] ?? '';
            if (fp.includes('test') || fp.includes('spec')) {
                result.test.push(fp);
            }
            else {
                result.modify.push(fp);
            }
            match = filePathRe.exec(content);
        }
    }
    return result;
}
/** Extract a clean file path from a bullet line, filtering placeholders. */
function extractFilePath(line) {
    // Strip bullet and markdown formatting
    let fp = line
        .replace(/^\s*[-*]\s+/, '')
        .replace(/`/g, '')
        .trim();
    // Remove trailing description after " — " or " - "
    fp = fp.replace(/\s+[—–-]\s+.*$/, '').trim();
    // Remove trailing parenthetical
    fp = fp.replace(/\s*\(.*\)\s*$/, '').trim();
    // Filter placeholders
    if (fp.length === 0 || fp.startsWith('(') || /^analyze|^list |^define |^no /i.test(fp)) {
        return null;
    }
    return fp;
}
/** Parse hours from various formats: "8h", "16-24 horas", "~20h", "82-116h" */
function parseHoursValue(val) {
    const rangeMatch = /(\d+(?:\.\d+)?)\s*[-–]\s*(\d+(?:\.\d+)?)/.exec(val);
    if (rangeMatch?.[1] && rangeMatch[2]) {
        return (parseFloat(rangeMatch[1]) + parseFloat(rangeMatch[2])) / 2;
    }
    const simpleMatch = /~?(\d+(?:\.\d+)?)/.exec(val);
    if (simpleMatch?.[1]) {
        return parseFloat(simpleMatch[1]);
    }
    return 0;
}
/** Classify a table row key into an estimation field. */
function classifyEstKey(key) {
    if (/dev\s*hours|horas?\s*(dev|desarrollo|totales)|horas?\s*estimad|tiempo\s*estimad/i.test(key)) {
        return 'devHours';
    }
    if (/review\s*hours|horas?\s*review/i.test(key)) {
        return 'reviewHours';
    }
    if (/total\s*cost|costo?\s*total/i.test(key)) {
        return 'totalCost';
    }
    if (/api\s*cost|costo?\s*api/i.test(key)) {
        return 'apiCost';
    }
    if (/human\s*cost|costo?\s*human/i.test(key)) {
        return 'humanCost';
    }
    if (/estimaci[oó]n/i.test(key)) {
        return 'estLabel';
    }
    if (/recommended\s*model|modelo/i.test(key)) {
        return 'model';
    }
    return null;
}
/** Extract estimation from old technical.md markdown tables (multiple formats). */
function extractEstimationFromTechnical(techContent) {
    const result = {};
    const tableRowRe = /\|\s*(.+?)\s*\|\s*(.+?)\s*\|/g;
    let match = tableRowRe.exec(techContent);
    while (match) {
        const key = (match[1] ?? '').trim().replace(/\**/g, '').toLowerCase();
        const val = (match[2] ?? '').trim().replace(/\**/g, '');
        const field = classifyEstKey(key);
        if (field === 'devHours' || field === 'reviewHours') {
            const h = parseHoursValue(val);
            if (h > 0) {
                result[field] = h;
            }
        }
        else if (field === 'totalCost') {
            result.totalCostUsd = parseFloat(val.replace(/[$,]/g, '')) || 0;
        }
        else if (field === 'apiCost') {
            result.apiCostUsd = parseFloat(val.replace(/[$,]/g, '')) || 0;
        }
        else if (field === 'humanCost') {
            result.humanCostUsd = parseFloat(val.replace(/[$,]/g, '')) || 0;
        }
        else if (field === 'estLabel' && /hora/i.test(val)) {
            const h = parseHoursValue(val);
            if (h > 0) {
                result.devHours = h;
            }
        }
        else if (field === 'model' && (val === 'opus' || val === 'sonnet' || val === 'mixed')) {
            result.recommendedModel = val;
        }
        match = tableRowRe.exec(techContent);
    }
    return result;
}
/** Build Estimation from old technical.md tables + frontmatter metadata. */
function buildEstimationFromOld(metadata, techContent) {
    const fromTech = extractEstimationFromTechnical(techContent);
    return {
        devHours: fromTech.devHours ?? (typeof metadata.devHours === 'number' ? metadata.devHours : 0),
        reviewHours: fromTech.reviewHours ?? (typeof metadata.reviewHours === 'number' ? metadata.reviewHours : 0),
        recommendedModel: fromTech.recommendedModel ?? 'sonnet',
        tokensOpus: 0,
        tokensSonnet: 0,
        apiCostUsd: fromTech.apiCostUsd ?? 0,
        hourlyRate: 0,
        humanCostUsd: fromTech.humanCostUsd ?? 0,
        totalCostUsd: fromTech.totalCostUsd ?? 0,
        tokenOptimization: { mode: 'local', reasoning: 'migrated', estimatedTokens: 0, savings: '' },
    };
}
/** Build a minimal Spec object from parsed frontmatter metadata. */
function buildSpecFromMetadata(metadata, id, title, specStatus, estimation, specPath, techPath) {
    return {
        id,
        title,
        slug: id.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
        type: (typeof metadata.type === 'string' ? metadata.type : 'feature'),
        scope: (typeof metadata.scope === 'string' ? metadata.scope : 'feature'),
        status: specStatus,
        difficulty: (typeof metadata.difficulty === 'number'
            ? metadata.difficulty
            : 3),
        risk: (typeof metadata.risk === 'string' ? metadata.risk : 'medium'),
        projectId: '',
        createdAt: typeof metadata.created === 'string'
            ? `${metadata.created}T00:00:00.000Z`
            : new Date().toISOString(),
        updatedAt: new Date().toISOString(),
        specPath,
        technicalPath: techPath,
        estimation,
        actuals: null,
        target: (typeof metadata.target === 'string' ? metadata.target : 'backend'),
        tags: Array.isArray(metadata.tags) ? metadata.tags : [],
        dependencies: [],
        blockedBy: [],
        gitBranch: typeof metadata.branch === 'string' ? metadata.branch : '',
        impactAnalysis: null,
    };
}
/** Write a verified backup of a file before overwriting. Returns backup path. Throws on failure. */
async function writeVerifiedBackup(filePath) {
    const content = await readFile(filePath, 'utf-8');
    const bkPath = `${filePath}.bak.${Date.now()}`;
    await atomicWriteFile(bkPath, content);
    const verify = await readFile(bkPath, 'utf-8');
    if (verify !== content) {
        throw new Error(`backup verify failed for ${filePath}: content mismatch`);
    }
    return bkPath;
}
/** Migrate a single spec directory from old format to lean format. */
export async function migrateSpecToLean(specDir, projectPath) {
    try {
        const specPath = join(specDir, 'spec.md');
        const techPath = join(specDir, 'technical.md');
        // Read old spec.md
        let specContent;
        try {
            specContent = await readFile(specPath, 'utf-8');
        }
        catch {
            return 'skipped'; // No spec.md — not a valid spec dir
        }
        // Skip if already lean
        if (isLeanFormat(specContent)) {
            return 'skipped';
        }
        // Parse frontmatter
        const { metadata } = parseFrontmatterLocal(specContent);
        const id = typeof metadata.id === 'string' ? metadata.id : 'SPEC-000';
        const title = typeof metadata.title === 'string' ? metadata.title : 'Untitled';
        const status = typeof metadata.status === 'string' ? metadata.status : 'draft';
        // Read old technical.md first (needed for estimation extraction)
        let oldTechContent = '';
        let files = { create: [], modify: [], test: [] };
        try {
            oldTechContent = await readFile(techPath, 'utf-8');
            files = extractFilesFromTechnical(oldTechContent);
        }
        catch {
            // No technical.md — use empty files and defaults
        }
        // Extract useful content from old spec
        const description = extractDescription(specContent);
        const criteria = extractOldCriteria(specContent);
        const estimation = buildEstimationFromOld(metadata, oldTechContent);
        const spec = buildSpecFromMetadata(metadata, id, title, status, estimation, specPath, techPath);
        // If status is done, mark all criteria as done
        if (status === 'done') {
            for (const c of criteria) {
                c.done = true;
            }
        }
        // Generate lean spec.md with extracted criteria
        const leanSpecLines = generateLeanSpecContent({ spec, description, estimation });
        // Inject real criteria (replace the default one)
        const leanSpecWithCriteria = replaceCriteria(leanSpecLines, criteria);
        // Generate the legacy technical body, then fold it into unified spec.md.
        const leanTech = generateLeanTechnicalContent({
            specId: id,
            filesToCreate: files.create.map((p) => ({ path: p, status: 'pending' })),
            filesToModify: files.modify.map((p) => ({ path: p, status: 'pending' })),
            filesToTest: files.test.map((p) => ({ path: p, status: 'pending' })),
        });
        // SPEC-771: backup spec.md before overwriting (verified write-back)
        try {
            await writeVerifiedBackup(specPath);
        }
        catch (backupErr) {
            return `backup failed: ${backupErr instanceof Error ? backupErr.message : String(backupErr)}`;
        }
        // Write one unified spec.md.
        await atomicWriteFile(specPath, buildUnifiedSpecContent(leanSpecWithCriteria, leanTech));
        // Delete obsolete files
        for (const file of OBSOLETE_FILES) {
            const filePath = join(specDir, file);
            if (projectPath) {
                await safeUnlink(projectPath, filePath);
            }
            else {
                try {
                    const { unlink } = await import('node:fs/promises');
                    await unlink(filePath);
                }
                catch {
                    // File doesn't exist — fine
                }
            }
        }
        return 'migrated';
    }
    catch (err) {
        return err instanceof Error ? err.message : String(err);
    }
}
/** Replace criteria in lean spec content with extracted old criteria. */
function replaceCriteria(content, criteria) {
    // Find the criteria section in YAML and replace it
    const criteriaLines = criteria.map((c) => `  - text: "${c.text.replace(/"/g, '\\"')}"\n    done: ${String(c.done)}`);
    const newCriteria = `criteria:\n${criteriaLines.join('\n')}`;
    // Replace the existing criteria block
    return content.replace(/criteria:\n(?: {2}- text:.*\n {4}done:.*\n?)+/, `${newCriteria}\n`);
}
/** Migrate all specs in a project to lean format. */
export async function migrateAllSpecsToLean(projectPath) {
    const result = { migratedCount: 0, skippedCount: 0, errors: [] };
    // Find spec directories
    const specsRoots = [join(projectPath, 'planu/specs'), join(projectPath, 'docs/sdd/specs')];
    for (const root of specsRoots) {
        let entries;
        try {
            entries = await readdir(root);
        }
        catch {
            continue; // Root doesn't exist
        }
        for (const entry of entries) {
            const specDir = join(root, entry);
            try {
                const s = await stat(specDir);
                if (!s.isDirectory()) {
                    continue;
                }
            }
            catch {
                continue;
            }
            const migrationResult = await migrateSpecToLean(specDir, projectPath);
            if (migrationResult === 'migrated') {
                result.migratedCount++;
            }
            else if (migrationResult === 'skipped') {
                result.skippedCount++;
            }
            else {
                result.errors.push(`${entry}: ${migrationResult}`);
            }
        }
    }
    return result;
}
//# sourceMappingURL=lean-migration.js.map