import { DateTime } from 'luxon';

function parseDateOnly(iso: string, zone: string): DateTime | null {
  const s = String(iso || '').trim().slice(0, 10);
  if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return null;
  const d = DateTime.fromISO(s, { zone });
  return d.isValid ? d.startOf('day') : null;
}

function rangesOverlap(
  aFrom: DateTime,
  aTo: DateTime,
  bFrom: DateTime,
  bTo: DateTime
): boolean {
  return aFrom <= bTo && aTo >= bFrom;
}

/**
 * O&M anniversary display year (1–10) containing `anchorDateIso`, same windows as computeOmTenYearRows (B/C).
 */
export function resolveOmDisplayYearContainingAnchor(
  omStartDateIso: string,
  anchorDateIso: string,
  zone: string
): number | null {
  const omStart = parseDateOnly(omStartDateIso, zone);
  const anchor = parseDateOnly(anchorDateIso, zone);
  if (!omStart || !anchor) return null;
  const ad = anchor.startOf('day');
  for (let y = 1; y <= 10; y++) {
    const yearStart = omStart.plus({ years: y - 1 }).startOf('day');
    const yearEnd = yearStart.plus({ years: 1 }).minus({ days: 1 }).startOf('day');
    if (ad >= yearStart && ad <= yearEnd) return y;
  }
  return null;
}

/**
 * Authoritative 1–10 O&M display year for `asOfDay` (calendar in `zone`): same windows as
 * {@link resolveOmDisplayYearContainingAnchor}. Before start date → 1; on/after last day of year 10 → 10.
 */
export function resolveOmSchemeYearAsOfCalendar(
  omStartDateIso: string,
  asOfDay: DateTime,
  zone: string
): number {
  const omStart = parseDateOnly(omStartDateIso, zone);
  const asOf = asOfDay.setZone(zone).startOf('day');
  if (!omStart) return 1;
  if (asOf < omStart.startOf('day')) return 1;
  const iso = asOf.toISODate();
  if (!iso) return 1;
  const y = resolveOmDisplayYearContainingAnchor(omStartDateIso, iso, zone);
  if (y != null) return y;
  return 10;
}

/**
 * Prefer year band containing billing `pendingFrom`; else any band overlapping `[pendingFrom, pendingTo]`.
 */
export function resolveCurrentOmDisplayYearForPendingBill(
  omStartDateIso: string,
  pendingFromIso: string,
  pendingToIso: string,
  zone: string
): number | null {
  const omStart = parseDateOnly(omStartDateIso, zone);
  const pf = parseDateOnly(pendingFromIso, zone);
  const pt = parseDateOnly(pendingToIso, zone);
  if (!omStart || !pf || !pt) return null;

  const fromDisplay = resolveOmDisplayYearContainingAnchor(omStartDateIso, pf.toISODate()!, zone);
  if (fromDisplay != null) return fromDisplay;

  const pfD = pf.startOf('day');
  const ptD = pt.startOf('day');
  for (let y = 1; y <= 10; y++) {
    const ys = omStart.plus({ years: y - 1 }).startOf('day');
    const ye = ys.plus({ years: 1 }).minus({ days: 1 }).startOf('day');
    if (rangesOverlap(pfD, ptD, ys, ye)) return y;
  }
  /** Entirely before service start (legacy uncapped period) — treat as first display year. */
  if (ptD < omStart.startOf('day')) return 1;
  return null;
}

/** 1-based display year (matches `MULTIPLIER_BY_YEAR` keys); null if anchor cannot be placed in years 1–10. */
export function omYearFromInvoiceAnchorDate(
  omStartDateIso: string | null | undefined,
  invoiceFromDateOrBillingIso: string | null | undefined,
  zone: string
): number | null {
  const om = omStartDateIso?.trim();
  const anchorRaw = invoiceFromDateOrBillingIso?.trim()?.slice(0, 10) || '';
  if (!om || !/^\d{4}-\d{2}-\d{2}$/.test(anchorRaw)) return null;
  const displayYear = resolveOmDisplayYearContainingAnchor(om, anchorRaw, zone);
  if (displayYear == null) return null;
  return displayYear;
}

/**
 * Same calendar day as `om_start_date + 10 years` (start of day in `zone`). The O&M service period for the
 * **last** scheme year ends the day **before** this. `period_to` must be **strictly before** this date.
 */
export function omTenYearExclusiveEndDateTime(omStartDateIso: string, zone: string): DateTime | null {
  const omStart = parseDateOnly(omStartDateIso, zone);
  if (!omStart) return null;
  return omStart.plus({ years: 10 }).startOf('day');
}

/**
 * True if the scheduler billing anchor (typically “tomorrow”) may still produce a scheduled O&M invoice.
 * Anchors **on** `om_start_date + 10 years` are allowed (that run bills through the last day of year 10).
 * The first disallowed anchor is the **day after** `om_start_date + 10 years`.
 */
export function isOmScheduledBillingAnchorWithinTenYears(
  omStartDateIso: string,
  anchorDate: DateTime,
  zone: string
): boolean {
  const lastAllowedAnchor = omTenYearExclusiveEndDateTime(omStartDateIso, zone);
  if (!lastAllowedAnchor) return false;
  const anchor = anchorDate.setZone(zone).startOf('day');
  if (!anchor.isValid) return false;
  return anchor <= lastAllowedAnchor;
}

/** True if O&M line `period_to` (from `computeOmBillPeriod` in `omBillingPeriod`) is still within the 10-year window. */
export function isOmBillPeriodEndWithinTenYears(
  omStartDateIso: string,
  periodTo: DateTime,
  zone: string
): boolean {
  const endExclusive = omTenYearExclusiveEndDateTime(omStartDateIso, zone);
  if (!endExclusive) return false;
  const p = periodTo.setZone(zone).startOf('day');
  if (!p.isValid) return false;
  return p < endExclusive;
}
