import { z } from "zod";

const line = z
  .string()
  .max(300)
  .refine((value) => !/[\u0000-\u001f]/.test(value));
const nullableLine = line.nullable();
const metadata = {
  status: z.enum(["available", "unavailable", "invalid"]),
  source: z.enum(["1", "2", "3", "4"]).nullable(),
  lastUpdated: z.iso.date().nullable(),
};
const scalarSchema = z
  .object({ ...metadata, value: nullableLine, code: nullableLine })
  .strict();
type Scalar = z.infer<typeof scalarSchema>;
export const identityLabels = {
  uinfin: "NRIC / FIN",
  aliasname: "Alias name",
  dob: "Date of birth",
  sex: "Sex",
  marital: "Marital status",
  birthcountry: "Country / place of birth",
  nationality: "Nationality / citizenship",
  residentialstatus: "Residential status",
  passtype: "Pass type",
  passstatus: "Pass status",
  passexpirydate: "Pass expiry date",
  hdbtype: "HDB flat type",
  housingtype: "Private housing type",
  hdbownership: "Owns the HDB flat at the registered address",
} as const;
export const addressLabels = {
  block: "Block / house",
  street: "Street",
  building: "Building",
  floor: "Floor",
  unit: "Unit",
  postal: "Postal code",
  country: "Country",
  line1: "Address line 1",
  line2: "Address line 2",
} as const;
export const myinfoIdentitySchema = z
  .object({
    uinfin: scalarSchema,
    aliasname: scalarSchema,
    dob: scalarSchema,
    birthcountry: scalarSchema,
    nationality: scalarSchema,
    residentialstatus: scalarSchema,
    passtype: scalarSchema,
    passstatus: scalarSchema,
    passexpirydate: scalarSchema,
    sex: scalarSchema.optional(),
    marital: scalarSchema.optional(),
    hdbtype: scalarSchema.optional(),
    housingtype: scalarSchema.optional(),
    hdbownership: scalarSchema.optional(),
    regadd: z
      .object({
        ...metadata,
        type: z.enum(["SG", "UNFORMATTED"]).nullable(),
        block: nullableLine,
        street: nullableLine,
        building: nullableLine,
        floor: nullableLine,
        unit: nullableLine,
        postal: nullableLine,
        country: nullableLine,
        countryCode: nullableLine,
        line1: nullableLine,
        line2: nullableLine,
      })
      .strict(),
  })
  .strict();
export type MyinfoIdentity = z.infer<typeof myinfoIdentitySchema>;
export const MYINFO_IDENTITY_SCOPES =
  "uinfin aliasname dob sex marital birthcountry nationality residentialstatus regadd hdbtype housingtype hdbownership.address passtype passstatus passexpirydate";
const object = (input: unknown): Record<string, unknown> =>
  input && typeof input === "object" && !Array.isArray(input)
    ? (input as Record<string, unknown>)
    : {};
const absent = (input: unknown) =>
  input == null || input === "" || input === "NA" || input === "-";
function meta(field: Record<string, unknown>) {
  const source = z.enum(["1", "2", "3", "4"]).safeParse(field.source),
    date = z.iso.date().safeParse(field.lastupdated);
  return {
    source: source.success ? source.data : null,
    lastUpdated: date.success ? date.data : null,
  };
}
function scalar(input: unknown, validator: z.ZodType = line) {
  const field = object(input),
    metadata = meta(field);
  const empty = {
    ...metadata,
    status: "unavailable" as const,
    value: null,
    code: null,
  };
  if (field.unavailable === true || field.source === "3") return empty;
  const value = !absent(field.value)
    ? field.value
    : !absent(field.desc)
      ? field.desc
      : field.code;
  if (absent(value)) return empty;
  const parsed = validator.safeParse(value),
    code = nullableLine.safeParse(absent(field.code) ? null : field.code);
  if (!parsed.success || !code.success)
    return { ...empty, status: "invalid" as const };
  return {
    ...metadata,
    status: "available" as const,
    value: parsed.data as string,
    code: code.data,
  };
}
const addressPart = (value: string | null, trimZeros = false) => {
  const text = (value ?? "").toUpperCase().replace(/[\s-]/g, "");
  return trimZeros ? text.replace(/^0+(?=.)/, "") : text;
};
function hdbOwnership(
  input: unknown,
  regadd: MyinfoIdentity["regadd"],
): Scalar {
  const empty = {
    source: null,
    lastUpdated: null,
    status: "unavailable" as const,
    value: null,
    code: null,
  };
  if (input == null || object(input).unavailable === true) return empty;
  if (
    !Array.isArray(input) ||
    input.length > 50 ||
    input.some((row) => !row || typeof row !== "object" || Array.isArray(row))
  )
    return { ...empty, status: "invalid" };
  if (regadd.status !== "available" || regadd.type !== "SG" || !regadd.postal)
    return empty;
  const readable = input
    .map(object)
    .filter((row) => row.unavailable !== true && row.source !== "3")
    .map((row) => {
      const address = object(row.address);
      return {
        row,
        parts: {
          postal: scalar(address.postal),
          block: scalar(address.block),
          floor: scalar(address.floor),
          unit: scalar(address.unit),
        },
      };
    })
    .filter(({ parts }) =>
      (["postal", "block", "floor", "unit"] as const).every(
        (key) =>
          parts[key].status === "available" ||
          (key !== "postal" &&
            !regadd[key] &&
            parts[key].status === "unavailable"),
      ),
    );
  const owned = readable.find(
    ({ parts }) =>
      addressPart(parts.postal.value) === addressPart(regadd.postal) &&
      addressPart(parts.block.value) === addressPart(regadd.block) &&
      addressPart(parts.floor.value, true) ===
        addressPart(regadd.floor, true) &&
      addressPart(parts.unit.value, true) === addressPart(regadd.unit, true),
  );
  if (!owned && readable.length !== input.length) return empty;
  const record = (owned ?? readable[0])?.row;
  return {
    ...(record ? meta(record) : { source: null, lastUpdated: null }),
    status: "available",
    value: owned ? "Yes" : "No",
    code: null,
  };
}
const birthDate = z.union([
  z.iso.date(),
  z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/),
  z.string().regex(/^\d{4}$/),
]);
export function mapMyinfoIdentity(input: unknown): MyinfoIdentity {
  const person = object(input),
    address = object(person.regadd);
  const blank = {
    ...meta(address),
    status: "unavailable" as const,
    type: null,
    block: null,
    street: null,
    building: null,
    floor: null,
    unit: null,
    postal: null,
    country: null,
    countryCode: null,
    line1: null,
    line2: null,
  };
  let regadd: MyinfoIdentity["regadd"] = blank;
  if (
    address.unavailable !== true &&
    address.source !== "3" &&
    !absent(address.type)
  ) {
    const keys =
      address.type === "SG"
        ? ([
            "block",
            "street",
            "building",
            "floor",
            "unit",
            "postal",
            "country",
          ] as const)
        : (["line1", "line2"] as const);
    const fields = keys.map((key) => [key, scalar(address[key])] as const);
    const invalid =
      !["SG", "UNFORMATTED"].includes(String(address.type)) ||
      fields.some(([, field]) => field.status === "invalid");
    regadd = invalid
      ? { ...blank, status: "invalid" }
      : {
          ...blank,
          type: address.type as "SG" | "UNFORMATTED",
          status: fields.some(([, field]) => field.status === "available")
            ? "available"
            : "unavailable",
          ...Object.fromEntries(
            fields.map(([key, field]) => [key, field.value]),
          ),
          countryCode:
            address.type === "SG" ? scalar(address.country).code : null,
        };
  }
  return myinfoIdentitySchema.parse({
    uinfin: scalar(person.uinfin, z.string().regex(/^[STFGM]\d{7}[A-Z]$/)),
    aliasname: scalar(person.aliasname),
    dob: scalar(person.dob, birthDate),
    birthcountry: scalar(person.birthcountry),
    nationality: scalar(person.nationality),
    residentialstatus: scalar(person.residentialstatus),
    passtype: scalar(person.passtype),
    passstatus: scalar(person.passstatus),
    passexpirydate: scalar(person.passexpirydate, z.iso.date()),
    sex: scalar(person.sex),
    marital: scalar(person.marital),
    hdbtype: scalar(person.hdbtype),
    housingtype: scalar(person.housingtype),
    hdbownership: hdbOwnership(person.hdbownership, regadd),
    regadd,
  });
}
