// The "generic way to commemorate an octave" — a lookback over the past
// week (a feast's own day plus up to 7 more) collecting every octave
// still active on a given date, from *both* sanctoral saints
// (calendar/feasts.ts) and temporal feasts (calendar/temporal-feasts.ts).
// Deliberately a post-processing layer over calendar/index.ts's existing
// occurrence/transfer pipeline, not a change to calendar/commemorations.ts
// itself — an octave doesn't change *how* a single day's precedence
// contest is decided, it just adds commemorations on top of whatever that
// contest already produced, and occasionally overrides the winner when a
// too-minor saint would otherwise have taken the day from it.
import type { FeastClass, LiturgicalDay, OctaveConfig } from './types';
import { getSanctoralCandidatesFor, getSaintRecord } from './feasts';
import { getTemporalFeastRecord, temporalFeastIdsStartingOn } from './temporal-feasts';
import { addDays, daysBetween } from './date-math';
import { compareFeastClass, minimumOutrightWinningRank } from './commemorations';

export interface ActiveOctave {
  id: string;
  name: string;
  /** Latin form of `name`, when authored — see calendar/types.ts's
   * `SanctoralIdentity.nameLa` doc comment for the display fallback. */
  nameLa?: string;
  /** This octave's own effective rank *today* — the ordinary `wins`
   * threshold on every day except its own closing day, where it's
   * `closingDayRank` instead (see calendar/types.ts's OctaveConfig).
   * Doubles as both "how strong a rival saint must be to displace this
   * octave" and "this octave's own strength when compared against
   * another simultaneously-active octave" (resolveActiveOctave) — same
   * underlying question, two different comparison partners. */
  wins: FeastClass;
  /** 1 on the feast's own day, counting up from there. */
  dayNumber: number;
  /** Whether `dayNumber` is this octave's own final ("in Octava") day —
   * i.e. whether `wins` above came from `closingDayRank` rather than the
   * ordinary `wins` config. Used by calendar/index.ts's applyOctaves to
   * decide which side a *tied* rank favors — see its own doc comment. */
  isClosingDay: boolean;
}

const DEFAULT_DAYS = 8;
const DEFAULT_WINS: FeastClass = 'semiduplex';
const DEFAULT_CLOSING_DAY_RANK: FeastClass = 'duplex';
/** How far back to look for an octave's own start date — must cover the
 * longest configured `days` a caller might use; 7 covers the standard
 * 8-day octave (day 1 = the start itself, day 8 = 7 days later). */
const LOOKBACK_DAYS = 7;

function considerCandidate(
  active: ActiveOctave[],
  seen: Set<string>,
  isoDate: string,
  startDate: string,
  id: string,
  name: string,
  nameLa: string | undefined,
  octave: OctaveConfig | undefined,
): void {
  if (!octave?.enabled || seen.has(id)) {
    return;
  }
  const days = octave.days ?? DEFAULT_DAYS;
  const offset = daysBetween(startDate, isoDate);
  if (offset < 0 || offset >= days) {
    return;
  }
  seen.add(id);
  const dayNumber = offset + 1;
  const isClosingDay = dayNumber === days;
  const wins = isClosingDay ? (octave.closingDayRank ?? DEFAULT_CLOSING_DAY_RANK) : (octave.wins ?? DEFAULT_WINS);
  active.push({ id, name, nameLa, wins, dayNumber, isClosingDay });
}

/** Every octave (sanctoral or temporal) whose window covers `isoDate`,
 * oldest-started first (so a stack like Christmas/Stephen/John/Innocents
 * reads in the order each one actually began, matching how they'd be
 * listed at Matins/Lauds/Vespers). */
export function activeOctavesFor(isoDate: string): ActiveOctave[] {
  const active: ActiveOctave[] = [];
  const seen = new Set<string>();

  for (let back = LOOKBACK_DAYS; back >= 0; back--) {
    const candidateDate = addDays(isoDate, -back);

    for (const candidate of getSanctoralCandidatesFor(candidateDate)) {
      const record = getSaintRecord(candidate.id);
      if (record) {
        considerCandidate(active, seen, isoDate, candidateDate, record.id, record.name, record.nameLa, record.octave);
      }
    }

    for (const feastId of temporalFeastIdsStartingOn(candidateDate)) {
      const record = getTemporalFeastRecord(feastId);
      if (record) {
        considerCandidate(active, seen, isoDate, candidateDate, record.id, record.name, record.nameLa, record.octave);
      }
    }
  }

  return active;
}

/** The strictest (highest) `wins` threshold among a set of active octaves —
 * what an occurring saint needs to clear to keep the day against all of
 * them at once. */
export function strictestThreshold(octaves: ActiveOctave[]): FeastClass {
  return octaves.reduce<FeastClass>(
    (max, o) => (compareFeastClass(o.wins, max) > 0 ? o.wins : max),
    octaves[0]?.wins ?? DEFAULT_WINS,
  );
}

/**
 * Which single active octave actually governs a day's own content/label
 * when more than one is active at once (this app's first real case: St.
 * Lawrence's, Aug 10-17, and the Assumption's, Aug 15-22, genuinely
 * overlap every year) and no rival saint has already displaced all of
 * them outright (that's calendar/index.ts's applyOctaves — this only
 * runs octave-vs-octave). Per direct instruction:
 *
 * - Highest effective rank (`wins`, already elevated on either octave's
 *   own closing day) wins outright.
 * - Tied rank, one of them on its own closing day: the closing day wins
 *   — its "in Octava" day is the octave's own culminating day, so it
 *   takes precedence over an ordinary day of an equally-ranked octave
 *   even if that other octave started more recently (2026-08 user
 *   instruction, concrete case: Aug 17, St. Lawrence's own closing day,
 *   vs. the Assumption's ordinary day 3, both `duplex` once Assumption's
 *   octave was strengthened — Lawrence's closing day wins).
 * - Tied rank, neither (or both) on their own closing day: the more
 *   recently *started* octave wins (smaller `dayNumber` today) — the
 *   reasoning given was that day 1 of a newly started octave needs to be
 *   fully present, which is the whole point of it starting; the older
 *   octave that's already been running is commemorated instead, same as
 *   any octave that loses this comparison.
 *
 * Undefined when no octave is active at all. Every other active octave
 * still gets commemorated regardless of which one wins here — this
 * function only decides whose *content* (and day-label name) governs,
 * not who gets left out of the commemoration list entirely (see
 * calendar/index.ts's applyOctaves, unchanged by this).
 */
export function resolveActiveOctave(isoDate: string): ActiveOctave | undefined {
  return pickWinningOctave(activeOctavesFor(isoDate));
}

/**
 * Whether an active octave's own effective rank clears the bar a real
 * sanctoral candidate would need to win `day`'s temporal category outright
 * — i.e. whether the octave governs the day's content/label even though
 * no *saint* occurring there was strong enough to (a "foreign" octave
 * superimposed on a season it doesn't belong to, e.g. the Immaculate
 * Conception's own octave running Dec 8-15, squarely inside Advent).
 * Live-verified against Divino Afflatu 1954: Dec 9/10/12/14 (ordinary
 * octave days, Semiduplex) and Dec 15 (the octave's own elevated closing
 * day, Duplex majus) all win outright over Advent's own privileged-feria-
 * minor ferias, exactly the same threshold `decideOccurrence` already
 * uses for a real Semiduplex+ saint there (St. Nicholas, Dec 6).
 *
 * Deliberately excludes `ordinary-feria` (unconditional there already —
 * any active octave governs regardless of rank, handled separately by
 * each caller via `resolveActiveOctave` directly) and `christmastide`
 * (Christmas's own stacked octaves — Christmas, St. Stephen, St. John,
 * Holy Innocents — are structurally already that season's own temporal
 * content, not a foreign add-on contesting it the way the Immaculate
 * Conception's octave contests Advent: live-verified, Dec 30 keeps
 * "Dominica Infra Octavam Nativitatis" as the real DO title regardless of
 * actual weekday or which of the four octaves is active, never an octave
 * name — see calendar/day-label.ts's and hours/resolve-common.ts's own
 * Dec-30 doc comments for the fuller reasoning already established
 * there). */
export function octaveGoverningPrivilegedDay(day: LiturgicalDay): ActiveOctave | undefined {
  if (day.temporalCategory === 'ordinary-feria' || day.season === 'christmastide') {
    return undefined;
  }
  const minRank = minimumOutrightWinningRank(day.temporalCategory);
  if (!minRank) {
    return undefined;
  }
  const octave = resolveActiveOctave(day.date);
  if (octave && compareFeastClass(octave.wins, minRank) >= 0) {
    return octave;
  }
  return undefined;
}

/** The comparison itself, factored out from resolveActiveOctave so the
 * precedence rule (rank, then recency) is directly unit-testable against
 * synthetic ActiveOctave data — no real equal-rank overlap exists yet in
 * this app's own calendar to exercise the tie-break against. */
export function pickWinningOctave(active: ActiveOctave[]): ActiveOctave | undefined {
  return active.reduce<ActiveOctave | undefined>((best, candidate) => {
    if (!best) {
      return candidate;
    }
    const rankCmp = compareFeastClass(candidate.wins, best.wins);
    if (rankCmp > 0) {
      return candidate;
    }
    if (rankCmp < 0) {
      return best;
    }
    if (candidate.isClosingDay !== best.isClosingDay) {
      return candidate.isClosingDay ? candidate : best;
    }
    return candidate.dayNumber < best.dayNumber ? candidate : best;
  }, undefined);
}
