/**
 * `runBehaviours()` — engines 6.29.0+. The LLM-heavy second phase of the
 * analyse/behaviours split.
 *
 * Reads an analysed spec dir (output of `spv ai analyse --no-behaviours`
 * or any caller that produced skeleton + facts sidecars) and fills in
 * action bodies via per-action LLM micro-calls. Surgical-retries on
 * validation failures. Writes the filled spec back to the same dir
 * (or to a fresh outputDir if specified).
 *
 * Why this is separate from runAnalyse: the deterministic phase (prepass +
 * skeleton + manifest + deployment) finishes in ~30s; the LLM phase takes
 * 5-30 min depending on size. Splitting lets users:
 *
 *   - Iterate on adapter/extraction logic without paying for LLM time
 *   - Inspect the analysed scaffold before committing budget to bodies
 *   - Resume after a body-fill failure (the analysed spec is still on
 *     disk, just re-run `spv ai behaviours` against it)
 *
 * Inputs the runner reads from disk:
 *   - `<analysedSpecDir>/specs/skeleton.specly` (or `main.specly` if
 *     skeleton was promoted)
 *   - `<analysedSpecDir>/facts/skeleton-provenance.json`
 *   - `<analysedSpecDir>/facts/facts.json` (to rebuild candidateSteps)
 *   - `<analysedSpecDir>/manifests/implementation.yaml` (preserved)
 *
 * Inputs it does NOT recompute:
 *   - The deployment block in the spec — preserved as-is.
 *   - The manifest — preserved as-is.
 *   - Component / model / view declarations — preserved as-is.
 *
 * Only action bodies are filled. Everything else is a faithful
 * pass-through from the analysed spec.
 */
import { writeFileSync, readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { spawnSync } from 'child_process';
import { llmOutputDir, factsDir, specsDir, ensureSubdirs, } from './run-paths.js';
import { resolveModel } from './model-resolver.js';
function runValidate(specPath) {
    const result = spawnSync('spv', ['validate', specPath], {
        encoding: 'utf8',
        timeout: 30_000,
    });
    if (result.status === 0)
        return { ok: true, errors: '' };
    const errors = (result.stderr || '') + (result.stdout || '');
    return { ok: false, errors };
}
/**
 * Run the LLM-heavy behaviours phase against an analysed spec dir.
 */
export async function runBehaviours(opts) {
    const inputDir = opts.analysedSpecDir;
    const outputDir = opts.outputDir ?? inputDir;
    ensureSubdirs(outputDir);
    // ── Read analysed inputs from disk ──────────────────────────────────
    const skeletonFilename = opts.specFilename ?? 'skeleton.specly';
    const skeletonPath = join(specsDir(inputDir), skeletonFilename);
    if (!existsSync(skeletonPath)) {
        throw new Error(`runBehaviours: skeleton not found at ${skeletonPath}. ` +
            `Run \`spv ai analyse --no-behaviours <source> -o ${inputDir}\` first.`);
    }
    const skeleton = readFileSync(skeletonPath, 'utf8');
    const skeletonProvenancePath = join(factsDir(inputDir), 'skeleton-provenance.json');
    if (!existsSync(skeletonProvenancePath)) {
        throw new Error(`runBehaviours: skeleton-provenance.json not found at ${skeletonProvenancePath}.`);
    }
    const skeletonProvenance = JSON.parse(readFileSync(skeletonProvenancePath, 'utf8'));
    const factsPath = join(factsDir(inputDir), 'facts.json');
    if (!existsSync(factsPath)) {
        throw new Error(`runBehaviours: facts.json not found at ${factsPath}.`);
    }
    const facts = JSON.parse(readFileSync(factsPath, 'utf8'));
    // ── Rebuild candidateStepsByMethodKey from facts ────────────────────
    const candidateStepsByMethodKey = new Map();
    for (const cls of facts.candidateMethods ?? []) {
        for (let mi = 0; mi < cls.methods.length; mi++) {
            const m = cls.methods[mi];
            const lines = m.candidates.map((c, i) => `${i + 1}. [${c.kind}] ${c.summary}`);
            candidateStepsByMethodKey.set(`${cls.entityName}::${mi}`, lines.join('\n'));
        }
    }
    // ── Build processContextByMethodKey (TODO #51 phase 2C, 6.32.0+) ────
    // GitNexus Process nodes already loaded into facts.processes by the
    // analyse phase (capability-gated). Build per-stub formatted timelines
    // matching by file-tail + method name; cleanly degrades to empty Map
    // when processes are absent (walk-backend or filtered out).
    const { buildProcessContextByMethodKey } = await import('./processes-to-microcall-context.js');
    const processContextByMethodKey = buildProcessContextByMethodKey({
        processes: facts.processes,
        actionStubs: skeletonProvenance.actionStubs,
    });
    // ── Resolve model (caller may have pre-built one) ───────────────────
    const model = opts.model ?? resolveModel({
        timeout: opts.timeoutMsPerCall ?? 15 * 60_000,
        sessionId: opts.sessionId,
    });
    // ── Run microcalls ──────────────────────────────────────────────────
    const { runActionMicrocalls, assembleFinalSpec, classifyValidationErrors, runSurgicalRetry } = await import('./microcall-orchestrator.js');
    const totalStubs = skeletonProvenance.actionStubs.length;
    const concurrencyCap = opts.microcallConcurrency ?? 5;
    console.log(`  behaviours: dispatching ${totalStubs} action stub(s) (concurrency=${concurrencyCap}, timeout=5m/call)`);
    const t0 = Date.now();
    let completed = 0;
    const microcallResult = await runActionMicrocalls({
        skeleton,
        skeletonProvenance,
        candidateStepsByMethodKey,
        processContextByMethodKey,
        model,
        concurrencyCap,
        timeoutMsPerCall: opts.timeoutMsPerCall ?? 5 * 60_000,
        // Manifest already in place from the analyse phase; don't re-emit.
        manifestInputs: undefined,
        onProgress: (record) => {
            completed++;
            const status = record.parsedBody ? '✓' : '✗';
            const owner = record.stubRef.ownerName;
            const action = record.stubRef.actionName;
            const lat = (record.latencyMs / 1000).toFixed(1);
            const retried = record.rateLimitRetries && record.rateLimitRetries > 0
                ? ` [retried ${record.rateLimitRetries}×]` : '';
            const fail = record.parsedBody ? '' : ` — ${(record.failure ?? '').slice(0, 60)}`;
            console.log(`    ${String(completed).padStart(String(totalStubs).length)}/${totalStubs} ${status} ${owner}.${action} (${lat}s)${retried}${fail}`);
        },
    });
    // Forward-log microcall provenance.
    writeFileSync(join(llmOutputDir(outputDir), 'microcall-decisions.json'), JSON.stringify(microcallResult.provenance, null, 2));
    // ── Assemble filled spec ────────────────────────────────────────────
    // assembleFinalSpec fills action body lines that match yamlPaths in
    // the provenance; it leaves the rest of the skeleton alone. The
    // skeleton file on disk is the *components-only* form — deployments
    // get appended below from a fresh emitDefaultDeployments call.
    let filledSpec = assembleFinalSpec(skeleton, skeletonProvenance, microcallResult.actionBodies);
    // ── Compute deterministic deployments block (engines 6.32.2+) ───────
    // Behaviours-runner re-emits the deployments block from facts so the
    // output of `spv ai behaviours` is consistent with the analyse-phase
    // spec shape. Without this re-emission, the per-action microcall
    // assembly drops the deployment topology that `spv realize` needs to
    // materialise controllers / services / storage / views — turning a
    // working analyse spec into a no-op realize.
    //
    // The emitter is deterministic given facts; computing it once here
    // and re-appending after every assembleFinalSpec call below keeps
    // the surgical-retry path consistent too.
    const hasContent = (facts.entities?.length ?? 0) > 0
        || (facts.candidateMethods?.length ?? 0) > 0
        || (facts.views?.length ?? 0) > 0;
    let deploymentsYaml = '';
    if (hasContent) {
        const { emitFaithfulSkeleton } = await import('./skeleton-emitter.js');
        const { classesByComponent, entityHome } = emitFaithfulSkeleton(facts);
        const { emitDefaultDeployments } = await import('./deployment-emitter.js');
        const result = emitDefaultDeployments({
            facts,
            classesByComponent,
            entityHome,
        });
        deploymentsYaml = result.deploymentsYaml;
        console.log(`  deployments: re-emitted with ${result.counts.controllers} controller(s), ${result.counts.services} service(s), ${result.counts.storage} storage, ${result.counts.views} view(s), ${result.counts.infrastructure} infra, ${result.counts.communications} comms`);
    }
    const appendDeployments = (spec) => deploymentsYaml ? spec.replace(/\n*$/, '\n') + deploymentsYaml + '\n' : spec;
    filledSpec = appendDeployments(filledSpec);
    // ── Validate + surgical retry ───────────────────────────────────────
    const filledSpecPath = join(specsDir(outputDir), 'main.specly');
    writeFileSync(filledSpecPath, filledSpec);
    let validateResult = runValidate(filledSpecPath);
    let surgicalRetryPasses = 0;
    if (!validateResult.ok) {
        let priorError = validateResult.errors;
        let currentBodies = microcallResult.actionBodies;
        const MAX_SURGICAL_RETRIES = 3;
        for (let attempt = 1; attempt <= MAX_SURGICAL_RETRIES; attempt++) {
            const classification = classifyValidationErrors(priorError, skeletonProvenance);
            if (classification.actionPathErrors.size === 0) {
                // Only structural errors — LLM retry can't fix those.
                break;
            }
            console.log(`  behaviours: surgical retry ${attempt} — re-firing ${classification.actionPathErrors.size} stub(s)`);
            const surgical = await runSurgicalRetry({
                skeleton,
                skeletonProvenance,
                candidateStepsByMethodKey,
                processContextByMethodKey,
                prior: { actionBodies: currentBodies, classification },
                model,
                timeoutMsPerCall: opts.timeoutMsPerCall ?? 5 * 60_000,
            });
            currentBodies = surgical.actionBodies;
            filledSpec = appendDeployments(assembleFinalSpec(skeleton, skeletonProvenance, currentBodies));
            writeFileSync(filledSpecPath, filledSpec);
            validateResult = runValidate(filledSpecPath);
            surgicalRetryPasses++;
            if (validateResult.ok)
                break;
            priorError = validateResult.errors;
        }
    }
    const wallTimeMs = Date.now() - t0;
    const succeeded = microcallResult.provenance.totals.actionCallsSucceeded;
    const attempted = microcallResult.provenance.totals.actionCallsAttempted;
    console.log(`  behaviours: ${succeeded}/${attempted} succeeded in ${(wallTimeMs / 1000).toFixed(1)}s` +
        (surgicalRetryPasses > 0 ? ` (+${surgicalRetryPasses} surgical retry pass(es))` : ''));
    return {
        filledSpecPath,
        microcallStats: {
            attempted,
            succeeded,
            failed: attempted - succeeded,
            wallTimeMs,
        },
        validateFinal: validateResult,
        surgicalRetryPasses,
    };
}
//# sourceMappingURL=behaviours-runner.js.map