import { telemetryBatchSchema } from "@circuy/contracts";
import { and, eq, inArray } from "drizzle-orm";
import { Hono } from "hono";
import { bodyLimit } from "hono/body-limit";
import type { Database } from "./db/client.js";
import { projects, tourEvents, tours } from "./db/schema.js";
import { jsonError, zodField } from "./errors.js";
import type { RateLimiter } from "./rate-limit.js";

const EVENTS_MAX_BYTES = 64 * 1024;

function clientIp(c: {
	req: { header: (name: string) => string | undefined };
}): string {
	return c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
}

function withOrigin(response: Response, origin: string | undefined): Response {
	if (origin) {
		response.headers.set("access-control-allow-origin", origin);
		response.headers.set("vary", "Origin");
	}
	return response;
}

export function eventsRoutes(db: Database, limiter: RateLimiter) {
	return new Hono()
		.on("OPTIONS", "/events", (c) => {
			const origin = c.req.header("origin");
			if (!origin) return c.body(null, 204);
			return c.body(null, 204, {
				"access-control-allow-origin": origin,
				"access-control-allow-methods": "POST, OPTIONS",
				"access-control-allow-headers": "content-type",
				"access-control-max-age": "86400",
				vary: "Origin",
			});
		})
		.post(
			"/events",
			bodyLimit({
				maxSize: EVENTS_MAX_BYTES,
				onError: (c) =>
					withOrigin(
						jsonError(
							c,
							413,
							"payload_too_large",
							"Le lot depasse 64 kilooctets.",
						),
						c.req.header("origin"),
					),
			}),
			async (c) => {
				let raw: unknown;
				try {
					raw = JSON.parse(await c.req.text());
				} catch {
					return withOrigin(
						jsonError(c, 400, "invalid_body", "JSON invalide."),
						c.req.header("origin"),
					);
				}

				const parsed = telemetryBatchSchema.safeParse(raw);
				if (!parsed.success) {
					const issue = parsed.error.issues[0];
					return withOrigin(
						jsonError(
							c,
							400,
							"invalid_body",
							issue?.message ?? "Lot d'evenements invalide.",
							issue ? zodField(issue.path) : undefined,
						),
						c.req.header("origin"),
					);
				}

				const origin = c.req.header("origin");
				const project = await db
					.select({
						id: projects.id,
						allowedOrigins: projects.allowedOrigins,
					})
					.from(projects)
					.where(eq(projects.publicKey, parsed.data.key))
					.limit(1)
					.then((rows) => rows[0]);

				if (!project) {
					return withOrigin(
						jsonError(c, 404, "not_found", "Projet introuvable."),
						origin,
					);
				}

				if (!origin || !project.allowedOrigins.includes(origin)) {
					return withOrigin(
						jsonError(
							c,
							403,
							"origin_not_allowed",
							"Origine non autorisee pour cette cle publique.",
						),
						origin,
					);
				}

				if (!limiter.allow(`${parsed.data.key}\n${clientIp(c)}`)) {
					return withOrigin(
						jsonError(
							c,
							429,
							"rate_limited",
							"Trop de lots recus pour cette cle.",
						),
						origin,
					);
				}

				const tourIds = [
					...new Set(parsed.data.events.map((event) => event.tourId)),
				];
				const owned = await db
					.select({ id: tours.id })
					.from(tours)
					.where(
						and(eq(tours.projectId, project.id), inArray(tours.id, tourIds)),
					);
				if (owned.length !== tourIds.length) {
					return withOrigin(
						jsonError(
							c,
							400,
							"invalid_body",
							"Un evenement reference un parcours inconnu de ce projet.",
							"events",
						),
						origin,
					);
				}

				await db.insert(tourEvents).values(
					parsed.data.events.map((event) => ({
						projectId: project.id,
						tourId: event.tourId,
						tourVersion: event.tourVersion,
						stepId: event.stepId,
						type: event.type,
						choiceId: event.choiceId,
						sessionId: event.sessionId,
					})),
				);

				return c.body(null, 204, {
					"access-control-allow-origin": origin,
					vary: "Origin",
				});
			},
		);
}
