/**
 * Library-runtime emitter — V2 Phase 3, Round 1.
 *
 * Handles `topology: 'library'` factories: providers that are consumed
 * as in-process imports, with no deployment instance entry.
 *
 * For each `import:` entry the consumer declares, this emitter produces:
 *   clients/<provider-name>.ts   — typed client module
 *
 * The typed client module:
 *   - Exports a `<ProviderName>Client` TypeScript interface matching
 *     the selected operations from `import.select`.
 *   - Exports a `<providerName>Client` constant that implements the
 *     interface by delegating to the resolved provider:
 *       * source = 'local'  → relative import to the provider's module.
 *       * source = 'npm'    → `import { X } from '<package-specifier>'`.
 *       * source = 'registry-*' → TODO comment + runtime throw (Round 1).
 *
 * The consuming controller (SalesReportController.ts, emitted by the
 * per-action-emitter's mechanical scaffold) imports this client module
 * and receives the client via constructor injection.
 *
 * --- Select grammar ---
 *
 * `import.select` entries follow two forms (V2 D2 resolution):
 *   - Dotted: `Charge.create`  → typed function for the specific operation.
 *   - Bare:   `AuthService`    → re-export of the entity's interface.
 *
 * Both forms share the same file; the emitter deduplicates by head entity.
 *
 * --- Round 1 limitations ---
 *
 * - No actual operation signature lookup (operations are typed as
 *   `(...args: unknown[]) => Promise<unknown>` unless the spec is provided
 *   and the target operation can be located). Exact signatures land in
 *   Round 2 when the resolver wires in full cross-spec type lookup.
 * - Registry sources: the resolver fetches real content (the registry resolver
 *   is wired). For `registry-component` the fetched `.specly` is persisted next
 *   to the client (`clients/<from>.imported.specly`) so the consume-half is
 *   non-lossy.
 *
 * --- Round 2 (model libraries) ---
 *
 * For `registry-component` imports that select MODELS (value objects /
 * entities — bare names matching a model in the fetched spec), the emitter
 * now produces REAL TypeScript interfaces (`export interface Money { ... }`)
 * derived from the imported model's attributes, instead of a throwing client.
 * See `imported-model-types.ts`. These are types, not runtime values, so no
 * client constant is emitted and `imports` stays empty — the consumer imports
 * the interfaces directly. Operation imports (dotted `Service.op` selects) and
 * registry-factory sources still emit the Round-1 throwing client (their
 * real-signature wiring is the remaining V2 "Round 2" for service topologies).
 */
import { extractImportedModelInterfaces } from './imported-model-types.js';
/**
 * Parse a select entry into head + optional tail.
 *
 * Dotted: `Charge.create`  → { head: 'Charge', tail: 'create' }
 * Bare:   `AuthService`    → { head: 'AuthService', tail: undefined }
 *
 * The head is the entity/service name; the tail is the operation name.
 * Dotted entries can have multiple tail segments (`A.B.C` → head A, tail 'B.C')
 * but in practice V2 only uses one level (entity.operation).
 */
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.
 *
 * Handles:
 *   'ProfitCalculator'         → ['profit', 'calculator']
 *   '@stripe/api'              → ['stripe', 'api']   (scope + package combined)
 *   '@specverse/gitnexus-adapter' → ['specverse', 'gitnexus', 'adapter']
 *   'gitnexus'                 → ['gitnexus']
 */
function toWords(fromName) {
    // For scoped packages (@scope/pkg), replace the / with a space and strip @.
    const normalised = fromName.replace(/^@/, '').replace(/\//g, '-');
    // Split on camelCase boundaries and non-alphanumeric runs.
    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 safe identifier prefix (lower-camelCase, no
 * special characters). Used for the client constant name.
 *
 * Examples:
 *   'ProfitCalculator'   → 'profitCalculator'
 *   '@stripe/api'        → 'stripeApi'
 *   'gitnexus'           → 'gitnexus'
 */
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.
 *
 * Examples:
 *   'ProfitCalculator'   → 'ProfitCalculator'
 *   '@stripe/api'        → 'StripeApi'
 *   'gitnexus'           → 'Gitnexus'
 */
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 filename (kebab-case).
 *
 * Examples:
 *   'ProfitCalculator'           → 'profit-calculator'
 *   '@stripe/api'                → 'stripe-api'
 *   '@specverse/gitnexus-adapter'→ 'specverse-gitnexus-adapter'
 *   'gitnexus'                   → 'gitnexus'
 */
function toFileName(fromName) {
    return toWords(fromName).join('-');
}
/**
 * Generate the TypeScript interface for the client type.
 * Uses a placeholder signature `(...args: unknown[]) => Promise<unknown>`
 * in Round 1; Round 2 will wire in real operation signatures.
 */
function generateInterface(interfaceName, selectEntries) {
    const lines = [];
    lines.push(`export type ${interfaceName}Client = {`);
    // Collect unique operations grouped by head entity for clarity.
    // Bare entries produce a `<entity>: unknown` member (shape TBD by Round 2).
    // Dotted entries produce a typed function.
    const seen = new Set();
    for (const entry of selectEntries) {
        if (seen.has(entry))
            continue;
        seen.add(entry);
        const { head, tail } = parseSelectEntry(entry);
        if (tail === undefined) {
            // Bare entity import — expose as an opaque type in Round 1.
            // Round 2 will look up the entity interface and substitute the real type.
            lines.push(`  /** Re-export of ${head} entity interface. Round 2: substitute real type. */`);
            lines.push(`  ${head}: unknown;`);
        }
        else {
            // Dotted operation import — typed function.
            // Round 2 will substitute real input/output types from the spec.
            lines.push(`  /** ${head}.${tail} — Round 2: substitute real input/output types. */`);
            lines.push(`  ${tail}(...args: unknown[]): Promise<unknown>;`);
        }
    }
    lines.push('};');
    return lines.join('\n');
}
/**
 * Generate the import statement block for the client implementation.
 * Returns the import lines and the identifiers made available.
 */
function generateImportBlock(ctx, selectEntries) {
    const { resolved } = ctx;
    const importLines = [];
    // Maps select entry → the identifier available in the module scope.
    const availableIds = new Map();
    if (resolved.source === 'local') {
        // Relative import to the provider's module in the same spec's output tree.
        //
        // The fallback path `../${toFileName(...)}` is a heuristic that
        // assumes the provider lives one level up from the V2 client. In
        // practice the realize pipeline doesn't currently emit per-component
        // aggregate modules (`components/<Provider>/<from>.ts`) — providers
        // are emitted directly to deterministic backend dirs (services/,
        // controllers/, etc.). V2 client files end up with broken module
        // references regardless of which depth we compute. The resolver's
        // explicit `localRelativePath` is the authoritative form when
        // available; the fallback is best-effort.
        //
        // We append `.js` extension when missing — NodeNext module
        // resolution (the realized backend's default) requires explicit
        // extensions on relative imports. Without this, the stub trial
        // surfaced TS2834.
        const rawPath = resolved.localRelativePath ?? `../${toFileName(resolved.resolvedName)}`;
        const relativePath = /\.(m?[jt]sx?|json)$/.test(rawPath) ? rawPath : `${rawPath}.js`;
        // Group dotted entries by head (entity) — one import per head.
        const heads = new Set();
        const bareEntries = [];
        for (const entry of selectEntries) {
            const { head, tail } = parseSelectEntry(entry);
            if (tail !== undefined) {
                heads.add(head);
            }
            else {
                bareEntries.push(head);
            }
        }
        const allImports = [...Array.from(heads), ...bareEntries];
        if (allImports.length > 0) {
            importLines.push(`import { ${allImports.join(', ')} } from '${relativePath}';`);
            for (const id of allImports) {
                availableIds.set(id, id);
            }
        }
        // Map dotted entries: Charge.create → availableIds.get('Charge.create') = 'Charge'
        for (const entry of selectEntries) {
            const { head, tail } = parseSelectEntry(entry);
            if (tail !== undefined) {
                availableIds.set(entry, head);
            }
        }
    }
    else if (resolved.source === 'npm') {
        // npm package import — use package specifier directly.
        const pkg = resolved.packageSpecifier ?? resolved.resolvedName;
        const heads = new Set();
        const bareEntries = [];
        for (const entry of selectEntries) {
            const { head, tail } = parseSelectEntry(entry);
            if (tail !== undefined) {
                heads.add(head);
            }
            else {
                bareEntries.push(head);
            }
        }
        const allImports = [...Array.from(heads), ...bareEntries];
        if (allImports.length > 0) {
            importLines.push(`import { ${allImports.join(', ')} } from '${pkg}';`);
            for (const id of allImports) {
                availableIds.set(id, id);
            }
        }
        for (const entry of selectEntries) {
            const { head, tail } = parseSelectEntry(entry);
            if (tail !== undefined) {
                availableIds.set(entry, head);
            }
        }
    }
    else {
        // registry-component or registry-factory — Round 1 TODO.
        // No real import; the constant body will throw.
    }
    return { importLines, availableIds };
}
/**
 * Generate the client constant — the exported object that implements the
 * typed interface by delegating to the resolved provider.
 */
function generateClientConstant(clientName, interfaceName, selectEntries, resolved, availableIds) {
    const lines = [];
    lines.push(`export const ${clientName}Client: ${interfaceName}Client = {`);
    if (resolved.source === 'registry-component' || resolved.source === 'registry-factory') {
        // Round 1 placeholder — registry resolution not yet implemented.
        lines.push('  // TODO (Round 2): wire in registry resolver for this provider.');
        lines.push(`  // provider: '${resolved.resolvedName}' (source: ${resolved.source})`);
        // Emit a throw for each member so the type is satisfied but blows up
        // clearly at runtime rather than silently returning undefined.
        const seen = new Set();
        for (const entry of selectEntries) {
            if (seen.has(entry))
                continue;
            seen.add(entry);
            const { tail } = parseSelectEntry(entry);
            if (tail === undefined) {
                lines.push(`  get ${entry}(): never { throw new Error('${entry} (${resolved.resolvedName}): imported from registry but not yet realized into code — Round 2'); },`);
            }
            else {
                lines.push(`  ${tail}(..._args: unknown[]): never { throw new Error('${tail} (${resolved.resolvedName}): imported from registry but not yet realized into code — Round 2'); },`);
            }
        }
    }
    else {
        // local or npm — delegate to the imported identifiers.
        const seen = new Set();
        for (const entry of selectEntries) {
            if (seen.has(entry))
                continue;
            seen.add(entry);
            const { head, tail } = parseSelectEntry(entry);
            const srcId = availableIds.get(entry) ?? availableIds.get(head) ?? head;
            if (tail === undefined) {
                // Bare entity — expose directly.
                lines.push(`  ${head}: ${srcId},`);
            }
            else {
                // Dotted operation — delegate as method.
                lines.push(`  ${tail}: ${srcId}.${tail},`);
            }
        }
    }
    lines.push('};');
    return lines.join('\n');
}
// ─── Library emitter ─────────────────────────────────────────────────────────
/**
 * Library-runtime emitter implementation.
 *
 * One call to `emit()` processes a single `import:` entry and produces
 * one client file under `clients/<provider-filename>.ts`.
 */
export class LibraryRuntimeEmitter {
    topology = 'library';
    async emit(ctx) {
        const { importEntry, resolved, factory, outputDir } = ctx;
        const selectEntries = importEntry.select ?? [];
        // TODO #58 short-term fix: skip emission for local-resolved providers
        // whose path can't be wired. When the resolver reports
        // `source: 'local'` but doesn't supply a `localRelativePath`, the
        // realize pipeline doesn't know where the provider's aggregate
        // module lives — currently it doesn't emit one. The library
        // emitter's heuristic fallback (`../<from-name>`) points at a path
        // that doesn't exist, producing `Cannot find module` errors at
        // compile time. Until per-component aggregate modules ship (or the
        // resolver starts threading explicit localRelativePath), don't
        // emit the orphan client. The downstream `availableClients` slot
        // is also empty for this import, so the LLM falls back to its
        // own imports (PrismaClient, jsonwebtoken, etc.) — same outcome
        // as if V2 had never tried.
        if (resolved.source === 'local' && !resolved.localRelativePath) {
            return {
                files: [],
                imports: [],
                notes: [
                    `V2 import skipped — orphan local resolution for '${importEntry.from}'. ` +
                        `Provider component has no aggregate module emitted yet (TODO #58).`,
                ],
            };
        }
        // Derive naming from the import's `from:` field, not the factory name.
        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`;
        const absoluteFilePath = `${outputDir}/${relFilePath}`;
        // ─── Round 2: registry component library exporting MODELS ───────────────
        // When the import resolves to a registry component and the resolver
        // fetched the spec content, emit the SELECTED models as real TypeScript
        // interfaces (usable value-object types) — not a client that throws. This
        // is the model-library reuse path. Operation (dotted) or unknown selects
        // fall through to `remaining` and keep the Round-1 throwing-client shape so
        // mixed imports still type-check. Pure-model imports emit no runtime client
        // (types aren't runtime values), so `imports` stays empty — the consumer
        // imports the interfaces directly and the v2Clients aggregator is untouched.
        if (resolved.source === 'registry-component' && resolved.specContent) {
            const { interfaces, emitted, remaining } = extractImportedModelInterfaces(resolved.specContent, selectEntries);
            if (emitted.length > 0) {
                const modelHeader = [
                    '// FACTORY-EMITTED — DO NOT EDIT (R8)',
                    '// Generated by library-runtime emitter — V2 Phase 3, Round 2',
                    `// Imported model types from registry library: ${resolved.resolvedName}@${resolved.resolvedVersion ?? 'latest'}`,
                    `// Source spec persisted at ./${fileName}.imported.specly`,
                    '',
                ];
                const parts = [...modelHeader];
                parts.push(`// ${emitted.length} model type(s) emitted: ${emitted.join(', ')}`, '', interfaces.join('\n\n'), '');
                // Mixed import: remaining (operation / unknown) selects keep the
                // Round-1 throwing-client shape so the file still type-checks.
                let consumerImport;
                if (remaining.length > 0) {
                    parts.push(generateInterface(interfaceName, remaining), '', generateClientConstant(clientName, interfaceName, remaining, resolved, new Map()), '');
                    consumerImport = { from: `./clients/${fileName}`, specifier: `${clientName}Client` };
                }
                const files = [
                    { path: relFilePath, content: parts.join('\n') },
                    // Persist the fetched spec next to the types module (non-lossy).
                    { path: `clients/${fileName}.imported.specly`, content: resolved.specContent },
                ];
                const notes = [
                    `Registry library '${resolved.resolvedName}' → emitted ${emitted.length} model type(s) [${emitted.join(', ')}] as TypeScript interfaces (Round 2).`,
                ];
                if (remaining.length > 0) {
                    notes.push(`Non-model selects [${remaining.join(', ')}] from '${importEntry.from}' still emit throwing client members (operation imports — future).`);
                }
                return {
                    files,
                    imports: consumerImport ? [consumerImport] : [],
                    notes,
                };
            }
            // No selected names matched a model — fall through to existing behaviour.
        }
        // Build file header.
        const headerLines = [
            '// FACTORY-EMITTED — DO NOT EDIT (R8)',
            `// Generated by library-runtime emitter — V2 Phase 3`,
            `// Provider: ${importEntry.from} (${resolved.source}) via factory: ${factory.name}`,
        ];
        if (resolved.source === 'local') {
            headerLines.push(`// Local component: ${resolved.resolvedName}`);
        }
        else if (resolved.source === 'npm') {
            const pkg = resolved.packageSpecifier ?? resolved.resolvedName;
            headerLines.push(`// npm package: ${pkg}`);
        }
        else if (resolved.source === 'registry-component' && resolved.specContent) {
            headerLines.push(`// Registry library: ${resolved.resolvedName}@${resolved.resolvedVersion ?? 'latest'}`);
            headerLines.push(`// Fetched spec persisted at ./${fileName}.imported.specly — realize it to wire real code (Round 2).`);
        }
        else {
            headerLines.push(`// Registry source: ${resolved.resolvedName} (Round 2 TODO)`);
        }
        headerLines.push('');
        // Build import block.
        const { importLines, availableIds } = generateImportBlock(ctx, selectEntries);
        // Build interface + constant.
        const interfaceBlock = generateInterface(interfaceName, selectEntries);
        const constantBlock = generateClientConstant(clientName, interfaceName, selectEntries, resolved, availableIds);
        // Assemble file.
        const parts = [...headerLines];
        if (importLines.length > 0) {
            parts.push(...importLines, '');
        }
        parts.push(interfaceBlock, '', constantBlock, '');
        const content = parts.join('\n');
        const files = [{ path: relFilePath, content }];
        // Registry library: persist the resolver-fetched `.specly` next to the
        // client so the consume-half is non-lossy and Round 2 can realize it into
        // runnable modules. (Round 1: the client still throws at runtime — the
        // imported library isn't realized into importable code yet.)
        let persistedSpecPath;
        if (resolved.source === 'registry-component' && resolved.specContent) {
            persistedSpecPath = `clients/${fileName}.imported.specly`;
            files.push({ path: persistedSpecPath, content: resolved.specContent });
        }
        // Build the import entry for the consumer's controller file.
        const consumerImport = {
            from: `./clients/${fileName}`,
            specifier: `${clientName}Client`,
        };
        // Warnings for Round 1 limitations.
        const notes = [];
        if (selectEntries.length === 0) {
            notes.push(`Warning: import entry from '${importEntry.from}' has empty select: — no client members emitted.`);
        }
        if (resolved.source === 'registry-component' && persistedSpecPath) {
            notes.push(`Registry library '${resolved.resolvedName}' fetched + persisted at ${persistedSpecPath}; ` +
                `client throws until the imported spec is realized (Round 2).`);
        }
        else if (resolved.source === 'registry-component' ||
            resolved.source === 'registry-factory') {
            notes.push(`TODO (Round 2): registry provider '${resolved.resolvedName}' — client constant emits runtime throws until the imported provider is realized.`);
        }
        return {
            files,
            imports: [consumerImport],
            notes: notes.length > 0 ? notes : undefined,
        };
    }
}
//# sourceMappingURL=library.js.map