/**
 * Per-action realize runner — Phase 3 of the L3 redesign.
 *
 * The orchestration glue between the spec walker (controllers + services
 * + their actions/operations) and the per-action emitter. This is the
 * call site that REPLACES the legacy `generateAiBehaviors` step in
 * `realize/index.ts` when the per-action path is enabled.
 *
 * Responsibilities:
 *   1. Walk the spec's controllers + services and collect every custom
 *      action (controller actions + service operations) along with the
 *      owning entity name + step list + spec metadata.
 *   2. For each action, build an ActionSpec + RealizeContext and call
 *      emitActionBody. The matcher (prisma / mongo-native / pg-native)
 *      and conventions array are passed through as injected
 *      dependencies — same shape the legacy ai-behaviors path uses.
 *   3. Group results by owner and emit one `<Owner>.ai.ts` file per
 *      owner, containing one exported async function per action with
 *      the LLM-emitted (or stub-γ-emitted) body inline.
 *
 * Concurrency: the LLM call site (when a real LLM is configured) is
 * sequential per owner — keeps surgical-retry's parse-error feedback
 * clean and bounds the API spend in the worst case. A future Phase
 * could parallelise across owners; not the default in V1.
 *
 * The legacy `<Owner>.ai.ts` emitted by `ai-behaviors-generator.ts`
 * contains per-STEP helpers; the new file contains per-ACTION pure
 * functions. DR2 says keep the filename, repurpose the content shape.
 * Both shapes co-exist when this runner runs alongside the legacy
 * generator (gated by env flag); the new file overwrites the legacy
 * output when both are run for the same owner. The integration test
 * pins the new shape; the env flag governs which runs in realize.
 */
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
import { emitActionBody, } from './per-action-emitter.js';
/**
 * Walk the spec, dispatch each custom action through the per-action
 * emitter, and write per-owner `.ai.ts` files.
 *
 * Collection rules:
 *   - Controllers: every entry under `controller.actions` becomes a
 *     CollectedAction. CURVED ops (`controller.cured.{create,...}`)
 *     are skipped — the L1 instance-factory templates handle those
 *     deterministically.
 *   - Services: every entry under `service.operations` becomes a
 *     CollectedAction. Service operations don't have CURVED kinship.
 *   - Model behaviours (`model.behaviors.<name>`) are NOT collected by
 *     this runner — the controller / service is the realized surface;
 *     model behaviours flow through controllers via the inference
 *     pipeline before realize sees them. A future phase could collect
 *     them directly if needed.
 */
export async function runPerActionEmission(opts) {
    const collected = collectActions(opts.spec);
    const ownerGroups = groupByOwner(collected);
    const owners = [];
    const filesWritten = [];
    const totals = {
        actionCount: 0,
        ownerCount: 0,
        stubCount: 0,
        llmCount: 0,
        fullyMatchedCount: 0,
    };
    for (const [ownerName, actions] of ownerGroups) {
        const bodies = new Map();
        const specs = new Map();
        const availableClients = opts.availableClientsByOwner?.get(ownerName);
        for (const collectedAction of actions) {
            const realizeCtx = {
                componentName: ownerName,
                modelName: collectedAction.modelName,
                matcher: opts.matcher,
                conventions: opts.conventions,
                aiArgsExpr: opts.aiArgsExpr,
                llmEmit: opts.llmEmit,
                canCallLLM: opts.canCallLLM,
                availableClients,
            };
            const body = await emitActionBody(collectedAction.spec, realizeCtx);
            bodies.set(collectedAction.actionName, body);
            specs.set(collectedAction.actionName, collectedAction.spec);
            totals.actionCount++;
            // Heuristic counters — body shape tells us which branch fired.
            if (body.includes('throw new Error(') && body.includes(': not implemented")')) {
                totals.stubCount++;
            }
            else if (collectedAction.spec.steps.length > 0) {
                // No way to distinguish LLM vs full-convention from here, but
                // we can spot the "stub" branch above; everything else is a
                // composed body.
                totals.fullyMatchedCount++;
            }
        }
        const result = { ownerName, bodies, specs };
        owners.push(result);
        totals.ownerCount++;
        if (!opts.dryRun) {
            const filePath = writeOwnerAiFile(opts.outputDir, result);
            filesWritten.push(filePath);
            if (!opts.silent) {
                console.log(`   ✅ Per-action AI behaviors: ${ownerName}.ai.ts (${actions.length} action${actions.length === 1 ? '' : 's'})`);
            }
        }
    }
    return { owners, filesWritten, totals };
}
// ─── Spec walking ────────────────────────────────────────────────────
/**
 * Walk the spec and collect every custom controller action + service
 * operation that has a non-empty `steps:` block. Without steps, the
 * per-action emitter has no work to do — those actions stay as
 * placeholder methods on the controller / service.
 */
export function collectActions(spec) {
    const out = [];
    // Controllers — accept either {Name: {...}} object or [{name, ...}] array.
    // When keyed by name (the canonical spec shape), the entries don't
    // necessarily carry a `name` field; we fall back to the map key.
    const controllerEntries = Array.isArray(spec.controllers)
        ? spec.controllers.map((c) => [c?.name ?? '', c])
        : Object.entries(spec.controllers || {});
    for (const [keyName, controller] of controllerEntries) {
        if (!controller?.actions)
            continue;
        const ownerName = controller.name || keyName;
        if (!ownerName)
            continue;
        const modelName = controller.model || controller.modelReference || ownerName.replace(/Controller$/, '');
        for (const [actionName, actionRaw] of Object.entries(controller.actions)) {
            const action = normalizeActionSpec(actionRaw, actionName);
            if (action.steps.length === 0)
                continue;
            out.push({
                ownerKind: 'controller',
                ownerName,
                modelName,
                actionName,
                spec: { ...action, ownerName, ownerKind: 'controller' },
            });
        }
    }
    // Services — accept either {Name: {...}} object or [{name, ...}] array.
    const serviceEntries = Array.isArray(spec.services)
        ? spec.services.map((s) => [s?.name ?? '', s])
        : Object.entries(spec.services || {});
    for (const [keyName, service] of serviceEntries) {
        if (!service?.operations)
            continue;
        const ownerName = service.name || keyName;
        if (!ownerName)
            continue;
        const modelName = ownerName.replace(/Service$/, '');
        const operationEntries = Array.isArray(service.operations)
            ? service.operations.map((op) => [op.name, op])
            : Object.entries(service.operations);
        for (const [actionName, opRaw] of operationEntries) {
            const action = normalizeActionSpec(opRaw, actionName);
            if (action.steps.length === 0)
                continue;
            out.push({
                ownerKind: 'service',
                ownerName,
                modelName,
                actionName,
                spec: { ...action, ownerName, ownerKind: 'service' },
            });
        }
    }
    return out;
}
/** Coerce a raw action / operation spec object into the ActionSpec shape. */
function normalizeActionSpec(raw, actionName) {
    const stepsRaw = raw?.steps ?? raw?.implementation?.steps ?? [];
    const steps = Array.isArray(stepsRaw)
        ? stepsRaw.map((s) => (typeof s === 'string' ? s : s?.step)).filter((s) => typeof s === 'string')
        : [];
    return {
        name: actionName,
        ownerName: '', // filled in by caller
        ownerKind: 'controller', // overwritten by caller
        steps,
        description: raw?.description,
        parameters: raw?.parameters,
        returns: typeof raw?.returns === 'string' ? raw.returns : raw?.returns ? JSON.stringify(raw.returns) : undefined,
        requires: raw?.requires ?? raw?.preconditions,
        ensures: raw?.ensures ?? raw?.postconditions,
        publishes: raw?.publishes ?? raw?.events,
    };
}
/** Group collected actions by owner so we emit one .ai.ts file per owner. */
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;
}
// ─── File emission ───────────────────────────────────────────────────
/**
 * Write a single owner's `.ai.ts` file to disk. Returns the file path.
 *
 * The file is structured as:
 *   - Header docstring (DR2 framing — repurposed from per-step to per-action).
 *   - One `export async function <actionName>(input): Promise<...>` per
 *     action, with the body produced by emitActionBody.
 *
 * Signatures are intentionally loose (`input: any`) for V1; Phase 4 /
 * 5 will tighten them using the spec's `parameters:` types. The per-step
 * `.ai.ts` files used the same loose signature, so we don't regress.
 */
function writeOwnerAiFile(outputDir, owner) {
    const filePath = join(outputDir, 'backend', 'src', 'behaviors', `${owner.ownerName}.ai.ts`);
    const dir = dirname(filePath);
    if (!existsSync(dir))
        mkdirSync(dir, { recursive: true });
    writeFileSync(filePath, renderOwnerAiFile(owner), 'utf-8');
    return filePath;
}
/**
 * Render the `.ai.ts` content for one owner. Exported so tests can
 * assert on the produced shape without going to disk.
 */
export function renderOwnerAiFile(owner) {
    const blocks = [];
    blocks.push(`/**
 * ${owner.ownerName} — Per-action AI behaviors (Realize L3 per-action emission).
 *
 * One exported async function per spec action / operation. Each body
 * was emitted by the per-action realize pipeline:
 *
 *   - L2 convention matcher pre-baked deterministic snippets for known
 *     step patterns. Those snippets are present verbatim in the body.
 *   - The LLM (when available) wove the [WRITE]-step code around the
 *     pre-baked snippets to produce one coherent method body per action.
 *   - When neither convention nor LLM produced a coherent body, the
 *     function throws a "not implemented" stub at runtime and preserves
 *     the convention-matched scaffolding as a code-block comment for
 *     reviewer context (DR4-γ shape).
 *
 * REVIEW SURFACE — anything in this file that ISN'T deterministic
 * convention output came from an LLM. Inspect carefully before
 * deploying production logic.
 *
 * Generated: ${new Date().toISOString().split('T')[0]}
 */
`);
    for (const [actionName, spec] of owner.specs) {
        const body = owner.bodies.get(actionName) ?? `    throw new Error('${actionName}: missing body');`;
        blocks.push(renderActionFunction(actionName, spec, body));
    }
    return blocks.join('\n');
}
/** Render a single per-action `export async function ...` block. */
function renderActionFunction(actionName, spec, body) {
    const description = spec.description ? ` * ${spec.description}\n *` : '';
    const stepsDoc = spec.steps.length > 0
        ? ` *\n * Spec steps:\n${spec.steps.map((s) => ` *   - ${s}`).join('\n')}\n`
        : '';
    return `/**
 * ${actionName}
${description}${stepsDoc} */
export async function ${actionName}(input: any): Promise<any> {
${body}
}
`;
}
// ─── V2 imports → availableClients string (Round 3 follow-up) ────────
/**
 * Convert a V2 `import: from:` provider name to the client identifier
 * the runtime aggregator (`clients/index.ts`) exposes via `v2Clients`.
 *
 * Mirrors the `toClientName` logic in
 * `engines/src/realize/runtime-emitters/library.ts` so the prompt's
 * advertised name matches the actual client constant the consumer
 * spreads into `deps`. Kept local (rather than imported) because the
 * library emitter's helper is private and the conversion is a tiny
 * pure function — duplication is cheaper than a public re-export.
 */
function toClientName(fromName) {
    // Mirrors the library emitter's toWords: scope-strip, slash → dash,
    // split on camelCase boundary AND non-alphanumeric runs, lowercase.
    const normalised = fromName.replace(/^@/, '').replace(/\//g, '-');
    const words = normalised
        .replace(/([a-z])([A-Z])/g, '$1 $2')
        .replace(/[^a-zA-Z0-9]+/g, ' ')
        .trim()
        .split(' ')
        .filter(Boolean)
        .map((w) => w.toLowerCase());
    if (words.length === 0)
        return 'unknownClient';
    return (words[0] +
        words
            .slice(1)
            .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
            .join(''));
}
/**
 * Build a single component's `availableClients` prompt string from its
 * V2 `import:` block.
 *
 * Output shape (one line per importable operation):
 *   - deps.<clientName>.<operation>(input)  // from <fromName> (<topology>)
 *   - deps.<clientName>  // bare entity export from <fromName> (<topology>)
 *
 * Topology is included when known (caller can pass a resolver-result
 * lookup); when unknown, we omit the parenthetical. The Round-3 wire-up
 * in `realize/index.ts` runs BEFORE the V2 imports loop has resolved
 * factories, so most calls today omit topology — fine. The slot is
 * forward-compatible: a future revision can pass topology by name and
 * we'll surface it.
 *
 * Returns `null` when the component has no imports (caller should not
 * set `availableClients` at all in that case so the LLM hook falls
 * back to its "(none)" default).
 */
export function buildAvailableClientsForComponent(component, topologyByFrom) {
    const imports = Array.isArray(component?.import) ? component.import : [];
    if (imports.length === 0)
        return null;
    const lines = [];
    for (const entry of imports) {
        const fromName = entry?.from ?? entry?.package ?? entry?.file ?? '';
        if (!fromName)
            continue;
        const select = Array.isArray(entry?.select) ? entry.select : [];
        if (select.length === 0)
            continue;
        const clientName = toClientName(fromName);
        const topology = topologyByFrom?.get(fromName);
        const topoSuffix = topology ? ` (${topology})` : '';
        for (const sel of select) {
            const dotIndex = sel.indexOf('.');
            if (dotIndex === -1) {
                // Bare entity export: full interface, LLM picks the operation.
                lines.push(`  - deps.${clientName}  // bare entity export ${sel} from ${fromName}${topoSuffix}`);
            }
            else {
                // Dotted: <Entity>.<operation> — surface the operation directly.
                const operation = sel.slice(dotIndex + 1);
                lines.push(`  - deps.${clientName}.${operation}(input)  // from ${fromName}${topoSuffix}`);
            }
        }
    }
    if (lines.length === 0)
        return null;
    return ['Available imported clients (V2 imports):', ...lines].join('\n');
}
/**
 * Walk a parsed spec and build the per-owner `availableClients` map the
 * per-action runner threads into `RealizeContext.availableClients`.
 *
 * Resolution: each `spec.components[i]` carries an `import:` block AND
 * a set of contained controllers + services. Every controller / service
 * inside the component sees the same `availableClients` string (V2
 * imports are component-scoped; per-action filtering via `Action.uses`
 * is a future refinement).
 *
 * Components-without-imports do not contribute entries — owners in
 * those components won't have `availableClients` set, and the LLM hook
 * falls back to its "(none)" default. This keeps the prompt clean for
 * specs that don't use V2 imports at all.
 */
export function buildAvailableClientsByOwner(spec, topologyByFrom) {
    const out = new Map();
    const components = Array.isArray(spec?.components)
        ? spec.components
        : Object.values(spec?.components ?? {});
    for (const component of components) {
        const clientsString = buildAvailableClientsForComponent(component, topologyByFrom);
        if (!clientsString)
            continue;
        // Each contained controller and service shares this component's
        // imports. Controllers are shaped `{ Name: { ... } }` or
        // `[{ name, ... }]`; same for services.
        const controllerEntries = Array.isArray(component?.controllers)
            ? component.controllers.map((c) => [c?.name ?? '', c])
            : Object.entries(component?.controllers ?? {});
        for (const [keyName, c] of controllerEntries) {
            const ownerName = c?.name || keyName;
            if (!ownerName)
                continue;
            out.set(ownerName, clientsString);
        }
        const serviceEntries = Array.isArray(component?.services)
            ? component.services.map((s) => [s?.name ?? '', s])
            : Object.entries(component?.services ?? {});
        for (const [keyName, s] of serviceEntries) {
            const ownerName = s?.name || keyName;
            if (!ownerName)
                continue;
            out.set(ownerName, clientsString);
        }
    }
    return out;
}
//# sourceMappingURL=per-action-runner.js.map