// engine/scan-project/index.ts — 4-phase orchestrator for scan_project (SPEC-134)
import { join } from 'node:path';
import { mkdir, readdir, writeFile } from 'node:fs/promises';
import { replaceSectionInSpec } from '../spec-format/replace-section.js';
import { discoverModules } from './module-discoverer.js';
import { analyzeCrossModuleDependencies } from './cross-module-analyzer.js';
import { calculateProjectHealth } from './health-scorer.js';
import { generateProjectOverview } from './overview-generator.js';
import { generateTeamPlan } from './team-planner.js';
import { runDeepAnalysis } from '../reverse-engineer/index.js';
import { createSpec, listSpecs } from '../../storage/spec-store.js';
import { generateProgressContent } from '../progress-writer.js';
// Re-export submodules for barrel access
export { discoverModules } from './module-discoverer.js';
export { analyzeCrossModuleDependencies } from './cross-module-analyzer.js';
export { calculateProjectHealth } from './health-scorer.js';
export { generateProjectOverview } from './overview-generator.js';
export { generateTeamPlan } from './team-planner.js';
// ---------------------------------------------------------------------------
// Concurrency limiter (p-limit pattern — zero deps)
// ---------------------------------------------------------------------------
function pLimit(concurrency) {
    let active = 0;
    const queue = [];
    function next() {
        if (queue.length > 0 && active < concurrency) {
            active++;
            const run = queue.shift();
            run?.();
        }
    }
    return (fn) => new Promise((resolve, reject) => {
        const run = () => {
            fn()
                .then(resolve)
                .catch(reject)
                .finally(() => {
                active--;
                next();
            });
        };
        queue.push(run);
        next();
    });
}
// ---------------------------------------------------------------------------
// Phase 2: Parallel analysis with concurrency limit
// ---------------------------------------------------------------------------
async function analyzeModulesInParallel(projectPath, modules, concurrency) {
    const limit = pLimit(concurrency);
    const tasks = modules.map((mod) => limit(async () => {
        try {
            const modulePath = join(projectPath, mod.path);
            const analysis = await runDeepAnalysis(modulePath);
            return { module: mod, analysis, status: 'success' };
        }
        catch (error) {
            return {
                module: mod,
                analysis: emptyAnalysis(),
                error: error instanceof Error ? error.message : String(error),
                status: 'failed',
            };
        }
    }));
    return Promise.all(tasks);
}
// ---------------------------------------------------------------------------
// Phase 4: Spec generation
// ---------------------------------------------------------------------------
const SPEC_LOCATION = 'planu/specs';
async function writeSpecFiles(specDir, spec, body) {
    await mkdir(specDir, { recursive: true });
    const specMdPath = join(specDir, 'spec.md');
    await writeFile(specMdPath, body, 'utf-8');
    // SPEC-1010 PR-C: embed Technical and Progress into unified spec.md instead of
    // writing standalone technical.md / progress.md files.
    await replaceSectionInSpec(specMdPath, 'Technical', `Auto-generated by scan_project.\n`);
    await replaceSectionInSpec(specMdPath, 'Progress', generateProgressContent(spec));
}
async function generateSpecs(input, modules, moduleResults, crossAnalysis, healthScore) {
    const specsGenerated = [];
    const nextId = await getNextSpecId(input.projectId, input.path);
    let currentId = nextId;
    // Generate one spec per module
    for (const mod of modules) {
        const result = moduleResults.find((r) => r.module.name === mod.name);
        const specId = `SPEC-${String(currentId).padStart(3, '0')}`;
        const slug = `module-${mod.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
        const deps = crossAnalysis.dependencies.filter((d) => d.to === mod.name).map((d) => d.from);
        const spec = buildScanSpec({
            id: specId,
            title: `Module: ${mod.name}`,
            slug,
            scope: 'feature',
            projectId: input.projectId,
            tags: ['scan-project', `module:${mod.name}`, mod.language.toLowerCase()],
            dependencies: deps,
            gitBranch: `feat/${specId.toLowerCase()}-module-${mod.name.toLowerCase()}`,
            reviewNotes: result?.error ? [`Analysis failed: ${result.error}`] : [],
        });
        const specDir = join(input.path, SPEC_LOCATION, `${specId}-${slug}`);
        spec.specPath = join(specDir, 'spec.md');
        spec.technicalPath = spec.specPath;
        const body = `# ${spec.title}\n\nAuto-generated by scan_project.\n`;
        await writeSpecFiles(specDir, spec, body);
        await createSpec(input.projectId, spec);
        specsGenerated.push(specId);
        currentId++;
    }
    // Generate overview spec
    const overviewSpecId = `SPEC-${String(currentId).padStart(3, '0')}`;
    const overviewSlug = 'project-overview-scan';
    const overviewContent = input.generateOverview !== false
        ? generateProjectOverview(modules, moduleResults, crossAnalysis, healthScore)
        : '';
    const overviewSpec = buildScanSpec({
        id: overviewSpecId,
        title: 'Project Overview — Automated Scan',
        slug: overviewSlug,
        scope: 'cross-module',
        projectId: input.projectId,
        tags: ['scan-project', 'overview'],
        dependencies: specsGenerated,
        gitBranch: `feat/${overviewSpecId.toLowerCase()}-project-overview`,
        reviewNotes: overviewContent ? [overviewContent] : [],
    });
    const overviewDir = join(input.path, SPEC_LOCATION, `${overviewSpecId}-${overviewSlug}`);
    overviewSpec.specPath = join(overviewDir, 'spec.md');
    overviewSpec.technicalPath = overviewSpec.specPath;
    const overviewBody = overviewContent
        ? `# Project Overview — Automated Scan\n\n${overviewContent}\n`
        : `# Project Overview — Automated Scan\n\nAuto-generated by scan_project.\n`;
    await writeSpecFiles(overviewDir, overviewSpec, overviewBody);
    await createSpec(input.projectId, overviewSpec);
    return { specsGenerated, overviewSpecId };
}
const DEFAULT_ESTIMATION = {
    devHours: 0,
    reviewHours: 0,
    recommendedModel: 'sonnet',
    tokensOpus: 0,
    tokensSonnet: 0,
    apiCostUsd: 0,
    hourlyRate: 0,
    humanCostUsd: 0,
    totalCostUsd: 0,
    tokenOptimization: {
        mode: 'local',
        reasoning: 'scan-generated',
        estimatedTokens: 0,
        savings: 'N/A',
    },
};
function buildScanSpec(params) {
    const now = new Date().toISOString();
    return {
        id: params.id,
        title: params.title,
        slug: params.slug,
        type: 'feature',
        scope: params.scope,
        status: 'draft',
        difficulty: 2,
        risk: 'low',
        projectId: params.projectId,
        createdAt: now,
        updatedAt: now,
        specPath: '',
        technicalPath: '',
        estimation: DEFAULT_ESTIMATION,
        actuals: null,
        target: 'backend',
        tags: params.tags,
        dependencies: params.dependencies,
        blockedBy: [],
        gitBranch: params.gitBranch,
        impactAnalysis: null,
        reviewNotes: params.reviewNotes,
    };
}
/**
 * Extract the numeric part of a SPEC-NNN string. Returns 0 if no match.
 */
function specIdToNum(id) {
    const match = /^SPEC-(\d+)$/.exec(id);
    return match?.[1] ? parseInt(match[1], 10) : 0;
}
/**
 * Scan the planu/specs/ directory on disk to find the highest existing SPEC-NNN id.
 * This is the authoritative source when running inside worktrees where the store
 * may not reflect specs that already exist in the repository.
 */
async function maxSpecIdFromFilesystem(projectPath) {
    const specsDir = join(projectPath, SPEC_LOCATION);
    try {
        const entries = await readdir(specsDir, { withFileTypes: true });
        return entries
            .filter((e) => e.isDirectory())
            .reduce((max, e) => {
            const match = /^(SPEC-\d+)-/.exec(e.name);
            return match?.[1] ? Math.max(max, specIdToNum(match[1])) : max;
        }, 0);
    }
    catch {
        // Directory does not exist yet — no specs on disk
        return 0;
    }
}
/**
 * Return the next available spec number, taking the maximum across:
 * 1. The spec store (in-memory/JSON persistence for this project)
 * 2. The filesystem (planu/specs/ directories — source of truth in worktrees)
 */
async function getNextSpecId(projectId, projectPath) {
    const [storeSpecs, fsMax] = await Promise.all([
        listSpecs(projectId),
        maxSpecIdFromFilesystem(projectPath),
    ]);
    const storeMax = storeSpecs.reduce((max, spec) => Math.max(max, specIdToNum(spec.id)), 0);
    return Math.max(storeMax, fsMax) + 1;
}
// ---------------------------------------------------------------------------
// Empty analysis fallback (for failed modules)
// ---------------------------------------------------------------------------
function emptyAnalysis() {
    return {
        testCoverage: { testedModules: [], untestedModules: [], testToSourceRatio: 0 },
        dependencyGraph: { nodes: [], cycles: [], hubs: [], orphans: [] },
        apiRoutes: [],
        complexityMetrics: [],
        configAnalysis: { configFiles: [], envVars: [], featureFlags: [], infrastructure: [] },
        analysisTimeMs: 0,
    };
}
// ---------------------------------------------------------------------------
// Main orchestrator — 4 phases
// ---------------------------------------------------------------------------
export async function scanProject(input) {
    const startTime = Date.now();
    const concurrency = Math.min(Math.max(input.concurrency ?? 4, 1), 8);
    // Phase 1: Discovery
    const modules = await discoverModules(input.path, input.modules);
    if (modules.length === 0) {
        return emptyOutput(concurrency, Date.now() - startTime);
    }
    // Phase 2: Parallel analysis
    const moduleResults = await analyzeModulesInParallel(input.path, modules, concurrency);
    // Phase 3: Cross-module intelligence
    const crossAnalysis = analyzeCrossModuleDependencies(moduleResults);
    const healthScore = calculateProjectHealth(moduleResults, crossAnalysis);
    // Phase 4: Spec generation
    const { specsGenerated, overviewSpecId } = await generateSpecs(input, modules, moduleResults, crossAnalysis, healthScore);
    return {
        modulesDiscovered: modules.length,
        modules: modules.map((m) => ({
            name: m.name,
            path: m.path,
            files: m.estimatedFiles,
            language: m.language,
        })),
        projectHealth: healthScore,
        specsGenerated,
        overviewSpecId,
        crossModuleDeps: crossAnalysis.dependencies,
        antipatterns: crossAnalysis.antipatterns,
        teamPlan: input.generateTeamPlan !== false
            ? generateTeamPlan(modules, moduleResults, crossAnalysis, specsGenerated)
            : undefined,
        analysisTimeMs: Date.now() - startTime,
        concurrencyUsed: concurrency,
    };
}
function emptyOutput(concurrency, timeMs) {
    return {
        modulesDiscovered: 0,
        modules: [],
        projectHealth: {
            score: 0,
            breakdown: { testCoverage: 0, techDebt: 0, archConsistency: 0, depHealth: 0 },
            grade: 'F',
        },
        specsGenerated: [],
        overviewSpecId: '',
        crossModuleDeps: [],
        antipatterns: [],
        analysisTimeMs: timeMs,
        concurrencyUsed: concurrency,
    };
}
//# sourceMappingURL=index.js.map