/**
 * Per-action LLM emit hook — Phase 3 of the L3 redesign.
 *
 * Builds an `LlmEmitFn` (the dependency-injection seam exposed by
 * `per-action-emitter.ts`) that loads `realize-action.prompt.yaml`,
 * calls `runPrompt` with the assembled per-action context, runs the
 * structural validator over the LLM output, and surgically retries
 * once on parse failure (DR3-aligned).
 *
 * Three quality gates run on every LLM emission:
 *   1. Markdown extraction — pull the ```typescript fenced block out of
 *      the LLM response. Empty / missing block → null (caller falls
 *      through to γ stub).
 *   2. Structural preservation (DR3) — every pre-baked snippet must
 *      appear verbatim in the body. A rewritten snippet is a loud
 *      failure; we throw with a diff so the user knows the LLM
 *      misbehaved.
 *   3. esbuild parse check — the body must be syntactically valid TS.
 *      On parse failure we run ONE surgical retry with the parse error
 *      fed back as a userPrefix; if it fails again we return null and
 *      the caller emits the γ stub.
 *
 * The realize-action.prompt.yaml asset (Phase 2 deliverable) lives in
 * `@specverse/assets/prompts/core/standard/default/realize-action.prompt.yaml`
 * once that package is republished. Until then, we fall back to a
 * filesystem path under specverse-self/assets/ so Phase 3 doesn't have
 * to wait on a registry roundtrip.
 */
import { readFileSync, existsSync } from 'fs';
import { dirname, join, resolve } from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import yaml from 'js-yaml';
import { loadPrompt, runPrompt as runPromptFn, assembleSystem, assembleUser, } from '../ai/prompt-runner.js';
import { validateLlmOutputPreservesConventions, } from './structural-validator.js';
const require = createRequire(import.meta.url);
/**
 * Build an {@link LlmEmitFn} for use in per-action realize emission.
 *
 * The returned function:
 *   1. Builds the prompt input from the per-action emitter's request.
 *   2. Calls runPrompt({ operation: 'realize-action', ... }).
 *   3. Extracts the ```typescript block from the response.
 *   4. Runs the structural validator (DR3); throws on rewrite.
 *   5. Runs an esbuild parse check; surgical-retries once on failure.
 *   6. Returns the body string, or null when all attempts fail (caller
 *      falls through to γ stub).
 */
export function createRealizeActionLlmEmit(opts = {}) {
    const runPrompt = opts.runPromptOverride ?? runPromptFn;
    const loadPromptResolved = opts.loadPromptOverride ?? loadRealizeActionPrompt;
    const parseCheck = opts.parseCheckOverride ?? esbuildParseCheck;
    return async (request) => {
        // 1. Pre-bake snippets for the structural validator. Pull from the
        //    per-step matches; matched ones contribute, unmatched skip.
        const prebakedSnippets = [];
        request.perStepMatches.forEach((match, idx) => {
            if (match?.matched && match.call) {
                prebakedSnippets.push({
                    stepNum: idx + 1,
                    stepText: request.action.steps[idx] ?? '',
                    snippet: match.call,
                });
            }
        });
        // 2. Build prompt values.
        //    availableClients precedence: per-call (RealizeContext) > per-emitter
        //    (constructor opts) > "(none)". The per-call slot lets the runner
        //    feed the consumer component's V2 import block (different per
        //    owner) without rebuilding the emit hook for each owner.
        const promptValues = buildPromptValues({
            action: request.action,
            context: request.context,
            perStepMatches: request.perStepMatches,
            availableClients: request.context.availableClients ?? opts.availableClients,
            siblingActions: opts.siblingActions,
            processContext: opts.processContext,
        });
        // 3. Surgical retry loop (cap at 1).
        const maxAttempts = opts.disableSurgicalRetry ? 1 : 2;
        let lastParseError = null;
        for (let attempt = 0; attempt < maxAttempts; attempt++) {
            const userPrefix = lastParseError
                ? buildSurgicalRetryPrefix(lastParseError, request.action.name)
                : '';
            let runResult;
            try {
                const runOpts = {
                    operation: 'realize-action',
                    values: promptValues,
                    model: opts.model,
                    timeoutMs: opts.timeoutMs,
                    userPrefix,
                };
                // The default loadPrompt searches @specverse/assets first, then
                // engines' bundled assets dir. Most environments hit one of those.
                // For the local-development dev-loop where realize-action.prompt.yaml
                // is freshly added but assets isn't republished yet, we fall back
                // to a filesystem path inside specverse-self/assets/.
                runResult = await callRunPromptWithFallback(runPrompt, runOpts, loadPromptResolved);
            }
            catch (e) {
                // Diagnostic: surface what the LLM hook is throwing so silent
                // γ-stub fallbacks are debuggable. Gated behind SPECVERSE_VERBOSE
                // so it doesn't add noise to production runs. Set
                // SPECVERSE_VERBOSE=1 to see per-action LLM error details in
                // stderr. Permanent retry policy + structured sidecar will
                // replace this; for now, env-gated stderr is the minimal seam.
                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-action LLM emit threw for ${request.action.ownerName}.${request.action.name} [${cls}]: ${head}`);
                }
                // Any error from runPrompt — bubble up as null (γ stub).
                return null;
            }
            // 4. Extract the ```typescript fenced block.
            const body = extractTypescriptBlock(runResult.text);
            if (!body || body.trim().length === 0) {
                if (process.env.SPECVERSE_VERBOSE) {
                    const reason = body === undefined || body === null
                        ? 'no typescript code block in LLM response'
                        : 'typescript block was empty';
                    console.error(`   ✗ per-action LLM emit empty for ${request.action.ownerName}.${request.action.name}: ${reason}`);
                }
                // No usable body — γ stub.
                return null;
            }
            // 5. Structural validator — DR3. A rewritten pre-baked snippet
            //    is a LOUD failure (not a silent retry).
            const structural = validateLlmOutputPreservesConventions(body, prebakedSnippets);
            if (structural.ok === false) {
                throw new Error(`[realize-action] structural-preservation failed for ${request.action.ownerName}.${request.action.name}:\n${structural.reason}`);
            }
            // 6. esbuild parse check — surgical retry once on parse failure.
            const parseError = await parseCheck(body);
            if (parseError === null) {
                // All gates green — return the body.
                return body;
            }
            lastParseError = parseError;
            // Loop continues only if we have an attempt left.
        }
        // Exhausted retries — fall through to γ stub.
        if (process.env.SPECVERSE_VERBOSE) {
            const head = String(lastParseError ?? 'unknown').slice(0, 240).replace(/\s+/g, ' ');
            console.error(`   ✗ per-action LLM emit parse-failed after ${maxAttempts} attempt(s) for ${request.action.ownerName}.${request.action.name}: ${head}`);
        }
        return null;
    };
}
// ─── Prompt loading + assets fallback ────────────────────────────────
/**
 * Default loader — uses the canonical loadPrompt('realize-action') path
 * (@specverse/assets first, then engine-bundled assets).
 */
function loadRealizeActionPrompt() {
    return loadPrompt('realize-action');
}
/**
 * Wrapper around runPrompt that falls back to a workspace-relative
 * specverse-self/assets/ lookup when realize-action.prompt.yaml isn't
 * yet in the installed @specverse/assets package. Returns the same shape
 * as runPrompt.
 *
 * This is a Phase 3 bridging concern — once @specverse/assets ships
 * realize-action.prompt.yaml, the fallback is dead code (but harmless).
 */
async function callRunPromptWithFallback(runPromptImpl, opts, fallbackLoader) {
    // First try: just call runPrompt — it does its own loadPrompt.
    try {
        return await runPromptImpl(opts);
    }
    catch (e) {
        const msg = e?.message ?? String(e);
        if (!/prompt file not found for "realize-action"/i.test(msg)) {
            throw e;
        }
    }
    // Second try: load the prompt via the fallback resolver and call the
    // generateText path manually. We use runPrompt's public assembleSystem /
    // assembleUser helpers so we don't reimplement the substitution logic.
    const prompt = fallbackLoader();
    const system = assembleSystem(prompt);
    const baseUser = assembleUser(prompt, opts.values);
    const user = (opts.userPrefix || '') + baseUser;
    // Fire onAssembled so callers (audit-trail recorders) see the input.
    if (typeof opts.onAssembled === 'function') {
        opts.onAssembled({ system, user });
    }
    // Defer to the AI SDK directly — same shape as runPrompt's body but
    // with the prompt content already loaded.
    const aiSdk = await import('ai');
    const { resolveModel } = await import('../ai/model-resolver.js');
    const model = opts.model ?? resolveModel({ ...(opts.modelOptions ?? {}), timeout: opts.timeoutMs ?? 300_000 });
    const t0 = Date.now();
    const result = await aiSdk.generateText({
        model,
        system,
        prompt: user,
        abortSignal: AbortSignal.timeout(opts.timeoutMs ?? 300_000),
    });
    const duration_ms = Date.now() - t0;
    return {
        text: result.text,
        tokens: {
            input: result.usage?.inputTokens?.total ?? null,
            output: result.usage?.outputTokens?.total ?? null,
        },
        duration_ms,
        system,
        user,
        providerId: model.provider,
        modelId: model.modelId,
    };
}
/**
 * Filesystem fallback for realize-action.prompt.yaml when the installed
 * @specverse/assets package predates Phase 2's prompt addition.
 *
 * Searches a small fixed set of candidate paths relative to the engines
 * package's installed location and the worktree layout used in
 * specverse-self repo. Returns null when none are found; runPrompt then
 * surfaces the canonical "prompt file not found" error.
 */
export function loadRealizeActionPromptFromFallback() {
    const candidates = [];
    // 1. @specverse/assets package (covers the post-Phase-2 publish case).
    try {
        const pkg = require.resolve('@specverse/assets/package.json');
        candidates.push(join(dirname(pkg), 'prompts/core/standard/default/realize-action.prompt.yaml'), join(dirname(pkg), 'prompts/core/standard/v9/realize-action.prompt.yaml'));
    }
    catch {
        /* assets not installed */
    }
    // 2. Engine-bundled assets (engines ≤ 6.0.2-style layouts).
    try {
        const enginesPkg = require.resolve('@specverse/engines/package.json');
        candidates.push(join(dirname(enginesPkg), 'assets/prompts/core/standard/default/realize-action.prompt.yaml'), join(dirname(enginesPkg), 'assets/prompts/core/standard/v9/realize-action.prompt.yaml'));
    }
    catch {
        /* engines not installed at this path */
    }
    // 3. Worktree fallback — specverse-self lives next to specverse-engines.
    //    The path here is from `engines/dist/realize/per-action-llm-emit.js`
    //    (or `engines/src/realize/per-action-llm-emit.ts` in dev) up to the
    //    SpecVerse parent dir. Resolved at runtime so we tolerate either layout.
    try {
        const here = dirname(fileURLToPath(import.meta.url));
        const specverseSelfFallbacks = [
            resolve(here, '../../../../specverse-self/assets/prompts/core/standard/default/realize-action.prompt.yaml'),
            resolve(here, '../../../../specverse-self/assets/prompts/core/standard/v9/realize-action.prompt.yaml'),
            resolve(here, '../../../../../specverse-self/assets/prompts/core/standard/default/realize-action.prompt.yaml'),
            resolve(here, '../../../../../specverse-self/assets/prompts/core/standard/v9/realize-action.prompt.yaml'),
        ];
        candidates.push(...specverseSelfFallbacks);
    }
    catch {
        /* import.meta.url unavailable — skip */
    }
    for (const p of candidates) {
        if (existsSync(p)) {
            try {
                return yaml.load(readFileSync(p, 'utf-8'));
            }
            catch {
                /* try next */
            }
        }
    }
    return null;
}
/**
 * Convert an ActionSpec + per-step matcher results into the variable
 * map consumed by realize-action.prompt.yaml's `user.template`.
 *
 * Variables (matching the prompt template):
 *   - ownerKind, ownerName, actionName
 *   - actionSpec  — yaml block of the action's spec body
 *   - parameterList, returnType — for the signature line
 *   - perStepScaffolding — annotated [PRE-BAKED] / [WRITE] step list
 *   - availableClients, siblingActions, processContext — context
 *     strings the caller composes (default placeholder when omitted)
 */
function buildPromptValues(args) {
    const { action, perStepMatches, availableClients, siblingActions, processContext } = args;
    const actionSpecYaml = yamlDumpAction(action);
    const parameterList = buildParameterList(action);
    const returnType = buildReturnType(action);
    const perStepScaffolding = buildPerStepScaffolding(action.steps, perStepMatches);
    return {
        ownerKind: action.ownerKind,
        ownerName: action.ownerName,
        actionName: action.name,
        actionSpec: actionSpecYaml,
        parameterList,
        returnType,
        perStepScaffolding,
        availableClients: availableClients ?? '(none)',
        siblingActions: siblingActions ?? '(none listed)',
        processContext: processContext ?? '(no related processes)',
    };
}
/** Render the action's spec body as YAML for the SOURCE OF TRUTH section. */
function yamlDumpAction(action) {
    const body = {};
    if (action.description)
        body.description = action.description;
    if (action.parameters)
        body.parameters = action.parameters;
    if (action.returns)
        body.returns = action.returns;
    if (action.steps?.length)
        body.steps = action.steps;
    if (action.requires?.length)
        body.requires = action.requires;
    if (action.ensures?.length)
        body.ensures = action.ensures;
    if (action.publishes?.length)
        body.publishes = action.publishes;
    return yaml.dump({ [action.name]: body }, { lineWidth: 100, noRefs: true });
}
/** Build the param list for the signature line shown to the LLM. */
function buildParameterList(action) {
    if (!action.parameters || Object.keys(action.parameters).length === 0) {
        return '';
    }
    return Object.entries(action.parameters)
        .map(([name, spec]) => `${name}: ${specTypeToTs(spec)}`)
        .join(', ');
}
/** Build the return-type string for the signature line. */
function buildReturnType(action) {
    if (!action.returns)
        return 'void';
    return specTypeToTs(action.returns);
}
/**
 * Translate spec-type strings into approximate TS types. The realize
 * pipeline has a more rigorous translator; this is a "good enough for
 * the prompt" version. The LLM only uses it as documentation — the
 * realize template does the real signature emission.
 */
function specTypeToTs(specType) {
    // "UUID required" → "string"; "Decimal required" → "number"; etc.
    const tokens = specType.split(/\s+/);
    const head = tokens[0]?.toLowerCase() ?? '';
    switch (head) {
        case 'string':
        case 'uuid':
        case 'date':
        case 'datetime':
            return 'string';
        case 'integer':
        case 'int':
        case 'float':
        case 'number':
        case 'decimal':
            return 'number';
        case 'boolean':
        case 'bool':
            return 'boolean';
        case 'object':
        case 'json':
            return 'Record<string, unknown>';
        default:
            // Domain types like `OrderResult` come through verbatim.
            return tokens[0] ?? 'unknown';
    }
}
/** Render the per-step scaffolding block: annotated [PRE-BAKED] / [WRITE]. */
function buildPerStepScaffolding(steps, perStepMatches) {
    if (steps.length === 0)
        return '(no steps)';
    const lines = [];
    for (let i = 0; i < steps.length; i++) {
        const stepText = steps[i];
        const match = perStepMatches[i];
        if (match?.matched && match.call) {
            lines.push(`  ${i + 1}. [PRE-BAKED] "${stepText}"`);
            // Indent the snippet so it's clearly nested under the step.
            const snippetLines = match.call.split('\n').map((l) => `       ${l.trimStart()}`);
            lines.push(...snippetLines);
        }
        else {
            lines.push(`  ${i + 1}. [WRITE]      "${stepText}"`);
        }
    }
    return lines.join('\n');
}
// ─── Output extraction + parse check ─────────────────────────────────
/**
 * Pull the first ```typescript fenced block out of an LLM response.
 * Tolerates plain ``` (no language tag) when the body starts with
 * recognisably-TS content. Returns null if no block is found.
 */
export function extractTypescriptBlock(text) {
    const tsBlock = text.match(/```(?:typescript|ts)\s*\n([\s\S]*?)```/);
    if (tsBlock?.[1])
        return tsBlock[1].trim();
    // Generic ``` block — accept when the content looks like TS (has at
    // least one of: const, let, await, return, throw at start of line).
    const generic = text.match(/```\s*\n([\s\S]*?)```/);
    if (generic?.[1]) {
        const candidate = generic[1].trim();
        if (/^(const|let|await|return|throw|if|try)\b/m.test(candidate)) {
            return candidate;
        }
    }
    return null;
}
/**
 * esbuild parse check on a method body. We wrap the body in a synthetic
 * async function so esbuild can parse top-level await + return as
 * function-scoped statements.
 *
 * Returns null on success, a brief error message on failure. Skipped
 * silently (returns null) if esbuild isn't installed — matches the
 * existing per-step ai-behaviors-generator's tolerance.
 */
async function esbuildParseCheck(body) {
    try {
        const esbuild = await import('esbuild');
        const wrapped = `export async function __realizeAction(): Promise<unknown> {\n${body}\n}`;
        await esbuild.transform(wrapped, { loader: 'ts', format: 'esm', target: 'es2022' });
        return null;
    }
    catch (err) {
        const msg = err?.errors?.[0]?.text || err?.message || 'unknown parse error';
        return msg;
    }
}
/**
 * Build the user-prefix prepended on the surgical-retry attempt. Frames
 * the prior parse error as a fix-up directive without re-stating the
 * whole prompt.
 */
function buildSurgicalRetryPrefix(parseError, actionName) {
    return [
        `Your previous output for ${actionName} failed esbuild parse:`,
        '',
        parseError,
        '',
        'Re-emit the method body, fixing the syntax error. Same output format ' +
            '(```typescript fenced block, body only). Preserve all [PRE-BAKED] ' +
            'snippets verbatim.',
        '',
        '---',
        '',
    ].join('\n');
}
//# sourceMappingURL=per-action-llm-emit.js.map