// engine/spec-migrator/strict-planu-cleanup.ts — SPEC-1017
// Destructive cleanup for non-canonical files under planu/.
import { readdir, readFile, rm, stat } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { dirname, isAbsolute, join, relative } from 'node:path';
import { atomicWriteFile } from '../safety/atomic-write-file.js';
import { safeUnlink } from './git-aware-fs.js';
import { PLANU_CANONICAL_POLICY, canonicalContractText, isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isCanonicalReleaseFile, isCanonicalSpecDir, isCanonicalSpecFile, mustMergeBeforeDeleteSpecFile, } from './planu-canonical-policy.js';
const execFileAsync = promisify(execFile);
async function pathIsDirectory(path) {
    try {
        return (await stat(path)).isDirectory();
    }
    catch {
        return false;
    }
}
async function gitRmCached(projectPath, relPath) {
    if (!existsSync(join(projectPath, '.git'))) {
        return;
    }
    try {
        const args = ['rm', '--cached', '--quiet', '--ignore-unmatch'];
        if (await pathIsDirectory(join(projectPath, relPath))) {
            args.push('-r');
        }
        args.push(relPath);
        await execFileAsync('git', args, {
            cwd: projectPath,
            timeout: 5_000,
        });
    }
    catch {
        /* best-effort */
    }
}
function stripFrontmatter(content) {
    return content.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
}
function appendSectionIfMissing(specContent, heading, body) {
    if (body.trim().length === 0 || new RegExp(`\\n## ${heading}(\\n|$)`).test(specContent)) {
        return specContent;
    }
    return `${specContent.trimEnd()}\n\n## ${heading}\n\n${body.trim()}\n`;
}
async function mergeLegacySpecFile(projectPath, specDir, fileName) {
    const specPath = join(specDir, 'spec.md');
    const legacyPath = join(specDir, fileName);
    if (!existsSync(legacyPath) || !existsSync(specPath)) {
        return false;
    }
    const [specContent, legacyContent] = await Promise.all([
        readFile(specPath, 'utf-8'),
        readFile(legacyPath, 'utf-8'),
    ]);
    const body = stripFrontmatter(legacyContent);
    const section = fileName === 'progress.md' || fileName === 'PROGRESS.md'
        ? 'Progress'
        : fileName === 'technical.md' || fileName === 'FICHA-TECNICA.md'
            ? 'Technical'
            : 'Files';
    const merged = appendSectionIfMissing(specContent, section, body);
    if (merged !== specContent) {
        await atomicWriteFile(specPath, merged, {
            forceEdit: {
                reason: `SPEC-1017 strict cleanup is merging legacy ${fileName} into canonical spec.md.`,
            },
        });
    }
    await safeUnlink(projectPath, legacyPath);
    return true;
}
async function removePath(projectPath, absolutePath) {
    const rel = relative(projectPath, absolutePath);
    if (await pathIsDirectory(absolutePath)) {
        await gitRmCached(projectPath, rel);
        await rm(absolutePath, { recursive: true, force: true });
        return;
    }
    await safeUnlink(projectPath, absolutePath);
    await rm(absolutePath, { force: true });
}
async function updateGitignore(projectPath) {
    const gitignorePath = join(projectPath, '.gitignore');
    let current = '';
    try {
        current = await readFile(gitignorePath, 'utf-8');
    }
    catch {
        /* missing .gitignore is fine */
    }
    const required = [
        'planu/*.html',
        'planu/status.json',
        'planu/CHANGELOG.md',
        'planu/.housekeeping-history.jsonl',
        'planu/audits/',
        'planu/handoffs/',
        'planu/data/',
        'planu/state/',
        'planu/.locks/',
        'planu/specs/data/',
        'planu/specs/**/.analysis.json',
        'planu/specs/**/technical-report.html',
        'planu/specs/**/reference/',
        'planu/specs/**/*.bak.*',
    ];
    const missing = required.filter((entry) => !current.split('\n').includes(entry));
    if (missing.length === 0) {
        return false;
    }
    const separator = current === '' || current.endsWith('\n') ? '' : '\n';
    await atomicWriteFile(gitignorePath, `${current}${separator}# Planu generated/runtime\n${missing.join('\n')}\n`);
    return true;
}
async function walkSpecDirectory(projectPath, specDir, result) {
    const entries = await readdir(specDir).catch(() => []);
    for (const entry of entries) {
        const full = join(specDir, entry);
        if (mustMergeBeforeDeleteSpecFile(entry)) {
            if (await mergeLegacySpecFile(projectPath, specDir, entry)) {
                result.merged.push(relative(projectPath, full));
            }
            continue;
        }
        const isDir = await pathIsDirectory(full);
        if (entry === 'reference' ||
            (isDir ? !isCanonicalSpecDir(entry) : !isCanonicalSpecFile(entry))) {
            await removePath(projectPath, full);
            result.deleted.push(relative(projectPath, full));
        }
    }
}
export async function runStrictPlanuCleanup(projectPath) {
    const result = { deleted: [], merged: [], gitignoreUpdated: false };
    const planuDir = join(projectPath, 'planu');
    if (!existsSync(planuDir)) {
        return result;
    }
    const entries = await readdir(planuDir).catch(() => []);
    for (const entry of entries) {
        const full = join(planuDir, entry);
        const isDir = await pathIsDirectory(full);
        if (isDir && !isCanonicalPlanuRootDir(entry)) {
            await removePath(projectPath, full);
            result.deleted.push(relative(projectPath, full));
            continue;
        }
        if (!isDir && !isCanonicalPlanuRootFile(entry)) {
            await removePath(projectPath, full);
            result.deleted.push(relative(projectPath, full));
        }
    }
    const releasesDir = join(planuDir, 'releases');
    for (const entry of await readdir(releasesDir).catch(() => [])) {
        const rel = `releases/${entry}`;
        if (!isCanonicalReleaseFile(rel)) {
            const full = join(releasesDir, entry);
            await removePath(projectPath, full);
            result.deleted.push(relative(projectPath, full));
        }
    }
    const specsDir = join(planuDir, 'specs');
    for (const entry of await readdir(specsDir).catch(() => [])) {
        const full = join(specsDir, entry);
        if (!(await pathIsDirectory(full)) || entry === 'data') {
            await removePath(projectPath, full);
            result.deleted.push(relative(projectPath, full));
            continue;
        }
        await walkSpecDirectory(projectPath, full, result);
    }
    result.gitignoreUpdated = await updateGitignore(projectPath);
    return result;
}
function resolveSpecDirsForValidation(projectPath, specPath) {
    if (!specPath?.trim()) {
        return null;
    }
    const resolved = isAbsolute(specPath) ? specPath : join(projectPath, specPath);
    return [dirname(resolved)];
}
export async function validateStrictPlanuLayout(projectPath, options = {}) {
    const offenders = [];
    const planuDir = join(projectPath, 'planu');
    if (options.includeRoot !== false) {
        const entries = await readdir(planuDir).catch(() => []);
        for (const entry of entries) {
            const full = join(planuDir, entry);
            const isDir = await pathIsDirectory(full);
            if ((isDir && !isCanonicalPlanuRootDir(entry)) ||
                (!isDir && !isCanonicalPlanuRootFile(entry))) {
                offenders.push(relative(projectPath, full));
            }
        }
        for (const entry of await readdir(join(planuDir, 'releases')).catch(() => [])) {
            const rel = `releases/${entry}`;
            if (!isCanonicalReleaseFile(rel)) {
                offenders.push(`planu/${rel}`);
            }
        }
    }
    const scopedSpecDirs = resolveSpecDirsForValidation(projectPath, options.specPath) ??
        (await readdir(join(planuDir, 'specs')).catch(() => [])).map((specDir) => join(planuDir, 'specs', specDir));
    for (const full of scopedSpecDirs) {
        const specDir = relative(join(planuDir, 'specs'), full);
        if (!(await pathIsDirectory(full)) || specDir === 'data') {
            offenders.push(relative(projectPath, full));
            continue;
        }
        for (const entry of await readdir(full).catch(() => [])) {
            const entryPath = join(full, entry);
            const isDir = await pathIsDirectory(entryPath);
            if (isDir ? !isCanonicalSpecDir(entry) : !isCanonicalSpecFile(entry)) {
                offenders.push(relative(projectPath, entryPath));
            }
        }
    }
    return { ok: offenders.length === 0, offenders, contract: canonicalContractText() };
}
export { PLANU_CANONICAL_POLICY };
//# sourceMappingURL=strict-planu-cleanup.js.map