/* global React, window */
// Worship Planner — Prayers tab view.
//
// Renders window.PRAYERS[sunday.id] for the current Sunday as a single
// order-of-service column. What shows is driven by two inputs:
//
//   • Language register — Formal / Fresh / Casual. EXCLUSIVE, remembered across
//     dates (wp:prayerRegister). Gathering Words is single-register and shows
//     regardless of this choice. This is the Prayers tab's own control.
//
//   • Scripture used — OT / Psalm / Epistle / Gospel. NON-EXCLUSIVE. This is now
//     the SHARED service-wide selector (the "Scripture used" bar at the top of
//     every tab, owned by planner.jsx and persisted per-day in wp:hiddenByDay);
//     it arrives here as the `usedKeys` prop. The set of selected readings
//     resolves to one prayer permutation:
//        1 reading  → that reading's solo prayer
//        2 readings → the paired prayer for exactly those two
//        3 or 4     → the all-four prayer
//
// Illumination is written per reading, so it renders one prayer per selected
// reading. Benediction has combined OT+Ep / OT+Gos / Ep+Gos versions; any other
// multi-reading pick shows one sending blessing per selected reading.
//
// Opening Prayer, Illumination and Confession are congregational (unison) and
// printed bold. Call to Worship is responsive — People's responses are bold and
// indented.
//
// Exposed as window.PrayersView (planner.jsx mounts it on the "prayers" tab),
// plus window.resolvePrayerPick / window.prayerPickToLines for the service plan
// and export to reuse the same resolution logic.
// Props: { sunday, planPicks, onAddPrayer, onRemovePrayer }

(function () {
  const { useState, useEffect } = React;

  const PASSAGES = window.PRAYER_PASSAGES || [
    { key: "ot", code: "OT", label: "Old Testament", short: "OT" },
    { key: "psalm", code: "Ps", label: "Psalm", short: "Psalm" },
    { key: "epistle", code: "Ep", label: "Epistle", short: "Epistle" },
    { key: "gospel", code: "Gos", label: "Gospel", short: "Gospel" },
  ];
  const ORDER = PASSAGES.map((p) => p.key);
  const CODE = Object.fromEntries(PASSAGES.map((p) => [p.key, p.code]));
  const KEY_FOR_CODE = Object.fromEntries(PASSAGES.map((p) => [p.code, p.key]));
  const LEAD_PRIORITY = { gospel: 0, epistle: 1, ot: 2, psalm: 3 };
  const REGISTERS = [
    { id: "formal", label: "Formal" },
    { id: "fresh", label: "Fresh" },
    { id: "casual", label: "Casual" },
  ];

  const lsGet = (key, fallback) => {
    try {
      const raw = localStorage.getItem("wp:" + key);
      return raw == null ? fallback : JSON.parse(raw);
    } catch (_) { return fallback; }
  };
  const lsSet = (key, val) => {
    try { localStorage.setItem("wp:" + key, JSON.stringify(val)); } catch (_) {}
  };

  function permKey(selKeys) {
    const sel = ORDER.filter((k) => selKeys.includes(k)).map((k) => CODE[k]);
    if (sel.length === 0) return null;
    if (sel.length <= 2) return sel.join("+");
    if (sel.length === 4) return "OT+Ps+Ep+Gos";
    // exactly 3: the all-four liturgy applies only when the Psalm is the
    // omitted reading (OT+Ep+Gos all present). If the Psalm is one of the
    // three, set it aside and use the remaining pair.
    if (sel.includes("OT") && sel.includes("Ep") && sel.includes("Gos")) return "OT+Ps+Ep+Gos";
    return sel.filter((c) => c !== "Ps").join("+");
  }

  // Resolve a track-keyed perm node { <track>: <value> } down to one value,
  // given the active compound effective track. Falls back gracefully so older
  // single-track Sundays (all "n/a") and saved picks without a track still work.
  function pickPrayerTrack(node, effTrack, psalmMode) {
    if (!node) return null;
    // UMC alternate-psalm toggle: the effective track carries a "+alt" flag when
    // the alternate psalm is selected. The psalm-bearing perms ("Ps"/"OT+Ps")
    // carry an extra "alt" track (from the PsALT/OT+PsALT records); prefer it,
    // and fall through to the RCL psalm content where no alternate was authored.
    const raw = effTrack || "n/a";
    const bt = raw.replace(/\+alt$/, "");
    const altOn = bt !== raw;
    if (altOn && node.alt != null) return node.alt;
    // Psalm-slot alternate (e.g. Advent 3: Psalm 146 vs the Magnificat canticle).
    // Only fires on nodes that actually carry these keys, so every other Sunday
    // and every other track is untouched.
    if (node.psalm != null || node.canticle != null) {
      const pref = psalmMode === "canticle" ? ["canticle", "psalm"] : ["psalm", "canticle"];
      for (const k of pref) if (node[k] != null) return node[k];
    }
    const order = ({
      "cct-semi": ["cct-semi", "cct", "semi", "acts", "n/a"],
      "cct-comp": ["cct-comp", "cct", "comp", "acts", "n/a"],
      "cct":      ["cct", "acts", "n/a", "cct-semi", "cct-comp"],
      "semi":     ["semi", "n/a", "acts"],
      "comp":     ["comp", "n/a", "acts"],
      "n/a":      ["n/a", "acts", "semi", "comp"],
    })[bt] || ["n/a", "acts", "semi", "comp"];
    for (const k of order) if (node[k] != null && k !== "alt") return node[k];
    const keys = Object.keys(node).filter((k) => k !== "alt");
    return keys.length ? node[keys[0]] : null;
  }

  // Which cells to show for one element given the selected readings.
  // Returns [{ perm, pkey?, value, fallback? }] where value is text[] (single
  // register) or { formal, fresh, casual }.
  function cellsFor(element, permMap, selKeys, effTrack, psalmMode) {
    if (!permMap) return [];
    const sel = ORDER.filter((k) => selKeys.includes(k));
    if (sel.length === 0) return [];
    const val = (perm) => pickPrayerTrack(permMap[perm], effTrack, psalmMode);

    if (element.perReading) {
      return sel
        .map((k) => ({ perm: CODE[k], pkey: k, value: val(CODE[k]) }))
        .filter((c) => c.value);
    }

    if (element.allApplicable) {
      // every individual blessing for a selected reading, plus every combined
      // blessing whose readings all fall within the selection (benediction)
      const selCodes = sel.map((k) => CODE[k]);
      const cells = [];
      sel.forEach((k) => {
        if (val(CODE[k])) cells.push({ perm: CODE[k], pkey: k, value: val(CODE[k]) });
      });
      Object.keys(permMap).forEach((p) => {
        const codes = p.split("+");
        if (codes.length >= 2 && codes.every((c) => selCodes.includes(c)) && val(p)) {
          cells.push({ perm: p, value: val(p), combined: true });
        }
      });
      return cells;
    }

    const key = permKey(selKeys);
    if (val(key)) return [{ perm: key, value: val(key) }];

    const lead = sel.slice().sort((a, b) => LEAD_PRIORITY[a] - LEAD_PRIORITY[b])
      .find((k) => val(CODE[k]));
    if (lead) return [{ perm: CODE[lead], pkey: lead, value: val(CODE[lead]), fallback: true }];
    return [];
  }

  // ---- Affirmation of Faith (data-affirmations.js) ----
  // The affirmation rides the SAME readings multi-select as the prayers, with the
  // Psalm stripped out (Kevin's rule: psalms never decide the affirmation). The
  // remaining 1–3 readings map to one of seven combos.
  const AFF_COMBOS = ["OT", "EP", "GO", "OT+EP", "OT+GO", "EP+GO", "ALL"];
  const AFF_CODE = { ot: "OT", epistle: "EP", gospel: "GO" };
  const AFF_LESSON_LABEL = {
    ot: "Old Testament", ot_comp: "Old Testament (complementary)",
    epistle: "Epistle", epistle_cct: "Epistle (CCT — Acts)",
    gospel: "Gospel", "gospel-palms": "Gospel (Palms)", "gospel-passion": "Gospel (Passion)",
  };
  const affData = () => window.AFFIRMATIONS || null;
  const affDay = (sundayId) => {
    const A = affData();
    return (A && A.byDay && A.byDay[sundayId]) || null;
  };
  const affText = (id) => {
    const A = affData();
    return (A && A.texts && A.texts[id]) || null;
  };
  // selKeys (canonical passage keys) -> combo key, or null when nothing but the
  // Psalm is selected.
  function affComboKey(selKeys) {
    const sel = ["ot", "epistle", "gospel"].filter((k) => selKeys.includes(k));
    if (sel.length === 0) return null;
    if (sel.length === 3) return "ALL";
    return sel.map((k) => AFF_CODE[k]).join("+");
  }
  // desc = { kind:"affirmation", element:"affirmation", sundayId, combo, id, alternate }
  const isAffDesc = (d) => !!d && d.kind === "affirmation";
  function resolveAffPick(sunday, desc) {
    const t = affText(desc.id);
    if (!t) return null;
    const label = "Affirmation of Faith — " + t.short + (desc.alternate ? " (alternate)" : "");
    return {
      meta: { id: "affirmation", label: label, single: true, affirmation: true },
      register: null,
      affirmation: t,
      alternate: !!desc.alternate,
      cells: [{ perm: desc.combo, lines: t.text || [] }],
    };
  }

  // ---- shared resolver (used by the service plan + export) ----
  // desc = { element, register, perms:[permKey,...] }  — or an affirmation desc.
  function resolvePrayerPick(sunday, desc) {
    if (isAffDesc(desc)) return resolveAffPick(sunday, desc);
    const data = window.PRAYERS && window.PRAYERS[sunday.id];
    if (!data || !desc) return null;
    const meta = (window.PRAYER_ELEMENTS || []).find((e) => e.id === desc.element);
    if (!meta) return null;
    const permMap = (data.elements && data.elements[desc.element]) || {};
    const cells = (desc.perms || []).map((perm) => {
      const node = pickPrayerTrack(permMap[perm], desc.effTrack, desc.psalmMode);
      if (!node) return null;
      const lines = meta.single ? node : node[desc.register];
      return { perm, lines: lines || [] };
    }).filter(Boolean);
    if (!cells.length) return null;
    return { meta, register: desc.register, cells };
  }

  const stripBold = (s) => String(s).replace(/\*\*/g, "");

  // Flatten a pick to plain-text lines for export / clipboard.
  function prayerPickToLines(sunday, desc, indent) {
    const r = resolvePrayerPick(sunday, desc);
    if (!r) return [];
    const pad = indent || "";
    if (isAffDesc(desc)) {
      const out = [pad + "Affirmation of Faith: " + r.affirmation.display +
        (r.alternate ? " (alternate)" : "")];
      (r.cells[0].lines || []).forEach((ln) =>
        out.push(ln === "" ? "" : pad + "  " + stripBold(ln)));
      return out;
    }
    const tag = r.meta.single ? "" : (r.register ? " (" + r.register + ")" : "");
    const out = [pad + r.meta.label + tag];
    r.cells.forEach((c, i) => {
      if (r.cells.length > 1 && i > 0) out.push("");
      c.lines.forEach((ln) => out.push(ln === "" ? "" : pad + "  " + stripBold(ln)));
    });
    return out;
  }

  window.resolvePrayerPick = resolvePrayerPick;
  window.prayerPickToLines = prayerPickToLines;

  // Stable identity for a single prayer cell (element + permutation) — used as
  // the service-plan key so each individual liturgy is added/removed on its own.
  function prayerPickKey(desc) {
    // Affirmations use their own key grammar so one Sunday can hold a pick per
    // combo, and so they never collide with an element::perm prayer key.
    if (isAffDesc(desc)) return "affirmation|" + desc.sundayId + "|" + desc.combo + "|" + desc.id;
    const perm = (desc.perms && desc.perms.length) ? desc.perms.join("+") : (desc.perm || "");
    return desc.element + "::" + perm;
  }
  window.prayerPickKey = prayerPickKey;

  // ---- inline bold (**…**) ----
  function inline(text, k) {
    const parts = String(text).split(/(\*\*[^*]+\*\*)/g);
    return parts.map((p, i) =>
      /^\*\*[^*]+\*\*$/.test(p)
        ? <strong key={k + "-" + i}>{p.slice(2, -2)}</strong>
        : <React.Fragment key={k + "-" + i}>{p}</React.Fragment>);
  }

  function PrayerBody({ lines, responsive, congregational }) {
    const cls = "wp-pr-text" +
      (responsive ? " is-responsive" : "") +
      (congregational ? " is-congregational" : "");
    return (
      <div className={cls}>
        {lines.map((ln, i) => {
          if (ln === "") return <div key={i} className="wp-pr-stanza-gap" aria-hidden="true"></div>;
          if (responsive) {
            const m = ln.match(/^(Leader|People)(\s*\([^)]*\))?:\s*(.*)$/);
            if (m) {
              const isPeople = m[1] === "People";
              return (
                <p key={i} className={"wp-pr-resp " + (isPeople ? "is-people" : "is-leader")}>
                  <span className="wp-pr-resp-body">{inline(m[3], i)}</span>
                </p>);
            }
          }
          return <p key={i} className="wp-pr-line">{inline(ln, i)}</p>;
        })}
      </div>);
  }

  function PassagePill({ code }) {
    const k = KEY_FOR_CODE[code];
    const p = PASSAGES.find((x) => x.code === code);
    if (!p) return <span className="wp-pr-perm-pill">{code}</span>;
    return <span className="wp-pr-perm-pill" data-passage={k}>{p.short}</span>;
  }

  function PermBadge({ perm }) {
    const codes = perm.split("+");
    if (codes.length === 4) return <span className="wp-pr-perm"><span className="wp-pr-perm-all">All four readings</span></span>;
    return <span className="wp-pr-perm">{codes.map((c, i) => <PassagePill key={i} code={c} />)}</span>;
  }

  // Plain text -> rich HTML carrying an explicit point size, so pasting into
  // Word / Publisher lands at that size instead of the app's default.
  const COPY_PT = 11;
  const COPY_FONT = "'Palatino Linotype', 'Book Antiqua', Palatino, Georgia, serif";
  function htmlFromText(text) {
    const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    const style = "font-size:" + COPY_PT + "pt; font-family:" + COPY_FONT;
    const body = String(text).split("\n").map((ln) =>
      ln === "" ? "<div><br></div>" : '<div style="' + style + '">' + esc(ln) + "</div>"
    ).join("");
    return '<div style="' + style + '">' + body + "</div>";
  }

  function CopyBtn({ getText, label }) {
    const [done, setDone] = useState(false);
    const onClick = () => {
      const text = getText();
      const html = htmlFromText(text);
      const after = () => { setDone(true); setTimeout(() => setDone(false), 1400); };
      // Fallback: select a styled contenteditable node and let execCommand copy
      // it as rich text — preserves the point size even when the iframe is unfocused.
      const fallback = () => {
        const div = document.createElement("div");
        div.contentEditable = "true";
        div.innerHTML = html;
        div.style.position = "fixed"; div.style.top = "0"; div.style.left = "0";
        div.style.opacity = "0"; div.style.pointerEvents = "none";
        document.body.appendChild(div);
        const range = document.createRange();
        range.selectNodeContents(div);
        const sel = window.getSelection();
        sel.removeAllRanges(); sel.addRange(range);
        try { document.execCommand("copy"); } catch (_) {}
        sel.removeAllRanges();
        document.body.removeChild(div);
        after();
      };
      try {
        if (navigator.clipboard && window.ClipboardItem) {
          // Write both flavors; rejects in unfocused / sandboxed iframes -> fall back.
          const item = new ClipboardItem({
            "text/html": new Blob([html], { type: "text/html" }),
            "text/plain": new Blob([text], { type: "text/plain" }),
          });
          navigator.clipboard.write([item]).then(after, fallback);
        } else {
          fallback();
        }
      } catch (_) { fallback(); }
    };
    return (
      <button type="button" className={"wp-pr-act " + (done ? "is-done" : "")} onClick={onClick}>
        {done ? "Copied" : (label || "Copy")}
      </button>);
  }

  function EyeOffIcon() {
    return (
      <svg width="13" height="13" viewBox="0 0 16 16" fill="none" aria-hidden="true">
        <path d="M2 8s2.2-4 6-4 6 4 6 4-2.2 4-6 4-6-4-6-4Z" stroke="currentColor" strokeWidth="1.1" />
        <circle cx="8" cy="8" r="1.6" stroke="currentColor" strokeWidth="1.1" />
        <path d="M2.5 2.5 L13.5 13.5" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" />
      </svg>);
  }
  function EyeIcon() {
    return (
      <svg width="13" height="13" viewBox="0 0 16 16" fill="none" aria-hidden="true">
        <path d="M2 8s2.2-4 6-4 6 4 6 4-2.2 4-6 4-6-4-6-4Z" stroke="currentColor" strokeWidth="1.1" />
        <circle cx="8" cy="8" r="1.6" stroke="currentColor" strokeWidth="1.1" />
      </svg>);
  }

  function HideActions({ onHideToday, onHideAll }) {
    return (
      <div className="wp-pr-hide-actions">
        <button type="button" className="wp-pr-hidebtn" onClick={onHideToday}
          title="Collapse this prayer for this Sunday only">
          <EyeOffIcon /><span>Hide this Sunday</span>
        </button>
        <button type="button" className="wp-pr-hidebtn" onClick={onHideAll}
          title="Collapse this prayer on every Sunday and move it to the end of the list">
          <EyeOffIcon /><span>Hide on all Sundays</span>
        </button>
      </div>);
  }

  function CellActions({ inPlan, onTogglePlan, getText }) {
    return (
      <div className="wp-pr-cell-foot">
        <button type="button"
          className={"wp-pr-act wp-pr-act-plan " + (inPlan ? "is-on" : "")}
          aria-pressed={inPlan} onClick={onTogglePlan}>
          {inPlan
            ? <><span className="wp-pr-act-check" aria-hidden="true">✓</span> In plan</>
            : <>+ Add to plan</>}
        </button>
        <CopyBtn getText={getText} />
      </div>);
  }

  function ElementCard({ element, permMap, selKeys, register, readings, sunday, picks, onTogglePlan, collapsed, global, onHideToday, onHideAll, onShow, effTrack, psalmMode }) {
    if (collapsed) {
      return (
        <section className={"wp-pr-card is-collapsed" + (global ? " is-global-hidden" : "")} data-element={element.id}>
          <div className="wp-pr-collapsed">
            <div className="wp-pr-collapsed-l">
              <h3 className="wp-pr-el-label">{element.label}</h3>
              <span className="wp-pr-hidden-tag">{global ? "hidden on all Sundays" : "hidden this Sunday"}</span>
            </div>
            <button type="button" className="wp-pr-showbtn" onClick={onShow}>
              <EyeIcon /><span>{global ? "Show on all Sundays" : "Show"}</span>
            </button>
          </div>
        </section>);
    }

    const cells = cellsFor(element, permMap, selKeys, effTrack, psalmMode);
    if (cells.length === 0) {
      return (
        <section className="wp-pr-card is-empty" data-element={element.id}>
          <header className="wp-pr-card-head">
            <div className="wp-pr-head-main">
              <div className="wp-pr-el-id"><h3 className="wp-pr-el-label">{element.label}</h3></div>
            </div>
            <HideActions onHideToday={onHideToday} onHideAll={onHideAll} />
          </header>
          <p className="wp-pr-none">Not written for this combination of readings.</p>
        </section>);
    }

    return (
      <section className="wp-pr-card" data-element={element.id}>
        <header className="wp-pr-card-head">
          <div className="wp-pr-head-main">
            <div className="wp-pr-el-id">
              <h3 className="wp-pr-el-label">{element.label}</h3>
              {element.single
                ? <span className="wp-pr-reg-tag is-all">all registers</span>
                : <span className="wp-pr-reg-tag">{register}</span>}
              {element.congregational && <span className="wp-pr-unison">unison</span>}
            </div>
            {element.note && <p className="wp-pr-el-note">{element.note}</p>}
          </div>
          <HideActions onHideToday={onHideToday} onHideAll={onHideAll} />
        </header>

        {cells.map((cell, ci) => {
          const lines = element.single ? cell.value : cell.value[register];
          let reading = cell.pkey && (window.readingForTrack
            ? window.readingForTrack(sunday, cell.pkey, effTrack)
            : readings[cell.pkey]);
          const cMeta = window.PRAYER_CANTICLE && window.PRAYER_CANTICLE[sunday.id];
          if (cMeta && cMeta.options[psalmMode] && cell.pkey === "psalm")
            reading = { ref: cMeta.options[psalmMode].ref, theme: "" };
          const desc = {
            element: element.id,
            register: element.single ? null : register,
            perm: cell.perm,
            perms: [cell.perm],
            effTrack: effTrack,
            psalmMode: psalmMode,
          };
          const key = window.prayerPickKey(desc);
          const inPlan = !!picks[key];
          const cellText = () => {
            const head = sunday.day + " — " + element.label +
              (element.single ? "" : " (" + register + ")");
            return [head, "", ...prayerPickToLines(sunday, desc).slice(1)].join("\n");
          };
          return (
            <article key={ci} className={"wp-pr-cell" + (inPlan ? " is-in-plan" : "")}>
              <div className="wp-pr-cell-meta">
                <PermBadge perm={cell.perm} />
                {reading && reading.ref &&
                  <span className="wp-pr-cell-ref">{reading.ref}</span>}
                {element.allApplicable &&
                  <span className={"wp-pr-kind " + (cell.combined ? "is-combined" : "is-single")}>
                    {cell.combined ? "combined" : "individual"}
                  </span>}
                {cell.fallback &&
                  <span className="wp-pr-fallback" title="No combined version for this set — showing a single reading's sending blessing.">
                    sending blessing
                  </span>}
              </div>
              <PrayerBody lines={lines || []} responsive={!!element.responsive}
                congregational={!!element.congregational} />
              <CellActions inPlan={inPlan}
                onTogglePlan={() => onTogglePlan(key, desc)} getText={cellText} />
            </article>);
        })}
      </section>);
  }

  // ---- Affirmation of Faith card (top of the Prayers tab) ----
  // Reads window.AFFIRMATIONS.byDay[sunday.id] and the SAME readings selection the
  // element cards use, minus the Psalm. Renders the combo's recommended affirmation
  // (primary + optional alternate), its one-line rationale, an expandable full text,
  // the per-lesson matches, and an add-to-plan that reuses the prayer plan store.
  function AffComboPills({ combo }) {
    const KEYS = { OT: "ot", EP: "epistle", GO: "gospel" };
    const SHORT = { OT: "OT", EP: "Epistle", GO: "Gospel" };
    if (combo === "ALL") {
      return <span className="wp-pr-perm"><span className="wp-pr-perm-all">All three lessons</span></span>;
    }
    return (
      <span className="wp-pr-perm">
        {combo.split("+").map((c, i) =>
          <span key={i} className="wp-pr-perm-pill" data-passage={KEYS[c]}>{SHORT[c]}</span>)}
      </span>);
  }

  function AffirmationCard({ sunday, selKeys, picks, onTogglePlan }) {
    const [pickIdx, setPickIdx] = useState(0);
    const [openText, setOpenText] = useState(false);
    const [openDetails, setOpenDetails] = useState(false);
    const day = affDay(sunday.id);
    const combo = affComboKey(selKeys);
    useEffect(() => { setPickIdx(0); setOpenText(false); setOpenDetails(false); },
      [sunday.id, combo]);
    if (!day) return null;

    const head = (
      <header className="wp-pr-card-head">
        <div className="wp-pr-head-main">
          <div className="wp-pr-el-id">
            <h3 className="wp-pr-el-label">Affirmation of Faith</h3>
            {combo && <AffComboPills combo={combo} />}
          </div>
          <p className="wp-pr-el-note">
            Recommended for the lessons in play — the Psalm is never part of this choice.
          </p>
        </div>
      </header>);

    if (!combo) {
      return (
        <section className="wp-pr-card aff-card is-empty" data-element="affirmation">
          {head}
          <p className="wp-pr-none">
            Select a non-psalm reading (Old Testament, Epistle or Gospel) in
            <strong> Scripture used</strong> to see the affirmation for the day.
          </p>
        </section>);
    }

    const options = (day.combos && day.combos[combo]) || [];
    if (!options.length) {
      return (
        <section className="wp-pr-card aff-card is-empty" data-element="affirmation">
          {head}
          <p className="wp-pr-none">No affirmation keyed for this combination of lessons.</p>
        </section>);
    }
    const idx = Math.min(pickIdx, options.length - 1);
    const pick = options[idx];
    const t = affText(pick.id);
    if (!t) return null;
    const alternate = idx > 0;
    const desc = {
      kind: "affirmation", element: "affirmation",
      sundayId: sunday.id, combo: combo, id: pick.id, alternate: alternate,
      perms: [combo], register: null,
    };
    const key = window.prayerPickKey(desc);
    const inPlan = !!picks[key];
    const cardText = () => {
      const lead = sunday.day + " — Affirmation of Faith";
      return [lead, "", ...prayerPickToLines(sunday, desc).slice(1)].join("\n");
    };
    const showNicene = !!day.nicene && options.some((o) => o.id === "UMH-880");

    return (
      <section className={"wp-pr-card aff-card" + (inPlan ? " is-in-plan" : "")} data-element="affirmation">
        {head}

        {options.length > 1 &&
          <div className="aff-switch" role="group" aria-label="Affirmation choice">
            {options.map((o, i) => {
              const ot = affText(o.id);
              return (
                <button key={o.id} type="button"
                  className={"aff-chip " + (i === idx ? "on" : "")}
                  aria-pressed={i === idx}
                  onClick={() => { setPickIdx(i); setOpenText(false); }}>
                  {i > 0 && <span className="aff-chip-alt">alt</span>}
                  <span className="aff-chip-id">{ot ? ot.short : o.id}</span>
                </button>);
            })}
          </div>}

        <div className="aff-pick">
          <div className="aff-pick-head">
            <span className="aff-id">{t.short}</span>
            <span className="aff-title">{t.title}</span>
            {t.voice && <span className="wp-pr-unison">{t.voice}</span>}
          </div>
          {pick.why && <p className="aff-why">{pick.why}</p>}
        </div>

        {showNicene &&
          <p className="aff-nicene">
            <span className="aff-star" aria-hidden="true">★</span>
            <span>
              <strong>Nicene Creed eligible</strong> — the {day.nicene.lesson} quotes creed
              language ({day.nicene.anchor})
            </span>
          </p>}

        {day.comp_note && <p className="aff-note">{day.comp_note}</p>}
        {day.service_note && <p className="aff-note">{day.service_note}</p>}

        <button type="button" className="aff-disclose" aria-expanded={openText}
          onClick={() => setOpenText(!openText)}>
          <span className="aff-caret" aria-hidden="true">{openText ? "▾" : "▸"}</span>
          {openText ? "Hide full text" : "Show full text"}
        </button>
        {openText &&
          <div className="aff-text">
            <PrayerBody lines={t.text || []} responsive={t.voice === "responsive"}
              congregational={t.voice === "unison"} />
            {(t.footnotes || []).map((f, i) => <p key={i} className="aff-fine">{f}</p>)}
            {t.author && <p className="aff-fine">{t.author}</p>}
            {t.credit && <p className="aff-fine">{t.credit}</p>}
          </div>}

        <button type="button" className="aff-disclose" aria-expanded={openDetails}
          onClick={() => setOpenDetails(!openDetails)}>
          <span className="aff-caret" aria-hidden="true">{openDetails ? "▾" : "▸"}</span>
          Per-lesson matches
        </button>
        {openDetails &&
          <div className="aff-lessons">
            {Object.keys(day.lessons || {}).map((lk) => {
              const lv = day.lessons[lk];
              return (
                <div key={lk} className="aff-lesson">
                  <div className="aff-lesson-h">
                    <span className="aff-lesson-l">{AFF_LESSON_LABEL[lk] || lk}</span>
                    {lv.ref && <span className="wp-pr-cell-ref">{lv.ref}</span>}
                  </div>
                  <ul className="aff-match-list">
                    {(lv.matches || []).map((m, i) => {
                      const mt = affText(m.id);
                      return (
                        <li key={i} className="aff-match">
                          <span className={"aff-grade is-" + (m.match || "thematic")}>{m.match}</span>
                          <span className="aff-match-id">{mt ? mt.short : m.id}</span>
                          <span className="aff-match-note">{m.note}</span>
                        </li>);
                    })}
                  </ul>
                </div>);
            })}
          </div>}

        <CellActions inPlan={inPlan}
          onTogglePlan={() => onTogglePlan(key, desc)} getText={cardText} />
      </section>);
  }

  function PrayersView({ sunday, planPicks, onAddPrayer, onRemovePrayer, track, changeTrack, rclVersion, changeRclVersion, effTrack, usedKeys }) {
    const data = window.PRAYERS && window.PRAYERS[sunday.id];
    const picks = planPicks || {};

    const [register, setRegister] = useState(() => {
      const r = lsGet("prayerRegister", "formal");
      return REGISTERS.some((x) => x.id === r) ? r : "formal";
    });
    // Which readings are in play this Sunday is now a SHARED, service-wide
    // choice driven by the "Scripture used" bar at the top of every tab
    // (planner.jsx, persisted per-day in wp:hiddenByDay). The Prayers tab
    // resolves its liturgy permutation from that same set.
    const selKeys = Array.isArray(usedKeys)
      ? ORDER.filter((k) => usedKeys.includes(k))
      : ORDER.slice();

    useEffect(() => { lsSet("prayerRegister", register); }, [register]);

    // Psalm-slot alternate (Advent 3: Psalm 146 vs the Magnificat). Per-day so it
    // only touches Sundays that actually offer a canticle. Stored in
    // wp:prayerPsalmModeByDay.
    const [psalmModeByDay, setPsalmModeByDay] = useState(() => {
      const o = lsGet("prayerPsalmModeByDay", {});
      return o && typeof o === "object" && !Array.isArray(o) ? o : {};
    });
    useEffect(() => { lsSet("prayerPsalmModeByDay", psalmModeByDay); }, [psalmModeByDay]);
    const setPsalmMode = (m) => setPsalmModeByDay((prev) => ({ ...prev, [sunday.id]: m }));

    // hide / collapse state. Per-day collapses live in prayerHiddenByDay;
    // global hides (collapse everywhere + sort to the end) live in prayerHiddenAll.
    const [hiddenByDay, setHiddenByDay] = useState(() => {
      const o = lsGet("prayerHiddenByDay", {});
      return o && typeof o === "object" && !Array.isArray(o) ? o : {};
    });
    const [hiddenAll, setHiddenAll] = useState(() => {
      const a = lsGet("prayerHiddenAll", []);
      return Array.isArray(a) ? a : [];
    });
    useEffect(() => { lsSet("prayerHiddenByDay", hiddenByDay); }, [hiddenByDay]);
    useEffect(() => { lsSet("prayerHiddenAll", hiddenAll); }, [hiddenAll]);

    const onTogglePlan = (key, desc) => {
      if (picks[key]) onRemovePrayer && onRemovePrayer(key);
      else onAddPrayer && onAddPrayer(desc);
    };

    if (!data) {
      // No original prayer set for this service — the affirmation track may still
      // have this day, so show its card above the notice.
      return (
        <div className="wp-pr">
        <div className="wp-pr-order">
          <AffirmationCard sunday={sunday} selKeys={selKeys}
            picks={picks} onTogglePlan={onTogglePlan} />
        </div>
        <section className="wp-pr-unwritten">
          <div className="wp-pr-unwritten-icon" aria-hidden="true">
            <svg width="38" height="38" viewBox="0 0 40 40">
              <path d="M14 8 L14 32 M26 8 L26 32 M10 12 L30 12 M10 28 L30 28 M18 18 L22 18 M20 16 L20 22"
                stroke="currentColor" fill="none" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </div>
          <h2 className="wp-pr-unwritten-h">Prayers not yet written for {sunday.day}</h2>
          <p className="wp-pr-unwritten-sub">
            No original prayer set has been generated for this service yet.
            Each written Sunday carries gathering words, a call to worship, opening and illumination prayers,
            a confession paired with its assurance, an offertory, and a benediction — in three registers,
            for every combination of the day’s readings.
          </p>
        </section>
        </div>);
    }

    const readings = sunday.readings || {};
    const readingFor = (k) => (window.readingForTrack ? window.readingForTrack(sunday, k, effTrack) : readings[k]);
    const elements = window.PRAYER_ELEMENTS || [];
    // Canticle alternate for the psalm slot, if this Sunday offers one. `default`
    // matches sunday.readings.psalm; a saved per-day pick overrides it.
    const canticleMeta = (window.PRAYER_CANTICLE || {})[sunday.id] || null;
    const psalmOpts = canticleMeta ? canticleMeta.options : null;
    const savedPsalm = psalmModeByDay[sunday.id];
    const psalmMode = (savedPsalm === "psalm" || savedPsalm === "canticle")
      ? savedPsalm : (canticleMeta ? canticleMeta.default : "psalm");
    const showPsalmToggle = !!canticleMeta && selKeys.includes("psalm");
    const hasTracks = (window.trackOptsFor && window.trackOptsFor(sunday.id).length) ||
      (window.hasCctFor && window.hasCctFor(sunday.id));

    // hide handlers + ordered list (globally-hidden elements sort to the end)
    const allHiddenSet = new Set(hiddenAll);
    const dayHiddenSet = new Set(hiddenByDay[sunday.id] || []);
    const hideToday = (elId) => setHiddenByDay((prev) => {
      const cur = new Set(prev[sunday.id] || []); cur.add(elId);
      return { ...prev, [sunday.id]: Array.from(cur) };
    });
    const showToday = (elId) => setHiddenByDay((prev) => {
      const cur = new Set(prev[sunday.id] || []); cur.delete(elId);
      const next = { ...prev };
      if (cur.size) next[sunday.id] = Array.from(cur); else delete next[sunday.id];
      return next;
    });
    const hideAll = (elId) => {
      setHiddenAll((prev) => prev.includes(elId) ? prev : [...prev, elId]);
      showToday(elId);
    };
    const showAll = (elId) => {
      setHiddenAll((prev) => prev.filter((x) => x !== elId));
      showToday(elId);
    };
    const orderedElements = [
      ...elements.filter((e) => !allHiddenSet.has(e.id)),
      ...elements.filter((e) => allHiddenSet.has(e.id)),
    ];
    const selOrdered = ORDER.filter((k) => selKeys.includes(k));
    const effKey = permKey(selKeys);
    let summary, psalmAside = false;
    if (!effKey) summary = "Select at least one reading.";
    else if (effKey === "OT+Ps+Ep+Gos") summary = "Showing prayers that weave all four readings together.";
    else {
      const labels = effKey.split("+").map((c) => PASSAGES.find((p) => p.code === c).label);
      summary = "Showing prayers drawn from " + labels.join(labels.length === 2 ? " + " : ", ") + ".";
      psalmAside = selKeys.includes("psalm") && !effKey.split("+").includes("Ps");
    }

    // whole-service clipboard text
    const serviceText = () => {
      const out = [sunday.day, sunday.season || "", ""];
      const refLine = ORDER.filter((k) => readingFor(k) && readingFor(k).ref)
        .map((k) => PASSAGES.find((p) => p.key === k).short + ": " + readingFor(k).ref).join("  ·  ");
      if (refLine) { out.push(refLine); out.push(""); }
      orderedElements.forEach((el) => {
        if (allHiddenSet.has(el.id) || dayHiddenSet.has(el.id)) return;
        const cells = cellsFor(el, data.elements[el.id], selKeys, effTrack, psalmMode);
        if (!cells.length) return;
        const desc = { element: el.id, register: el.single ? null : register, perms: cells.map((c) => c.perm), effTrack: effTrack, psalmMode: psalmMode };
        out.push("──────────");
        prayerPickToLines(sunday, desc).forEach((l) => out.push(l));
        out.push("");
      });
      return out.join("\n").replace(/\n{3,}/g, "\n\n");
    };

    return (
      <div className="wp-pr">
        {/* ---- controls ---- */}
        <div className="wp-pr-controls">
          <div className="wp-pr-ctrl-stack">
            <div className="wp-pr-ctrl">
              <span className="wp-pr-ctrl-label">Language</span>
              <div className="wp-seg wp-pr-register" role="group" aria-label="Language register">
                {REGISTERS.map((r) =>
                  <button key={r.id} type="button"
                    className={"wp-seg-btn " + (register === r.id ? "on" : "")}
                    aria-pressed={register === r.id}
                    onClick={() => setRegister(r.id)}>{r.label}</button>)}
              </div>
            </div>

            {showPsalmToggle &&
              <div className="wp-pr-ctrl">
                <span className="wp-pr-ctrl-label">Psalm slot</span>
                <div className="wp-seg" role="group" aria-label="Psalm or canticle">
                  <button type="button"
                    className={"wp-seg-btn " + (psalmMode === "psalm" ? "on" : "")}
                    aria-pressed={psalmMode === "psalm"}
                    onClick={() => setPsalmMode("psalm")}>{psalmOpts.psalm.label}</button>
                  <button type="button"
                    className={"wp-seg-btn " + (psalmMode === "canticle" ? "on" : "")}
                    aria-pressed={psalmMode === "canticle"}
                    onClick={() => setPsalmMode("canticle")}>{psalmOpts.canticle.label}</button>
                </div>
              </div>}

            {hasTracks && window.TrackControls &&
              <div className="wp-pr-ctrl wp-pr-ctrl-track">
                <span className="wp-pr-ctrl-label">Lectionary</span>
                <div className="wp-pr-track">
                  <window.TrackControls sundayId={sunday.id} track={track} rclVersion={rclVersion}
                    changeTrack={changeTrack} changeRclVersion={changeRclVersion} />
                </div>
              </div>}
          </div>

          {selOrdered.length > 0 &&
            <div className="wp-pr-ctrl wp-pr-ctrl-copy">
              <CopyBtn getText={serviceText} label="Copy service set" />
            </div>}
        </div>

        <p className="wp-pr-summary" data-empty={selOrdered.length === 0 ? "true" : "false"}>
          {summary}
          {psalmAside &&
            <span className="wp-pr-summary-note"> The Psalm is set aside in this pairing; the Benediction still offers its blessing.</span>}
          {showPsalmToggle && !psalmAside &&
            <span className="wp-pr-summary-note"> {psalmOpts[psalmMode].label} ({psalmOpts[psalmMode].ref}) stands in the psalm slot.</span>}
        </p>

        {/* ---- affirmation of faith (rides the same reading selection, minus the Psalm) ---- */}
        <div className="wp-pr-order aff-top">
          <AffirmationCard sunday={sunday} selKeys={selKeys}
            picks={picks} onTogglePlan={onTogglePlan} />
        </div>

        {/* ---- order of service ---- */}
        {selOrdered.length === 0
          ? <section className="wp-pr-prompt">
              <p>Check one or more readings in <strong>Scripture used</strong> at the top of the page to see the prayers that use them.</p>
            </section>
          : <div className="wp-pr-order">
              {orderedElements.map((el) => {
                const collapsed = allHiddenSet.has(el.id) || dayHiddenSet.has(el.id);
                const global = allHiddenSet.has(el.id);
                return (
                  <ElementCard key={el.id} element={el}
                    permMap={data.elements[el.id]}
                    selKeys={selKeys} register={register} readings={readings}
                    sunday={sunday} effTrack={effTrack} psalmMode={psalmMode}
                    picks={picks} onTogglePlan={onTogglePlan}
                    collapsed={collapsed} global={global}
                    onHideToday={() => hideToday(el.id)}
                    onHideAll={() => hideAll(el.id)}
                    onShow={() => (global ? showAll(el.id) : showToday(el.id))} />);
              })}
            </div>}
      </div>);
  }

  window.PrayersView = PrayersView;
})();
