import { createHash } from "node:crypto";
import { and, eq } from "drizzle-orm";
import type { Context } from "hono";
import { Hono } from "hono";
import type { Database } from "./db/client.js";
import { projects, tourVersions, tours } from "./db/schema.js";
import { jsonError, parseUuid } from "./errors.js";

const MANIFEST_CACHE = "public, max-age=60, stale-while-revalidate=600";
const VERSION_CACHE = "public, max-age=31536000, immutable";
const PUBLIC_CORS = { "access-control-allow-origin": "*" };

export function etagFor(body: string): string {
	const digest = createHash("sha256").update(body).digest("base64url");
	return `"${digest.slice(0, 27)}"`;
}

function publicJson(c: Context, payload: unknown, cacheControl: string) {
	const body = JSON.stringify(payload);
	const etag = etagFor(body);
	const headers = {
		...PUBLIC_CORS,
		"cache-control": cacheControl,
		etag,
		"content-type": "application/json; charset=utf-8",
	};
	const noneMatch = c.req.header("if-none-match");
	if (noneMatch === etag) {
		return new Response(null, { status: 304, headers });
	}
	return new Response(body, { status: 200, headers });
}

export function publicRoutes(db: Database) {
	return new Hono()
		.get("/p/:publicKey/manifest.json", async (c) => {
			const publicKey = c.req.param("publicKey");
			const project = await db
				.select({ id: projects.id })
				.from(projects)
				.where(eq(projects.publicKey, publicKey))
				.limit(1)
				.then((rows) => rows[0]);
			if (!project) {
				return jsonError(c, 404, "not_found", "Projet introuvable.");
			}

			const rows = await db
				.select({
					id: tours.id,
					version: tours.publishedVersion,
					content: tourVersions.content,
				})
				.from(tours)
				.innerJoin(
					tourVersions,
					and(
						eq(tourVersions.tourId, tours.id),
						eq(tourVersions.version, tours.publishedVersion),
					),
				)
				.where(
					and(eq(tours.projectId, project.id), eq(tours.status, "published")),
				);

			const payload = {
				tours: rows.map((row) => {
					const content = row.content as {
						name: string;
						trigger: unknown;
					};
					return {
						id: row.id,
						version: row.version,
						name: content.name,
						trigger: content.trigger,
					};
				}),
			};

			return publicJson(c, payload, MANIFEST_CACHE);
		})
		.get("/p/:publicKey/tours/:tourId/:file", async (c) => {
			const publicKey = c.req.param("publicKey");
			const tourId = parseUuid(c.req.param("tourId"));
			if (!tourId) {
				return jsonError(c, 404, "not_found", "Version introuvable.");
			}
			const file = c.req.param("file");
			const match = /^(\d+)\.json$/.exec(file);
			if (!match) {
				return jsonError(c, 404, "not_found", "Version introuvable.");
			}
			const version = Number(match[1]);

			const project = await db
				.select({ id: projects.id })
				.from(projects)
				.where(eq(projects.publicKey, publicKey))
				.limit(1)
				.then((rows) => rows[0]);
			if (!project) {
				return jsonError(c, 404, "not_found", "Projet introuvable.");
			}

			const row = await db
				.select({ content: tourVersions.content })
				.from(tourVersions)
				.innerJoin(tours, eq(tours.id, tourVersions.tourId))
				.where(
					and(
						eq(tours.projectId, project.id),
						eq(tours.id, tourId),
						eq(tourVersions.version, version),
					),
				)
				.limit(1)
				.then((rows) => rows[0]);

			if (!row) {
				return jsonError(c, 404, "not_found", "Version introuvable.");
			}

			return publicJson(c, row.content, VERSION_CACHE);
		});
}
