import * as path from 'path';
import chalk from 'chalk';
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
import { closeServerBounded } from '../lib/secrets/agent.js';
import { createFileDeliveryStore, startWebhookServer, waitForListening } from '../lib/triggers/webhook.js';
import { getRuntimeStateDir } from '../lib/state.js';
const DEFAULT_HOST = '127.0.0.1';
const DEFAULT_PORT = 8787;
function positiveInt(value, fallback) {
    const parsed = Number.parseInt(value ?? '', 10);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function readWebhookSecrets(bundleName) {
    const { env } = readAndResolveBundleEnv(bundleName, {
        caller: 'webhooks serve',
        // `webhooks serve` is a long-running background server started to receive
        // signed webhooks, not a human at a Touch ID sheet — so the read is always
        // `agentOnly` (SEC-13: never pop biometry on its own). A `never`/no-ACL or
        // broker-held bundle resolves silently; a locked bundle THROWS the actionable
        // "unlock <name>" message, which propagates and fails the start LOUD rather
        // than popping an unanswerable prompt.
        agentOnly: true,
    });
    const secrets = {};
    if (env.GITHUB_WEBHOOK_SECRET)
        secrets.github = env.GITHUB_WEBHOOK_SECRET;
    if (env.LINEAR_WEBHOOK_SECRET)
        secrets.linear = env.LINEAR_WEBHOOK_SECRET;
    if (env.SLACK_SIGNING_SECRET)
        secrets.slack = env.SLACK_SIGNING_SECRET;
    if (!secrets.github && !secrets.linear && !secrets.slack) {
        throw new Error(`Bundle '${bundleName}' must contain GITHUB_WEBHOOK_SECRET, LINEAR_WEBHOOK_SECRET, or SLACK_SIGNING_SECRET.`);
    }
    return secrets;
}
/** Stop accepting requests and destroy persistent connections so shutdown is bounded. */
export async function closeWebhookServer(server, sockets) {
    const closing = closeServerBounded(server);
    for (const socket of sockets)
        socket.destroy();
    await closing;
}
export function registerWebhooksCommand(program) {
    const webhooks = program
        .command('webhooks')
        .description('Run a localhost signed webhook receiver for routine triggers.');
    webhooks
        .command('serve')
        .description('Receive signed GitHub/Linear/Slack webhooks on /hooks/<source> and fire matching routines and handlers.')
        .requiredOption('--secrets-bundle <name>', 'agents secrets bundle containing GITHUB_WEBHOOK_SECRET, LINEAR_WEBHOOK_SECRET, and/or SLACK_SIGNING_SECRET')
        .option('--bind <addr>', `Bind address (default ${DEFAULT_HOST})`, DEFAULT_HOST)
        .option('-p, --port <n>', `Local port (default ${DEFAULT_PORT})`, String(DEFAULT_PORT))
        .option('--rate-limit <n>', 'Accepted deliveries per source per minute', '60')
        .action(async (opts) => {
        let secrets;
        try {
            secrets = readWebhookSecrets(opts.secretsBundle);
        }
        catch (err) {
            console.error(chalk.red(err.message));
            process.exit(1);
        }
        const port = positiveInt(opts.port, DEFAULT_PORT);
        const rateLimit = positiveInt(opts.rateLimit, 60);
        try {
            const server = startWebhookServer({
                host: opts.bind ?? DEFAULT_HOST,
                port,
                secrets,
                rateLimitPerMinute: rateLimit,
                // Durable delivery dedup: replays survive a receiver restart (an
                // in-memory store would forget every seen delivery on restart).
                deliveryStore: createFileDeliveryStore(path.join(getRuntimeStateDir(), 'webhook', 'deliveries.json')),
                // Logged at MATCH time, before dispatch — a `run.command` handler can
                // block on a shelled-out agent run for minutes, and that must never
                // delay the log that says a delivery fired (RUSH-2722).
                onMatch: (webhook, matchedJobNames, matchedHandlerNames) => {
                    const parts = [];
                    if (matchedJobNames.length)
                        parts.push(`routines ${matchedJobNames.join(', ')}`);
                    if (matchedHandlerNames.length)
                        parts.push(`handlers ${matchedHandlerNames.join(', ')}`);
                    console.log(`${new Date().toISOString()} ${webhook.source}:${webhook.event} ` +
                        (parts.length ? `fired ${parts.join('; ')}` : 'no match'));
                },
                // The delivery is acked 202 before dispatch (RUSH-2548), so a dispatch
                // failure has no HTTP status left to ride — print it instead.
                onDeliveryError: (webhook, err) => {
                    console.error(chalk.red(`${new Date().toISOString()} ${webhook.source}:${webhook.event} dispatch failed after ack: ${err.message}`));
                },
            });
            const sockets = new Set();
            server.on('connection', (socket) => {
                sockets.add(socket);
                socket.once('close', () => sockets.delete(socket));
            });
            await waitForListening(server);
            const address = server.address();
            const bound = typeof address === 'object' && address ? address.port : port;
            console.log(`${chalk.green('agents webhooks')} ${chalk.dim('→')} ${chalk.cyan(`http://${opts.bind ?? DEFAULT_HOST}:${bound}`)}`);
            console.log(chalk.dim('signed · localhost by default · endpoints: /hooks/github, /hooks/linear, /hooks/slack · acks 202 then dispatches · Ctrl-C to stop'));
            console.log(chalk.dim('for a supervised receiver that survives reboot: agents daemon webhooks add --secrets-bundle <name>'));
            let shuttingDown = false;
            const shutdown = async () => {
                if (shuttingDown)
                    return;
                shuttingDown = true;
                await closeWebhookServer(server, sockets);
                process.exit(0);
            };
            process.on('SIGINT', () => { void shutdown(); });
            process.on('SIGTERM', () => { void shutdown(); });
        }
        catch (err) {
            console.error(chalk.red(`Could not start webhook receiver: ${err.message}`));
            process.exit(1);
        }
    });
}
