/**
 * Per-owner realize runner — Phase 1 of the 2026-05-11-PER-OWNER-VS-PER-ACTION
 * proposal.
 *
 * Sibling of `per-action-runner.ts`. SAME spec-walking + owner-grouping
 * logic, DIFFERENT emission granularity: instead of calling
 * `emitActionBody` once per action, this runner calls the per-owner
 * LLM emit hook ONCE per owner and writes the resulting whole-file
 * content as `<Owner>.ai.ts`.
 *
 * Why this exists: the per-action emitter (Realize L3 Phase 3) gave
 * the LLM narrow context — one action's steps, no class shell, no
 * sibling methods, no manifest capabilities. Empirically on
 * idle-meta sonnet, that produced 29 of 94 actions as throw-stubs
 * because the LLM correctly refused to invent imports it couldn't
 * see were available. Per-owner restores the holistic context.
 *
 * Same `<Owner>.ai.ts` output shape (exported async functions per
 * action) → no consumer-interface change. The downstream controller
 * still does `import * as aiBehaviors from '../behaviors/X.ai.js'`.
 */
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
import { collectActions, buildAvailableClientsByOwner, } from './owner-emit-shared.js';
import { buildPerActionScaffolding } from './per-owner-emit.js';
import { writeContextSnapshot } from './realize-context-snapshot.js';
import { safeFunctionName } from '@specverse/types';
/**
 * Run per-owner LLM emission across every controller / service the
 * spec declares. For each owner:
 *   1. Collect its actions and resolve per-step matcher results.
 *   2. Call the perOwnerEmit hook with the whole owner's context.
 *   3. Write the returned file contents to disk (or fall back to a
 *      placeholder file containing one γ stub per action when emit
 *      returned null).
 */
export async function runPerOwnerEmission(opts) {
    const collected = collectActions(opts.spec);
    const ownerGroups = groupByOwner(collected);
    const owners = [];
    const filesWritten = [];
    const totals = {
        ownerCount: 0,
        actionCount: 0,
        ownersEmittedByLLM: 0,
        ownersRecoveredByTier2: 0,
        ownersFellBack: 0,
    };
    // Tier 2 (per-action recovery) runtime gates. Read once.
    const recoveryTierEnabled = opts.perActionRecovery !== undefined &&
        process.env.SPECVERSE_REALIZE_NO_RECOVERY_TIER !== '1';
    const recoveryMaxActionsRaw = process.env.SPECVERSE_REALIZE_RECOVERY_MAX_ACTIONS;
    const recoveryMaxActions = recoveryMaxActionsRaw !== undefined && !Number.isNaN(parseInt(recoveryMaxActionsRaw, 10))
        ? parseInt(recoveryMaxActionsRaw, 10)
        : 40;
    // Matcher must be supplied by the caller — the libs/ step-matching
    // module sits OUTSIDE engines/src/'s tsconfig rootDir and can't be
    // statically imported here. realize/index.ts already resolves the
    // shared matcher via resolveGenPath + dynamic import and threads it
    // into per-action-runner; per-owner-runner uses the same matcher.
    if (!opts.matcher) {
        throw new Error('runPerOwnerEmission requires `matcher` in options — the caller (realize/index.ts) ' +
            'resolves the shared step-matching module via resolveGenPath + dynamic import.');
    }
    const matcher = opts.matcher;
    const conventions = opts.conventions ?? [];
    for (const [ownerName, actions] of ownerGroups) {
        // Resolve per-step matches for every action on this owner.
        const collectedOwnerActions = actions.map((action) => {
            const ctx = {
                componentName: ownerName,
                modelName: action.modelName,
                matcher,
                conventions,
            };
            const perStepMatches = matchSteps(action.spec, ctx, matcher, conventions);
            return {
                actionName: action.actionName,
                spec: action.spec,
                perStepMatches,
            };
        });
        // Owner-level spec block — re-derive from the spec for
        // self-contained prompt context. Best-effort; missing → empty obj.
        const ownerSpec = lookupOwnerSpec(opts.spec, ownerName, actions[0]?.ownerKind);
        // Isolate per-owner emission failures so a single owner's throw
        // (e.g. structural-validator rejection because the LLM dropped a
        // pre-baked snippet) doesn't kill emission for the other 27
        // owners. The 2026-05-11 trial run hit this: HealthController
        // failed structural-preservation, the throw bubbled out to
        // realize/index.ts's outer catch, and 25 owners that hadn't run
        // yet got skipped. Each owner's success is independent — catch
        // here, fall back to the deterministic placeholder file, log to
        // stderr (gated by SPECVERSE_VERBOSE), and keep walking.
        // Shared request object — Tier 1 (whole-owner) and Tier 2
        // (per-action recovery) both take the SAME owner context. Per-owner
        // emit ALWAYS targets the backend tree
        // (`backend/src/behaviors/<Owner>.ai.ts`); stating the runtime
        // upfront makes the constraint technology-explicit.
        const emitRequest = {
            ownerName,
            ownerKind: actions[0]?.ownerKind ?? 'model',
            ownerSpec,
            actions: collectedOwnerActions,
            availableClients: opts.availableClientsByOwner?.get(ownerName),
            availableCapabilities: opts.availableCapabilities,
            availableSpecSurface: buildAvailableSpecSurface(opts.spec, ownerName),
            targetRuntime: 'Node.js (backend)',
            processContext: opts.processContextByOwner?.get(ownerName),
        };
        // Tier 1 — whole-owner emit. Isolate failures so a single owner's
        // throw (e.g. structural-validator rejection) doesn't kill emission
        // for the other owners — catch here, fall through to Tier 2/3.
        let fileContents = null;
        try {
            fileContents = await opts.perOwnerEmit(emitRequest);
        }
        catch (e) {
            if (process.env.SPECVERSE_VERBOSE) {
                const cls = (e && e.constructor && e.constructor.name) || 'Error';
                const msg = (e && e.message) || String(e);
                const head = String(msg).slice(0, 240).replace(/\s+/g, ' ');
                console.error(`   ✗ per-owner emit threw for ${ownerName} [${cls}]: ${head}`);
            }
            fileContents = null;
        }
        // Tier 2 — per-action recovery. When whole-owner emit failed, retry
        // the owner action-by-action with full owner context before
        // dropping to a γ-stub. Gated by env + an action-count cap.
        let recoveredByTier2 = false;
        if (fileContents === null &&
            recoveryTierEnabled &&
            opts.perActionRecovery &&
            actions.length <= recoveryMaxActions) {
            try {
                const recovered = await opts.perActionRecovery(emitRequest);
                if (recovered !== null) {
                    fileContents = recovered;
                    recoveredByTier2 = true;
                }
            }
            catch (e) {
                if (process.env.SPECVERSE_VERBOSE) {
                    const head = String((e && e.message) || e).slice(0, 240).replace(/\s+/g, ' ');
                    console.error(`   ✗ per-action recovery threw for ${ownerName}: ${head}`);
                }
            }
        }
        // Tier 3 — deterministic γ-stub placeholder file.
        const finalContents = fileContents ?? renderFallbackPlaceholderFile(ownerName, actions);
        const llmEmitted = fileContents !== null;
        owners.push({
            ownerName,
            actionCount: actions.length,
            llmEmitted,
            fileContents: finalContents,
        });
        totals.ownerCount += 1;
        totals.actionCount += actions.length;
        if (llmEmitted)
            totals.ownersEmittedByLLM += 1;
        else
            totals.ownersFellBack += 1;
        if (recoveredByTier2)
            totals.ownersRecoveredByTier2 += 1;
        if (!opts.dryRun) {
            const filePath = writeOwnerAiFile(opts.outputDir, ownerName, finalContents);
            filesWritten.push(filePath);
            if (!opts.silent) {
                const tag = recoveredByTier2
                    ? 'per-action recovery'
                    : llmEmitted
                        ? 'LLM'
                        : 'placeholder';
                console.log(`   ✅ Per-owner AI behaviors: ${ownerName}.ai.ts (${actions.length} action${actions.length === 1 ? '' : 's'}, ${tag})`);
            }
            // Phase 3 of 2026-05-13-VERIFIER-DIAGNOSTIC-TREATMENT-SPLIT:
            // snapshot the per-owner realize context so the post-emit
            // feedback runner's reemit prompt has the same TARGET RUNTIME /
            // [PRE-BAKED] / cross-service / capabilities the original
            // per-owner emit had. Best-effort — failure here doesn't block
            // realize.
            writeContextSnapshot(opts.outputDir, {
                ownerName,
                targetRuntime: 'Node.js (backend)',
                perActionScaffolding: buildPerActionScaffolding(collectedOwnerActions),
                availableSpecSurface: buildAvailableSpecSurface(opts.spec, ownerName) ||
                    '(no other services / controllers in this spec)',
                availableCapabilities: opts.availableCapabilities ??
                    '(none — only `input: any` is available)',
                availableClients: opts.availableClientsByOwner?.get(ownerName) ?? '(none)',
            });
        }
    }
    return { owners, filesWritten, totals };
}
// ─── Internal helpers ────────────────────────────────────────────────
/** Group collected actions by owner name. Mirrors per-action-runner. */
function groupByOwner(actions) {
    const groups = new Map();
    for (const a of actions) {
        const list = groups.get(a.ownerName) ?? [];
        list.push(a);
        groups.set(a.ownerName, list);
    }
    return groups;
}
/** Find the owner's spec block in the inferred spec. Searches the
 *  flat-at-root `spec.controllers` / `spec.services` first (which
 *  inference + analyse pipeline both produce), falling back to
 *  per-component nesting. Returns an empty object if not found. */
function lookupOwnerSpec(spec, ownerName, ownerKind) {
    // Flat-at-root path.
    const lookups = [
        spec?.controllers,
        spec?.services,
    ];
    for (const block of lookups) {
        if (!block)
            continue;
        if (Array.isArray(block)) {
            const hit = block.find((b) => b?.name === ownerName);
            if (hit)
                return hit;
        }
        else if (typeof block === 'object') {
            if (block[ownerName])
                return block[ownerName];
        }
    }
    // Per-component nested path.
    const componentsList = Array.isArray(spec?.components)
        ? spec.components
        : Object.values(spec?.components ?? {});
    for (const comp of componentsList) {
        const compBlocks = [comp?.controllers, comp?.services];
        for (const block of compBlocks) {
            if (!block)
                continue;
            if (Array.isArray(block)) {
                const hit = block.find((b) => b?.name === ownerName);
                if (hit)
                    return hit;
            }
            else if (typeof block === 'object') {
                if (block[ownerName])
                    return block[ownerName];
            }
        }
    }
    return {};
}
/** Run every step of an action through the matcher. Returns null per
 *  step when the action has no steps to dispatch. Same as
 *  per-action-emitter's `matchSteps` — duplicated here so the
 *  per-owner runner doesn't need an extra import dance through the
 *  per-action emitter. */
function matchSteps(action, context, matcher, conventions) {
    if (!action.steps || action.steps.length === 0)
        return [];
    const aiArgsExpr = (inputs) => inputs.length > 0 ? `{ ${inputs.join(', ')} }` : '{}';
    const declaredVars = new Set();
    const parameterNames = action.parameters ? Object.keys(action.parameters) : [];
    const results = [];
    for (let i = 0; i < action.steps.length; i++) {
        const step = action.steps[i];
        const ctx = {
            modelName: context.modelName,
            serviceName: action.ownerName,
            operationName: action.name,
            stepNum: i + 1,
            parameterNames,
            declaredVars,
        };
        const result = matcher(step, ctx, conventions, aiArgsExpr);
        results.push(result);
    }
    return results;
}
// `safeFunctionName` from `@specverse/types` — single source for the
// suffix-rename-on-reserved trick we use to keep `export async function
// <action>` legal when `<action>` is a JS keyword (e.g. `delete`). See
// `renderFallbackPlaceholderFile` below.
/** Render a deterministic placeholder file used when the LLM emit
 *  returns null for an entire owner. One γ-shaped exported function
 *  per action so the downstream `import * as aiBehaviors` continues
 *  to resolve — the runtime stub explains the gap.
 *
 *  Reserved-word action names (e.g. `delete`, `class`) can't appear as
 *  `export async function <name>` declarations. Use a re-export rename
 *  in that case so consumers (`aiBehaviors.delete(...)`) still resolve.
 *  Caught empirically: the 2026-05-11 per-owner trial v4 produced a
 *  syntactically-broken placeholder for SaveGameService whose `delete`
 *  action name made the file fail TS parsing, which bailed tsc from
 *  the whole project. */
function renderFallbackPlaceholderFile(ownerName, actions) {
    const parts = [];
    parts.push(`/**`);
    parts.push(` * ${ownerName} — Per-owner AI behaviors (placeholder)`);
    parts.push(` *`);
    parts.push(` * The per-owner LLM emit returned null for this owner (LLM`);
    parts.push(` * declined, parse-failed twice, or rate-limited). Each action`);
    parts.push(` * is stubbed individually so the consumer's`);
    parts.push(` * \`import * as aiBehaviors\` continues to resolve. Inspect`);
    parts.push(` * the realize log for the failure reason (set SPECVERSE_VERBOSE=1`);
    parts.push(` * for diagnostic detail).`);
    parts.push(` *`);
    parts.push(` * Generated: ${new Date().toISOString().split('T')[0]}`);
    parts.push(` */`);
    parts.push('');
    for (const action of actions) {
        const originalName = action.actionName;
        const { name: safeName, reserved } = safeFunctionName(originalName);
        parts.push(`/** Placeholder — LLM declined to emit ${originalName}. */`);
        // Placeholder body is a throw — `input` is never read. Prefix
        // with `_` so TS's noUnusedParameters doesn't fire (TS6133). The
        // realized backend tsconfig sets `noUnusedParameters: true`, so
        // without the prefix we'd hit ~1 TS6133 per placeholder action.
        if (reserved) {
            // Declare under safeName (`<originalName>_`) then re-export under
            // the reserved-word name. Consumer `aiBehaviors.<originalName>(...)`
            // resolves through the re-export.
            parts.push(`async function ${safeName}(_input: any): Promise<any> {`);
            parts.push(`  throw new Error("${originalName}: not implemented (per-owner emit fell back)");`);
            parts.push(`}`);
            parts.push(`export { ${safeName} as ${originalName} };`);
        }
        else {
            parts.push(`export async function ${safeName}(_input: any): Promise<any> {`);
            parts.push(`  throw new Error("${originalName}: not implemented (per-owner emit fell back)");`);
            parts.push(`}`);
        }
        parts.push('');
    }
    return parts.join('\n');
}
/** Write the owner's `.ai.ts` to disk under
 *  `<outputDir>/backend/src/behaviors/<Owner>.ai.ts`. Returns the
 *  absolute path. */
function writeOwnerAiFile(outputDir, ownerName, fileContents) {
    const filePath = join(outputDir, 'backend', 'src', 'behaviors', `${ownerName}.ai.ts`);
    const dir = dirname(filePath);
    if (!existsSync(dir))
        mkdirSync(dir, { recursive: true });
    writeFileSync(filePath, fileContents, 'utf-8');
    return filePath;
}
/** Build the availableCapabilities string consumed by the per-owner
 *  prompt's `availableCapabilities` slot. Walks the manifest's
 *  capabilityMappings and renders one line per mapping with a brief
 *  hint about what the factory provides. Exported so the realize
 *  caller can build it once and feed it to every owner.
 *
 *  Format example:
 *    - orm.client      → PrismaORM (provides Prisma's typed query API)
 *    - jwt.signer      → JsonWebToken (jsonwebtoken.sign / verify)
 *    - event.bus       → InMemoryEventBus (publish / subscribe)
 */
export function buildAvailableCapabilitiesString(manifest) {
    if (!manifest || typeof manifest !== 'object')
        return '(no capabilities)';
    const mappings = collectCapabilityMappings(manifest);
    if (!Array.isArray(mappings) || mappings.length === 0) {
        return '(none — only `input: any` is available)';
    }
    const lines = [];
    for (const m of mappings) {
        if (!m?.capability || !m?.instanceFactory)
            continue;
        lines.push(`  - ${m.capability} → ${m.instanceFactory}`);
    }
    return lines.length > 0 ? lines.join('\n') : '(none — only `input: any` is available)';
}
/**
 * Build the `availableSpecSurface` string consumed by the per-owner
 * prompt's slot of the same name. Walks every controller and service
 * in the spec EXCEPT the current owner (which already has its own
 * spec block in the prompt) and emits each operation's signature
 * formatted as `name(param: Type, ...) → ReturnType`.
 *
 * Purpose (TODO #60): the realize-owner prompt previously gave the
 * LLM only the OWNER's own spec block, no view of OTHER services'
 * operation signatures. When an action's step text said
 * `[call] AuthService.setTokensFromResponse(...)`, the LLM
 * faithfully called the method — without being able to check that
 * `setTokensFromResponse` was a declared operation on AuthService
 * with that arity. Empirically on idle-meta sonnet, this hallucinated-
 * method pattern produced 18 TS2554 (wrong arity) + 22 TS2339 +
 * 5 TS2551 errors. Pre-fix the LLM had no source of truth for
 * cross-service signatures.
 *
 * The surface lists every callable method by class so the LLM can
 * (a) verify a method exists before calling it, (b) match the
 * declared arity, (c) fall back to a γ-stub when the spec's step
 * text references an undeclared method.
 *
 * Format example:
 *   services/
 *     AuthService:
 *       - revokeToken(refreshToken: string, callerId: string) → any
 *       - verifyAccessToken(token: string, config: any) → any
 *
 *   controllers/
 *     AuthController:
 *       - register(input: any) → any
 *       - login(input: any) → any
 */
export function buildAvailableSpecSurface(spec, currentOwnerName) {
    if (!spec || typeof spec !== 'object')
        return '(no other services / controllers)';
    const services = collectOwnerEntries(spec.services);
    const controllers = collectOwnerEntries(spec.controllers);
    const sections = [];
    if (services.length > 0) {
        const lines = ['services/'];
        let serviceCount = 0;
        for (const [name, service] of services) {
            if (name === currentOwnerName)
                continue;
            const ops = listOperationSignatures(service);
            if (ops.length === 0)
                continue;
            lines.push(`  ${name}:`);
            for (const op of ops)
                lines.push(`    - ${op}`);
            serviceCount += 1;
        }
        if (serviceCount > 0)
            sections.push(lines.join('\n'));
    }
    if (controllers.length > 0) {
        const lines = ['controllers/'];
        let controllerCount = 0;
        for (const [name, controller] of controllers) {
            if (name === currentOwnerName)
                continue;
            const ops = listActionSignatures(controller);
            if (ops.length === 0)
                continue;
            lines.push(`  ${name}:`);
            for (const op of ops)
                lines.push(`    - ${op}`);
            controllerCount += 1;
        }
        if (controllerCount > 0)
            sections.push(lines.join('\n'));
    }
    return sections.length > 0
        ? sections.join('\n\n')
        : '(no other services / controllers declare operations)';
}
/** Normalise spec.services / spec.controllers to [name, block] pairs. */
function collectOwnerEntries(block) {
    if (!block)
        return [];
    if (Array.isArray(block)) {
        return block.filter((b) => b?.name).map((b) => [b.name, b]);
    }
    if (typeof block === 'object') {
        return Object.entries(block);
    }
    return [];
}
/** List service operation signatures: name(params) → returns. */
function listOperationSignatures(service) {
    if (!service?.operations)
        return [];
    const entries = collectOwnerEntries(service.operations);
    return entries.map(([opName, op]) => formatSignature(opName, op));
}
/** List controller action signatures: same shape as service operations. */
function listActionSignatures(controller) {
    if (!controller?.actions)
        return [];
    const entries = collectOwnerEntries(controller.actions);
    return entries.map(([actionName, action]) => formatSignature(actionName, action));
}
/** Format one signature line. Keeps parameter types as-spec'd. */
function formatSignature(name, op) {
    const params = op?.parameters || {};
    const paramList = Object.entries(params)
        .map(([pName, pSpec]) => {
        // Spec param shape is usually "Type" or "Type required" or an
        // object with `.type`. Project to a TS-ish hint.
        const typeHint = typeof pSpec === 'string'
            ? (pSpec.split(/\s+/)[0] || 'any')
            : pSpec?.type || 'any';
        return `${pName}: ${typeHint}`;
    })
        .join(', ');
    const returnType = typeof op?.returns === 'string'
        ? (op.returns.split(/\s+/)[0] || 'any')
        : (op?.returns?.type || 'any');
    return `${name}(${paramList}) → ${returnType}`;
}
/** Walk both root and `manifests[].capabilityMappings` shapes. */
function collectCapabilityMappings(manifest) {
    if (!manifest || typeof manifest !== 'object')
        return [];
    if (Array.isArray(manifest.capabilityMappings))
        return manifest.capabilityMappings;
    if (Array.isArray(manifest.manifests)) {
        const out = [];
        for (const inner of manifest.manifests) {
            if (Array.isArray(inner?.capabilityMappings))
                out.push(...inner.capabilityMappings);
        }
        return out;
    }
    return [];
}
// Re-export buildAvailableClientsByOwner so callers that want both
// strings don't have to import from two modules.
export { buildAvailableClientsByOwner };
//# sourceMappingURL=per-owner-runner.js.map