import "server-only";
import { createHmac } from "node:crypto";
import { z } from "zod";
import { ApplicationError } from "./application-record";
import { staffDocumentKinds, staffDocumentLabels } from "./application-schema";
import {
  issueUploadTicket,
  readUploadTicket,
  uploadObjectPath,
  verifyUploadedDocument,
  UPLOAD_TICKET_LIFETIME_MS,
  UPLOAD_URL_LIFETIME_MS,
  type UploadTicket,
} from "./application-upload";
import type { ApplicationProviders } from "./application-providers";
import {
  attachVerifiedDocument,
  savedView,
  staffDocumentSchema,
  uploadableDocument,
  type StaffActor,
} from "./staff-service";
const RESIGN_MARGIN_MS = 30_000,
  CLOCK_MARGIN_MS = 60_000;
function ticketIssuedAt(ticket: string) {
  try {
    const issuedAt = (
      JSON.parse(
        Buffer.from(ticket.split(".")[0], "base64url").toString("utf8"),
      ) as { issuedAt?: unknown }
    ).issuedAt;
    return typeof issuedAt === "number" && Number.isFinite(issuedAt)
      ? issuedAt
      : null;
  } catch {
    return null;
  }
}
const uploadKey = (id: string, secret: string) =>
  createHmac("sha256", secret)
    .update(`maxcredit-staff-upload-v1.${id}`)
    .digest("hex");

export async function prepareStaffDocument(
  id: string,
  input: unknown,
  staff: StaffActor,
  providers: ApplicationProviders,
) {
  const parsed = staffDocumentSchema.safeParse(input);
  if (!parsed.success)
    throw new ApplicationError(
      400,
      "Choose a PDF, JPG or PNG file of up to 10 MB.",
    );
  const { name, ...descriptor } = parsed.data,
    kind = parsed.data.kind,
    secret = providers.config.APPLICATION_SIGNING_SECRET,
    key = uploadKey(id, secret),
    now = Date.now();
  const pending = uploadableDocument(
    await providers.repo.get(id),
    kind,
  ).documents.find((document) => document.kind === kind);
  let started: UploadTicket | null = null;
  if (pending) {
    try {
      started = readUploadTicket(pending.ticket, key, secret, now);
    } catch {
      started = null;
    }
  }
  const unverified =
    pending && !started ? ticketIssuedAt(pending.ticket) : null;
  const issuedAt =
    started?.issuedAt ??
    (unverified !== null &&
    unverified <= now + UPLOAD_URL_LIFETIME_MS + CLOCK_MARGIN_MS
      ? unverified
      : null);
  if (issuedAt !== null) {
    const windowEnds = issuedAt + UPLOAD_URL_LIFETIME_MS,
      label = staffDocumentLabels[kind];
    if (started && now < windowEnds - RESIGN_MARGIN_MS) {
      const sameFile =
        started.document.sha256 === descriptor.sha256 &&
        started.document.size === descriptor.size &&
        started.document.type === descriptor.type;
      if (!sameFile)
        throw new ApplicationError(
          409,
          `${label}: another upload started in the last 10 minutes. Finish it, or try again once it expires.`,
        );
      return { kind, ...(await providers.target(started, windowEnds)) };
    }
    if (now < windowEnds + CLOCK_MARGIN_MS)
      throw new ApplicationError(
        409,
        `${label}: an earlier upload is still finishing. Try again in a minute or two.`,
      );
  }
  if (pending) await providers.remove(pending.path, null);
  const issued = issueUploadTicket(descriptor, key, secret, now);
  await providers.repo.change(id, (current) => {
    const record = uploadableDocument(current, kind);
    if (
      record.documents.find((document) => document.kind === kind)?.ticket !==
      pending?.ticket
    )
      throw new ApplicationError(
        409,
        `${staffDocumentLabels[kind]}: another upload just started. Try again shortly.`,
      );
    record.documents = [
      ...record.documents.filter((document) => document.kind !== kind),
      {
        ...descriptor,
        name,
        ticket: issued.ticket,
        path: uploadObjectPath(issued.data),
        generation: null,
      },
    ];
    const boundary = now + UPLOAD_TICKET_LIFETIME_MS;
    if (Date.parse(record.retention.latestDocumentReceivedAt) < boundary)
      record.retention.latestDocumentReceivedAt = new Date(
        boundary,
      ).toISOString();
    return { record: savedView(record).record, result: undefined };
  });
  return { kind, ...(await providers.target(issued.data)) };
}

export async function verifyStaffDocument(
  id: string,
  input: unknown,
  staff: StaffActor,
  providers: ApplicationProviders,
) {
  const parsed = z
    .object({ kind: z.enum(staffDocumentKinds) })
    .strict()
    .safeParse(input);
  if (!parsed.success)
    throw new ApplicationError(400, "Choose the document to check.");
  const kind = parsed.data.kind,
    secret = providers.config.APPLICATION_SIGNING_SECRET;
  const pending = uploadableDocument(
    await providers.repo.get(id),
    kind,
  ).documents.find((document) => document.kind === kind);
  if (!pending)
    throw new ApplicationError(404, "Choose the file again to upload it.");
  let generation: string;
  try {
    generation = (
      await verifyUploadedDocument(
        readUploadTicket(
          pending.ticket,
          uploadKey(id, secret),
          secret,
          Date.now(),
        ),
        providers.storage,
      )
    ).generation;
  } catch {
    throw new ApplicationError(
      400,
      "The document could not be verified. Upload it again.",
    );
  }
  return providers.repo.change(id, (current) =>
    attachVerifiedDocument(
      uploadableDocument(current, kind),
      pending.ticket,
      generation,
      staff,
    ),
  );
}
