/**
 * slack:// adapter — outbound via @slack/web-api (`chat.postMessage`),
 * inbound via @slack/socket-mode (`SocketModeClient`).
 *
 * URI scheme: `slack://<workspace>/<channel>` where both segments are
 * `[\w-]+`. The workspace segment is informational — Slack routes by
 * the bot token's workspace, not by URI — but we keep it in the URI so
 * users can spot the destination without resolving credentials.
 *
 * Dependencies: both Slack SDKs are OPTIONAL peerDependencies, loaded
 * lazily on `start()` / `send()`. Users who never configure a Slack
 * channel never install them.
 *
 * Critical SLA: Slack requires `ack()` within 3 seconds on every inbound
 * event. The runtime contract here is "ack first, work async": the
 * adapter wraps every event listener so `ack()` is called synchronously
 * before the user-supplied handler runs, and the handler is then
 * scheduled on the next microtask. This makes it impossible for a slow
 * handler to miss the SLA.
 *
 * Security: bot + app tokens are closed over by this function and
 * scrubbed from any error path that might surface them.
 */
/** Parse `slack://<workspace>/<channel>` into `{ workspace, channel }`. */
function parseUri(uri) {
    const match = /^slack:\/\/([\w-]+)\/([\w-]+)$/.exec(uri);
    if (match === null)
        return null;
    const workspace = match[1];
    const channel = match[2];
    if (workspace === undefined || channel === undefined)
        return null;
    return { workspace, channel };
}
async function loadWebApi() {
    const mod = (await import('@slack/web-api'));
    return { WebClient: mod.WebClient };
}
async function loadSocketMode() {
    const mod = (await import('@slack/socket-mode'));
    return { SocketModeClient: mod.SocketModeClient };
}
function redact(message, ...secrets) {
    let out = message;
    for (const s of secrets) {
        if (s !== '')
            out = out.replaceAll(s, '[redacted]');
    }
    return out;
}
export function slackAdapter(opts) {
    let web = null;
    let socket = null;
    let started = false;
    const inboundListeners = new Set();
    const workspaceSegment = opts.workspace ?? 'workspace';
    /** Map a Slack envelope (event_callback) → unified InboundChannelEvent.
     *  Returns `null` for envelopes that don't carry a user-authored
     *  message (subtypes like bot_message, message_changed, etc). */
    function mapToInboundEvent(envelope) {
        if (envelope.type !== 'event_callback' && envelope.type !== 'events_api')
            return null;
        const body = envelope.body;
        const ev = body?.event;
        if (ev?.type !== 'message')
            return null;
        // Skip bot-authored + subtyped messages — loop-break first line of
        // defense + ignore edit/delete envelopes which aren't fresh inputs.
        if (ev.bot_id !== undefined && ev.bot_id !== '')
            return null;
        if (ev.subtype !== undefined && ev.subtype !== '')
            return null;
        if (ev.channel === undefined || ev.user === undefined)
            return null;
        const channelUri = `slack://${workspaceSegment}/${ev.channel}`;
        const event = {
            kind: 'inbound_channel',
            channelUri,
            sender: ev.user,
            text: ev.text ?? '',
            receivedAt: new Date().toISOString(),
            ...(ev.thread_ts !== undefined ? { threadKey: ev.thread_ts } : {}),
        };
        return event;
    }
    /**
     * ACK-first dispatch. We invoke `envelope.ack()` synchronously (the
     * Slack SDK accepts both sync and async return), then schedule the
     * user handler on the next microtask so the ack flushes before any
     * potentially-slow work. This guarantees the 3-second SLA regardless
     * of handler latency.
     */
    async function ackThenDispatch(envelope) {
        // 1) ACK first — never await user code before this.
        const ackResult = envelope.ack();
        if (ackResult instanceof Promise)
            await ackResult;
        // 2a) Dispatch legacy onEvent — back-compat path.
        if (opts.onEvent !== undefined) {
            try {
                await opts.onEvent({ type: envelope.type, body: envelope.body });
            }
            catch {
                // user handler errors are swallowed — never affect ack.
            }
        }
        // 2b) Dispatch AUTO.6 inbound listeners — only when the envelope
        // carries a user-authored message event.
        if (inboundListeners.size > 0) {
            const event = mapToInboundEvent(envelope);
            if (event !== null) {
                for (const fn of inboundListeners) {
                    try {
                        await fn(event);
                    }
                    catch {
                        // never bubble — same posture as onEvent.
                    }
                }
            }
        }
    }
    /** Ensure Socket Mode is connected. Mirrors `start()` but for the
     *  subscribeInbound auto-start path — only spins up if needed. */
    async function ensureSocketStarted() {
        if (socket !== null)
            return;
        const { SocketModeClient } = await loadSocketMode();
        const s = new SocketModeClient({ appToken: opts.appToken });
        s.on('slack_event', (envelope) => {
            void ackThenDispatch(envelope);
        });
        await s.start();
        socket = s;
    }
    return {
        scheme: 'slack',
        validate(uri) {
            return /^slack:\/\/[\w-]+\/[\w-]+$/.test(uri);
        },
        async start() {
            if (started)
                return;
            started = true;
            const { WebClient } = await loadWebApi();
            web = new WebClient(opts.botToken);
            // Only attach socket mode when the caller registered an inbound
            // handler — outbound-only deployments stay cheap.
            if (opts.onEvent !== undefined) {
                await ensureSocketStarted();
            }
        },
        async stop() {
            if (!started)
                return;
            started = false;
            // Clear AUTO.6 listeners FIRST so an in-flight envelope can't reach
            // a stale handler after stop() resolves.
            inboundListeners.clear();
            if (socket !== null) {
                try {
                    await socket.disconnect();
                }
                catch {
                    // best-effort
                }
                socket = null;
            }
            web = null;
        },
        async send(uri, message) {
            const parsed = parseUri(uri);
            if (parsed === null)
                return { ok: false, error: 'bad uri' };
            if (web === null) {
                try {
                    const { WebClient } = await loadWebApi();
                    web = new WebClient(opts.botToken);
                }
                catch (e) {
                    const msg = e instanceof Error ? e.message : String(e);
                    return { ok: false, error: redact(msg, opts.botToken, opts.appToken) };
                }
            }
            try {
                await web.chat.postMessage({ channel: parsed.channel, text: message.text });
                return { ok: true };
            }
            catch (e) {
                const msg = e instanceof Error ? e.message : String(e);
                return { ok: false, error: redact(msg, opts.botToken, opts.appToken) };
            }
        },
        async handleInbound(envelope) {
            await ackThenDispatch(envelope);
        },
        async subscribeInbound(handler) {
            inboundListeners.add(handler);
            // Auto-attach Socket Mode if start() hasn't been called yet, OR if
            // start() ran without `onEvent` (outbound-only path). Idempotent.
            started = true;
            if (web === null) {
                const { WebClient } = await loadWebApi();
                web = new WebClient(opts.botToken);
            }
            await ensureSocketStarted();
            return {
                // eslint-disable-next-line @typescript-eslint/require-await -- async to satisfy InboundSubscription contract
                unsubscribe: async () => {
                    inboundListeners.delete(handler);
                },
            };
        },
    };
}
//# sourceMappingURL=slack.js.map