/**
 * src/delivery/rate-limit.ts
 *
 * Outbound circuit-breaker. The single biggest way autonomous outreach can hurt
 * the owner is a runaway blast — a loop bug or an over-eager initiative batch
 * firing dozens of WhatsApp messages, which gets the Business number BANNED and
 * trips PDPL. This caps external sends per tenant+channel over rolling windows.
 *
 * Owner-facing notifications (heartbeat, scorecard, reflection brief) pass
 * `audience:'owner'` (or `bypassRateLimit:true`) and are never throttled — the
 * cap only guards customer-facing outbound. In-memory sliding window: correct
 * for the single-process device gateway; a restart resets it, which is an
 * acceptable fail-open for a safety breaker (the daily cap still bounds abuse
 * within a session).
 */
import { createChildLogger } from '../utils/logger.js';
const log = createChildLogger('delivery:rate-limit');
const num = (env, dflt) => {
    const v = Number(process.env[env]);
    return Number.isFinite(v) && v > 0 ? v : dflt;
};
// Generous enough for normal owner+customer traffic, tight enough to stop a
// blast. Tunable via env without a rebuild.
function limitFor(channel) {
    switch (channel) {
        case 'whatsapp':
            return { perHour: num('MUASSIS_RL_WHATSAPP_HOUR', 25), perDay: num('MUASSIS_RL_WHATSAPP_DAY', 150) };
        case 'email':
            return { perHour: num('MUASSIS_RL_EMAIL_HOUR', 50), perDay: num('MUASSIS_RL_EMAIL_DAY', 300) };
        case 'slack':
        case 'discord':
            return { perHour: num('MUASSIS_RL_CHAT_HOUR', 60), perDay: num('MUASSIS_RL_CHAT_DAY', 400) };
        default:
            return null; // web / webhook / unknown → not rate-limited
    }
}
const HOUR_MS = 60 * 60 * 1000;
const DAY_MS = 24 * HOUR_MS;
// key = `${tenantId}:${channel}` → ascending send timestamps (ms).
const sends = new Map();
/**
 * Check whether an outbound send is allowed, and if so RECORD it. Call once per
 * actual external send. Returns allowed=false (never throws) when a window cap
 * is hit so the caller can spool/escalate instead of blasting.
 */
export function checkAndRecordSend(tenantId, channel) {
    const limit = limitFor(channel);
    if (!limit)
        return { allowed: true };
    const key = `${tenantId}:${channel}`;
    const now = Date.now();
    const arr = (sends.get(key) ?? []).filter((t) => now - t < DAY_MS);
    const inHour = arr.filter((t) => now - t < HOUR_MS).length;
    const inDay = arr.length;
    if (inHour >= limit.perHour) {
        const oldestInHour = arr.find((t) => now - t < HOUR_MS) ?? now;
        log.warn({ tenantId, channel, inHour, cap: limit.perHour }, 'outbound hourly cap hit — send blocked');
        sends.set(key, arr);
        return { allowed: false, reason: `hourly ${channel} cap (${limit.perHour}) reached`, retryAfterMs: HOUR_MS - (now - oldestInHour) };
    }
    if (inDay >= limit.perDay) {
        const oldest = arr[0] ?? now;
        log.warn({ tenantId, channel, inDay, cap: limit.perDay }, 'outbound daily cap hit — send blocked');
        sends.set(key, arr);
        return { allowed: false, reason: `daily ${channel} cap (${limit.perDay}) reached`, retryAfterMs: DAY_MS - (now - oldest) };
    }
    arr.push(now);
    sends.set(key, arr);
    return { allowed: true };
}
/** Test/diagnostic helper — current counts without recording. */
export function peekSendCounts(tenantId, channel) {
    const now = Date.now();
    const arr = (sends.get(`${tenantId}:${channel}`) ?? []).filter((t) => now - t < DAY_MS);
    return { inHour: arr.filter((t) => now - t < HOUR_MS).length, inDay: arr.length };
}
/** Test helper — reset a tenant+channel window. */
export function _resetSendWindow(tenantId, channel) {
    sends.delete(`${tenantId}:${channel}`);
}
//# sourceMappingURL=rate-limit.js.map