import { isLegacyLocation, createPlanuConfig, serializePlanuConfig, } from './planu-config-writer.js';
import { mkdir, rename, readdir, rmdir, access, writeFile, readFile } from 'node:fs/promises';
import { join, dirname } from 'node:path';
const LEGACY_TO_MODERN_NAMES = {
    'HU.md': 'spec.md',
    'FICHA-TECNICA.md': 'technical.md',
    'PROGRESS.md': 'progress.md',
};
/**
 * Detect specs stored under a legacy location (docs/sdd/specs/).
 * Caller provides the list of specs (from storage layer).
 */
export function filterLegacySpecs(specs) {
    return specs.filter((s) => isLegacyLocation(s.specPath) || isLegacyLocation(s.technicalPath));
}
/**
 * Migrate legacy specs to planu/specs/ with modern file naming.
 * Moves files on disk and calls the provided updateSpec for each migrated spec.
 * Also rewrites planu.json.
 */
export async function migrateSpecs(projectPath, legacySpecs, deps) {
    const errors = [];
    let migratedCount = 0;
    for (const spec of legacySpecs) {
        try {
            await migrateSingleSpec(projectPath, spec, deps);
            migratedCount++;
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            errors.push(`${spec.id}: ${msg}`);
        }
    }
    // Rewrite planu.json with modern config
    const planuConfigPath = join(projectPath, 'planu.json');
    const planuConfig = createPlanuConfig();
    await writeFile(planuConfigPath, serializePlanuConfig(planuConfig), 'utf-8');
    return { migratedCount, errors };
}
async function migrateSingleSpec(projectPath, spec, deps) {
    const oldSpecDir = dirname(join(projectPath, spec.specPath));
    // Flatten: remove any subcategory between planu/specs/ and the spec folder.
    // Legacy: docs/sdd/specs/{category}/{slug}/spec.md -> planu/specs/{slug}/spec.md
    const newRelDir = flattenSpecPath(spec.specPath.replace(/docs\/sdd\/specs/, 'planu/specs').replace(/\/[^/]+$/, ''));
    const newAbsDir = join(projectPath, newRelDir);
    // Create target directory
    await mkdir(newAbsDir, { recursive: true });
    // Move and rename known files
    const dirEntries = await safeReaddir(oldSpecDir);
    for (const entry of dirEntries) {
        const modernName = LEGACY_TO_MODERN_NAMES[entry] ?? entry;
        const src = join(oldSpecDir, entry);
        const dest = join(newAbsDir, modernName);
        await rename(src, dest);
    }
    // Update spec paths in storage
    const updates = {
        specPath: rewritePath(spec.specPath),
        technicalPath: rewritePath(spec.technicalPath),
    };
    if (spec.progressPath) {
        updates.progressPath = rewritePath(spec.progressPath);
    }
    if (spec.executiveReportPath) {
        updates.executiveReportPath = rewritePath(spec.executiveReportPath);
    }
    if (spec.technicalReportPath) {
        updates.technicalReportPath = rewritePath(spec.technicalReportPath);
    }
    if (spec.planPath) {
        updates.planPath = rewritePath(spec.planPath);
    }
    await deps.updateSpec(spec.projectId, spec.id, updates);
    // Cleanup empty legacy directories
    await cleanupEmptyDirs(oldSpecDir);
}
function rewritePath(path) {
    const modernized = path
        .replace(/docs\/sdd\/specs/, 'planu/specs')
        .replace(/\/HU\.md$/, '/spec.md')
        .replace(/\/FICHA-TECNICA\.md$/, '/technical.md')
        .replace(/\/PROGRESS\.md$/, '/progress.md');
    return flattenSpecPath(modernized);
}
/**
 * Flatten nested spec paths to the new flat structure.
 * Converts `planu/specs/{category}/{slug}/file.md` to `planu/specs/{slug}/file.md`.
 * Already-flat paths are returned unchanged.
 */
function flattenSpecPath(p) {
    // Match: planu/specs/{category}/{specFolder}[/rest]
    // A spec folder starts with SPEC- or is the direct slug folder.
    // We detect nesting by counting segments after planu/specs/.
    const match = /^(planu\/specs\/)([^/]+)\/([^/]+)(\/.*)?$/.exec(p);
    if (!match) {
        return p;
    }
    const [, prefix, segment1, segment2, rest] = match;
    // If segment1 is already a spec folder (starts with SPEC-), it's flat — no change.
    if (segment1?.startsWith('SPEC-')) {
        return p;
    }
    // segment1 is a subcategory — remove it, promote segment2 to direct child.
    return `${prefix}${segment2}${rest ?? ''}`;
}
async function safeReaddir(dir) {
    try {
        return await readdir(dir);
    }
    catch {
        return [];
    }
}
async function cleanupEmptyDirs(dir) {
    try {
        const entries = await readdir(dir);
        if (entries.length === 0) {
            await rmdir(dir);
            // Try to clean parent too (e.g., docs/sdd/specs/ itself)
            await cleanupEmptyDirs(dirname(dir));
        }
    }
    catch {
        // Directory might not exist or not be empty — safe to ignore
    }
}
// --- Spec folder prefix migration (SPEC-XXX-slug) ---
const SPEC_ID_PREFIX_RE = /^SPEC-\d{3,}/;
/**
 * Detect specs whose folder name lacks the SPEC-XXX- prefix.
 * E.g., folder is `planu/specs/login-form/` but should be `planu/specs/SPEC-012-login-form/`.
 */
export function filterUnprefixedSpecs(specs) {
    return specs.filter((s) => {
        if (!s.specPath) {
            return false;
        }
        const parts = s.specPath.split('/');
        // The folder is the second-to-last segment (before spec.md)
        const folder = parts[parts.length - 2];
        return folder !== undefined && !SPEC_ID_PREFIX_RE.test(folder);
    });
}
/**
 * Rename spec folders on disk and update storage paths to include the SPEC-XXX- prefix.
 */
export async function migrateSpecFolderNames(projectPath, specs, deps) {
    const errors = [];
    let migratedCount = 0;
    for (const spec of specs) {
        try {
            const oldDir = dirname(join(projectPath, spec.specPath));
            const parentDir = dirname(oldDir);
            const oldFolderName = oldDir.split('/').pop() ?? '';
            const newFolderName = `${spec.id}-${oldFolderName}`;
            const newDir = join(parentDir, newFolderName);
            // Skip if already correct or target exists
            if (oldFolderName.startsWith(spec.id)) {
                continue;
            }
            try {
                await access(newDir);
                continue; // Target already exists — skip
            }
            catch {
                /* target doesn't exist — proceed */
            }
            await rename(oldDir, newDir);
            // Update all path fields in storage
            const rewrite = (p) => p.replace(`/${oldFolderName}/`, `/${newFolderName}/`);
            const updates = { specPath: rewrite(spec.specPath) };
            if (spec.technicalPath) {
                updates.technicalPath = rewrite(spec.technicalPath);
            }
            if (spec.progressPath) {
                updates.progressPath = rewrite(spec.progressPath);
            }
            if (spec.executiveReportPath) {
                updates.executiveReportPath = rewrite(spec.executiveReportPath);
            }
            if (spec.technicalReportPath) {
                updates.technicalReportPath = rewrite(spec.technicalReportPath);
            }
            if (spec.planPath) {
                updates.planPath = rewrite(spec.planPath);
            }
            await deps.updateSpec(spec.projectId, spec.id, updates);
            migratedCount++;
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            errors.push(`${spec.id}: ${msg}`);
        }
    }
    return { migratedCount, errors };
}
/**
 * Discover specs buried in subcategory folders and flatten them to planu/specs/{slug}/.
 * Also converts old blockquote metadata to YAML frontmatter.
 * This runs on init_project to fix specs created by older Planu versions.
 */
export async function discoverAndFlattenSpecs(projectPath) {
    const specsRoot = join(projectPath, 'planu/specs');
    const errors = [];
    let flattenedCount = 0;
    let convertedCount = 0;
    // Recursive scan: find all spec.md files at any depth
    const specFiles = await findSpecFiles(specsRoot, specsRoot);
    for (const relPath of specFiles) {
        // relPath is like "category/slug/spec.md" or "SPEC-001-slug/spec.md"
        const parts = relPath.split('/');
        if (parts.length < 2) {
            continue;
        }
        // Already flat: spec.md is at depth 1 (e.g., "slug/spec.md")
        if (parts.length === 2) {
            // Check if frontmatter conversion needed
            const fullPath = join(specsRoot, relPath);
            try {
                const converted = await convertBlockquoteToFrontmatter(fullPath);
                if (converted) {
                    convertedCount++;
                }
            }
            catch (err) {
                errors.push(`frontmatter ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
            }
            continue;
        }
        // Nested: "category/slug/spec.md" → move "slug/" to specsRoot
        const category = parts[0] ?? '';
        const slug = parts[1] ?? '';
        const srcDir = join(specsRoot, category, slug);
        const destDir = join(specsRoot, slug);
        try {
            // Check destination doesn't already exist
            try {
                await access(destDir);
                errors.push(`skip ${category}/${slug}: destination already exists`);
                continue;
            }
            catch {
                /* good — destination doesn't exist */
            }
            await rename(srcDir, destDir);
            flattenedCount++;
            // Cleanup empty category folder
            await cleanupEmptyDirs(join(specsRoot, category));
            // Convert frontmatter if needed
            const specMdPath = join(destDir, 'spec.md');
            try {
                const converted = await convertBlockquoteToFrontmatter(specMdPath);
                if (converted) {
                    convertedCount++;
                }
            }
            catch {
                /* best-effort */
            }
        }
        catch (err) {
            errors.push(`flatten ${category}/${slug}: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    // Phase 2: Rename folders that lack SPEC-XXX- prefix
    // Re-scan after flattening to catch newly moved folders
    let renamedCount = 0;
    try {
        const rootEntries = await readdir(specsRoot, { withFileTypes: true });
        for (const entry of rootEntries) {
            if (!entry.isDirectory() || SPEC_ID_PREFIX_RE.test(entry.name)) {
                continue;
            }
            // Read spec.md to extract SPEC ID from heading
            const specMdPath = join(specsRoot, entry.name, 'spec.md');
            try {
                const content = await readFile(specMdPath, 'utf-8');
                // Try frontmatter title first, then heading
                const headingMatch = /^#\s+(SPEC-\d{3,})/.exec(content.replace(/^---[\s\S]*?---\n*/, ''));
                if (!headingMatch?.[1]) {
                    continue;
                }
                const specId = headingMatch[1];
                const newName = `${specId}-${entry.name}`;
                const newDir = join(specsRoot, newName);
                try {
                    await access(newDir);
                    continue; // Target exists — skip
                }
                catch {
                    /* proceed */
                }
                await rename(join(specsRoot, entry.name), newDir);
                renamedCount++;
            }
            catch {
                /* spec.md unreadable — skip */
            }
        }
    }
    catch {
        /* specsRoot unreadable — skip */
    }
    return { flattenedCount, convertedCount, renamedCount, errors };
}
/** Recursively find all spec.md files, returning paths relative to root. */
async function findSpecFiles(dir, root) {
    const results = [];
    try {
        const entries = await readdir(dir, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = join(dir, entry.name);
            if (entry.isDirectory()) {
                const nested = await findSpecFiles(fullPath, root);
                results.push(...nested);
            }
            else if (entry.name === 'spec.md') {
                const rel = fullPath.slice(root.length + 1); // +1 for trailing /
                results.push(rel);
            }
        }
    }
    catch {
        /* directory unreadable */
    }
    return results;
}
/** Convert old blockquote metadata to YAML frontmatter. Returns true if converted. */
async function convertBlockquoteToFrontmatter(specMdPath) {
    const content = await readFile(specMdPath, 'utf-8');
    // Already has frontmatter — skip
    if (content.startsWith('---\n')) {
        return false;
    }
    // Detect blockquote metadata pattern:
    // > **Type:** feature | **Scope:** cross-module | **Target:** backend
    // > **Status:** draft | **Difficulty:** 3/5 | **Risk:** medium
    const blockquoteRe = /^#\s+(?:SPEC-\d+:\s*)?(.+)\n\n(?:>\s+\*\*\w+:\*\*[^\n]+\n)+/;
    const match = blockquoteRe.exec(content);
    if (!match) {
        return false;
    }
    const title = match[1]?.trim() ?? '';
    const blockquoteBlock = match[0];
    // Extract fields from blockquote lines
    const fields = {};
    const fieldRe = /\*\*(\w+):\*\*\s*([^|*]+)/g;
    let fm;
    while ((fm = fieldRe.exec(blockquoteBlock)) !== null) {
        const key = (fm[1] ?? '').toLowerCase().trim();
        const val = (fm[2] ?? '').trim();
        if (key && val) {
            fields[key] = val;
        }
    }
    // Build YAML frontmatter
    const yamlLines = ['---'];
    if (title) {
        yamlLines.push(`title: "${title.replace(/"/g, '\\"')}"`);
    }
    if (fields.type) {
        yamlLines.push(`type: ${fields.type}`);
    }
    if (fields.scope) {
        yamlLines.push(`scope: ${fields.scope}`);
    }
    if (fields.target) {
        yamlLines.push(`target: ${fields.target}`);
    }
    if (fields.status) {
        yamlLines.push(`status: ${fields.status}`);
    }
    if (fields.difficulty) {
        yamlLines.push(`difficulty: ${fields.difficulty}`);
    }
    if (fields.risk) {
        yamlLines.push(`risk: ${fields.risk}`);
    }
    yamlLines.push('---');
    yamlLines.push('');
    // Replace blockquote header with frontmatter + clean title
    const rest = content.slice(blockquoteBlock.length);
    const specIdMatch = /^#\s+(SPEC-\d+)/.exec(content);
    const heading = specIdMatch ? `# ${specIdMatch[1]}: ${title}` : `# ${title}`;
    const newContent = yamlLines.join('\n') + heading + '\n' + rest;
    await writeFile(specMdPath, newContent, 'utf-8');
    return true;
}
/**
 * Reconcile specs.json paths with actual filesystem locations.
 * After discoverAndFlattenSpecs() renames/moves folders, specs.json may still
 * reference old paths. This function detects broken paths and fixes them by
 * scanning planu/specs/ for the matching SPEC-XXX folder.
 */
export async function reconcileSpecPaths(projectPath, deps, projectId) {
    const specs = await deps.listSpecs(projectId);
    const specsRoot = join(projectPath, 'planu/specs');
    const errors = [];
    let reconciledCount = 0;
    // Build a map of actual folder names on disk for fast lookup
    let entries;
    try {
        entries = await readdir(specsRoot, { withFileTypes: true });
    }
    catch {
        return { reconciledCount: 0, errors: [] };
    }
    const diskFolders = entries.filter((e) => e.isDirectory()).map((e) => e.name);
    for (const spec of specs) {
        if (!spec.specPath) {
            continue;
        }
        // Check if current specPath exists on disk
        const fullSpecPath = join(projectPath, spec.specPath);
        try {
            await access(fullSpecPath);
            continue; // Path is valid — skip
        }
        catch {
            /* path broken — needs reconciliation */
        }
        // Find the matching folder by SPEC ID prefix (e.g., SPEC-001-)
        const matchingFolder = diskFolders.find((f) => f.startsWith(`${spec.id}-`));
        if (!matchingFolder) {
            continue; // No matching folder found — can't fix
        }
        // Build updated paths
        const newFolder = `planu/specs/${matchingFolder}`;
        const rewrite = (p) => {
            // Replace the entire folder path portion, preserving the filename
            const filename = p.split('/').pop() ?? '';
            return `${newFolder}/${filename}`;
        };
        try {
            const updates = {
                specPath: rewrite(spec.specPath),
            };
            if (spec.technicalPath) {
                updates.technicalPath = rewrite(spec.technicalPath);
            }
            if (spec.progressPath) {
                updates.progressPath = rewrite(spec.progressPath);
            }
            if (spec.executiveReportPath) {
                updates.executiveReportPath = rewrite(spec.executiveReportPath);
            }
            if (spec.technicalReportPath) {
                updates.technicalReportPath = rewrite(spec.technicalReportPath);
            }
            if (spec.planPath) {
                updates.planPath = rewrite(spec.planPath);
            }
            await deps.updateSpec(projectId, spec.id, updates);
            reconciledCount++;
        }
        catch (err) {
            errors.push(`${spec.id}: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    return { reconciledCount, errors };
}
/**
 * Check if a project path has a planu.json with legacy specLocation.
 */
export async function hasLegacyConfig(projectPath) {
    try {
        await access(join(projectPath, 'planu.json'));
        const content = await readFile(join(projectPath, 'planu.json'), 'utf-8');
        const config = JSON.parse(content);
        return config.specLocation ? isLegacyLocation(config.specLocation) : false;
    }
    catch {
        return false;
    }
}
//# sourceMappingURL=spec-migrator.js.map