import { ValidationError } from '../errors/index.js';

/**
 * Philippine formatting and normalization (SPEC.md §6, §10).
 *
 * Normalizing at the edge — once, on the way in — is what makes duplicate
 * detection by mobile number work at all: `0917 123 4567`, `+639171234567` and
 * `639171234567` are the same person, and only a canonical form reveals that.
 */

export const INSTITUTION_TIMEZONE = 'Asia/Manila';
export const CURRENCY_CODE = 'PHP';

/**
 * Converts any common Philippine mobile format to E.164 (`+639171234567`).
 *
 * Accepts `09XXXXXXXXX`, `639XXXXXXXXX`, `+639XXXXXXXXX` and the same with
 * spaces, dashes or parentheses. Returns `null` for anything else rather than
 * throwing, so a landline or a foreign number can be stored verbatim in a
 * separate field instead of being rejected outright.
 */
export function normalizePhilippineMobile(input: string): string | null {
  const digits = input.replace(/[\s\-().]/g, '').replace(/^\+/, '');
  if (!/^\d+$/.test(digits)) return null;

  // 09XXXXXXXXX — the local dialling form.
  if (/^09\d{9}$/.test(digits)) return `+63${digits.slice(1)}`;
  // 639XXXXXXXXX — country code without the plus.
  if (/^639\d{9}$/.test(digits)) return `+${digits}`;
  // 9XXXXXXXXX — entered without the leading zero.
  if (/^9\d{9}$/.test(digits)) return `+63${digits}`;

  return null;
}

export function isValidPhilippineMobile(input: string): boolean {
  return normalizePhilippineMobile(input) !== null;
}

/** Renders E.164 back into the local display form: `0917 123 4567`. */
export function formatPhilippineMobile(e164: string): string {
  const match = /^\+63(9\d{2})(\d{3})(\d{4})$/.exec(e164);
  if (!match) return e164;
  return `0${match[1]} ${match[2]} ${match[3]}`;
}

/**
 * Canonical email form for uniqueness and duplicate detection: trimmed and
 * lowercased. Sub-addressing (`+tag`) is deliberately preserved — two students
 * legitimately sharing a family mailbox with different tags are different
 * people, and stripping the tag would merge them.
 */
export function normalizeEmail(input: string): string {
  return input.trim().toLowerCase();
}

const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export function isValidEmail(input: string): boolean {
  return EMAIL_PATTERN.test(input.trim());
}

/** Collapses whitespace and title-cases a personal name for consistent display. */
export function normalizePersonName(input: string): string {
  return input
    .trim()
    .replace(/\s+/g, ' ')
    .replace(/\b[\p{L}'-]+/gu, (word) => {
      const lower = word.toLowerCase();
      // Filipino and Spanish surname particles stay lowercase: "dela Cruz",
      // "de los Santos", "van der Berg". Written as separate words or joined,
      // both spellings occur in Philippine records.
      if (/^(de|del|dela|delas|delos|las|los|da|di|van|von|der|y)$/.test(lower)) return lower;
      if (lower.startsWith("o'") && lower.length > 2) {
        return `O'${lower.charAt(2).toUpperCase()}${lower.slice(3)}`;
      }
      return lower.charAt(0).toUpperCase() + lower.slice(1);
    });
}

/** `Dela Cruz, Juan Miguel P.` — the form used on official lists. */
export function formatFullNameLastFirst(person: {
  lastName: string;
  firstName: string;
  middleName?: string | null;
  suffix?: string | null;
}): string {
  const middleInitial = person.middleName?.trim()
    ? ` ${person.middleName.trim().charAt(0).toUpperCase()}.`
    : '';
  const suffix = person.suffix?.trim() ? `, ${person.suffix.trim()}` : '';
  return `${person.lastName}, ${person.firstName}${middleInitial}${suffix}`;
}

export function formatFullName(person: {
  lastName: string;
  firstName: string;
  middleName?: string | null;
  suffix?: string | null;
}): string {
  const parts = [person.firstName, person.middleName?.trim(), person.lastName, person.suffix?.trim()];
  return parts.filter((part): part is string => Boolean(part && part.length > 0)).join(' ');
}

const DATE_FORMATTER = new Intl.DateTimeFormat('en-PH', {
  timeZone: INSTITUTION_TIMEZONE,
  year: 'numeric',
  month: 'long',
  day: 'numeric',
});

const DATETIME_FORMATTER = new Intl.DateTimeFormat('en-PH', {
  timeZone: INSTITUTION_TIMEZONE,
  year: 'numeric',
  month: 'short',
  day: 'numeric',
  hour: 'numeric',
  minute: '2-digit',
  hour12: true,
});

const SHORT_DATE_FORMATTER = new Intl.DateTimeFormat('en-PH', {
  timeZone: INSTITUTION_TIMEZONE,
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
});

/** `25 August 2026`, always rendered in Asia/Manila regardless of server locale. */
export function formatDate(date: Date): string {
  return DATE_FORMATTER.format(date);
}

/** `25 Aug 2026, 2:30 PM` in Asia/Manila. */
export function formatDateTime(date: Date): string {
  return DATETIME_FORMATTER.format(date);
}

/** `08/25/2026` — for dense tables where the long form does not fit. */
export function formatShortDate(date: Date): string {
  return SHORT_DATE_FORMATTER.format(date);
}

/**
 * Parses a `YYYY-MM-DD` date-only value as midnight UTC.
 *
 * Date-only fields (a birth date, a term start) are calendar facts, not
 * instants. Anchoring them at UTC midnight keeps them stable no matter which
 * timezone the server runs in — a birth date must never shift by a day.
 */
export function parseDateOnly(input: string): Date {
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(input.trim());
  if (!match) {
    throw new ValidationError(`"${input}" is not a valid date. Use the format YYYY-MM-DD.`);
  }
  const [, year, month, day] = match;
  const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
  if (
    date.getUTCFullYear() !== Number(year) ||
    date.getUTCMonth() !== Number(month) - 1 ||
    date.getUTCDate() !== Number(day)
  ) {
    throw new ValidationError(`"${input}" is not a real calendar date.`);
  }
  return date;
}

export function toDateOnlyString(date: Date): string {
  return date.toISOString().slice(0, 10);
}

/** Whole years elapsed, used for age-band analytics rather than exact ages. */
export function computeAge(birthDate: Date, asOf: Date = new Date()): number {
  let age = asOf.getUTCFullYear() - birthDate.getUTCFullYear();
  const monthDelta = asOf.getUTCMonth() - birthDate.getUTCMonth();
  if (monthDelta < 0 || (monthDelta === 0 && asOf.getUTCDate() < birthDate.getUTCDate())) {
    age -= 1;
  }
  return age;
}

/**
 * Age bands for demographic reporting. Reporting a band rather than an exact
 * age is one of the data-minimization measures in SPEC.md §24.
 */
export function ageBand(age: number): string {
  if (age < 18) return 'Under 18';
  if (age <= 20) return '18–20';
  if (age <= 24) return '21–24';
  if (age <= 29) return '25–29';
  if (age <= 39) return '30–39';
  return '40 and above';
}
