/**
 * Messaging-runtime emitter — V2 Phase 3, Round 2.
 *
 * Handles `topology: 'messaging'` factories: async pub/sub providers reached
 * through external brokers (RabbitMQ, Kafka, NATS, AWS SQS).
 *
 * Conceptual difference from sync topologies (library / service / managed /
 * executable):
 *   - Sync topologies have a request/response contract: call op, get result.
 *   - Messaging has a fire-and-forget contract: publish event, receive response
 *     asynchronously or via subscription.
 *
 * For each `import:` entry the consumer declares, this emitter produces:
 *   clients/<kebab-provider>.ts   — broker publish wrapper
 *
 * The publish wrapper:
 *   - Reads `factory.v2metadata.protocol` to pick the SDK and init pattern:
 *       amqp   → amqplib-based publisher with assertExchange + publish
 *       kafka  → kafkajs producer
 *       nats   → TODO stub (clear comment + right SDK shape)
 *       sqs    → TODO stub (clear comment + right SDK shape)
 *       other  → TODO stub (clear comment + right SDK shape)
 *   - For each dotted op `Order.created`: emits a method that publishes the
 *     message to the appropriate routing-key/topic/queue (`order.created`).
 *   - Reads `factory.v2metadata.config?.exchange` / `config?.topic` /
 *     `config?.queue` for routing config.
 *   - Methods return `Promise<void>` (fire-and-forget).
 *
 * Connection lifecycle:
 *   - Emits `init()` (call at app startup) and `shutdown()` (call at graceful
 *     shutdown) on the client.
 *   - Round 2 does NOT auto-wire these into the consumer's startup — that is a
 *     Round 3 concern.
 *
 * No server-bind file:
 *   - Messaging brokers are external services, not something the consumer hosts.
 *   - Consumers PUBLISH to an existing broker; subscribe-side handlers come in
 *     Round 3 via `subscribes_to:` blocks.
 *
 * Deployment instance hint:
 *   - `result.notes` describes where the provider lands in the deployment spec:
 *     `deployments.<env>.instances.communications.<provider>`.
 *
 * --- Select grammar ---
 *
 * `import.select` entries follow the same dotted grammar as library.ts:
 *   - Dotted: `Order.created`  → publishes to routing-key `order.created`
 *   - Bare:   `Events`         → stub method; routing-key inferred from name
 *
 * --- Round 2 limitations ---
 *
 * - Subscribe-side handlers (receiving events from the broker) are deferred to
 *   Round 3; all `select:` entries are treated as publish-side by default.
 * - NATS and SQS emit TODO stubs rather than real SDK code.
 * - Config routing values (exchange, topic, queue) are read from
 *   `factory.v2metadata` when present; absent values use reasonable defaults.
 */
// ─── Helpers shared with library.ts (inlined to keep emitters self-contained) ──
/** Parse a select entry into head + optional tail (mirrors library.ts). */
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 lowercase words. */
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 prefix. */
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 PascalCase for type/interface names. */
function toInterfaceName(fromName) {
    const words = toWords(fromName);
    return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('');
}
/** Convert a from-name to kebab-case for filenames. */
function toFileName(fromName) {
    return toWords(fromName).join('-');
}
// ─── Routing-key convention ───────────────────────────────────────────────────
/**
 * Derive a messaging routing-key / topic / queue name from an operation entry.
 *
 * Convention:
 *   - Dotted `Order.created`  → `order.created`   (head.tail both lowercased)
 *   - Bare   `Events`         → `events`           (just lowercased)
 *
 * This mirrors the natural AMQP / Kafka topic naming convention where dotted
 * path segments separate domain entity from event name.
 */
function toRoutingKey(entry) {
    const { head, tail } = parseSelectEntry(entry);
    if (tail === undefined) {
        return head.toLowerCase();
    }
    return `${head.toLowerCase()}.${tail.toLowerCase()}`;
}
/** Derive a safe TypeScript method name from a select entry. */
function toMethodName(entry) {
    const { head, tail } = parseSelectEntry(entry);
    if (tail === undefined) {
        // Bare entry: use head as camelCase
        const w = toWords(head);
        if (w.length === 0)
            return 'publish';
        return w[0] + w.slice(1).map((x) => x.charAt(0).toUpperCase() + x.slice(1)).join('');
    }
    // Dotted: combine head + tail → e.g. Order.created → orderCreated
    const headWords = toWords(head);
    const tailWords = toWords(tail);
    const allWords = [...headWords, ...tailWords];
    if (allWords.length === 0)
        return 'publish';
    return (allWords[0] +
        allWords
            .slice(1)
            .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
            .join(''));
}
function extractRoutingConfig(factory) {
    // factory.v2metadata?.config is free-form — cast via unknown
    const cfg = factory.v2metadata?.['config'];
    return {
        exchange: typeof cfg?.['exchange'] === 'string' ? cfg['exchange'] : 'events',
        topic: typeof cfg?.['topic'] === 'string' ? cfg['topic'] : null,
        queue: typeof cfg?.['queue'] === 'string' ? cfg['queue'] : null,
    };
}
// ─── amqp (RabbitMQ) ─────────────────────────────────────────────────────────
function generateAmqpClient(interfaceName, clientName, selectEntries, routing) {
    const exchange = routing.exchange;
    const methods = buildPublishMethods(selectEntries, (entry, routingKey) => {
        return [
            `  async ${toMethodName(entry)}(payload: unknown): Promise<void> {`,
            `    if (!this.channel) throw new Error('${clientName}: not initialised — call init() first');`,
            `    await this.channel.assertExchange('${exchange}', 'topic', { durable: true });`,
            `    this.channel.publish(`,
            `      '${exchange}',`,
            `      '${routingKey}',`,
            `      Buffer.from(JSON.stringify(payload)),`,
            `      { persistent: true },`,
            `    );`,
            `  }`,
        ].join('\n');
    });
    return [
        `import amqplib, { type Channel, type Connection } from 'amqplib';`,
        ``,
        `// FACTORY-EMITTED — DO NOT EDIT (R8)`,
        `// Generated by messaging-runtime emitter — V2 Phase 3, Round 2`,
        `// Protocol: amqp (RabbitMQ via amqplib)`,
        ``,
        `export type ${interfaceName}Client = {`,
        `  init(): Promise<void>;`,
        `  shutdown(): Promise<void>;`,
        ...selectEntries
            .filter((v, i, a) => a.indexOf(v) === i)
            .map((entry) => {
            const methodName = toMethodName(entry);
            return `  ${methodName}(payload: unknown): Promise<void>;`;
        }),
        `};`,
        ``,
        `class ${interfaceName}Publisher implements ${interfaceName}Client {`,
        `  private connection: Connection | null = null;`,
        `  private channel: Channel | null = null;`,
        ``,
        `  constructor(private readonly brokerUrl: string) {}`,
        ``,
        `  async init(): Promise<void> {`,
        `    this.connection = await amqplib.connect(this.brokerUrl);`,
        `    this.channel = await this.connection.createConfirmChannel();`,
        `  }`,
        ``,
        `  async shutdown(): Promise<void> {`,
        `    await this.channel?.close();`,
        `    await this.connection?.close();`,
        `    this.channel = null;`,
        `    this.connection = null;`,
        `  }`,
        ``,
        ...methods,
        `}`,
        ``,
        `const brokerUrl = process.env['AMQP_URL'] ?? 'amqp://localhost';`,
        `export const ${clientName}Client: ${interfaceName}Client = new ${interfaceName}Publisher(brokerUrl);`,
        ``,
    ].join('\n');
}
// ─── kafka ────────────────────────────────────────────────────────────────────
function generateKafkaClient(interfaceName, clientName, selectEntries, routing) {
    const methods = buildPublishMethods(selectEntries, (entry, routingKey) => {
        const topic = routing.topic ?? routingKey;
        return [
            `  async ${toMethodName(entry)}(payload: unknown): Promise<void> {`,
            `    await this.producer.send({`,
            `      topic: '${topic}',`,
            `      messages: [{ value: JSON.stringify(payload) }],`,
            `    });`,
            `  }`,
        ].join('\n');
    });
    return [
        `import { Kafka, type Producer } from 'kafkajs';`,
        ``,
        `// FACTORY-EMITTED — DO NOT EDIT (R8)`,
        `// Generated by messaging-runtime emitter — V2 Phase 3, Round 2`,
        `// Protocol: kafka (Apache Kafka via kafkajs)`,
        ``,
        `export type ${interfaceName}Client = {`,
        `  init(): Promise<void>;`,
        `  shutdown(): Promise<void>;`,
        ...selectEntries
            .filter((v, i, a) => a.indexOf(v) === i)
            .map((entry) => {
            const methodName = toMethodName(entry);
            return `  ${methodName}(payload: unknown): Promise<void>;`;
        }),
        `};`,
        ``,
        `class ${interfaceName}Producer implements ${interfaceName}Client {`,
        `  private producer: Producer;`,
        ``,
        `  constructor(brokers: string[]) {`,
        `    const kafka = new Kafka({`,
        `      clientId: '${clientName}',`,
        `      brokers,`,
        `    });`,
        `    this.producer = kafka.producer();`,
        `  }`,
        ``,
        `  async init(): Promise<void> {`,
        `    await this.producer.connect();`,
        `  }`,
        ``,
        `  async shutdown(): Promise<void> {`,
        `    await this.producer.disconnect();`,
        `  }`,
        ``,
        ...methods,
        `}`,
        ``,
        `const brokers = (process.env['KAFKA_BROKERS'] ?? 'localhost:9092').split(',');`,
        `export const ${clientName}Client: ${interfaceName}Client = new ${interfaceName}Producer(brokers);`,
        ``,
    ].join('\n');
}
// ─── TODO stub (nats / sqs / unknown) ────────────────────────────────────────
function generateTodoStubClient(interfaceName, clientName, selectEntries, protocol) {
    const sdkHint = protocol === 'nats' ? 'nats.ws / nats.js' : protocol === 'sqs' ? '@aws-sdk/client-sqs' : `SDK for protocol '${protocol}'`;
    return [
        `// FACTORY-EMITTED — DO NOT EDIT (R8)`,
        `// Generated by messaging-runtime emitter — V2 Phase 3, Round 2`,
        `// Protocol: ${protocol} — TODO: install ${sdkHint} and implement`,
        ``,
        `export type ${interfaceName}Client = {`,
        `  init(): Promise<void>;`,
        `  shutdown(): Promise<void>;`,
        ...selectEntries
            .filter((v, i, a) => a.indexOf(v) === i)
            .map((entry) => {
            const methodName = toMethodName(entry);
            return `  ${methodName}(payload: unknown): Promise<void>;`;
        }),
        `};`,
        ``,
        `// TODO (Round 3): implement ${protocol.toUpperCase()} publisher using ${sdkHint}.`,
        `// Methods below throw at runtime to surface missing wiring clearly.`,
        `export const ${clientName}Client: ${interfaceName}Client = {`,
        `  async init(): Promise<void> {`,
        `    throw new Error('${clientName}: ${protocol} publisher not yet implemented — Round 3 TODO');`,
        `  },`,
        `  async shutdown(): Promise<void> {`,
        `    // no-op until implemented`,
        `  },`,
        ...selectEntries
            .filter((v, i, a) => a.indexOf(v) === i)
            .map((entry) => {
            const methodName = toMethodName(entry);
            return [
                `  async ${methodName}(_payload: unknown): Promise<void> {`,
                `    throw new Error('${methodName} (${clientName}): ${protocol} publisher not yet implemented — Round 3 TODO');`,
                `  },`,
            ].join('\n');
        }),
        `};`,
        ``,
    ].join('\n');
}
// ─── Shared helper: build publish method bodies ───────────────────────────────
/**
 * Build deduplicated publish method bodies for a set of select entries.
 * Returns an array of method strings (no trailing blank lines between them —
 * caller joins the sections).
 */
function buildPublishMethods(selectEntries, methodBody) {
    const seen = new Set();
    const parts = [];
    for (const entry of selectEntries) {
        if (seen.has(entry))
            continue;
        seen.add(entry);
        const routingKey = toRoutingKey(entry);
        parts.push(methodBody(entry, routingKey));
        parts.push('');
    }
    return parts;
}
// ─── Messaging emitter ────────────────────────────────────────────────────────
/**
 * Messaging-runtime emitter implementation.
 *
 * One call to `emit()` processes a single `import:` entry and produces
 * one client file under `clients/<provider-filename>.ts`.
 *
 * The client file contains:
 *   - A typed `<Provider>Client` interface with all selected operations as
 *     `(payload: unknown) => Promise<void>` methods, plus `init()` / `shutdown()`.
 *   - A protocol-specific implementation class (amqp / kafka) or a TODO stub.
 *   - An exported singleton constant `<provider>Client` for the consumer to import.
 *
 * No server-bind file is emitted — the broker is an external service.
 */
export class MessagingRuntimeEmitter {
    topology = 'messaging';
    async emit(ctx) {
        const { importEntry, factory, outputDir } = ctx;
        const selectEntries = importEntry.select ?? [];
        // 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`;
        // Detect the protocol from factory v2metadata.
        const protocol = factory.v2metadata?.protocol ?? 'unknown';
        const routing = extractRoutingConfig(factory);
        // Generate the content based on the protocol.
        let content;
        switch (protocol) {
            case 'amqp':
                content = generateAmqpClient(interfaceName, clientName, selectEntries, routing);
                break;
            case 'kafka':
                content = generateKafkaClient(interfaceName, clientName, selectEntries, routing);
                break;
            case 'nats':
            case 'sqs':
            default:
                content = generateTodoStubClient(interfaceName, clientName, selectEntries, protocol);
                break;
        }
        // Build the import entry for the consumer's controller file.
        const consumerImport = {
            from: `./clients/${fileName}`,
            specifier: `${clientName}Client`,
        };
        // Warnings + notes.
        const notes = [];
        // Always emit the deployment instance hint.
        notes.push(`Deployment instance hint: add provider under deployments.<env>.instances.communications.${clientName}` +
            ` with config: { broker: '<url>'` +
            (routing.exchange !== 'events' ? `, exchange: '${routing.exchange}'` : '') +
            (routing.topic ? `, topic: '${routing.topic}'` : '') +
            (routing.queue ? `, queue: '${routing.queue}'` : '') +
            ` }.`);
        if (selectEntries.length === 0) {
            notes.push(`Warning: import entry from '${importEntry.from}' has empty select: — no publish methods emitted.`);
        }
        if (protocol !== 'amqp' && protocol !== 'kafka') {
            notes.push(`TODO (Round 3): protocol '${protocol}' emits a stub — implement the publisher using the appropriate SDK.`);
        }
        const config = { broker: '<url>', protocol };
        if (routing.exchange && routing.exchange !== 'events')
            config.exchange = routing.exchange;
        if (routing.topic)
            config.topic = routing.topic;
        if (routing.queue)
            config.queue = routing.queue;
        const reviewNotes = [];
        if (protocol !== 'amqp' && protocol !== 'kafka') {
            reviewNotes.push(`Protocol '${protocol}' emits a stub — implement the publisher using the appropriate SDK.`);
        }
        const deploymentSuggestion = {
            provider: importEntry.from,
            consumerComponent: ctx.consumerComponent?.name,
            category: 'communications',
            key: clientName,
            config,
            factory: factory.name,
            topology: 'messaging',
            ...(reviewNotes.length > 0 ? { reviewNotes } : {}),
        };
        return {
            files: [{ path: relFilePath, content }],
            imports: [consumerImport],
            notes,
            deploymentSuggestion,
        };
    }
}
//# sourceMappingURL=messaging.js.map