import { z } from "zod";
import {
  ApplicationError,
  applicationRecordSchema,
  type ApplicationRecord,
} from "./application-record";
import {
  documentKinds,
  staffDocumentKinds,
  staffDocumentLabels,
} from "./application-schema";
import { RETENTION_POLICY_VERSION } from "./application-retention";
import {
  DECLARATION_VERSION,
  declarationKeys,
  dueDiligenceKeys,
  type DueDiligenceKey,
} from "./application-declaration";
import type { Particulars } from "./application-particulars";
import type { LoanContext } from "./application-loan-context";
import type { ApplicationRepository } from "./application-repository";
import { demoMyinfoProfile } from "./myinfo-demo";
import { staffView } from "./staff-model";
import {
  applyStaffChange,
  attachVerifiedDocument,
  createStaffApplication,
  savedView,
  staffDocumentSchema,
  uploadableDocument,
} from "./staff-service";
import { recentPage, searchStaffApplications } from "./staff-search";

export const PREVIEW_ACTOR = "Preview reviewer";
export const PREVIEW_TEAM = [
  "preview.reviewer@maxcredit-preview.sg",
  "loans.officer@maxcredit-preview.sg",
];
const PREVIEW_ME = PREVIEW_TEAM[0];
const MINUTE = 60_000,
  HOUR = 60 * MINUTE,
  DAY = 24 * HOUR;
type Status = ApplicationRecord["status"];
type Sample = Pick<
  ApplicationRecord,
  "id" | "reference" | "fields" | "assessment"
> & {
  particulars: Particulars;
  loanContext: LoanContext;
  answers?: Partial<Record<DueDiligenceKey, "Yes" | "No">>;
  received: number;
  submitted: number | null;
  changes?: [number, Status][];
  review?: [number, string];
  archived?: number;
  assigned?: [number, string];
  notes?: [number, string, string][];
  documents: [
    (typeof documentKinds)[number],
    string,
    number,
    verified?: boolean,
  ][];
  myinfo?: boolean;
};
const samples: Sample[] = [
  {
    id: "5b2f7c1e-3a4d-4e8f-9b6a-1c2d3e4f5a61",
    reference: "MC-6E1F0A9B4C7D2E8F1A3B5C7D9E0F2A4B",
    received: 2 * HOUR,
    submitted: 2 * HOUR - 12 * MINUTE,
    myinfo: true,
    fields: {
      name: "Tan Wei Ling",
      email: "weiling.tan@maxcredit-preview.sg",
      phone: "+65 9123 4567",
      interest: "Personal Loan",
      amount: "8000",
      employment: "Employed full-time",
      monthlyIncome: "4500",
    },
    particulars: {
      idType: "NRIC",
      idNumber: "S9812381D",
      dob: "1998-01-15",
      nationality: "Singaporean",
      residentialStatus: "Singapore citizen",
      maritalStatus: "Married",
      dependants: "0",
      sex: "Female",
      education: "Degree",
      block: "123",
      street: "ANG MO KIO AVENUE 6",
      building: "",
      floor: "08",
      unit: "112",
      postal: "560123",
      housingType: "HDB flat (4 room)",
      homeOwnership: "Owned",
    },
    loanContext: {
      purpose: "Medical bills",
      purposeOther: "",
      purposeProof: "Yes",
      repaymentSource: "Monthly salary",
      repaymentOther: "",
      referral: "Google search",
      dateJoined: "2022-03-01",
      companyAddress: "",
      payday: "Month end",
      statementDelivery: "Email",
    },
    assessment: {
      employer: "NORTHPOINT LOGISTICS PTE LTD",
      occupation: "Operations Executive",
      monthlyExpenses: "1400",
      monthlyDebtPayments: "0",
    },
    documents: [
      ["identity", "nric-front-and-back.pdf", 412_804],
      ["payslip", "payslip-latest-month.pdf", 96_512],
      ["cpf", "cpf-contribution-history.pdf", 128_330],
    ],
  },
  {
    id: "8d4a6b2c-9e1f-4a3b-8c5d-7e6f5a4b3c22",
    reference: "MC-2C9D4E7F1A0B3C6D8E2F4A7B9C1D3E5F",
    received: 3 * HOUR,
    submitted: null,
    fields: {
      name: "Chloe Ong",
      email: "chloe.ong@maxcredit-preview.sg",
      phone: "8765 4321",
      interest: "Payday Loan",
      amount: "1500",
      employment: "Employed part-time",
      monthlyIncome: "2100",
    },
    particulars: {
      idType: "NRIC",
      idNumber: "T0312876D",
      dob: "2003-05-18",
      nationality: "Singaporean",
      residentialStatus: "Singapore citizen",
      maritalStatus: "Single",
      dependants: "0",
      sex: "Female",
      education: "A level or diploma",
      block: "678A",
      street: "PUNGGOL DRIVE",
      building: "",
      floor: "14",
      unit: "509",
      postal: "821678",
      housingType: "HDB flat (4 room)",
      homeOwnership: "Living with family",
    },
    loanContext: {
      purpose: "Education expenses",
      purposeOther: "",
      purposeProof: "No",
      repaymentSource: "Monthly salary",
      repaymentOther: "",
      referral: "Loan comparison website",
      dateJoined: "2025-06-02",
      companyAddress: "",
      payday: "Twice a month",
      statementDelivery: "Email",
    },
    documents: [
      ["identity", "nric.pdf", 305_117],
      ["payslip", "payslip.pdf", 88_064, false],
    ],
  },
  {
    id: "1e7c3f5a-2b4d-4c6e-a8f0-3b5d7f9a1c43",
    reference: "MC-9A8B7C6D5E4F3A2B1C0D9E8F7A6B5C4D",
    received: 29 * HOUR,
    submitted: 29 * HOUR - 18 * MINUTE,
    changes: [[26 * HOUR, "in_review"]],
    assigned: [25 * HOUR, PREVIEW_ME],
    notes: [
      [
        25 * HOUR - 30 * MINUTE,
        PREVIEW_ME,
        "Called the applicant to confirm the renovation quotation.\nThe contractor's invoice will follow by Friday.",
      ],
    ],
    fields: {
      name: "Muhammad Hafiz bin Rahman",
      email: "hafiz.rahman@maxcredit-preview.sg",
      phone: "9876 5432",
      interest: "Home Renovation Loan",
      amount: "15000",
      employment: "Employed full-time",
      monthlyIncome: "5200",
    },
    particulars: {
      idType: "NRIC",
      idNumber: "S8734512B",
      dob: "1987-06-22",
      nationality: "Singaporean",
      residentialStatus: "Singapore citizen",
      maritalStatus: "Married",
      dependants: "2",
      sex: "Male",
      education: "ITE or Nitec",
      block: "456",
      street: "TAMPINES STREET 42",
      building: "",
      floor: "11",
      unit: "234",
      postal: "520456",
      housingType: "HDB flat (5 room or executive)",
      homeOwnership: "Owned",
    },
    loanContext: {
      purpose: "Home renovation",
      purposeOther: "",
      purposeProof: "Yes",
      repaymentSource: "Monthly salary",
      repaymentOther: "",
      referral: "Friend or family",
      dateJoined: "2015-08-17",
      companyAddress: "",
      payday: "Month end",
      statementDelivery: "Home address",
    },
    documents: [
      ["identity", "nric-scan.pdf", 356_902],
      ["payslip", "payslips-last-3-months.pdf", 241_776],
      ["purpose", "renovation-quotation.pdf", 184_320],
    ],
  },
  {
    id: "c3a5e7f9-4b6d-4f8a-9c1e-5d7f9b1d3e84",
    reference: "MC-4D6F8A0C2E4B6D8F0A2C4E6B8D0F2A4C",
    received: 9 * DAY,
    submitted: 9 * DAY - 25 * MINUTE,
    changes: [
      [8 * DAY + 20 * HOUR, "in_review"],
      [7 * DAY, "approved"],
    ],
    review: [
      7 * DAY - HOUR,
      "Active loan. Retain until both the loan and the borrower relationship have ended.",
    ],
    assigned: [8 * DAY + 21 * HOUR, PREVIEW_TEAM[1]],
    notes: [
      [
        8 * DAY + 19 * HOUR,
        PREVIEW_TEAM[1],
        "Existing debts checked against the credit bureau report. Consolidating three credit card balances.",
      ],
      [
        7 * DAY + HOUR,
        PREVIEW_TEAM[1],
        "Approved at SGD 20,000 over 24 months. Loan agreement signed at the office.",
      ],
    ],
    fields: {
      name: "Priya Nair",
      email: "priya.nair@maxcredit-preview.sg",
      phone: "9012 3456",
      interest: "Debt Consolidation Plan",
      amount: "20000",
      employment: "Employed full-time",
      monthlyIncome: "7800",
    },
    particulars: {
      idType: "NRIC",
      idNumber: "S7621458A",
      dob: "1976-11-03",
      nationality: "Indian",
      residentialStatus: "Permanent resident",
      maritalStatus: "Married",
      dependants: "1",
      sex: "Female",
      education: "Postgraduate",
      block: "31",
      street: "SIMEI STREET 4",
      building: "",
      floor: "05",
      unit: "07",
      postal: "529871",
      housingType: "Condominium or private apartment",
      homeOwnership: "Owned",
    },
    loanContext: {
      purpose: "Debt consolidation",
      purposeOther: "",
      purposeProof: "Yes",
      repaymentSource: "Monthly salary",
      repaymentOther: "",
      referral: "Returning customer",
      dateJoined: "2011-02-14",
      companyAddress: "",
      payday: "25th",
      statementDelivery: "Email",
    },
    assessment: {
      employer: "HARBOURLIGHT CONSULTING PTE LTD",
      occupation: "Finance Manager",
      monthlyExpenses: "2600",
      monthlyDebtPayments: "1850",
    },
    documents: [
      ["identity", "nric.pdf", 298_441],
      ["payslip", "payslip-august.pdf", 102_400],
      ["cpf", "cpf-statement.pdf", 150_223],
      ["tax", "noa-latest-year.pdf", 211_009],
    ],
  },
  {
    id: "f6b8d0a2-5c7e-4a9b-b1d3-6e8a0c2e4f15",
    reference: "MC-7B9D1F3A5C7E9B1D3F5A7C9E1B3D5F7A",
    received: 20 * DAY,
    submitted: 20 * DAY - 15 * MINUTE,
    changes: [
      [19 * DAY, "in_review"],
      [18 * DAY, "rejected"],
    ],
    review: [
      18 * DAY - 2 * HOUR,
      "Unapproved application. Retain for five years from receipt.",
    ],
    answers: { badDebt: "Yes" },
    fields: {
      name: "Rahul Verma",
      email: "rahul.verma@maxcredit-preview.sg",
      phone: "8123 9876",
      interest: "Foreigner Loan",
      amount: "3000",
      employment: "Employed full-time",
      monthlyIncome: "3200",
    },
    particulars: {
      idType: "FIN",
      idNumber: "G5528391K",
      dob: "1994-04-09",
      nationality: "Indian",
      residentialStatus: "Foreigner",
      maritalStatus: "Single",
      dependants: "0",
      sex: "Male",
      education: "Degree",
      block: "210",
      street: "JURONG EAST STREET 21",
      building: "",
      floor: "07",
      unit: "318",
      postal: "600210",
      housingType: "HDB flat (3 room)",
      homeOwnership: "Rented",
    },
    loanContext: {
      purpose: "Household bills",
      purposeOther: "",
      purposeProof: "No",
      repaymentSource: "Monthly salary",
      repaymentOther: "",
      referral: "Social media",
      dateJoined: "2025-11-03",
      companyAddress: "",
      payday: "7th",
      statementDelivery: "Email",
    },
    documents: [
      ["identity", "work-pass.pdf", 276_530],
      ["payslip", "payslip.pdf", 91_648],
    ],
  },
  {
    id: "2a4c6e8b-7d9f-4b1a-8e3c-9f1b3d5a7c96",
    reference: "MC-3E5A7C9B1D3F5E7A9C1B3D5F7E9A1C3B",
    received: 34 * DAY,
    submitted: 34 * DAY - 40 * MINUTE,
    changes: [
      [33 * DAY, "in_review"],
      [31 * DAY, "withdrawn"],
    ],
    fields: {
      name: "Lim Kok Seng",
      email: "kokseng.lim@maxcredit-preview.sg",
      phone: "6123 4567",
      interest: "Business Loan",
      amount: "50000",
      employment: "Self-employed",
      monthlyIncome: "9500",
    },
    particulars: {
      idType: "NRIC",
      idNumber: "S9245067H",
      dob: "1992-09-30",
      nationality: "Singaporean",
      residentialStatus: "Singapore citizen",
      maritalStatus: "Married",
      dependants: "3",
      sex: "Male",
      education: "A level or diploma",
      block: "14",
      street: "JALAN KEMBANGAN",
      building: "",
      floor: "",
      unit: "",
      postal: "419110",
      housingType: "Landed property",
      homeOwnership: "Owned",
    },
    loanContext: {
      purpose: "Business expenses",
      purposeOther: "",
      purposeProof: "Yes",
      repaymentSource: "Business income",
      repaymentOther: "",
      referral: "Walk-in",
      dateJoined: "",
      companyAddress: "10 Ubi Crescent, #03-12, Singapore 408564",
      payday: "Varies",
      statementDelivery: "Company address",
    },
    documents: [
      ["identity", "nric.pdf", 318_207],
      ["tax", "noa-2025-and-2026.pdf", 402_118],
    ],
  },
  {
    id: "4d6f8b0c-1a3e-4c5d-9f7b-2e4a6c8d0b37",
    reference: "MC-5F7B9D1E3A5C7E9B1D3F5A7C9E1B3D5A",
    received: 15 * DAY,
    submitted: null,
    archived: 12 * DAY,
    fields: {
      name: "Daniel Koh",
      email: "daniel.koh@maxcredit-preview.sg",
      phone: "9345 6789",
      interest: "Personal Loan",
      amount: "3000",
      employment: "Self-employed",
      monthlyIncome: "3800",
    },
    particulars: {
      idType: "NRIC",
      idNumber: "S8812345Z",
      dob: "1988-02-11",
      nationality: "Singaporean",
      residentialStatus: "Singapore citizen",
      maritalStatus: "Married",
      dependants: "1",
      sex: "Male",
      education: "Degree",
      block: "38",
      street: "BEDOK NORTH STREET 3",
      building: "",
      floor: "04",
      unit: "221",
      postal: "460038",
      housingType: "HDB flat (3 room)",
      homeOwnership: "Owned",
    },
    loanContext: {
      purpose: "Credit card bills",
      purposeOther: "",
      purposeProof: "No",
      repaymentSource: "Business income",
      repaymentOther: "",
      referral: "Google search",
      dateJoined: "",
      companyAddress: "",
      payday: "Varies",
      statementDelivery: "Email",
    },
    documents: [],
  },
];

function build(sample: Sample, index: number, now: number): ApplicationRecord {
  const at = (ago: number) => new Date(now - ago).toISOString();
  const createdAt = at(sample.received),
    submittedAt = sample.submitted === null ? null : at(sample.submitted);
  let status: Status = submittedAt ? "new" : "pending",
    revision = submittedAt ? 1 : 0;
  let lifecycle: ApplicationRecord["retention"]["lifecycle"] = {
    outcome: "unapproved",
  };
  const history: ApplicationRecord["history"] = [];
  for (const [ago, next] of sample.changes ?? []) {
    history.push({
      actor: PREVIEW_ACTOR,
      at: at(ago),
      field: "status",
      before: status,
      after: next,
    });
    if (next === "approved") {
      const before = JSON.stringify(lifecycle);
      lifecycle = {
        outcome: "approved",
        loanEndedAt: null,
        borrowerRelationshipEndedAt: null,
      };
      history.push({
        actor: PREVIEW_ACTOR,
        at: at(ago),
        field: "retention",
        before,
        after: JSON.stringify(lifecycle),
      });
    }
    status = next;
    revision++;
  }
  let retention: ApplicationRecord["retention"] = {
    policyVersion: RETENTION_POLICY_VERSION,
    receivedAt: createdAt,
    latestDocumentReceivedAt: submittedAt ?? createdAt,
    lifecycle,
    classificationReviewed: false,
    hold: "none",
  };
  let retentionReview: ApplicationRecord["retentionReview"] = {
    basis: "Awaiting classification",
    nextReviewAt: null,
  };
  if (sample.review) {
    const [ago, basis] = sample.review,
      reviewed = { ...retention, classificationReviewed: true },
      review = { basis, nextReviewAt: null };
    history.push({
      actor: PREVIEW_ACTOR,
      at: at(ago),
      field: "retention",
      before: JSON.stringify({ retention, review: retentionReview }),
      after: JSON.stringify({ retention: reviewed, review }),
    });
    retention = reviewed;
    retentionReview = review;
    revision++;
  }
  let archivedAt: string | null = null;
  if (sample.archived) {
    archivedAt = at(sample.archived);
    history.push({
      actor: PREVIEW_ACTOR,
      at: archivedAt,
      field: "archive",
      before: "active",
      after: "archived",
    });
    revision++;
  }
  let assignee: string | null = null;
  if (sample.assigned) {
    const [ago, email] = sample.assigned;
    history.push({
      actor: PREVIEW_ACTOR,
      at: at(ago),
      field: "assignment",
      before: "unassigned",
      after: email,
    });
    assignee = email;
    revision++;
  }
  history.sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0));
  const declaration = {
    ...Object.fromEntries(
      dueDiligenceKeys.map((key) => [
        key,
        key === "stillEmployed" ? "Yes" : "No",
      ]),
    ),
    ...sample.answers,
    bankruptcyDetails: "",
    convictionDetails: "",
    ...Object.fromEntries(declarationKeys.map((key) => [key, true])),
    version: DECLARATION_VERSION,
  };
  const myinfo = sample.myinfo
    ? demoMyinfoProfile(new Date(now - sample.received - 10 * MINUTE)).evidence
        ?.snapshot
    : undefined;
  return applicationRecordSchema.parse({
    id: sample.id,
    reference: sample.reference,
    ownerDigest: String(index).repeat(64),
    fingerprint: String(index + 1).repeat(64),
    fields: sample.fields,
    particulars: sample.particulars,
    loanContext: sample.loanContext,
    declaration,
    assessment: sample.assessment,
    myinfo,
    documents: sample.documents.map(
      ([kind, name, size, verified = true], position) => ({
        kind,
        name,
        type: "application/pdf",
        size,
        sha256: String(position).repeat(64),
        ticket: "preview-sample",
        path: `applications/uploads/${sample.id.slice(0, 35)}${position}`,
        generation: verified ? "1" : null,
      }),
    ),
    createdAt,
    submittedAt,
    status,
    revision,
    retention,
    retentionReview,
    deleting: false,
    archivedAt,
    assignee,
    history,
    notes: (sample.notes ?? []).map(([ago, author, text], index) => ({
      id: `${sample.id.slice(0, 34)}${String(index).padStart(2, "0")}`,
      author,
      at: at(ago),
      text,
    })),
    notification: {
      state: submittedAt ? "sent" : "waiting",
      leaseUntil: 0,
      attempts: submittedAt ? 1 : 0,
    },
  });
}
export const sampleApplications = (now = Date.now()) =>
  samples.map((sample, index) => build(sample, index, now));

let store: Map<string, ApplicationRecord> | undefined;
const records = () =>
  (store ??= new Map(
    sampleApplications().map((record) => [record.id, record]),
  ));
const uploads = new Map<string, Blob>();
export function resetStaffDemo() {
  store = undefined;
  uploads.clear();
}
export function staffDemoUpload(id: string, kind: string, blob: Blob) {
  uploads.set(`${id}:${kind}`, blob);
}
const hexDigest = async (blob: Blob) =>
  Array.from(
    new Uint8Array(
      await crypto.subtle.digest("SHA-256", await blob.arrayBuffer()),
    ),
    (byte) => byte.toString(16).padStart(2, "0"),
  ).join("");
const repo: ApplicationRepository = {
  get: async (id) => structuredClone(records().get(id) ?? null),
  change: async (id, update) => {
    const { record, result } = update(
      structuredClone(records().get(id) ?? null),
    );
    if (record) records().set(id, applicationRecordSchema.parse(record));
    return result;
  },
  list: async (after, limit = 50) =>
    [...records().values()]
      .filter((record) => !after || record.id > after)
      .sort((a, b) => a.id.localeCompare(b.id))
      .slice(0, limit),
  recent: async (query) =>
    structuredClone(recentPage([...records().values()], query)),
  ids: async (after, limit = 50) =>
    (await repo.list(after, limit)).map((record) => record.id),
  pendingNotifications: async () => [],
};

export function samplePdf(lines: string[]) {
  const text = lines
    .map(
      (line, index) =>
        `BT /F1 ${index ? 12 : 18} Tf 72 ${720 - index * 28} Td (${line.replace(/[^\x20-\x7e]/g, "").replace(/[\\()]/g, "\\$&")}) Tj ET`,
    )
    .join("\n");
  const objects = [
    "<< /Type /Catalog /Pages 2 0 R >>",
    "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
    "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>",
    "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
    `<< /Length ${text.length} >>\nstream\n${text}\nendstream`,
  ];
  let pdf = "%PDF-1.4\n";
  const offsets = objects.map((object, index) => {
    const offset = pdf.length;
    pdf += `${index + 1} 0 obj\n${object}\nendobj\n`;
    return offset;
  });
  const xref = pdf.length;
  return `${pdf}xref\n0 ${objects.length + 1}\n0000000000 65535 f \n${offsets.map((offset) => `${String(offset).padStart(10, "0")} 00000 n \n`).join("")}trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
}

export async function staffDemoApi(
  path: string,
  method = "GET",
  body?: unknown,
): Promise<unknown> {
  const url = new URL(path, "https://preview.invalid"),
    parts = url.pathname.split("/").slice(1);
  try {
    if (url.pathname === "/api/staff/session" && method === "DELETE")
      return { ok: true };
    if (url.pathname === "/api/staff/team" && method === "GET")
      return { me: PREVIEW_ME, team: PREVIEW_TEAM };
    if (
      parts.length < 3 ||
      parts.length > 5 ||
      parts.slice(0, 3).join("/") !== "api/staff/applications"
    )
      throw new ApplicationError(404, "Not found.");
    if (parts.length === 3 && method === "POST")
      return await createStaffApplication(
        body,
        { actor: PREVIEW_ACTOR, author: PREVIEW_ME, team: PREVIEW_TEAM },
        repo,
      );
    if (parts.length === 4 && parts[3] === "search" && method === "POST")
      return await searchStaffApplications(body, repo);
    const id = z.uuid().safeParse(parts[3]);
    if (parts.length === 4 && method === "GET") {
      if (!id.success)
        throw new ApplicationError(404, "Application not found.");
      const record = await repo.get(id.data);
      if (!record || record.deleting)
        throw new ApplicationError(404, "Application unavailable.");
      return staffView(record);
    }
    if (parts.length === 4 && method === "PATCH") {
      if (!id.success)
        throw new ApplicationError(404, "Application not found.");
      return await applyStaffChange(
        id.data,
        body,
        { actor: PREVIEW_ACTOR, author: PREVIEW_ME, team: PREVIEW_TEAM },
        repo,
      );
    }
    if (parts.length === 5 && parts[4] === "documents" && method === "POST") {
      const request = z
        .object({ action: z.enum(["prepare", "verify"]), value: z.unknown() })
        .strict()
        .safeParse(body);
      if (!id.success || !request.success)
        throw new ApplicationError(400, "Invalid document request.");
      if (request.data.action === "prepare") {
        const parsed = staffDocumentSchema.safeParse(request.data.value);
        if (!parsed.success)
          throw new ApplicationError(
            400,
            "Choose a PDF, JPG or PNG file of up to 10 MB.",
          );
        const kind = parsed.data.kind;
        await repo.change(id.data, (current) => {
          const record = uploadableDocument(current, kind);
          record.documents = [
            ...record.documents.filter((document) => document.kind !== kind),
            {
              ...parsed.data,
              ticket: `preview-${crypto.randomUUID()}`,
              path: `applications/uploads/${crypto.randomUUID()}`,
              generation: null,
            },
          ];
          return savedView(record);
        });
        uploads.delete(`${id.data}:${kind}`);
        return { kind, url: "https://preview.invalid/upload", headers: {} };
      }
      const parsed = z
        .object({ kind: z.enum(staffDocumentKinds) })
        .strict()
        .safeParse(request.data.value);
      if (!parsed.success)
        throw new ApplicationError(400, "Choose the document to check.");
      const kind = parsed.data.kind,
        pending = uploadableDocument(
          await repo.get(id.data),
          kind,
        ).documents.find((document) => document.kind === kind);
      if (!pending)
        throw new ApplicationError(404, "Choose the file again to upload it.");
      const blob = uploads.get(`${id.data}:${kind}`);
      if (
        !blob ||
        blob.size !== pending.size ||
        (await hexDigest(blob)) !== pending.sha256
      )
        throw new ApplicationError(
          400,
          "The document could not be verified. Upload it again.",
        );
      return await repo.change(id.data, (current) =>
        attachVerifiedDocument(
          uploadableDocument(current, kind),
          pending.ticket,
          "1",
          { actor: PREVIEW_ACTOR, author: PREVIEW_ME, team: PREVIEW_TEAM },
        ),
      );
    }
    if (parts.length === 5 && parts[4] === "download" && method === "POST") {
      const input = z
        .object({ kind: z.enum(staffDocumentKinds) })
        .strict()
        .safeParse(body);
      if (!id.success || !input.success)
        throw new ApplicationError(400, "Document unavailable.");
      const record = await repo.get(id.data);
      if (!record || record.deleting)
        throw new ApplicationError(404, "Document unavailable.");
      const document = record.documents.find(
        (document) => document.kind === input.data.kind,
      );
      if (!document?.generation)
        throw new ApplicationError(
          404,
          "This document has not been verified or is unavailable.",
        );
      const pdf = samplePdf([
        "MaxCredit staff desk preview",
        "SAMPLE DOCUMENT - NOT A REAL RECORD",
        `${staffDocumentLabels[document.kind]} for ${record.fields.name}`,
        `Application ${record.reference}`,
        "This synthetic file stands in for the applicant's upload.",
      ]);
      const uploaded = uploads.get(`${record.id}:${document.kind}`);
      return {
        url: URL.createObjectURL(
          uploaded ?? new Blob([pdf], { type: document.type }),
        ),
        name: document.name,
        expiresIn: 60,
      };
    }
    throw new ApplicationError(404, "Not found.");
  } catch (error) {
    throw new Error(
      error instanceof ApplicationError
        ? error.message
        : "The request failed. Please retry.",
    );
  }
}
