/**
 * `InboundRouter` — AUTO.6 inbound-channel dispatcher.
 *
 * Authoritative source: `docs/tasks/automation.md` AUTO.6. Walks the user's
 * `RoutingConfig.channelMapping`, finds the adapter for each concrete URI,
 * and (when the adapter implements `subscribeInbound`) attaches a per-
 * abstract-channel listener. Inbound messages flow:
 *
 *   adapter.subscribeInbound(handler)
 *     → handler(event)
 *       → CapabilityGate.check(pack, 'send_message', sender)   [optional]
 *         → if allowed → dispatch(event)
 *         → if denied  → audit + drop
 *
 * Sender allowlist is intentionally enforced via the existing AUTO.3
 * capability gate (`send_message` capability with `channels:` allowlist
 * — repurposed for inbound: a sender ID is the principal, mapped against
 * the same pack-declared list). The router does NOT introduce a new gate
 * surface; reuse keeps audit + override semantics identical.
 *
 * Engine-vocabulary: emits `InboundChannelEvent` only; never knows what
 * downstream evaluator does with the event.
 *
 * Imports from: ../runtime/event.js, ../runtime/capability_gate.js,
 * ./types.js. Imported by: src/runtime/daemon.ts (later) + tests.
 */
const noopAudit = () => {
    /* default audit sink */
};
export class InboundRouter {
    subscriptions = [];
    auditLog;
    capabilityGate;
    state = 'idle';
    constructor(opts = {}) {
        this.auditLog = opts.auditLog ?? noopAudit;
        this.capabilityGate = opts.capabilityGate;
    }
    /**
     * Walk `bindings`, resolve each abstract channel → URI via
     * `routing.channelMapping`, look up the adapter by URI scheme, and call
     * its `subscribeInbound` to attach a sender-gated dispatcher. Skips +
     * audits any binding whose channel is unmapped, whose adapter scheme
     * isn't registered, or whose adapter doesn't implement `subscribeInbound`.
     *
     * Idempotent: a second `start()` while already running throws so the
     * caller surfaces a lifecycle bug rather than double-subscribing.
     */
    async start(bindings, routing, adapters, dispatch) {
        if (this.state !== 'idle') {
            throw new Error(`InboundRouter.start: invalid state "${this.state}"`);
        }
        this.state = 'running';
        for (const binding of bindings) {
            const uri = routing.channelMapping[binding.channel];
            if (uri === undefined || uri === '') {
                this.auditLog({
                    event: 'inbound_unmapped',
                    pack: binding.pack,
                    channel: binding.channel,
                });
                continue;
            }
            const schemePart = uri.split('://')[0];
            if (schemePart === undefined || schemePart === '') {
                this.auditLog({
                    event: 'inbound_unmapped',
                    pack: binding.pack,
                    channel: binding.channel,
                });
                continue;
            }
            const adapter = adapters.get(schemePart);
            if (adapter === undefined) {
                this.auditLog({
                    event: 'inbound_no_adapter',
                    pack: binding.pack,
                    channel: binding.channel,
                    scheme: schemePart,
                });
                continue;
            }
            if (adapter.subscribeInbound === undefined) {
                this.auditLog({
                    event: 'inbound_adapter_not_inboundable',
                    pack: binding.pack,
                    channel: binding.channel,
                    uri,
                    scheme: schemePart,
                });
                continue;
            }
            // Closure captures `binding` so the gate + audit + dispatch path
            // can reference the right pack/channel without threading them
            // through `subscribeInbound`'s handler signature.
            const handler = async (event) => {
                await this.handleInbound(binding, event, dispatch);
            };
            const sub = await adapter.subscribeInbound(handler);
            this.subscriptions.push({ pack: binding.pack, channel: binding.channel, sub });
            this.auditLog({
                event: 'inbound_subscribed',
                pack: binding.pack,
                channel: binding.channel,
                uri,
                scheme: schemePart,
            });
        }
    }
    /**
     * Unsubscribe every active binding in reverse order. Idempotent — a
     * second stop() is a no-op. Each unsubscribe is best-effort; failures
     * are swallowed so a single bad adapter doesn't strand the rest.
     */
    async stop() {
        if (this.state !== 'running') {
            this.state = 'stopped';
            return;
        }
        this.state = 'stopped';
        while (this.subscriptions.length > 0) {
            const entry = this.subscriptions.pop();
            if (entry === undefined)
                break;
            try {
                await entry.sub.unsubscribe();
            }
            catch {
                // best-effort — audit-and-continue, never throw out of stop().
            }
        }
    }
    /** Test/inspection — number of active subscriptions. */
    activeSubscriptionCount() {
        return this.subscriptions.length;
    }
    // -------------------------------------------------------------------------
    // Internals.
    async handleInbound(binding, event, dispatch) {
        // Sender allowlist via capability gate. Repurposes `send_message`'s
        // `channels:` allowlist as the sender principal set — the gate's
        // glob-list matcher handles user-id allowlists cleanly.
        if (this.capabilityGate !== undefined) {
            const verdict = await this.capabilityGate.check({
                pack: binding.pack,
                capability: 'send_message',
                target: event.sender,
                context: { inbound: true, channelUri: event.channelUri },
            });
            if (!verdict.allowed) {
                this.auditLog({
                    event: 'inbound_sender_denied',
                    pack: binding.pack,
                    channel: binding.channel,
                    sender: event.sender,
                    reason: verdict.message ?? verdict.source,
                });
                return;
            }
        }
        try {
            await dispatch(event);
            this.auditLog({
                event: 'inbound_dispatched',
                pack: binding.pack,
                channel: binding.channel,
                uri: event.channelUri,
                sender: event.sender,
            });
        }
        catch (err) {
            this.auditLog({
                event: 'inbound_dispatch_error',
                pack: binding.pack,
                channel: binding.channel,
                sender: event.sender,
                reason: err instanceof Error ? err.message : String(err),
            });
        }
    }
}
//# sourceMappingURL=inbound_router.js.map