import { registration } from "../../../config/registration.ts";
import { boundedJson } from "../../../server/security.ts";
import { env } from "../../../lib/server-env";

function validValues(v: unknown): v is Record<string, string> {
  return (
    Boolean(v) &&
    typeof v === "object" &&
    !Array.isArray(v) &&
    Object.keys(v as object).length <= 300 &&
    Object.entries(v as object).every(
      ([k, v]) => k.length <= 100 && typeof v === "string" && v.length <= 1000,
    ) &&
    JSON.stringify(v).length <= 60000
  );
}
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const b64 = (bytes: Uint8Array) => btoa(String.fromCharCode(...bytes));
const unb64 = (value: string) =>
  Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
async function key() {
  const secret = env.DRAFT_ENCRYPTION_KEY;
  if (!secret || secret.length < 32) throw new Error("invalid_draft_key");
  const raw = await crypto.subtle.digest("SHA-256", encoder.encode(secret));
  return crypto.subtle.importKey("raw", raw, "AES-GCM", false, [
    "encrypt",
    "decrypt",
  ]);
}
async function hash(token: string) {
  return Array.from(
    new Uint8Array(
      await crypto.subtle.digest("SHA-256", encoder.encode(token)),
    ),
    (byte) => byte.toString(16).padStart(2, "0"),
  ).join("");
}
async function encrypt(payload: unknown) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    await key(),
    encoder.encode(JSON.stringify(payload)),
  );
  return { iv: b64(iv), payload: b64(new Uint8Array(encrypted)) };
}
async function decrypt(payload: string, iv: string) {
  const clear = await crypto.subtle.decrypt(
    { name: "AES-GCM", iv: unb64(iv) },
    await key(),
    unb64(payload),
  );
  return JSON.parse(decoder.decode(clear));
}

export async function POST(request: Request) {
  try {
    const body = (await boundedJson(request)) as {
      values?: Record<string, string>;
      step?: number;
    };
    if (!validValues(body.values))
      return Response.json({ error: "invalid" }, { status: 400 });
    const token = b64(crypto.getRandomValues(new Uint8Array(24)))
      .replaceAll("/", "_")
      .replaceAll("+", "-")
      .replaceAll("=", "");
    const encrypted = await encrypt(body.values);
    const now = new Date();
    const expires = new Date(now.getTime() + registration.draftDays * 86400000);
    await env.DB.prepare(
      "INSERT INTO application_drafts (id,token_hash,encrypted_payload,iv,current_step,expires_at,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)",
    )
      .bind(
        crypto.randomUUID(),
        await hash(token),
        encrypted.payload,
        encrypted.iv,
        Math.min(
          registration.steps.length - 1,
          Math.max(0, Math.floor(Number(body.step) || 0)),
        ),
        expires.toISOString(),
        now.toISOString(),
        now.toISOString(),
      )
      .run();
    return Response.json(
      { ok: true, token, expiresAt: expires.toISOString() },
      { status: 201 },
    );
  } catch {
    return Response.json({ error: "unavailable" }, { status: 503 });
  }
}
export async function PUT(request: Request) {
  try {
    const body = (await boundedJson(request)) as {
      token?: string;
      values?: Record<string, string>;
      step?: number;
    };
    if (
      typeof body.token !== "string" ||
      !/^[A-Za-z0-9_-]{32}$/.test(body.token) ||
      !validValues(body.values)
    )
      return Response.json({ error: "invalid" }, { status: 400 });
    const encrypted = await encrypt(body.values);
    const result = await env.DB.prepare(
      "UPDATE application_drafts SET encrypted_payload=?,iv=?,current_step=?,updated_at=? WHERE token_hash=? AND expires_at>?",
    )
      .bind(
        encrypted.payload,
        encrypted.iv,
        Math.min(
          registration.steps.length - 1,
          Math.max(0, Math.floor(Number(body.step) || 0)),
        ),
        new Date().toISOString(),
        await hash(body.token),
        new Date().toISOString(),
      )
      .run();
    if (!result.meta.changes)
      return Response.json({ error: "not_found" }, { status: 404 });
    return Response.json({ ok: true });
  } catch {
    return Response.json({ error: "unavailable" }, { status: 503 });
  }
}
export async function GET(request: Request) {
  try {
    const token = new URL(request.url).searchParams.get("token");
    if (!token || !/^[A-Za-z0-9_-]{32}$/.test(token))
      return Response.json({ error: "invalid" }, { status: 400 });
    const row = await env.DB.prepare(
      "SELECT encrypted_payload,iv,current_step,expires_at FROM application_drafts WHERE token_hash=? AND expires_at>?",
    )
      .bind(await hash(token), new Date().toISOString())
      .first<{
        encrypted_payload: string;
        iv: string;
        current_step: number;
        expires_at: string;
      }>();
    if (!row) return Response.json({ error: "not_found" }, { status: 404 });
    return Response.json({
      values: await decrypt(row.encrypted_payload, row.iv),
      step: row.current_step,
      expiresAt: row.expires_at,
    });
  } catch {
    return Response.json({ error: "unavailable" }, { status: 503 });
  }
}
