import { readMyinfoEvidence } from "./myinfo-evidence";
import type { MyinfoSnapshot } from "./application-assessment";
import "server-only";
import { createHash } from "node:crypto";
import {
  ApplicationError,
  type ApplicationRecord,
  type PrepareApplication,
} from "./application-record";
import {
  issueUploadTicket,
  readUploadTicket,
  uploadObjectPath,
  verifyUploadedDocument,
  UploadRejected,
  UPLOAD_TICKET_LIFETIME_MS,
} from "./application-upload";
import { staffDocumentLabels } from "./application-schema";
import { RETENTION_POLICY_VERSION } from "./application-retention";
import type { ApplicationProviders } from "./application-providers";
import {
  identityTypeFor,
  myinfoIdentityNumber,
} from "./application-particulars";
export const applicationDigest = (value: string) =>
  createHash("sha256").update(value).digest("hex");
export function ownedRecord(
  record: ApplicationRecord | null,
  capability: string,
): ApplicationRecord {
  if (!record || record.ownerDigest !== applicationDigest(capability))
    throw new ApplicationError(
      404,
      "Application attempt not found. Restore your draft and retry.",
    );
  if (record.deleting)
    throw new ApplicationError(
      409,
      "This record is being deleted and cannot be changed.",
    );
  return record;
}
export async function prepareApplication(
  input: PrepareApplication,
  providers: ApplicationProviders,
) {
  const now = Date.now(),
    receivedAt = new Date(now).toISOString();
  const fingerprint = applicationDigest(
    JSON.stringify({
      fields: input.fields,
      particulars: input.particulars,
      loanContext: input.loanContext,
      declaration: input.declaration,
      documents: input.documents,
      ...(input.assessment ? { assessment: input.assessment } : {}),
      ...(input.myinfoReceipt
        ? { myinfoReceiptDigest: applicationDigest(input.myinfoReceipt) }
        : {}),
    }),
  );
  let myinfo: MyinfoSnapshot | undefined;
  const existing = input.myinfoReceipt
    ? await providers.repo.get(input.requestId)
    : null;
  if (input.myinfoReceipt && !existing) {
    try {
      myinfo = await readMyinfoEvidence(
        input.myinfoReceipt,
        providers.config.APPLICATION_SIGNING_SECRET,
        providers.origin,
      );
      if (!providers.mock && myinfo.environment !== "production")
        throw new Error();
      if (
        myinfo.principalName &&
        myinfo.principalName.trim() !== input.fields.name
      )
        throw new Error();
      const identityNumber = myinfoIdentityNumber(myinfo.identity);
      if (
        identityNumber &&
        (identityNumber !== input.particulars.idNumber ||
          identityTypeFor(identityNumber) !== input.particulars.idType)
      )
        throw new Error();
    } catch {
      throw new ApplicationError(
        400,
        "Myinfo records expired or could not be verified. Retrieve them again or remove them and continue manually.",
      );
    }
  }
  const documents = input.documents.map((document) => {
    const { name, ...descriptor } = document;
    const issued = issueUploadTicket(
      descriptor,
      input.capability,
      providers.config.APPLICATION_SIGNING_SECRET,
      now,
    );
    return {
      ...document,
      name,
      ticket: issued.ticket,
      path: uploadObjectPath(issued.data),
      generation: null,
    };
  });
  const record = await providers.repo.change(input.requestId, (current) => {
    if (current) {
      const record = ownedRecord(current, input.capability);
      if (record.fingerprint !== fingerprint)
        throw new ApplicationError(
          409,
          "This attempt has different details. Start a new attempt before submitting changes.",
        );
      return { record, result: record };
    }
    if (input.myinfoReceipt && !myinfo)
      throw new ApplicationError(
        409,
        "This attempt changed. Start a new attempt.",
      );
    const record: ApplicationRecord = {
      id: input.requestId,
      reference: `MC-${input.requestId.replaceAll("-", "").toUpperCase()}`,
      ownerDigest: applicationDigest(input.capability),
      fingerprint,
      fields: input.fields,
      particulars: input.particulars,
      loanContext: input.loanContext,
      declaration: input.declaration,
      ...(input.assessment ? { assessment: input.assessment } : {}),
      ...(myinfo ? { myinfo } : {}),
      documents,
      createdAt: receivedAt,
      submittedAt: null,
      status: "pending",
      revision: 0,
      deleting: false,
      archivedAt: null,
      notes: [],
      assignee: null,
      source: "applicant",
      retention: {
        policyVersion: RETENTION_POLICY_VERSION,
        receivedAt,
        latestDocumentReceivedAt: new Date(
          now + (documents.length ? UPLOAD_TICKET_LIFETIME_MS : 0),
        ).toISOString(),
        lifecycle: { outcome: "unapproved" },
        classificationReviewed: false,
        hold: "none",
      },
      retentionReview: { basis: "Awaiting classification", nextReviewAt: null },
      notification: { state: "waiting", leaseUntil: 0, attempts: 0 },
      history: [],
    };
    return { record, result: record };
  });
  if (record.submittedAt)
    return { submitted: true, reference: record.reference, targets: [] };
  if (record.status !== "pending")
    throw new ApplicationError(
      409,
      "This attempt has been replaced. Restore the latest draft.",
    );
  const targets = await Promise.all(
    record.documents.map(async (document) => ({
      kind: document.kind,
      ...(await providers.target(
        readUploadTicket(
          document.ticket,
          input.capability,
          providers.config.APPLICATION_SIGNING_SECRET,
          now,
        ),
      )),
    })),
  );
  return { submitted: false, reference: record.reference, targets };
}
export async function notifyApplication(
  id: string,
  providers: ApplicationProviders,
) {
  const now = Date.now();
  const claimed = await providers.repo.change(id, (record) => {
    if (
      !record ||
      !record.submittedAt ||
      record.deleting ||
      record.notification.state !== "pending" ||
      record.notification.leaseUntil > now
    )
      return { record, result: null };
    if (now - Date.parse(record.submittedAt) >= 23 * 3600000) {
      record.notification.state = "review";
      return { record, result: null };
    }
    record.notification.leaseUntil = now + 60000;
    record.notification.attempts++;
    return { record, result: record };
  });
  if (!claimed) return;
  try {
    await providers.notify(claimed.id, claimed.reference);
    await providers.repo.change(id, (record) => {
      if (
        record &&
        !record.deleting &&
        record.notification.leaseUntil === now + 60000
      ) {
        record.notification.state = "sent";
        record.notification.leaseUntil = 0;
      }
      return { record, result: undefined };
    });
  } catch {}
}
export async function finalizeApplication(
  id: string,
  capability: string,
  providers: ApplicationProviders,
) {
  const record = ownedRecord(await providers.repo.get(id), capability);
  if (!record.submittedAt) {
    if (record.status !== "pending")
      throw new ApplicationError(
        409,
        "This attempt has been replaced. Restore the latest draft.",
      );
    const documents = await Promise.all(
      record.documents.map(async (document) => {
        try {
          const verified = await verifyUploadedDocument(
            readUploadTicket(
              document.ticket,
              capability,
              providers.config.APPLICATION_SIGNING_SECRET,
              Date.now(),
            ),
            providers.storage,
          );
          return { ...document, generation: verified.generation };
        } catch (error) {
          if (error instanceof UploadRejected && error.content)
            throw new ApplicationError(
              422,
              `We could not verify the file you attached as "${staffDocumentLabels[document.kind]}". Select Check result and edit, remove that document and attach the original file again.`,
            );
          throw error;
        }
      }),
    );
    await providers.repo.change(id, (current) => {
      const latest = ownedRecord(current, capability);
      if (!latest.submittedAt) {
        if (latest.status !== "pending")
          throw new ApplicationError(
            409,
            "This attempt has been replaced. Restore the latest draft.",
          );
        latest.documents = documents;
        latest.submittedAt = new Date().toISOString();
        latest.status = "new";
        latest.revision++;
        latest.history.push({
          actor: "applicant",
          at: latest.submittedAt,
          field: "status",
          before: "pending",
          after: "new",
        });
        if (latest.archivedAt) {
          latest.history.push({
            actor: "applicant",
            at: latest.submittedAt,
            field: "archive",
            before: "archived",
            after: "active",
          });
          latest.archivedAt = null;
        }
        latest.notification.state = "pending";
      }
      return { record: latest, result: undefined };
    });
  }
  await notifyApplication(id, providers);
  return { ok: true, reference: record.reference, mock: providers.mock };
}

export async function reviseApplication(
  id: string,
  capability: string,
  providers: ApplicationProviders,
) {
  return providers.repo.change(id, (current) => {
    if (!current)
      return { record: null, result: { submitted: false, reference: "" } };
    const record = ownedRecord(current, capability);
    if (record.submittedAt)
      return {
        record,
        result: { submitted: true, reference: record.reference },
      };
    if (record.status !== "withdrawn") {
      record.history.push({
        actor: "applicant",
        at: new Date().toISOString(),
        field: "status",
        before: record.status,
        after: "withdrawn",
      });
      record.status = "withdrawn";
      record.revision++;
    }
    return { record, result: { submitted: false, reference: "" } };
  });
}
