import { ApiError } from "./errors.js";
import { mergeNotifyData, deleteNotifyData } from "./hooks.js";
import { notifyHealth } from "./health.js";
import { emit } from "./emit.js";
import { registerPushToken } from "./devices.js";
import { getPrefs, putPrefs } from "./prefs.js";
import { handleProducerEmit } from "./producer-emit.js";
import { familyRoot } from "./registry.js";
const PREFIX = "/notify/v1";
const TEST_LIMIT = 5;
const TEST_WINDOW_MS = 3600_000;
const testSends = new Map();
export async function handleNotifyRequest(req, deps) {
    try {
        if (req.path === `${PREFIX}/health` && req.method.toUpperCase() === "GET") {
            const health = await notifyHealth();
            const { outbox, last_48h, never_delivered, ...checks } = health.checks;
            return json(health.ok ? 200 : 503, { ...health, checks });
        }
        return json(200, await dispatch(req, deps));
    }
    catch (err) {
        if (err instanceof ApiError)
            return json(err.status, err.toBody());
        console.error("[notify] unhandled error", err);
        return json(500, { error: { code: "internal", message: "Internal error" } });
    }
}
async function dispatch(req, deps) {
    const method = req.method.toUpperCase();
    if (!req.path.startsWith(`${PREFIX}/`)) {
        throw new ApiError(404, "not_found", "Unknown endpoint");
    }
    const rest = req.path.slice(PREFIX.length + 1).split("/").filter(Boolean);
    if (rest.length === 1 && rest[0] === "emit" && method === "POST") {
        return handleProducerEmit(req.header, req.rawBody, req.body);
    }
    const claims = await authenticate(req, deps);
    const home = { ...claims, appId: familyRoot(claims.appId) };
    if (rest.length === 1 && rest[0] === "prefs") {
        if (method === "GET")
            return getPrefs(home);
        if (method === "PUT")
            return putPrefs(home, req.body);
    }
    if (rest.length === 1 && rest[0] === "test" && method === "POST") {
        return sendTestPush(home);
    }
    if (rest.length === 2 && rest[0] === "devices" && rest[1] === "push-token" && method === "PUT") {
        return registerPushToken(home, req.body);
    }
    if (rest.length === 1 && rest[0] === "me" && method === "DELETE") {
        await deleteNotifyData(home.userId, home.appId);
        return { ok: true };
    }
    if (rest.length === 1 && rest[0] === "supersede" && method === "POST") {
        return supersede(claims, req.body, deps);
    }
    throw new ApiError(404, "not_found", "Unknown endpoint");
}
async function sendTestPush(claims) {
    const now = Date.now();
    if (testSends.size > 10_000)
        testSends.clear();
    const stamps = (testSends.get(claims.userId) ?? []).filter((t) => now - t < TEST_WINDOW_MS);
    if (stamps.length >= TEST_LIMIT) {
        throw new ApiError(429, "rate_limited", "Too many test sends; try again later");
    }
    stamps.push(now);
    testSends.set(claims.userId, stamps);
    await emit({
        app_id: claims.appId,
        category: "notify.test",
        recipients: [claims.userId],
        title: "Test notification",
        body: "Push delivery is working on this device.",
        ttl_s: 900,
        priority: "high",
    });
    return { ok: true };
}
async function authenticate(req, deps) {
    const header = req.header("authorization");
    if (!header?.startsWith("Bearer ")) {
        throw new ApiError(401, "invalid_token", "Authorization header is required");
    }
    try {
        return await deps.verifyToken(header.slice("Bearer ".length));
    }
    catch {
        throw new ApiError(401, "invalid_token", "Access token is invalid or expired");
    }
}
async function supersede(claims, body, deps) {
    if (!deps.supersede)
        throw new ApiError(404, "not_found", "Unknown endpoint");
    const token = body?.supersession_token;
    if (typeof token !== "string" || !token) {
        throw new ApiError(400, "invalid_body", "supersession_token is required");
    }
    let moved;
    try {
        moved = await deps.supersede(token, claims);
    }
    catch {
        throw new ApiError(403, "invalid_supersession", "That statement was not issued for this caller");
    }
    await mergeNotifyData(moved.previousUserId, claims.userId);
    return { ok: true };
}
function json(status, body) {
    return { status, contentType: "application/json", body: JSON.stringify(body) };
}
