import { z } from "zod";
import { bumpCounter } from "./counters.js";
import { enqueueEvent } from "./emit.js";
import { ApiError } from "./errors.js";
import { authenticateProducer, claimProducerRequest, releaseProducerRequest, } from "./producers.js";
const REFUSALS_PER_HOUR = 100;
const REFUSAL_WINDOW_MS = 3600_000;
const refusals = new Map();
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const producerEventSchema = z
    .object({
    category: z
        .string()
        .max(80)
        .regex(/^[a-z0-9_]+(\.[a-z0-9_]+)+$/, "category is producer-namespaced, dot-separated"),
    recipients: z
        .array(z.string().regex(UUID_RE, "recipients are Zaparo ID user ids"))
        .min(1),
    actor_user_id: z.string().regex(UUID_RE).optional(),
    title: z.string().min(1).max(200),
    body: z.string().min(1).max(1000),
    coalesced_body: z.string().min(1).max(1000).optional(),
    data: z.record(z.string().min(1).max(40), z.string().max(500)).optional(),
    collapse_key: z.string().min(1).max(120).optional(),
    dedupe_key: z.string().min(1).max(200).optional(),
    coalesce_window_s: z.number().int().min(0).max(3600).optional(),
    ttl_s: z.number().int().min(60).max(7 * 24 * 3600).optional(),
    priority: z.enum(["normal", "high"]).optional(),
})
    .strict();
export async function handleProducerEmit(header, rawBody, body) {
    const { credential, signature } = await authenticateProducer(header, rawBody);
    const event = validate(credential, body);
    const claim = await claimProducerRequest(credential, signature);
    if (claim === "replay")
        return { ok: true, replayed: true };
    if (claim === "rate_limited") {
        throw new ApiError(429, "producer_rate_limited", `This credential is capped at ${credential.hourly_limit} events per hour`);
    }
    try {
        const result = await enqueueEvent({ ...event, app_id: credential.app_id });
        return { ok: true, ...result };
    }
    catch (err) {
        await releaseProducerRequest(credential, signature);
        throw refuse(credential, event.category, 400, "invalid_body", err instanceof Error ? err.message : "The event was refused");
    }
}
function validate(credential, body) {
    const parsed = producerEventSchema.safeParse(body);
    if (!parsed.success) {
        const issue = parsed.error.issues[0];
        const named = JSON.stringify(issue ?? {});
        const message = named.includes("app_id")
            ? "app_id comes from the credential and must not be sent"
            : [issue?.path.join("."), issue?.message ?? "Invalid event"].filter(Boolean).join(": ");
        throw refuse(credential, undefined, 400, "invalid_body", message);
    }
    const event = parsed.data;
    if (!credential.categories.includes(event.category)) {
        throw refuse(credential, event.category, 403, "category_not_allowed", `This credential may emit ${credential.categories.join(", ")}`);
    }
    if (event.recipients.length > credential.max_recipients) {
        throw refuse(credential, event.category, 403, "too_many_recipients", `At most ${credential.max_recipients} recipients per event`);
    }
    return event;
}
function refuse(credential, category, status, code, message) {
    const now = Date.now();
    if (refusals.size > 10_000)
        refusals.clear();
    const stamps = (refusals.get(credential.key_id) ?? []).filter((t) => now - t < REFUSAL_WINDOW_MS);
    stamps.push(now);
    refusals.set(credential.key_id, stamps);
    const attributed = category && credential.categories.includes(category) ? category : "notify.rejected";
    void bumpCounter(credential.app_id, attributed, "rejected");
    if (stamps.length > REFUSALS_PER_HOUR) {
        return new ApiError(429, "producer_rate_limited", "Too many refused requests from this credential; fix the request rather than retrying");
    }
    return new ApiError(status, code, message);
}
