import { PLAN_OPTIONS } from '../constants';
import { type LoanPaymentPlanOption } from '../types';

import { computePlanTotalsFromMultiplier } from './buildLoanPaymentPlan';
import { roundMoneyForDisplay } from './roundMoneyForDisplay';

const currencyIntegerFormatter = new Intl.NumberFormat('es-MX', {
  style: 'currency',
  currency: 'MXN',
  minimumFractionDigits: 0,
  maximumFractionDigits: 0,
});

const MEXICO_CITY_TZ = 'America/Mexico_City';

const API_DATE_ONLY_YMD = /^(\d{4})-(\d{2})-(\d{2})$/;

export function isDueDatePastInMexicoCity(
  due: Date | null,
  now: Date = new Date(),
): boolean {
  if (!due) {
    return false;
  }
  const dueYmd = new Intl.DateTimeFormat('en-CA', {
    timeZone: MEXICO_CITY_TZ,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(due);
  const nowYmd = new Intl.DateTimeFormat('en-CA', {
    timeZone: MEXICO_CITY_TZ,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(now);
  return dueYmd < nowYmd;
}

const MS_PER_HOUR = 1000 * 60 * 60;
const MS_PER_DAY = MS_PER_HOUR * 24;

export type OfferExpirationInfo =
  | { kind: 'today' }
  | { kind: 'tomorrow' }
  | { kind: 'days'; count: number };

export function getOfferExpirationInfo(
  expiresAt: string | undefined,
  now: Date = new Date(),
): OfferExpirationInfo | null {
  const expires = parseApiDateString(expiresAt);
  if (!expires) {
    return null;
  }
  const diffMs = expires.getTime() - now.getTime();
  if (diffMs <= 0) {
    return null;
  }
  const hoursLeft = diffMs / MS_PER_HOUR;
  if (hoursLeft < 24) {
    return { kind: 'today' };
  }
  if (hoursLeft < 48) {
    return { kind: 'tomorrow' };
  }
  return { kind: 'days', count: Math.ceil(diffMs / MS_PER_DAY) };
}

export function parseApiDateString(iso: string | undefined): Date | null {
  if (!iso) {
    return null;
  }
  const trimmed = iso.trim();
  const ymd = API_DATE_ONLY_YMD.exec(trimmed);
  if (ymd) {
    const y = Number(ymd[1]);
    const mo = Number(ymd[2]);
    const d = Number(ymd[3]);
    if (
      !Number.isFinite(y) ||
      !Number.isFinite(mo) ||
      !Number.isFinite(d) ||
      mo < 1 ||
      mo > 12 ||
      d < 1 ||
      d > 31
    ) {
      return null;
    }
    return new Date(Date.UTC(y, mo - 1, d, 12, 0, 0));
  }
  const parsed = new Date(trimmed);
  return Number.isNaN(parsed.getTime()) ? null : parsed;
}

const weekdayShortToDow: Record<string, number> = {
  Sun: 0,
  Mon: 1,
  Tue: 2,
  Wed: 3,
  Thu: 4,
  Fri: 5,
  Sat: 6,
};

function getMexicoCityYmdAndDow(now: Date): {
  year: number;
  month: number;
  day: number;
  dow: number;
} {
  const ymd = new Intl.DateTimeFormat('en-CA', {
    timeZone: MEXICO_CITY_TZ,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(now);
  const [year, month, day] = ymd.split('-').map(Number);
  const wdShort = new Intl.DateTimeFormat('en-US', {
    timeZone: MEXICO_CITY_TZ,
    weekday: 'short',
  }).format(now);
  const dow = weekdayShortToDow[wdShort] ?? 0;
  return { year, month, day, dow };
}

function daysUntilNextPayrollFriday(dow: number): number {
  if (dow === 5) {
    return 7;
  }
  return (5 - dow + 7) % 7;
}

export function getNextPayrollFridayDateMexico(now: Date = new Date()): Date {
  const { year, month, day, dow } = getMexicoCityYmdAndDow(now);
  const addDays = daysUntilNextPayrollFriday(dow);
  return new Date(Date.UTC(year, month - 1, day + addDays, 12, 0, 0));
}

export const clampAmount = (value: number, min: number, max: number) =>
  Math.min(max, Math.max(min, Math.round(value)));

export const formatCurrencyInteger = (value: number) =>
  currencyIntegerFormatter.format(roundMoneyForDisplay(value));

const currencyDecimalFormatter = new Intl.NumberFormat('es-MX', {
  style: 'currency',
  currency: 'MXN',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
});

export const formatCurrencyDecimal = (value: number) =>
  currencyDecimalFormatter.format(roundMoneyForDisplay(value, 2));

const shortPaymentDateMXCache = new Map<string, Intl.DateTimeFormat>();
const longPaymentDateMXCache = new Map<string, Intl.DateTimeFormat>();

export const formatPaymentDateMX = (date: Date, locale: Locale) => {
  const code = locale.code ?? 'es-MX';
  let formatter = shortPaymentDateMXCache.get(code);
  if (!formatter) {
    formatter = new Intl.DateTimeFormat(code, {
      day: 'numeric',
      month: 'short',
      year: 'numeric',
      timeZone: MEXICO_CITY_TZ,
    });
    shortPaymentDateMXCache.set(code, formatter);
  }
  return formatter.format(date);
};

export function formatCloseDate(value: string, locale: Locale): string {
  const parsed = parseApiDateString(value);
  if (!parsed) {
    return value || '—';
  }
  return formatPaymentDateMX(parsed, locale);
}

export const formatLongPaymentDateMX = (date: Date, locale: Locale) => {
  const code = locale.code ?? 'es-MX';
  let formatter = longPaymentDateMXCache.get(code);
  if (!formatter) {
    formatter = new Intl.DateTimeFormat(code, {
      day: 'numeric',
      month: 'long',
      year: 'numeric',
      timeZone: MEXICO_CITY_TZ,
    });
    longPaymentDateMXCache.set(code, formatter);
  }
  return formatter.format(date);
};

export function formatBankAccountDisplay(value: string): string {
  return value.replace(/(\d{3})(\d{3})(\d{11})(\d{1})/, '$1 $2 $3 $4');
}

export function resolvePlanPaymentTotals(
  amountMxn: number,
  selectedWeeks: number,
  simulatedOption?: Pick<
    LoanPaymentPlanOption,
    'weeks' | 'weeklyPaymentMxn' | 'totalPaymentMxn'
  > | null,
): { weekly: number; total: number } {
  if (
    simulatedOption &&
    simulatedOption.weeks === selectedWeeks &&
    amountMxn > 0
  ) {
    return {
      weekly: simulatedOption.weeklyPaymentMxn,
      total: simulatedOption.totalPaymentMxn,
    };
  }

  const plan = PLAN_OPTIONS.find(option => option.weeks === selectedWeeks);
  if (plan) {
    return computePlanTotalsFromMultiplier(
      amountMxn,
      plan.totalMultiplier,
      plan.weeks,
    );
  }

  return computePlanTotalsFromMultiplier(amountMxn, 1, selectedWeeks);
}
