import {
  myinfoIdentitySchema,
  mapMyinfoIdentity,
  MYINFO_IDENTITY_SCOPES,
} from "./myinfo-identity";
import { z } from "zod";

const text = z
  .string()
  .max(200)
  .refine((value) => !/[\u0000-\u001f]/.test(value), "Use a single line.");
const optionalAmount = z.union([
  z.literal(""),
  z
    .string()
    .regex(/^\d{1,8}(\.\d{1,2})?$/)
    .refine((value) => Number(value) <= 10000000),
]);
export const assessmentSchema = z
  .object({
    employer: text,
    occupation: text,
    monthlyExpenses: optionalAmount,
    monthlyDebtPayments: optionalAmount,
  })
  .strict();
export const assessmentDraftSchema = z
  .object({
    employer: z.string().max(200),
    occupation: z.string().max(200),
    monthlyExpenses: z.string().max(12),
    monthlyDebtPayments: z.string().max(12),
  })
  .strict();
export type Assessment = z.infer<typeof assessmentSchema>;
export const emptyAssessment: Assessment = {
  employer: "",
  occupation: "",
  monthlyExpenses: "",
  monthlyDebtPayments: "",
};
export const assessmentLabels: Record<keyof Assessment, string> = {
  employer: "Current employer (optional)",
  occupation: "Occupation (optional)",
  monthlyExpenses: "Monthly living expenses (SGD, optional)",
  monthlyDebtPayments: "Existing monthly debt payments (SGD, optional)",
};

const money = z
  .string()
  .regex(/^-?\d{1,12}(\.\d{1,6})?$/)
  .nullable();
const month = z
  .string()
  .regex(/^\d{4}-(0[1-9]|1[0-2])$/)
  .nullable();
const metadata = {
  status: z.enum(["available", "unavailable", "invalid"]),
  source: z.enum(["1", "2", "3", "4"]).nullable(),
  lastUpdated: z.iso.date().nullable(),
};
export const cpfEntrySchema = z
  .object({
    paidOn: z.iso.date().nullable(),
    month,
    employer: text.max(100).nullable(),
    amount: money,
  })
  .strict();
export const noaEntrySchema = z
  .object({
    year: z
      .string()
      .regex(/^\d{4}$/)
      .nullable(),
    category: text.nullable(),
    taxClearance: z.enum(["Y", "N"]).nullable(),
    assessableIncome: money,
    employmentIncome: money,
    tradeIncome: money,
    rentIncome: money,
    interestIncome: money,
  })
  .strict();
export const myinfoSnapshotSchema = z
  .object({
    version: z.union([z.literal(1), z.literal(2)]),
    identity: myinfoIdentitySchema.optional(),
    environment: z.enum(["staging", "production"]),
    retrievedAt: z.iso.datetime(),
    principalName: text.nullable(),
    cpf: z
      .object({ ...metadata, entries: z.array(cpfEntrySchema).max(500) })
      .strict(),
    tax: z
      .object({ ...metadata, entries: z.array(noaEntrySchema).max(20) })
      .strict(),
    employer: z.object({ ...metadata, value: text.nullable() }).strict(),
    occupation: z.object({ ...metadata, value: text.nullable() }).strict(),
  })
  .strict()
  .refine(
    (value) => value.version === 1 || Boolean(value.identity),
    "Identity records required for snapshot version 2.",
  );
export type MyinfoSnapshot = z.infer<typeof myinfoSnapshotSchema>;
export const myinfoEvidenceSchema = z
  .object({
    snapshot: myinfoSnapshotSchema,
    receipt: z.string().min(1).max(400000),
    expiresAt: z.iso.datetime(),
  })
  .strict();
export type MyinfoEvidence = z.infer<typeof myinfoEvidenceSchema>;
export const MYINFO_SCOPES = `openid name email mobileno cpfcontributions noahistory employment occupation ${MYINFO_IDENTITY_SCOPES}`;

const object = (input: unknown): Record<string, unknown> =>
  input && typeof input === "object" && !Array.isArray(input)
    ? (input as Record<string, unknown>)
    : {};
const rawValue = (input: unknown): unknown => {
  const field = object(input);
  return field.unavailable === true ? null : (field.value ?? null);
};
const stringValue = (input: unknown): unknown => {
  const value = rawValue(input);
  return value === null || value === "" || value === "NA" || value === "-"
    ? null
    : typeof value === "number" && Number.isFinite(value)
      ? String(value)
      : value;
};
function meta(input: Record<string, unknown>) {
  const source = z.enum(["1", "2", "3", "4"]).safeParse(input.source);
  const date = z.iso.date().safeParse(input.lastupdated);
  return {
    source: source.success ? source.data : null,
    lastUpdated: date.success ? date.data : null,
  };
}
function collection<T>(
  input: unknown,
  key: string,
  schema: z.ZodType<T>,
  max: number,
  map: (row: Record<string, unknown>) => unknown,
) {
  const field = object(input),
    metadata = meta(field);
  if (field.unavailable === true || field[key] == null)
    return { ...metadata, status: "unavailable" as const, entries: [] as T[] };
  const rows = field[key];
  const parsed = z
    .array(schema)
    .max(max)
    .safeParse(
      Array.isArray(rows)
        ? rows.map((row) =>
            row && typeof row === "object" && !Array.isArray(row)
              ? map(object(row))
              : row,
          )
        : rows,
    );
  return parsed.success
    ? { ...metadata, status: "available" as const, entries: parsed.data }
    : { ...metadata, status: "invalid" as const, entries: [] as T[] };
}
function scalar(input: unknown) {
  const field = object(input),
    metadata = meta(field),
    value = stringValue(field);
  if (value === null)
    return { ...metadata, status: "unavailable" as const, value: null };
  const parsed = text.safeParse(value);
  return parsed.success
    ? { ...metadata, status: "available" as const, value: parsed.data }
    : { ...metadata, status: "invalid" as const, value: null };
}
export function mapMyinfoSnapshot(
  input: unknown,
  environment: "staging" | "production",
  retrievedAt = new Date().toISOString(),
): MyinfoSnapshot {
  const person = object(input);
  const cpf = collection(
    person.cpfcontributions,
    "history",
    cpfEntrySchema,
    500,
    (row) => ({
      paidOn: stringValue(row.date),
      month: stringValue(row.month),
      employer: stringValue(row.employer),
      amount: stringValue(row.amount),
    }),
  );
  cpf.entries.sort(
    (a, b) =>
      (a.paidOn ?? "").localeCompare(b.paidOn ?? "") ||
      (a.month ?? "").localeCompare(b.month ?? ""),
  );
  const tax = collection(
    person.noahistory,
    "noas",
    noaEntrySchema,
    20,
    (row) => ({
      year: stringValue(row.yearofassessment),
      category: stringValue(row.category),
      taxClearance: stringValue(row.taxclearance),
      assessableIncome: stringValue(row.amount),
      employmentIncome: stringValue(row.employment),
      tradeIncome: stringValue(row.trade),
      rentIncome: stringValue(row.rent),
      interestIncome: stringValue(row.interest),
    }),
  );
  return myinfoSnapshotSchema.parse({
    version: 2,
    identity: mapMyinfoIdentity(person),
    environment,
    retrievedAt,
    principalName: scalar(person.name).value,
    cpf,
    tax,
    employer: scalar(person.employment),
    occupation: scalar(person.occupation),
  });
}
export function assessmentFromMyinfo(
  snapshot: MyinfoSnapshot,
): Partial<Pick<Assessment, "employer" | "occupation">> {
  const since = new Date(Date.parse(snapshot.retrievedAt) - 92 * 24 * 3600_000)
    .toISOString()
    .slice(0, 10);
  const recent =
    snapshot.cpf.status === "available"
      ? [...snapshot.cpf.entries]
          .reverse()
          .find(
            (entry) => entry.employer && entry.paidOn && entry.paidOn >= since,
          )
      : undefined;
  const result: Partial<Pick<Assessment, "employer" | "occupation">> = {};
  for (const [key, value] of [
    ["employer", snapshot.employer.value ?? recent?.employer],
    ["occupation", snapshot.occupation.value],
  ] as const) {
    if (value && assessmentSchema.shape[key].safeParse(value).success)
      result[key] = value;
  }
  return result;
}
export function myinfoLockedAssessment(
  current: Assessment,
  snapshot: MyinfoSnapshot | undefined,
): ("employer" | "occupation")[] {
  const suggested = snapshot ? assessmentFromMyinfo(snapshot) : {};
  return (["employer", "occupation"] as const).filter(
    (key) => suggested[key] !== undefined && current[key] === suggested[key],
  );
}
