import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart } from './types';
import type { LiturgicalDay } from '../calendar/types';
import { resolveEveningDay } from '../calendar/vespers';
import { getDayLabel } from '../calendar/day-label';
import { getPsalmVerses } from '../psalter';
import { getHymnDoxologyId } from './hymn-doxology';
import { getOpeningVersicleId } from './opening-versicle';
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
import { isDoubleOrHigher } from './antiphon';
import { resolveCommon, appendDoxology, splitNamedAntiphon, openingAntiphon, resolveResponsory } from './resolve-common';
import { isAllSouls, buildComplineParts, DAY_LABEL } from './all-souls';
import complineDefinitionData from '../data/hours/compline.yml';

const complineDefinition = complineDefinitionData as HourDefinition;

// Real practice starts this at Lent proper (Ash Wednesday), not the
// preceding Septuagesima-tide — unlike the opening versicle's Alleluia
// suppression, which does start at Septuagesima (see
// data/hours/opening-by-season.yml). Two different season keys deliberately
// treated differently, not an oversight.
const LENTEN_HYMN_SEASONS = new Set(['lent', 'passiontide']);

function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
  switch (part.kind) {
    case 'hymn': {
      // Lent replaces the whole hymn (not just its doxology) with "Christe,
      // qui lux es et dies" — self-contained, so no appendDoxology here.
      // See compline-hymn-lenten.yml for real caveats about how confident
      // to be that this actually triggers in live practice.
      if (LENTEN_HYMN_SEASONS.has(day.season)) {
        return [{ kind: 'hymn', text: resolveCommon('compline-hymn-lenten') }];
      }
      const body = resolveCommon(part.textRef.id);
      const doxology = resolveCommon(getHymnDoxologyId(day, 'compline-hymn-doxology-per-annum'));
      return [{ kind: 'hymn', text: appendDoxology(body, doxology) }];
    }
    case 'chapter':
    case 'versicle':
    case 'prayer':
      return [{ kind: part.kind, text: resolveCommon(part.textRef.id) }];
    case 'responsory':
      return [{ kind: 'responsory', text: resolveResponsory(resolveCommon(part.textRef.id), day) }];
    case 'preces': {
      // On a Double-or-higher, the full secular Preces (compline-preces —
      // already the fuller Tridentine 1906/1910 text, see compline.yml's
      // own header) steps aside for the plain Kyrie/Pater-Noster short
      // litany (lauds-short-litany, already reused by Vespers' own
      // 'vespers-preces' case the same way) rather than vanishing
      // entirely — a deliberate choice to always keep a short litany +
      // Our Father, even though real practice (both Monastic and
      // secular) drops Compline's Preces to nothing on a Double.
      const id = part.omitOnDouble && isDoubleOrHigher(day.winner) ? 'lauds-short-litany' : part.textRef.id;
      return [{ kind: 'preces', text: resolveCommon(id), label: part.label }];
    }
    case 'lesson':
      return [{ kind: 'lesson', text: resolveCommon(part.textRef.id), label: part.label }];
    case 'opening-versicle':
      return [{ kind: 'versicle', text: resolveCommon(getOpeningVersicleId(day.season, day.winner)) }];
    case 'psalm':
      return [
        {
          kind: 'psalm',
          psalmNumber: part.psalmNumber,
          verses: getPsalmVerses(part.psalmNumber, part.verses).map((v) => ({
            n: v.n,
            text: v.text,
            status: v.status,
          })),
        },
      ];
    case 'nunc-dimittis': {
      const antiphonText = resolveCommon('nunc-dimittis-antiphon');
      const opening = openingAntiphon(antiphonText, day.winner);
      const { full } = splitNamedAntiphon(antiphonText);
      return [
        { kind: 'canticle', canticleId: 'nunc-dimittis', text: resolveCommon('nunc-dimittis'), antiphon: opening },
        { kind: 'antiphon', text: full },
      ];
    }
    case 'marian-antiphon': {
      const id = getMarianAntiphonId(day);
      return [{ kind: 'preces', text: resolveCommon(id), label: getMarianAntiphonLabel(id) }];
    }
    case 'canticle':
      return [{ kind: 'canticle', canticleId: part.canticleId, text: resolveCommon(part.textRef.id) }];
    // Not used by Compline — Prime-only, or milestone-4-only. Throwing
    // rather than silently dropping: if one of these ever shows up in
    // compline.yml, that's a mistake worth surfacing loudly, not hiding.
    case 'martyrology':
    case 'rule-reading':
    case 'creed':
    case 'closing-antiphon':
    case 'by-day-kind':
    case 'variable':
    case 'day-collect':
    case 'lauds-psalmody':
    case 'lauds-office':
    case 'benedictus':
    case 'suffrages':
    case 'lauds-preces':
    case 'day-collects':
    case 'vespers-psalmody':
    case 'vespers-office':
    case 'magnificat':
    case 'vespers-preces':
      throw new Error(`Compline's ordo doesn't support a '${part.kind}' part`);
  }
}

export function resolveOrdo(date: string): ResolvedOrdo {
  // Every season/occurring-dependent part below (hymn, doxology, Preces
  // omitOnDouble, opening versicle, Nunc Dimittis rank, Marian antiphon) is
  // an evening-hour concern, so the whole ordo resolves off whichever day's
  // identity actually governs tonight — see calendar/vespers.ts. Nothing
  // here is date-literal the way Prime's Martyrology is, so there's no
  // need to keep a separate "real" day around.
  if (isAllSouls(date)) {
    // Confiteor/Misereatur/Indulgentiam is unaffected by All Souls' Day —
    // reused directly (same text every day) rather than duplicated in
    // hours/all-souls.ts; everything after it (psalms onward) is special.
    const parts: ResolvedPart[] = [{ kind: 'preces', text: resolveCommon('compline-confiteor') }, ...buildComplineParts()];
    return { hourId: 'compline', date, parts, dayLabel: DAY_LABEL };
  }
  const day = resolveEveningDay(date);
  const parts = complineDefinition.parts.flatMap((part) => resolvePart(part, day));
  return { hourId: 'compline', date, parts, dayLabel: getDayLabel(day) };
}
