/**
 * Executable-runtime emitter — V2 Phase 3, Round 2.
 *
 * Handles `topology: 'executable'` factories: providers reached via
 * `spawnSync` / `execSync` — local CLI binaries such as gitnexus, ffmpeg,
 * or custom binaries.
 *
 * For each `import:` entry the consumer declares, this emitter produces:
 *   clients/<kebab-provider>.ts   — spawnSync wrapper with typed interface
 *
 * The generated client module:
 *   - Emits an `assertBinaryAvailable()` function called at module load
 *     that runs `spawnSync('which', [binary])` (or `spawnSync(binary, ['--version'])`
 *     as fallback) to fail loud at boot if the binary isn't on PATH.
 *   - Exports a `<ProviderName>Client` TypeScript interface matching the
 *     selected operations from `import.select`.
 *   - Exports a `<providerName>Client` constant that implements the interface
 *     by dispatching to `spawnSync`:
 *       * Dotted op `Subcommand.action`: runs `spawnSync(binary, ['subcommand', 'action', ...args])`.
 *       * Bare entity: runs `spawnSync(binary, [entity.toLowerCase(), ...args])`.
 *     stdout is parsed as JSON; non-zero exit throws a clear Error.
 *
 * --- Binary name resolution ---
 *
 * The binary name is read from:
 *   1. `factory.v2metadata.config?.binary` (explicit override)
 *   2. `factory.name` lowercased (e.g. 'Gitnexus' → 'gitnexus')
 *
 * --- Select grammar ---
 *
 * Follows V2 D2 resolution (same as library.ts):
 *   - Dotted: `Subcommand.action` → dispatches `spawnSync(binary, ['subcommand', 'action', ...])`.
 *   - Bare:   `EntityName`        → dispatches `spawnSync(binary, [entityname, ...])`.
 *
 * --- No server file ---
 *
 * Subprocess providers don't have a server-bind file.
 *
 * --- Deployment instance hint ---
 *
 * Emitter notes include a hint to add the provider to
 * `deployments.<env>.instances.infrastructure.<provider>` with
 * `config: { binary: <name> }`.
 */
import { extractImportedOperationSignatures } from './imported-operation-signatures.js';
// ─── Naming helpers (mirrors library.ts) ─────────────────────────────────────
/**
 * Parse a select entry into head + optional tail.
 * Dotted: `Subcommand.action` → { head: 'Subcommand', tail: 'action' }
 * Bare:   `EntityName`        → { head: 'EntityName', tail: undefined }
 */
function parseSelectEntry(entry) {
    const dotIndex = entry.indexOf('.');
    if (dotIndex === -1) {
        return { head: entry, tail: undefined };
    }
    return {
        head: entry.slice(0, dotIndex),
        tail: entry.slice(dotIndex + 1),
    };
}
/**
 * Normalise a `from:` name into an array of words for identifier generation.
 * Same implementation as library.ts — handles camelCase, scoped packages, hyphens.
 */
function toWords(fromName) {
    const normalised = fromName.replace(/^@/, '').replace(/\//g, '-');
    return normalised
        .replace(/([a-z])([A-Z])/g, '$1 $2')
        .replace(/[^a-zA-Z0-9]+/g, ' ')
        .trim()
        .split(' ')
        .filter(Boolean)
        .map((w) => w.toLowerCase());
}
/**
 * Convert a from-name to a lower-camelCase client constant name.
 * 'Gitnexus' → 'gitnexus', 'GitNexus' → 'gitNexus', '@ffmpeg/core' → 'ffmpegCore'
 */
function toClientName(fromName) {
    const words = toWords(fromName);
    if (words.length === 0)
        return 'unknownClient';
    return (words[0] +
        words
            .slice(1)
            .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
            .join(''));
}
/**
 * Convert a from-name to a PascalCase interface name suffix.
 * 'Gitnexus' → 'Gitnexus', 'GitNexus' → 'Gitnexus', '@ffmpeg/core' → 'FfmpegCore'
 */
function toInterfaceName(fromName) {
    const words = toWords(fromName);
    return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('');
}
/**
 * Convert a from-name to a filesystem-safe kebab-case filename.
 * 'Gitnexus' → 'gitnexus', 'GitNexus' → 'git-nexus', '@ffmpeg/core' → 'ffmpeg-core'
 */
function toFileName(fromName) {
    return toWords(fromName).join('-');
}
// ─── Binary name resolution ───────────────────────────────────────────────────
/**
 * Resolve the binary name for spawnSync.
 *
 * Priority:
 *   1. `factory.v2metadata.config?.binary` (explicit override)
 *   2. `factory.name` lowercased (e.g. 'Gitnexus' → 'gitnexus',
 *      'SubprocessCLIAdapter' → 'subprocesscliadapter' — callers should
 *      prefer explicit `config.binary` for multi-word names)
 */
function resolveBinaryName(factory) {
    const configBinary = factory.v2metadata
        ?.config?.binary;
    if (configBinary && typeof configBinary === 'string' && configBinary.length > 0) {
        return configBinary;
    }
    return factory.name.toLowerCase();
}
// ─── Code-generation helpers ──────────────────────────────────────────────────
/**
 * Generate the `assertBinaryAvailable()` function that runs at module load.
 *
 * Uses `spawnSync('which', [binary])` as primary check (POSIX); on non-zero
 * exit falls back to `spawnSync(binary, ['--version'])`. If both fail, throws
 * a descriptive error pointing at the env-var escape hatch.
 *
 * The env-var name is SPECVERSE_<BINARY_UPPER>_PATH (e.g. SPECVERSE_PROFIT_CALC_PATH).
 */
function generateBinaryAvailabilityCheck(binary) {
    const envVar = `SPECVERSE_${binary.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_PATH`;
    const lines = [];
    lines.push(`import { spawnSync } from 'child_process';`);
    lines.push('');
    lines.push(`const BINARY = '${binary}';`);
    lines.push(`const ENV_OVERRIDE = process.env['${envVar}'];`);
    lines.push(`const EFFECTIVE_BINARY = ENV_OVERRIDE ?? BINARY;`);
    lines.push('');
    lines.push(`/** Assert the required binary is available on PATH before any calls are made. */`);
    lines.push(`function assertBinaryAvailable(): void {`);
    lines.push(`  const check = spawnSync('which', [EFFECTIVE_BINARY], { encoding: 'utf-8' });`);
    lines.push(`  if (check.status !== 0) {`);
    lines.push(`    // Fallback: try --version directly (useful when 'which' itself is absent)`);
    lines.push(`    const direct = spawnSync(EFFECTIVE_BINARY, ['--version'], { encoding: 'utf-8' });`);
    lines.push(`    if (direct.status !== 0 && direct.error) {`);
    lines.push(`      throw new Error(\`Required binary \${EFFECTIVE_BINARY} not found on PATH. ` +
        `Install it or update ${envVar} env var.\`);`);
    lines.push(`    }`);
    lines.push(`  }`);
    lines.push(`}`);
    lines.push('');
    lines.push(`// Run at module load — catches missing-binary errors at app boot.`);
    lines.push(`assertBinaryAvailable();`);
    return lines.join('\n');
}
/**
 * Generate the TypeScript interface for the executable client type.
 *
 * Dotted entries (`Subcommand.action`) → typed `(...args: unknown[]) => unknown` method.
 * Bare entries (`EntityName`) → typed `(...args: unknown[]) => unknown` method (passthrough).
 *
 * Unlike the library emitter, subprocess calls are synchronous (`spawnSync`), so
 * the return type is `unknown` (not `Promise<unknown>`). Callers may wrap in a
 * Promise if they need async composition; the underlying I/O is blocking.
 */
function generateInterface(interfaceName, selectEntries, opMap) {
    const lines = [];
    lines.push(`export type ${interfaceName}Client = {`);
    const seen = new Set();
    for (const entry of selectEntries) {
        if (seen.has(entry))
            continue;
        seen.add(entry);
        const { head, tail } = parseSelectEntry(entry);
        const op = opMap.get(entry);
        if (op) {
            // #81: real signature. Subprocess calls are SYNCHRONOUS, so the return
            // type is the bare type (not Promise).
            lines.push(`  /** ${entry} — spawnSync(binary, ['${head.toLowerCase()}', '${op.tail}', JSON]) (real signature). */`);
            lines.push(`  ${op.tail}(${op.paramType}): ${op.returnType};`);
        }
        else if (tail === undefined) {
            // Bare entity — passthrough all args as JSON-encoded CLI arguments.
            lines.push(`  /** Invokes: spawnSync(binary, ['${head.toLowerCase()}', ...JSON-encoded args]) */`);
            lines.push(`  ${head}(...args: unknown[]): unknown;`);
        }
        else {
            // Dotted operation — subcommand + action dispatch.
            lines.push(`  /** Invokes: spawnSync(binary, ['${head.toLowerCase()}', '${tail}', ...JSON-encoded args]) */`);
            lines.push(`  ${tail}(...args: unknown[]): unknown;`);
        }
    }
    lines.push('};');
    return lines.join('\n');
}
/**
 * Generate the client constant — spawnSync-backed implementation.
 *
 * Each method:
 *   1. JSON-encodes each argument via JSON.stringify.
 *   2. Calls spawnSync with [subcommand, action, ...encodedArgs] (dotted) or
 *      [entityname, ...encodedArgs] (bare).
 *   3. On non-zero exit, throws Error('<binary> <subcommand>: <stderr>').
 *   4. Parses stdout as JSON and returns the result.
 */
function generateClientConstant(clientName, interfaceName, selectEntries, opMap) {
    const lines = [];
    lines.push(`export const ${clientName}Client: ${interfaceName}Client = {`);
    const seen = new Set();
    for (const entry of selectEntries) {
        if (seen.has(entry))
            continue;
        seen.add(entry);
        const { head, tail } = parseSelectEntry(entry);
        const op = opMap.get(entry);
        if (op) {
            // #81: real signature + spawnSync transport (synchronous). The typed
            // `input` object is passed as a single JSON-encoded argument.
            const subcommand = head.toLowerCase();
            const arg = op.paramType ? 'JSON.stringify(input)' : "'{}'";
            lines.push(`  ${op.tail}(${op.paramType}): ${op.returnType} {`);
            lines.push(`    const result = spawnSync(EFFECTIVE_BINARY, ['${subcommand}', '${op.tail}', ${arg}], { encoding: 'utf-8' });`);
            lines.push(`    if (result.status !== 0) {`);
            lines.push(`      throw new Error(\`\${EFFECTIVE_BINARY} ${subcommand}: \${result.stderr}\`);`);
            lines.push(`    }`);
            if (op.returnType !== 'void') {
                lines.push(`    return JSON.parse(result.stdout) as ${op.returnType};`);
            }
            lines.push(`  },`);
        }
        else if (tail === undefined) {
            // Bare entity passthrough — proxy all args as JSON-encoded CLI arguments.
            const subcommand = head.toLowerCase();
            lines.push(`  ${head}(...args: unknown[]): unknown {`);
            lines.push(`    const encodedArgs = args.map((a) => JSON.stringify(a));`);
            lines.push(`    const result = spawnSync(EFFECTIVE_BINARY, ['${subcommand}', ...encodedArgs], { encoding: 'utf-8' });`);
            lines.push(`    if (result.status !== 0) {`);
            lines.push(`      throw new Error(\`\${EFFECTIVE_BINARY} ${subcommand}: \${result.stderr}\`);`);
            lines.push(`    }`);
            lines.push(`    return JSON.parse(result.stdout);`);
            lines.push(`  },`);
        }
        else {
            // Dotted operation — subcommand (head) + action (tail) dispatch.
            const subcommand = head.toLowerCase();
            lines.push(`  ${tail}(...args: unknown[]): unknown {`);
            lines.push(`    const encodedArgs = args.map((a) => JSON.stringify(a));`);
            lines.push(`    const result = spawnSync(EFFECTIVE_BINARY, ['${subcommand}', '${tail}', ...encodedArgs], { encoding: 'utf-8' });`);
            lines.push(`    if (result.status !== 0) {`);
            lines.push(`      throw new Error(\`\${EFFECTIVE_BINARY} ${subcommand}: \${result.stderr}\`);`);
            lines.push(`    }`);
            lines.push(`    return JSON.parse(result.stdout);`);
            lines.push(`  },`);
        }
    }
    lines.push('};');
    return lines.join('\n');
}
// ─── Executable emitter ───────────────────────────────────────────────────────
/**
 * Executable-runtime emitter implementation.
 *
 * One call to `emit()` processes a single `import:` entry and produces
 * one client file under `clients/<provider-filename>.ts`.
 * No server-bind file is emitted (subprocess providers have no server).
 */
export class ExecutableRuntimeEmitter {
    topology = 'executable';
    async emit(ctx) {
        const { importEntry, resolved, factory } = ctx;
        const selectEntries = importEntry.select ?? [];
        // Resolve binary name: config?.binary → factory.name.toLowerCase()
        const binary = resolveBinaryName(factory);
        // Derive naming from the import's `from:` field.
        const fileName = toFileName(importEntry.from);
        const clientName = toClientName(importEntry.from);
        const interfaceName = toInterfaceName(importEntry.from);
        // File path relative to outputDir.
        const relFilePath = `clients/${fileName}.ts`;
        // Build file header.
        const headerLines = [
            '// FACTORY-EMITTED — DO NOT EDIT (R8)',
            `// Generated by executable-runtime emitter — V2 Phase 3`,
            `// Provider: ${importEntry.from} (${resolved.source}) via factory: ${factory.name}`,
            `// Binary: ${binary}`,
            '',
        ];
        // Build availability check block (includes `import { spawnSync }` and constant declaration).
        const availabilityBlock = generateBinaryAvailabilityCheck(binary);
        // #81: derive real operation signatures from the provider spec when fetched.
        const dotted = selectEntries.filter((e) => e.includes('.'));
        const opResult = resolved.specContent
            ? extractImportedOperationSignatures(resolved.specContent, dotted)
            : null;
        const opMap = new Map((opResult?.operations ?? []).map((o) => [o.entry, o]));
        const modelBlock = opResult && opResult.modelInterfaces.length > 0
            ? [opResult.modelInterfaces.join('\n\n'), '']
            : [];
        // Build interface.
        const interfaceBlock = generateInterface(interfaceName, selectEntries, opMap);
        // Build client constant.
        const constantBlock = generateClientConstant(clientName, interfaceName, selectEntries, opMap);
        // Assemble file.
        const parts = [
            ...headerLines,
            availabilityBlock,
            '',
            ...modelBlock,
            interfaceBlock,
            '',
            constantBlock,
            '',
        ];
        const content = parts.join('\n');
        // Build the import entry for the consumer's controller file.
        const consumerImport = {
            from: `./clients/${fileName}`,
            specifier: `${clientName}Client`,
        };
        // Notes: warnings + deployment instance hint.
        const notes = [];
        if (selectEntries.length === 0) {
            notes.push(`Warning: import entry from '${importEntry.from}' has empty select: — no client members emitted.`);
        }
        // Deployment instance hint — provider lands in infrastructure category.
        notes.push(`Deployment hint: add '${importEntry.from}' to deployments.<env>.instances.infrastructure` +
            ` with config: { binary: '${binary}' }. No process lifecycle; just a binary path requirement.`);
        const deploymentSuggestion = {
            provider: importEntry.from,
            consumerComponent: ctx.consumerComponent?.name,
            category: 'infrastructure',
            key: clientName,
            config: {
                binary,
            },
            factory: factory.name,
            topology: 'executable',
        };
        return {
            files: [{ path: relFilePath, content }],
            imports: [consumerImport],
            notes,
            deploymentSuggestion,
        };
    }
}
//# sourceMappingURL=executable.js.map