// "Day being celebrated" label — combines an ordinal week-within-season
// label (pure date arithmetic on the season anchors already computed
// elsewhere in calendar/) with a feast name when
// calendar/commemorations.ts's occurrence decision says one applies.
//
// Counting convention is Trinity-counted ("Nth Sunday/week after Trinity"),
// not Divinum Officium's own "after Pentecost" counting — a deliberate
// choice, one week off from Pentecost-counting for the same date. Meant to
// become a configurable choice later (the same day->id indirection
// philosophy already used for the sanctoral calendar), not hardcoded here
// forever — just not built yet.
import type { LiturgicalDay } from './types';
import { easterSunday } from './easter';
import { adventStart, firstSundayStrictlyAfter, sundayOnOrBefore } from './temporal';
import { addDays, daysBetween, toIsoDate } from './date-math';
import { getTemporalFeastRecord } from './temporal-feasts';
import { resolveActiveOctave, type ActiveOctave } from './octaves';

function capitalize(text: string): string {
  return text.charAt(0).toUpperCase() + text.slice(1);
}

function ordinal(n: number): string {
  const mod100 = n % 100;
  if (mod100 >= 11 && mod100 <= 13) {
    return `${n}th`;
  }
  switch (n % 10) {
    case 1:
      return `${n}st`;
    case 2:
      return `${n}nd`;
    case 3:
      return `${n}rd`;
    default:
      return `${n}th`;
  }
}

interface OrdinalSeason {
  /** ISO date of the season's own anchor day, for a given calendar year. */
  anchorDate(year: number): string;
  /** Advent: the anchor Sunday itself is "week 1". Trinity/Epiphany/Easter/
   * Lent: the anchor is its own named day, excluded from the count — the
   * numbered weeks start the following Sunday. */
  includeAnchorWeek: boolean;
  /** Used in "Weekday after {anchorName}" for the anchor's own partial week. */
  anchorName: string;
  /** Used in "the Nth Sunday/week {preposition} {ordinalName}". */
  ordinalName: string;
  preposition: 'after' | 'of';
}

const ORDINAL_SEASONS: Partial<Record<string, OrdinalSeason>> = {
  advent: {
    anchorDate: adventStart,
    includeAnchorWeek: true,
    anchorName: 'Advent',
    ordinalName: 'Advent',
    preposition: 'of',
  },
  epiphanytide: {
    anchorDate: (year) => `${year}-01-06`,
    includeAnchorWeek: false,
    anchorName: 'Epiphany',
    ordinalName: 'Epiphany',
    preposition: 'after',
  },
  lent: {
    anchorDate: (year) => addDays(toIsoDate(easterSunday(year)), -46),
    includeAnchorWeek: false,
    anchorName: 'Ash Wednesday',
    ordinalName: 'Lent',
    preposition: 'of',
  },
  eastertide: {
    anchorDate: (year) => toIsoDate(easterSunday(year)),
    includeAnchorWeek: false,
    anchorName: 'Easter',
    ordinalName: 'Easter',
    preposition: 'after',
  },
  trinitytide: {
    anchorDate: (year) => addDays(toIsoDate(easterSunday(year)), 56),
    includeAnchorWeek: false,
    anchorName: 'Trinity Sunday',
    ordinalName: 'Trinity',
    preposition: 'after',
  },
};

/** The anchor day itself (Ash Wednesday, Epiphany, Easter Sunday, Trinity
 * Sunday) is its own named day, not "day after itself" — and outranks
 * everything else this module computes (an active octave, an ordinal week
 * label), since it's the day's real primary identity in the live engine
 * too (e.g. Trinity Sunday is also technically day 8 of Pentecost's own
 * octave, but nobody calls it that). Advent's own anchor (Advent I Sunday)
 * doesn't take this branch — it's already "the 1st Sunday of Advent" via
 * the ordinal path below. */
function anchorDayName(day: LiturgicalDay): string | undefined {
  const config = ORDINAL_SEASONS[day.season];
  if (!config || config.includeAnchorWeek) {
    return undefined;
  }
  const year = Number(day.date.slice(0, 4));
  if (day.date === config.anchorDate(year)) {
    return config.anchorName;
  }
  return undefined;
}

function temporalLabel(day: LiturgicalDay): string {
  const weekdayName = capitalize(day.weekday);
  const config = ORDINAL_SEASONS[day.season];
  if (!config) {
    // No ordinal convention modeled for this season (Septuagesima-tide,
    // Passiontide, Ascensiontide, Pentecost, Christmastide, the
    // Corpus-Christi/Sacred-Heart single-day seasons) — their few days
    // mostly have their own proper names rather than ordinal counting, so
    // this fallback is expected to be seen, not a gap to fill later.
    return `${weekdayName} in ${capitalize(day.season.replace(/-/g, ' '))}`;
  }

  const year = Number(day.date.slice(0, 4));
  const anchor = config.anchorDate(year);
  const anchorName = anchorDayName(day);
  if (anchorName) {
    return anchorName;
  }

  const firstNumberedSunday = config.includeAnchorWeek ? anchor : firstSundayStrictlyAfter(anchor);
  if (day.date < firstNumberedSunday) {
    return `${weekdayName} after ${config.anchorName}`;
  }

  const weeksSince = daysBetween(firstNumberedSunday, sundayOnOrBefore(day.date)) / 7;
  const ordinalStr = ordinal(weeksSince + 1);
  if (day.weekday === 'sunday') {
    return `The ${ordinalStr} Sunday ${config.preposition} ${config.ordinalName}`;
  }
  return `${weekdayName} in the ${ordinalStr} week ${config.preposition} ${config.ordinalName}`;
}

/** "Third Day within the Octave of St. Lawrence" — the real DO title an
 * octave day carries on its own (e.g. "Tertia die infra Octavam S.
 * Laurentii Martyris") when nothing else has displaced it. Day 1 shouldn't
 * normally reach this (that day's own winner is the feast itself, handled
 * above before this is ever called) — kept simple rather than
 * special-cased for that rare edge case (see applyOctaves's own
 * `isOwnStartDay` comment in calendar/index.ts for when it can happen). */
function octaveLabel(octave: ActiveOctave): string {
  return `${ordinal(octave.dayNumber)} Day within the Octave of ${octave.name}`;
}

/**
 * The full "day being celebrated" label: a feast name when
 * calendar/commemorations.ts says the day has one, combined with (or
 * replaced by) the ordinal temporal label depending on whether the feast
 * won outright or was merely commemorated. See the plan discussion this
 * came from for the three cases.
 */
export function getDayLabel(day: LiturgicalDay): string {
  if (day.winner.kind === 'sanctoral') {
    return day.winner.name;
  }

  // A named temporal feast (Christmas, Pentecost, Marian Saturday, ...)
  // shows its own name rather than the ordinal week label — same
  // "winner displaces, doesn't combine" rule a sanctoral winner gets
  // above. Most temporal ids don't have a record at all (see
  // temporal-feasts.ts) and fall through to the ordinal label as before.
  const namedFeast = getTemporalFeastRecord(day.winner.id);
  if (namedFeast) {
    return namedFeast.name;
  }

  const commemoratedSaint = day.commemorations.find((c) => c.kind === 'sanctoral');

  // A season's own named anchor day (Trinity Sunday, Easter, Ash
  // Wednesday, Epiphany) outranks an active octave, same reasoning as
  // anchorDayName's own doc comment — checked before the octave case
  // below since Trinity Sunday, e.g., also happens to be day 8 of
  // Pentecost's octave, and the anchor name is what actually governs.
  const anchorName = anchorDayName(day);
  if (anchorName) {
    return commemoratedSaint ? `${commemoratedSaint.name} — ${anchorName}` : anchorName;
  }

  // An active octave (St. Lawrence's, ...) is this day's real primary
  // identity in the live engine, not a footnote — e.g. "Tertia die infra
  // Octavam S. Laurentii Martyris", not "Wednesday in the 11th week after
  // Trinity" — but *only* when the temporal day itself has no standing of
  // its own (`ordinary-feria`), same gate as hours/resolve-common.ts's
  // resolveOfficeWinner and for the same reason: live-verified
  // counterexample is the Christmas Octave's own stack (Dec 30, e.g.,
  // `privileged-feria-minor`), where the real title stays the temporal
  // Sunday's own ("De Dominica Infra Octavam Nativitatis") with no octave
  // name in it at all — this label agreeing with resolveOfficeWinner
  // about which one wins is what makes "the office is Lawrence's" and
  // "the label says Lawrence" consistent instead of two independent
  // guesses that can disagree. When more than one octave is active at
  // once (resolveActiveOctave), the highest-ranked wins the headline
  // (ties broken by whichever started more recently) — the others still
  // ride along as ordinary octave commemorations, just not separately
  // named here.
  const activeOctave = day.temporalCategory === 'ordinary-feria' ? resolveActiveOctave(day.date) : undefined;
  if (activeOctave) {
    const primary = octaveLabel(activeOctave);
    return commemoratedSaint ? `${primary} — ${commemoratedSaint.name}` : primary;
  }

  const temporal = temporalLabel(day);
  return commemoratedSaint ? `${commemoratedSaint.name} — ${temporal}` : temporal;
}
