import { z } from "zod";
import type { MyinfoIdentity } from "./myinfo-identity";
import { hasVisibleText, singleLineText } from "./application-text";
export const idTypes = ["NRIC", "FIN", "Passport"] as const;
export const residentialStatuses = [
  "Singapore citizen",
  "Permanent resident",
  "Foreigner",
] as const;
export const maritalStatuses = [
  "Single",
  "Married",
  "Divorced",
  "Widowed",
] as const;
export const dependantOptions = ["0", "1", "2", "3", "4", "5 or more"] as const;
export const sexOptions = ["Female", "Male"] as const;
export const educationLevels = [
  "Primary",
  "Secondary",
  "ITE or Nitec",
  "A level or diploma",
  "Degree",
  "Postgraduate",
] as const;
export const housingTypes = [
  "HDB flat (1 or 2 room)",
  "HDB flat (3 room)",
  "HDB flat (4 room)",
  "HDB flat (5 room or executive)",
  "Condominium or private apartment",
  "Landed property",
  "Dormitory or shared accommodation",
  "Other",
] as const;
export const homeOwnerships = [
  "Owned",
  "Rented",
  "Living with family",
] as const;

export const particularsKeys = [
  "idType",
  "idNumber",
  "dob",
  "nationality",
  "residentialStatus",
  "maritalStatus",
  "dependants",
  "sex",
  "education",
  "block",
  "street",
  "building",
  "floor",
  "unit",
  "postal",
  "housingType",
  "homeOwnership",
] as const;
export type ParticularsKey = (typeof particularsKeys)[number];
export type Particulars = Record<ParticularsKey, string>;
export const emptyParticulars = Object.fromEntries(
  particularsKeys.map((key) => [key, ""]),
) as Particulars;
export const particularsSections: readonly {
  title: string;
  keys: readonly ParticularsKey[];
}[] = [
  {
    title: "Identification",
    keys: ["idType", "idNumber", "dob", "nationality", "residentialStatus"],
  },
  {
    title: "Household",
    keys: ["maritalStatus", "dependants", "sex", "education"],
  },
  {
    title: "Home address",
    keys: [
      "block",
      "street",
      "building",
      "floor",
      "unit",
      "postal",
      "housingType",
      "homeOwnership",
    ],
  },
];
export const particularsLabels: Record<ParticularsKey, string> = {
  idType: "Identification type",
  idNumber: "NRIC, FIN or passport number",
  dob: "Date of birth",
  nationality: "Nationality",
  residentialStatus: "Residential status",
  maritalStatus: "Marital status",
  dependants: "Number of dependants",
  sex: "Sex",
  education: "Highest education",
  block: "Block or house number",
  street: "Street name",
  building: "Building name",
  floor: "Floor",
  unit: "Unit number",
  postal: "Postal code",
  housingType: "Type of residence",
  homeOwnership: "Home ownership",
};
export const optionalParticulars: readonly ParticularsKey[] = [
  "dependants",
  "sex",
  "education",
  "building",
  "floor",
  "unit",
];
export const particularOptions: Partial<
  Record<ParticularsKey, readonly string[]>
> = {
  idType: idTypes,
  residentialStatus: residentialStatuses,
  maritalStatus: maritalStatuses,
  dependants: dependantOptions,
  sex: sexOptions,
  education: educationLevels,
  housingType: housingTypes,
  homeOwnership: homeOwnerships,
};

const weights = [2, 7, 6, 5, 4, 3, 2];
export function validIdentityNumber(value: string) {
  const match = /^([STFGM])(\d{7})([A-Z])$/.exec(value);
  if (!match) return false;
  const [, prefix, digits, check] = match;
  if (prefix === "M") return true;
  const sum = [...digits].reduce(
    (total, digit, index) => total + Number(digit) * weights[index],
    prefix === "T" || prefix === "G" ? 4 : 0,
  );
  return (
    (prefix === "S" || prefix === "T" ? "JZIHGFEDCBA" : "XWUTRQPNMLK")[
      sum % 11
    ] === check
  );
}
export const singaporeDate = (now = Date.now()) =>
  new Intl.DateTimeFormat("en-CA", {
    timeZone: "Asia/Singapore",
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  }).format(now);
export function ageOn(dob: string, today: string) {
  const [year, month, day] = dob.split("-").map(Number),
    [nowYear, nowMonth, nowDay] = today.split("-").map(Number);
  return (
    nowYear -
    year -
    (nowMonth < month || (nowMonth === month && nowDay < day) ? 1 : 0)
  );
}

const required = (message: string, max: number) =>
  singleLineText(max).refine(hasVisibleText, message);
const optionalChoice = <T extends readonly [string, ...string[]]>(options: T) =>
  z.union([z.literal(""), z.enum(options)], "Choose an option from the list.");
const shape = {
  idType: z.enum(idTypes, "Choose an identification type."),
  idNumber: z
    .string()
    .trim()
    .toUpperCase()
    .min(1, "Enter your identification number.")
    .max(20, "Use no more than 20 characters."),
  dob: z
    .string()
    .min(1, "Enter your date of birth.")
    .pipe(z.iso.date("Enter a valid date of birth.")),
  nationality: required("Enter your nationality.", 60).regex(
    /^\p{L}[\p{L} .,'()-]*$/u,
    "Use letters only for your nationality.",
  ),
  residentialStatus: z.enum(
    residentialStatuses,
    "Choose your residential status.",
  ),
  maritalStatus: z.enum(maritalStatuses, "Choose your marital status."),
  dependants: optionalChoice(dependantOptions),
  sex: optionalChoice(sexOptions),
  education: optionalChoice(educationLevels),
  block: required("Enter your block or house number.", 10).regex(
    /^[A-Z0-9][A-Z0-9 /-]*$/i,
    "Use letters, digits, spaces, slashes or hyphens.",
  ),
  street: required("Enter your street name.", 100),
  building: singleLineText(100),
  floor: z
    .string()
    .trim()
    .regex(/^(\d{1,3})?$/, "Enter the floor as 1 to 3 digits."),
  unit: z
    .string()
    .trim()
    .regex(
      /^([A-Z0-9][A-Z0-9-]{0,5})?$/i,
      "Enter a unit number of up to 6 letters, digits or hyphens.",
    ),
  postal: z
    .string()
    .trim()
    .regex(/^\d{6}$/, "Enter a 6-digit postal code."),
  housingType: z.enum(housingTypes, "Choose your type of residence."),
  homeOwnership: z.enum(homeOwnerships, "Choose your home ownership."),
} satisfies Record<ParticularsKey, z.ZodType<string>>;

function crossIssues(input: Particulars, checkAge: boolean) {
  const issues: { key: ParticularsKey; message: string }[] = [];
  const id = input.idNumber.trim().toUpperCase(),
    type = input.idType,
    status = input.residentialStatus;
  if (id && type === "NRIC" && !(/^[ST]/.test(id) && validIdentityNumber(id)))
    issues.push({
      key: "idNumber",
      message: "Enter a valid NRIC, such as S1234567D.",
    });
  if (id && type === "FIN" && !(/^[FGM]/.test(id) && validIdentityNumber(id)))
    issues.push({
      key: "idNumber",
      message: "Enter a valid FIN, such as G1234567X.",
    });
  if (id && type === "Passport" && !/^[A-Z0-9]{5,20}$/.test(id))
    issues.push({
      key: "idNumber",
      message: "Enter a passport number of 5 to 20 letters or digits.",
    });
  if (type === "NRIC" && status === "Foreigner")
    issues.push({
      key: "residentialStatus",
      message:
        "Choose Singapore citizen or permanent resident for an NRIC, or check your identification type.",
    });
  if (
    (type === "FIN" || type === "Passport") &&
    (status === "Singapore citizen" || status === "Permanent resident")
  )
    issues.push({
      key: "residentialStatus",
      message:
        "Choose Foreigner for a FIN or passport, or check your identification type.",
    });
  const floor = input.floor.trim(),
    unit = input.unit.trim();
  if (Boolean(floor) !== Boolean(unit))
    issues.push({
      key: floor ? "unit" : "floor",
      message: "Enter both the floor and unit number, or leave both empty.",
    });
  if (checkAge && z.iso.date().safeParse(input.dob).success) {
    const today = singaporeDate(),
      age = ageOn(input.dob, today);
    if (input.dob > today || age > 120)
      issues.push({ key: "dob", message: "Enter a valid date of birth." });
    else if (age < 18)
      issues.push({
        key: "dob",
        message: "You must be at least 18 years old to apply.",
      });
  }
  return issues;
}
const baseSchema = z.object(shape).strict();
const withChecks = (checkAge: boolean) =>
  baseSchema.superRefine((value, ctx) => {
    for (const issue of crossIssues(value, checkAge))
      ctx.addIssue({
        code: "custom",
        path: [issue.key],
        message: issue.message,
      });
  });
export const particularsSchema = withChecks(true);
export const particularsRecordSchema = z
  .object(
    Object.fromEntries(
      particularsKeys.map((key) => [key, z.string().max(250)]),
    ) as Record<ParticularsKey, z.ZodString>,
  )
  .partial()
  .catchall(z.string().max(250));
export const particularsDraftSchema = z
  .object(
    Object.fromEntries(
      particularsKeys.map((key) => [key, z.string().max(120)]),
    ) as Record<ParticularsKey, z.ZodString>,
  )
  .strict();

export function particularsFailures(
  value: Particulars,
): Partial<Record<ParticularsKey, string>> {
  const failures: Partial<Record<ParticularsKey, string>> = {};
  const parsed = baseSchema.safeParse(value);
  if (!parsed.success)
    for (const issue of parsed.error.issues) {
      const key = issue.path[0];
      if (
        typeof key === "string" &&
        (particularsKeys as readonly string[]).includes(key)
      )
        failures[key as ParticularsKey] ??= issue.message;
    }
  for (const issue of crossIssues(value, true))
    failures[issue.key] ??= issue.message;
  return failures;
}

export function myinfoIdentityNumber(identity: MyinfoIdentity | undefined) {
  const value =
    identity?.uinfin.status === "available" ? identity.uinfin.value : null;
  return value && validIdentityNumber(value) ? value : null;
}
export const identityTypeFor = (number: string) =>
  /^[ST]/.test(number) ? "NRIC" : "FIN";
const residentialCodes: Record<string, (typeof residentialStatuses)[number]> = {
  C: "Singapore citizen",
  P: "Permanent resident",
  A: "Foreigner",
};
const maritalCodes: Record<string, (typeof maritalStatuses)[number]> = {
  "1": "Single",
  "2": "Married",
  "3": "Widowed",
  "5": "Divorced",
};
const sexCodes: Record<string, (typeof sexOptions)[number]> = {
  F: "Female",
  M: "Male",
};
const housingCodes: Record<string, (typeof housingTypes)[number]> = {
  "111": "HDB flat (1 or 2 room)",
  "112": "HDB flat (1 or 2 room)",
  "118": "HDB flat (1 or 2 room)",
  "113": "HDB flat (3 room)",
  "114": "HDB flat (4 room)",
  "115": "HDB flat (5 room or executive)",
  "116": "HDB flat (5 room or executive)",
  "121": "Landed property",
  "122": "Landed property",
  "123": "Landed property",
  "131": "Condominium or private apartment",
  "132": "Condominium or private apartment",
  "139": "Condominium or private apartment",
};
const coded = <T extends string>(
  field: MyinfoIdentity["sex"],
  codes: Record<string, T>,
) =>
  field?.status === "available" &&
  field.code &&
  Object.hasOwn(codes, field.code)
    ? codes[field.code]
    : undefined;
const titleCase = (value: string) =>
  value
    .toLowerCase()
    .replace(
      /(^|[\s'(-])(\p{L})/gu,
      (_, lead: string, letter: string) => lead + letter.toUpperCase(),
    );
export function particularsFromMyinfo(
  identity: MyinfoIdentity | undefined,
): Partial<Particulars> {
  const result: Partial<Particulars> = {};
  if (!identity) return result;
  const accept = (key: ParticularsKey, value: string | null) => {
    const parsed = value ? shape[key].safeParse(value) : null;
    if (parsed?.success && parsed.data) result[key] = parsed.data;
  };
  const id = myinfoIdentityNumber(identity);
  if (id) {
    result.idType = identityTypeFor(id);
    result.idNumber = id;
  }
  if (identity.dob.status === "available") accept("dob", identity.dob.value);
  const status =
    identity.residentialstatus.status === "available"
      ? residentialCodes[identity.residentialstatus.code ?? ""]
      : undefined;
  if (status) result.residentialStatus = status;
  else if (result.idType === "FIN") result.residentialStatus = "Foreigner";
  if (identity.nationality.status === "available")
    accept(
      "nationality",
      identity.nationality.code === "SG"
        ? "Singaporean"
        : titleCase(identity.nationality.value ?? ""),
    );
  const marital = coded(identity.marital, maritalCodes),
    sex = coded(identity.sex, sexCodes);
  if (marital) result.maritalStatus = marital;
  if (sex) result.sex = sex;
  const address = identity.regadd;
  if (address.status === "available" && address.type === "SG") {
    const keys = [
      "block",
      "street",
      "building",
      "floor",
      "unit",
      "postal",
    ] as const;
    const parts = keys.map(
      (key) => [key, shape[key].safeParse(address[key] ?? "")] as const,
    );
    const value = (key: (typeof keys)[number]) =>
      parts.find(([name]) => name === key)![1].data;
    if (
      parts.every(([, parsed]) => parsed.success) &&
      value("block") &&
      value("street") &&
      value("postal") &&
      Boolean(value("floor")) === Boolean(value("unit"))
    ) {
      for (const [key, parsed] of parts) result[key] = parsed.data!;
      const housing =
        coded(identity.hdbtype, housingCodes) ??
        coded(identity.housingtype, housingCodes);
      if (housing) result.housingType = housing;
      if (
        identity.hdbownership?.status === "available" &&
        identity.hdbownership.value === "Yes"
      )
        result.homeOwnership = "Owned";
    }
  }
  return result;
}
const homeKeys = ["postal", "block", "floor", "unit"] as const;
const comparableAddress = (key: (typeof homeKeys)[number], value: string) => {
  const text = value
    .toUpperCase()
    .replace(/[\s.-]/g, "")
    .replace(key === "block" ? /^(BLOCK|BLK)(?=\w)/ : /^$/, "");
  return key === "floor" || key === "unit"
    ? text.replace(/^0+(?=.)/, "")
    : text;
};
export function particularsWithMyinfo(
  current: Particulars,
  identity: MyinfoIdentity | undefined,
): Particulars {
  const prefill = particularsFromMyinfo(identity);
  const moved =
    prefill.postal !== undefined &&
    Boolean(current.postal.trim()) &&
    homeKeys.some((key) => {
      const typed = comparableAddress(key, current[key]);
      return (
        Boolean(typed) && typed !== comparableAddress(key, prefill[key] ?? "")
      );
    });
  return {
    ...current,
    ...(moved ? { housingType: "", homeOwnership: "" } : {}),
    ...prefill,
  };
}
const registeredAddressKeys: readonly ParticularsKey[] = [
  "block",
  "street",
  "building",
  "floor",
  "unit",
  "postal",
];
export function myinfoLockedParticulars(
  current: Particulars,
  identity: MyinfoIdentity | undefined,
): ParticularsKey[] {
  const prefill = particularsFromMyinfo(identity);
  const matches = (key: ParticularsKey) =>
    prefill[key] !== undefined && current[key] === prefill[key];
  const address =
    prefill.postal !== undefined && registeredAddressKeys.every(matches);
  return particularsKeys.filter((key) => {
    if (key === "idType" || key === "idNumber")
      return matches("idType") && matches("idNumber");
    if (
      registeredAddressKeys.includes(key) ||
      key === "housingType" ||
      key === "homeOwnership"
    )
      return address && matches(key);
    return matches(key);
  });
}
