/* global React, ReactDOM, JOURNEY, META_RX, SKIP_OPENERS_RX, EditorProvider, EditableText, FeatureBlock */
const { useState, useEffect, useRef, useMemo } = React;
const PRIVATE_MESSAGE_RX = /\b(porn|sexual|sex|spank\w*|naked|chok\w*|jerk\w*|rape\w*|period|tested|black|indian|racist|race)\b/i;

// --------------------------------------------------------------------
// Data extraction helpers
// --------------------------------------------------------------------

function buildNarrative(items) {
  // Concatenate non-meta summary sentences, grouped into paragraphs.
  const sents = items
    .map((it) => it.summary_sentence || "")
    .filter(Boolean)
    .filter((s) => !window.SKIP_OPENERS_RX.test(s.trim()))
    .filter((s) => !window.META_RX.test(s));
  // group into paragraphs of ~3 sentences
  const paras = [];
  for (let i = 0; i < sents.length; i += 3) {
    paras.push(sents.slice(i, i + 3).join(" "));
  }
  // strip backticks, replace fenced numbers
  return paras.map((p) =>
    p.replace(/`([^`]+)`/g, "$1").replace(/\s+/g, " ").trim()
  );
}

function pickFeaturedMessages(items, count = 4) {
  const all = collectOrderedMessages(items);
  if (!all.length) return [];
  if (all.length <= count) return all;

  let bestStart = 0;
  let bestScore = -Infinity;
  for (let i = 0; i <= all.length - count; i++) {
    const window = all.slice(i, i + count);
    const score = scoreMessageWindow(window, i, all.length);
    if (score > bestScore) {
      bestScore = score;
      bestStart = i;
    }
  }

  return all.slice(bestStart, bestStart + count);
}

function buildFeaturedMessageSets(meta, items) {
  if (Array.isArray(meta.messageSets) && meta.messageSets.length) {
    return meta.messageSets
      .map((set, setIndex) => {
        const id = set.id || `set-${setIndex}`;
        const messages = (set.messages || [])
          .map((message, messageIndex) => ({
            message_id: `${id}-${messageIndex}`,
            sender: message.sender,
            text: message.text,
          }))
          .filter((message) => message.sender && (message.text || "").trim());

        return {
          id,
          caption: set.caption,
          messages,
          afterParagraph: set.afterParagraph,
        };
      })
      .filter((set) => set.messages.length);
  }

  const fallback = pickFeaturedMessages(items, 4);
  return fallback.length ? [{ id: "auto", messages: fallback }] : [];
}

function collectOrderedMessages(items) {
  const messages = [];
  const seen = new Set();
  for (const it of items) {
    if (!it.messages) continue;
    for (const m of it.messages) {
      const t = (m.text || "").trim();
      if (!t) continue;
      const key = [
        m.message_id || "",
        m.source_rowid || "",
        m.local_datetime || "",
        m.sender || "",
        t,
      ].join("|");
      if (seen.has(key)) continue;
      seen.add(key);
      messages.push({ ...m, text: t });
    }
  }

  return messages.sort((a, b) => {
    const byTime = (a.local_datetime || "").localeCompare(b.local_datetime || "");
    if (byTime) return byTime;
    const byRow = (a.source_rowid || 0) - (b.source_rowid || 0);
    if (byRow) return byRow;
    return (a.message_id || 0) - (b.message_id || 0);
  });
}

function scoreMessageWindow(messages, start, total) {
  let score = 0;
  const senders = new Set(messages.map((m) => m.sender));
  const lengths = messages.map((m) => (m.text || "").trim().length);
  const times = messages.map((m) => Date.parse(m.local_datetime || ""));

  score += senders.size > 1 ? 80 : 0;
  for (let i = 1; i < messages.length; i++) {
    if (messages[i].sender !== messages[i - 1].sender) score += 22;
    if (Number.isFinite(times[i]) && Number.isFinite(times[i - 1])) {
      const gapMinutes = (times[i] - times[i - 1]) / 60000;
      if (gapMinutes <= 2) score += 36;
      else if (gapMinutes <= 15) score += 24;
      else if (gapMinutes <= 60) score += 8;
      else if (gapMinutes <= 360) score -= 18;
      else score -= 46;
    }
  }

  for (const len of lengths) {
    if (len >= 10 && len <= 180) score += 18;
    if (len < 3) score -= 20;
    if (len > 260) score -= 28;
  }

  const text = messages.map((m) => m.text || "").join(" ");
  if (/^https?:\/\//i.test(text.trim())) score -= 50;
  if (text.includes("\uFFFC")) score -= 120;
  if (PRIVATE_MESSAGE_RX.test(text)) score -= 1000;

  const firstTime = times[0];
  const lastTime = times[times.length - 1];
  if (Number.isFinite(firstTime) && Number.isFinite(lastTime)) {
    const durationMinutes = (lastTime - firstTime) / 60000;
    if (durationMinutes <= 15) score += 40;
    else if (durationMinutes <= 60) score += 16;
    else if (durationMinutes > 360) score -= 35;
  }

  const middle = (total - messages.length) / 2;
  score -= Math.abs(start - middle) * 0.02;
  return score;
}

function buildDailyCounts(items, scope) {
  // Prefer explicit daily_counts in any item
  for (const it of items) {
    const dc = it.database_evidence && it.database_evidence.daily_counts;
    if (Array.isArray(dc) && dc.length > 0) {
      return dc.map((d) => ({
        day: d.day,
        michael: d.michael || 0,
        christina: d.christina || 0,
        total: d.total_rows || (d.michael || 0) + (d.christina || 0),
      }));
    }
  }
  // Fallback: count from embedded messages
  const map = new Map();
  for (const it of items) {
    if (!it.messages) continue;
    for (const m of it.messages) {
      if (!m.date) continue;
      if (!map.has(m.date)) map.set(m.date, { michael: 0, christina: 0 });
      const e = map.get(m.date);
      if (m.sender === "Michael") e.michael++;
      else if (m.sender === "Christina") e.christina++;
    }
  }
  if (!map.size) return null;
  const days = [...map.keys()].sort();
  return days.map((day) => {
    const e = map.get(day);
    return { day, michael: e.michael, christina: e.christina, total: e.michael + e.christina };
  });
}

function findStats(items) {
  let total = null, mike = null, chris = null;
  for (const it of items) {
    const ev = it.database_evidence || {};
    if (typeof ev.total_rows === "number" && total == null) total = ev.total_rows;
    if (Array.isArray(ev.by_sender)) {
      for (const s of ev.by_sender) {
        if (s.sender === "Michael" && mike == null) mike = s.total_rows;
        if (s.sender === "Christina" && chris == null) chris = s.total_rows;
      }
    }
  }
  // Also try to read total_rows from scope
  return { total, mike, chris };
}

// --------------------------------------------------------------------
// Hero — animated message handoff
// --------------------------------------------------------------------
function Hero() {
  // sequence: typing → "Hello Christina!" → typing → "Mike from hinge"
  const [stage, setStage] = useState(0);
  useEffect(() => {
    const seq = [800, 1400, 1800, 1300, 1700];
    let t = 0;
    const timers = [];
    seq.forEach((d, i) => {
      t += d;
      timers.push(setTimeout(() => setStage(i + 1), t));
    });
    return () => timers.forEach(clearTimeout);
  }, []);

  return (
    <section className="hero">
      <EditableText
        id="hero.label"
        text="A Story in Messages"
        as="div"
        className="label reveal in"
      />

      <div className="hero-stage">
        {stage >= 1 && stage < 2 && <TypingBubble side="michael" />}
        {stage >= 2 && (
          <Bubble id="hero.bubble.hello" sender="Michael" text="Hello Christina!" animate />
        )}
        {stage >= 3 && stage < 4 && <TypingBubble side="michael" />}
        {stage >= 4 && (
          <Bubble id="hero.bubble.hinge" sender="Michael" text="Mike from hinge" animate />
        )}
        {stage >= 5 && (
          <EditableText
            id="hero.meta"
            text="May 13, 2025 · 4:07 pm"
            as="div"
            className="hero-meta"
            style={{ opacity: 0, animation: "pop-in 0.8s 0.1s forwards" }}
          />
        )}
      </div>

      <Reveal>
        <h1 className="hero-title">
          <EditableText id="hero.title.one" text="One" />{" "}
          <em>
            <EditableText id="hero.title.year" text="Year" />
          </em>
        </h1>
      </Reveal>
      <Reveal delay={2}>
        <p className="hero-sub">
          <EditableText
            id="hero.subtitle"
            text="Twelve months of messages, from the first hello to here — a journey through every chapter, every constellation of days, every word that became us."
          />
        </p>
      </Reveal>

      <EditableText id="hero.scroll" text="scroll" as="div" className="scroll-cue" />
    </section>
  );
}

// --------------------------------------------------------------------
// Chapter
// --------------------------------------------------------------------
function Chapter({ meta, data, idx }) {
  const items = data.items || [];
  const narrative = useMemo(() => buildNarrative(items), [data]);
  const stats = useMemo(() => findStats(items), [data]);
  const featuredSets = useMemo(() => buildFeaturedMessageSets(meta, items), [meta, items]);
  const days = useMemo(() => buildDailyCounts(items, data.scope), [data]);

  // Pick a poetic pull-quote from the narrative or fallback to chapter title
  const pullText = narrative.find((p) => p.length > 80 && p.length < 220) || meta.epigraph;

  // split paragraphs into "before pull" and "after pull"
  const split = Math.max(1, Math.floor(narrative.length / 2));
  const before = narrative.slice(0, split);
  const after = narrative.slice(split);

  const total = stats.total || (data.scope && data.scope.total_rows) || null;
  const idBase = `chapter.${idx}`;
  const featuredByParagraph = useMemo(() => {
    const groups = new Map();
    const lastParagraph = Math.max(0, narrative.length - 1);

    featuredSets.forEach((set, index) => {
      const fallback = index === 0 ? Math.max(0, split - 1) : lastParagraph;
      const requested = Number.isFinite(set.afterParagraph) ? set.afterParagraph : fallback;
      const afterParagraph = Math.max(-1, Math.min(lastParagraph, requested));
      if (!groups.has(afterParagraph)) groups.set(afterParagraph, []);
      groups.get(afterParagraph).push(set);
    });

    return groups;
  }, [featuredSets, narrative.length, split]);

  const renderFeaturedAfter = (paragraphIndex) => (
    (featuredByParagraph.get(paragraphIndex) || []).map((set) => (
      <FeatureBlock
        key={set.id}
        messages={set.messages}
        caption={set.caption}
        idBase={`${idBase}.featured.${set.id}`}
      />
    ))
  );

  return (
    <section
      className="chapter"
      id={`m-${idx}`}
      data-screen-label={`${String(idx + 1).padStart(2, "0")} ${meta.short} ${meta.year}`}
    >
      <div className="col">
        <Reveal className="chapter-header">
          <EditableText
            id={`${idBase}.roman`}
            text={meta.chapter}
            as="div"
            className="chapter-roman"
          />
          <h2 className="chapter-month">
            <em>
              <EditableText id={`${idBase}.month`} text={meta.short} />
            </em>
          </h2>
          <EditableText
            id={`${idBase}.year`}
            text={meta.year}
            as="div"
            className="chapter-year"
          />
          <EditableText
            id={`${idBase}.title`}
            text={meta.title}
            as="div"
            className="chapter-title"
          />
          <EditableText
            id={`${idBase}.epigraph`}
            text={meta.epigraph}
            as="div"
            className="chapter-epigraph"
          />
        </Reveal>

        {(total || stats.mike || stats.chris) && (
          <StatRow
            stats={[
              total && { num: total.toLocaleString(), label: "messages" },
              stats.mike && { num: stats.mike.toLocaleString(), label: "Michael" },
              stats.chris && { num: stats.chris.toLocaleString(), label: "Christina" },
            ].filter(Boolean)}
            idBase={`${idBase}.stats`}
          />
        )}

        <div className="chapter-body">
          {renderFeaturedAfter(-1)}
          {before.map((p, i) => (
            <React.Fragment key={`b-${i}`}>
              <Reveal as="p">
                <EditableText id={`${idBase}.body.before.${i}`} text={p} />
              </Reveal>
              {renderFeaturedAfter(i)}
            </React.Fragment>
          ))}
        </div>

        {pullText && before.length > 0 && (
          <Pull id={`${idBase}.pull`}>{`“${pullText}”`}</Pull>
        )}

        <div className="chapter-body">
          {after.map((p, i) => (
            <React.Fragment key={`a-${i}`}>
              <Reveal as="p">
                <EditableText id={`${idBase}.body.after.${i}`} text={p} />
              </Reveal>
              {renderFeaturedAfter(i + split)}
            </React.Fragment>
          ))}
        </div>
      </div>

      {days && days.length > 1 && (
        <Constellation
          days={days}
          monthLabel={meta.monthLabel}
          totalRows={total}
          idBase={`${idBase}.constellation`}
        />
      )}

      <Divider />
    </section>
  );
}

// --------------------------------------------------------------------
// Rail nav — floating right side
// --------------------------------------------------------------------
function Rail({ active, onJump }) {
  return (
    <nav className="rail" aria-label="Months">
      {JOURNEY.map((m, i) => (
        <button
          key={m.file}
          className={`rail-item ${active === i ? "active" : ""}`}
          onClick={() => onJump(i)}
        >
          <span className="rail-dot"></span>
          <span className="rail-label">{m.short} {m.year.slice(2)}</span>
        </button>
      ))}
    </nav>
  );
}

// --------------------------------------------------------------------
// App
// --------------------------------------------------------------------
function App() {
  const [data, setData] = useState(null);
  const [active, setActive] = useState(0);

  useEffect(() => {
    Promise.all(
      JOURNEY.map((m) => fetch(m.file).then((r) => r.json()))
    ).then((arr) => setData(arr));
  }, []);

  // Scroll-spy for rail
  useEffect(() => {
    if (!data) return;
    const handler = () => {
      let best = 0;
      for (let i = 0; i < JOURNEY.length; i++) {
        const el = document.getElementById(`m-${i}`);
        if (!el) continue;
        const r = el.getBoundingClientRect();
        if (r.top < window.innerHeight * 0.5) best = i;
      }
      setActive(best);
    };
    handler();
    window.addEventListener("scroll", handler, { passive: true });
    return () => window.removeEventListener("scroll", handler);
  }, [data]);

  const onJump = (i) => {
    const el = document.getElementById(`m-${i}`);
    if (el) window.scrollTo({ top: el.offsetTop - 40, behavior: "smooth" });
  };

  if (!data) return <div className="loading">loading the year…</div>;

  return (
    <>
      <Hero />
      <Rail active={active} onJump={onJump} />
      {JOURNEY.map((meta, i) => (
        <Chapter key={meta.file} meta={meta} data={data[i]} idx={i} />
      ))}

      <section className="coda">
        <EditableText id="coda.label" text="Coda" as="div" className="reveal in label" />
        <Reveal>
          <EditableText id="coda.title" text="One year, ours." as="h2" />
        </Reveal>
        <Reveal delay={2}>
          <p>
            <EditableText
              id="coda.text"
              text="From a Hinge handoff in May to a rocket launch in April — every word, every silence, every Sunday became the shape of us."
            />
          </p>
        </Reveal>
        <div className="coda-heart" aria-hidden="true"></div>
      </section>
    </>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(
  <EditorProvider>
    <App />
  </EditorProvider>
);
