/* global React, Icon */
/*
 * "Did the buyer move?" — the one surface the rest of the app couldn't already
 * draw.
 *
 * Everything else this deal needs is rendered by components that already exist:
 * the raw feed is ActivityTab, the stakeholder map is StakeholderTab, and the
 * three actions are AIActionsTab reading `deal.nudges`. What was missing is a
 * read of where the BUYER is — kept separately from the CRM stage, allowed to
 * disagree with it, and expandable into the raw events behind every move — plus
 * the comparison against this org's own closed deals.
 *
 * Nothing on this screen is authored. Every date, count, quote and percentage
 * arrives from movement-engine.js, which reads the raw fixtures.
 */
const { useState: useMvState, useMemo: useMvMemo, useRef: useMvRef, useEffect: useMvEffect } = React;

const MV_SOURCE_LABEL = { email: "Email", call: "Meeting", note: "Note", slack: "Slack", linkedin: "LinkedIn" };

// Read the date straight off the ISO string. The fixtures are stamped +02:00,
// so the UTC getters would pull anything before 02:00 back a day.
const MV_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
function mvShortDate(iso, withYear) {
  const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || ""));
  if (!m) return String(iso || "");
  const d = `${parseInt(m[3], 10)} ${MV_MONTHS[parseInt(m[2], 10) - 1]}`;
  return withYear ? `${d} ${m[1].slice(2)}` : d;
}
// What a rep would call this thing. Event ids are wiring — they identify a row
// in a fixture and mean nothing to the person reading the deal.
function mvEventLabel(ev, names) {
  if (!ev) return "";
  const p = ev.payload || {};
  const when = mvShortDate(ev.timestamp);
  const who = (addr) => (names && names[addr]) || String(addr || "").split("@")[0].replace(/[._]/g, " ");
  if (ev.source === "email") {
    if (p.kind === "no_reply") return `No reply · ${when}`;
    return `Email from ${who(p.from)} · ${when}`;
  }
  if (ev.source === "call") {
    const them = (p.attendees || []).filter((a) => a.org !== "Addvocate").map((a) => a.name.split(" ")[0]);
    return `${p.internal ? "Internal call" : "Call with " + (them.join(", ") || "the buyer")} · ${when}`;
  }
  if (ev.source === "note") return `Your note · ${when}`;
  if (ev.source === "slack") return `Slack ${p.channel} · ${when}`;
  return `${p.actor} on LinkedIn · ${when}`;
}

// One source, named, plus a count. Listing five of them turns the column into a
// wall and repeats the word "LinkedIn" three times.
function mvEvidenceLabel(ids, byId, names) {
  const list = (ids || []).map((id) => byId[id]).filter(Boolean);
  if (!list.length) return "";
  const head = mvEventLabel(list[0], names);
  return list.length > 1 ? `${head}  +${list.length - 1} more` : head;
}

// One chip per kind of proof behind a move. "7 events" says how MUCH there is;
// it does not say what it is made of, which is the part a rep judges on — three
// LinkedIn reactions and a 42-minute call with the CFO are not the same claim.
const MV_SOURCE_ORDER = ["call", "email", "slack", "note", "linkedin"];
const MV_SOURCE_NOUN = {
  call: ["meeting", "meetings"],
  email: ["email", "emails"],
  slack: ["Slack thread", "Slack threads"],
  note: ["note", "notes"],
  linkedin: ["LinkedIn signal", "LinkedIn signals"],
};
function mvSourceCounts(ids, byId) {
  const n = {};
  (ids || []).forEach((id) => {
    const ev = byId[id];
    if (ev) n[ev.source] = (n[ev.source] || 0) + 1;
  });
  return MV_SOURCE_ORDER.filter((s) => n[s]).map((s) => ({
    src: s, n: n[s], noun: MV_SOURCE_NOUN[s][n[s] === 1 ? 0 : 1],
  }));
}

// Who is speaking, read off the attendee list on the same event. A verbatim is
// proof when the BUYER said it; our own lines in the room are context, so the
// transcript is allowed to say which is which instead of shouting every name.
function mvSpeakers(p) {
  const out = {};
  ((p || {}).attendees || []).forEach((a) => {
    const first = String(a.name || "").split(" ")[0];
    out[first.toUpperCase()] = { first, role: a.role || "", ours: a.org === "Addvocate" };
  });
  return out;
}
function mvTitleCase(s) {
  return String(s || "").charAt(0).toUpperCase() + String(s || "").slice(1).toLowerCase();
}
// Fixture bodies are hard-wrapped at ~70 columns. Rendering them in a <pre>
// preserves a line length nobody chose; paragraphs are what was written.
function mvParagraphs(body) {
  return String(body || "").split(/\n\s*\n/).map((p) => p.replace(/\s*\n\s*/g, " ").trim()).filter(Boolean);
}

// Addresses → the names on the account map, so no raw mailbox ever shows.
function mvNameMap(analysis) {
  const out = {
    "ridha@addvocate.ai": "Ridha", "priya@addvocate.ai": "Priya Raman",
    "dan@addvocate.ai": "Dan Okafor", "sofia@addvocate.ai": "Sofia Berg",
  };
  ((analysis.roles || {}).people || []).forEach((x) => { if (x.email) out[x.email] = x.name; });
  return out;
}

function mvDaysBetween(a, b) {
  return Math.round((new Date(b + "T00:00:00Z") - new Date(a + "T00:00:00Z")) / 86400000);
}

// ── the words themselves ─────────────────────────────────────────────────────
// The proof under an event is always a captioned block: who said it, when, and
// the line as it was said. A transcript is a two-column read (speaker | words),
// not a paragraph with a name buried in it, and a repeated speaker drops their
// name so consecutive lines read as one turn.
function MvEvidence({ ev }) {
  const p = ev.payload || {};

  if (p.transcript) {
    const who = mvSpeakers(p);
    const theirs = p.transcript.filter((l) => !(who[String(l.speaker || "").toUpperCase()] || {}).ours).length;
    return (
      <div className="bm-ev-body">
        <div className="bm-proof">
          <div className="bm-proof-cap">
            Transcript · {p.transcript.length} lines{theirs && theirs < p.transcript.length ? ` · ${theirs} from the buyer` : ""}
          </div>
          <ol className="bm-quotes">
            {p.transcript.map((l, i) => {
              const key = String(l.speaker || "").toUpperCase();
              const s = who[key];
              const cont = i > 0 && String(p.transcript[i - 1].speaker || "").toUpperCase() === key;
              return (
                <li key={i} className={`bm-quote ${s && s.ours ? "is-ours" : ""} ${cont ? "is-cont" : ""}`}>
                  <span className="bm-quote-t">{l.t}</span>
                  <span className="bm-quote-who" title={s && s.role ? `${s.first} · ${s.role}` : ""}>
                    {cont ? "" : (s ? s.first : mvTitleCase(l.speaker))}
                  </span>
                  <span className="bm-quote-txt">{l.text}</span>
                </li>
              );
            })}
          </ol>
        </div>
      </div>
    );
  }

  if (p.messages) {
    return (
      <div className="bm-ev-body">
        <div className="bm-proof">
          <div className="bm-proof-cap">The thread, in full</div>
          <ol className="bm-quotes is-thread">
            {p.messages.map((m, i) => {
              const cont = i > 0 && p.messages[i - 1].author === m.author;
              return (
                <li key={i} className={`bm-quote ${cont ? "is-cont" : ""}`}>
                  <span className="bm-quote-who">{cont ? "" : String(m.author || "").split(" ")[0]}</span>
                  <span className="bm-quote-txt">{m.text}</span>
                </li>
              );
            })}
          </ol>
        </div>
      </div>
    );
  }

  // A LinkedIn row has no body to quote — the head already says who and what.
  // What it does have is the part that makes it evidence: the role, the org,
  // how many times, and what the profile used to say. Those are facts, so they
  // are rendered as facts rather than dumped as a sentence.
  if (ev.source === "linkedin") {
    const verb = { posted: "Posted", commented: "Commented on", reacted: "Reacted to",
                   viewed_profile: "Viewed a profile", followed_company: "Followed",
                   profile_observed: "Profile read" }[p.action]
                 || mvTitleCase(String(p.action || "").replace(/_/g, " "));
    const prev = (p.profile_history || [])[0];
    const tie = p.connections_in_common_with;
    const obj = String(p.object || "");
    const rows = [
      // A company posts as itself: actor and org are the same string, and
      // "Sightline · Sightline" reads like a bug.
      ["Who", [p.actor, p.actor_role, p.actor_org]
        .filter(Boolean)
        .filter((x, i, a) => a.findIndex((y) => y.toLowerCase() === x.toLowerCase()) === i)
        .join(" · ")],
      ["Did", verb + (p.count > 1 ? ` · ${p.count} times` : "")],
      obj && obj.length < 90 ? ["On", obj.replace(/\((\d{4})-(\d{2})-(\d{2})\)/,
        (_, y, mo, d) => `· ${parseInt(d, 10)} ${MV_MONTHS[parseInt(mo, 10) - 1]}`)] : null,
      prev ? ["Previously", `${prev.title} at ${prev.org}`] : null,
      tie ? ["In common", `${tie.count} connections at ${tie.org}`] : null,
    ].filter(Boolean);
    return (
      <div className="bm-ev-body">
        <div className="bm-proof">
          <div className="bm-proof-cap">What the network showed</div>
          <dl className="bm-facts">
            {rows.map(([k, v]) => (
              <div key={k} className="bm-fact"><dt>{k}</dt><dd>{v}</dd></div>
            ))}
          </dl>
          {obj && obj.length >= 90 && (
            <blockquote className="bm-body">
              {mvParagraphs(obj).map((para, i) => <p key={i}>{para}</p>)}
            </blockquote>
          )}
        </div>
      </div>
    );
  }

  // The caption names the record, never repeats the subject line sitting two
  // rows above it.
  if (p.body) {
    const cap = ev.source === "email"
      ? (p.kind === "no_reply" ? "What we sent, unanswered" : "The mail, in full")
      : "The note, in full";
    return (
      <div className="bm-ev-body">
        <div className="bm-proof">
          <div className="bm-proof-cap">{cap}</div>
          <blockquote className="bm-body">
            {mvParagraphs(p.body).map((para, i) => <p key={i}>{para}</p>)}
          </blockquote>
        </div>
      </div>
    );
  }

  return (
    <div className="bm-ev-body">
      <div className="bm-proof">
        <div className="bm-proof-cap">As it arrived</div>
        <pre className="bm-raw">{JSON.stringify(p, null, 2)}</pre>
      </div>
    </div>
  );
}

// ── one raw event, as it arrived ─────────────────────────────────────────────
function MvRawEvent({ ev, causal }) {
  const p = ev.payload || {};
  const [open, setOpen] = useMvState(false);
  const label = MV_SOURCE_LABEL[ev.source] || ev.source;
  let head, detail;
  if (ev.source === "email") {
    head = p.kind === "no_reply" ? "No reply" : p.subject;
    detail = p.kind === "no_reply" ? p.body : `${p.from} → ${(p.to || []).join(", ")}`;
  } else if (ev.source === "call") {
    head = `${p.internal ? "Internal call" : "Call"}${p.duration_min ? ` · ${p.duration_min} min` : ""}`;
    detail = (p.attendees || []).map((a) => a.name).join(", ");
  } else if (ev.source === "slack") {
    head = `Slack · ${p.channel}`;
    detail = `${(p.messages || []).length} messages`;
  } else if (ev.source === "note") {
    head = "Note"; detail = p.author;
  } else if (p.action === "profile_observed") {
    // Not an action the buyer took — it is what their profile says. Render the
    // part that matters on this deal rather than the internal record name.
    const prev = (p.profile_history || [])[0];
    const tie = p.connections_in_common_with;
    head = `${p.actor} · profile`;
    detail = [
      prev && `Previously ${prev.title} at ${prev.org}`,
      tie && `${tie.count} connections at ${tie.org}`,
    ].filter(Boolean).join(" · ");
  } else {
    const verb = { posted: "posted", commented: "commented on", reacted: "reacted to",
                   viewed_profile: "viewed", followed_company: "followed" }[p.action]
                 || String(p.action || "").replace(/_/g, " ");
    head = `${p.actor} · ${verb}`;
    // Fixture objects carry an ISO date in brackets; nobody reads a deal in
    // ISO. Swap it for the same short date the rest of the panel uses.
    const obj = String(p.object || "").replace(/\((\d{4})-(\d{2})-(\d{2})\)/,
      (_, y, mo, d) => `· ${parseInt(d, 10)} ${MV_MONTHS[parseInt(mo, 10) - 1]}`);
    detail = obj.length < 80 ? obj : "";
  }

  return (
    <div className={`bm-ev ${causal ? "is-causal" : ""} ${open ? "is-open" : ""}`}>
      <button type="button" className="bm-ev-head" onClick={() => setOpen((o) => !o)} aria-expanded={open}>
        <span className="bm-ev-src">{label}</span>
        <span className="bm-ev-when mono">{mvShortDate(ev.timestamp)}</span>
        <span className="bm-ev-head-txt">
          <b>{head}</b>
          {detail ? <em>{detail}</em> : null}
        </span>
        {causal ? <span className="bm-ev-flag">moved it</span> : null}
        <Icon name="chevronR" size={11} className="bm-ev-caret" />
      </button>
      {open && <MvEvidence ev={ev} />}
    </div>
  );
}

// ── the two lines ────────────────────────────────────────────────────────────
// CRM stage above, buyer state below. The shaded area between them is the only
// thing this product is really selling — so the vertical axis is named: without
// it the line just drops, and you have to read the list underneath to find out
// what it dropped THROUGH.
function MvLines({ series, timeline, onPick, activeDate }) {
  const pts = series.points;
  if (pts.length < 2) return null;
  const W = 1000, H = 224, TOP = 18, BOT = 190;
  const x = (i) => (i / (pts.length - 1)) * W;
  const y = (v) => BOT - ((v || 0) / 100) * (BOT - TOP);
  const pctX = (i) => (x(i) / W) * 100;
  const pctY = (v) => (y(v) / H) * 100;

  const STATES = (window.MovementEngine && window.MovementEngine.STATES) || { forward: [], value: {} };
  // The ladder the buyer climbs. Backwards states share levels with it by
  // design, so they are named on the line where they happen instead.
  const ladder = STATES.forward.slice().reverse();

  const step = (key, from = 0, to = pts.length - 1) => {
    let d = "";
    for (let i = from; i <= to; i++) {
      const vy = y(pts[i][key]);
      d += i === from ? `M ${x(i)} ${vy}` : ` L ${x(i)} ${y(pts[i - 1][key])} L ${x(i)} ${vy}`;
    }
    return d;
  };

  // Shade only where the buyer is actually behind the CRM. Filling the whole
  // chart would tint the months when the deal was genuinely running ahead, and
  // the shaded area is supposed to BE the claim, not decoration.
  const runs = [];
  let run = null;
  pts.forEach((p, i) => {
    const behind = (p.buyer || 0) < (p.crm || 0);
    if (behind && run === null) run = i > 0 ? i - 1 : 0;
    if (!behind && run !== null) { runs.push([run, i]); run = null; }
  });
  if (run !== null) runs.push([run, pts.length - 1]);
  const gapPaths = runs.map(([a, z]) => {
    let d = `M ${x(a)} ${y(pts[a].crm)}`;
    for (let i = a + 1; i <= z; i++) d += ` L ${x(i)} ${y(pts[i - 1].crm)} L ${x(i)} ${y(pts[i].crm)}`;
    for (let i = z; i > a; i--) d += ` L ${x(i)} ${y(pts[i].buyer)} L ${x(i)} ${y(pts[i - 1].buyer)}`;
    return d + " Z";
  });

  // The buyer line is neutral while the deal is moving forward and turns red at
  // the first step backwards — the moment the whole tab is about.
  const firstBack = timeline.filter((t) => t.direction === "backward")[0];
  let splitIdx = pts.length - 1;
  if (firstBack) { splitIdx = pts.findIndex((p) => p.date >= firstBack.date); if (splitIdx < 0) splitIdx = pts.length - 1; }

  const marks = timeline.map((t) => {
    let idx = pts.findIndex((p) => p.date >= t.date);
    if (idx < 0) idx = pts.length - 1;
    return { t, i: idx, v: STATES.value[t.to] };
  });

  return (
    <div className="bm-lines">
      <div className="bm-lines-key">
        <span className="bm-key bm-key-crm">CRM stage</span>
        <span className="bm-key bm-key-buyer">Where the buyer actually is</span>
        <span className="bm-key bm-key-back">Moving backwards</span>
      </div>

      <div className="bm-chart">
        <div className="bm-yaxis" aria-hidden="true">
          {ladder.map((s) => (
            <span key={s} className="bm-ytick" style={{ top: `${pctY(STATES.value[s])}%` }}>{s}</span>
          ))}
        </div>
        <div className="bm-plot">
          <svg className="bm-svg" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" role="img"
               aria-label="CRM stage against derived buyer state over time">
            {ladder.map((s) => (
              <line key={s} className="bm-grid" x1="0" x2={W} y1={y(STATES.value[s])} y2={y(STATES.value[s])}
                    vectorEffect="non-scaling-stroke" />
            ))}
            {gapPaths.map((d, k) => <path key={k} className="bm-gap" d={d} />)}
            <path className="bm-line bm-line-crm" d={step("crm")} vectorEffect="non-scaling-stroke" />
            <path className="bm-line bm-line-buyer" d={step("buyer", 0, splitIdx)} vectorEffect="non-scaling-stroke" />
            {firstBack && splitIdx < pts.length - 1 && (
              <path className="bm-line bm-line-back" d={step("buyer", splitIdx, pts.length - 1)} vectorEffect="non-scaling-stroke" />
            )}
            {marks.map((m) => (
              <line key={m.t.date} className="bm-mark-rule" x1={x(m.i)} x2={x(m.i)} y1={TOP - 8} y2={BOT + 8}
                    vectorEffect="non-scaling-stroke" />
            ))}
          </svg>

          {/* Dots and the names of the backwards states live in HTML so they
              stay round and legible under the chart's non-uniform scaling. */}
          {marks.map((m) => {
            const back = m.t.direction === "backward";
            return (
              <button
                key={m.t.date}
                type="button"
                className={`bm-dot ${back ? "is-back" : ""} ${activeDate === m.t.date ? "is-active" : ""}`}
                style={{ left: `${pctX(m.i)}%`, top: `${pctY(pts[m.i].buyer)}%` }}
                onClick={() => onPick && onPick(m.t.date)}
                title={`${mvShortDate(m.t.date)} · ${m.t.from} → ${m.t.to}`}
                aria-label={`${mvShortDate(m.t.date)}, ${m.t.from} to ${m.t.to}. Open the events behind it.`}
              >
                {back && <span className="bm-dot-label">{m.t.to}</span>}
              </button>
            );
          })}
        </div>
      </div>

      <div className="bm-lines-foot">
        <span className="mono">{mvShortDate(pts[0].date)}</span>
        <span className="mono">{mvShortDate(pts[pts.length - 1].date)}</span>
      </div>
    </div>
  );
}

// ── the transitions, each opening onto its week of raw events ────────────────
function MvTransitions({ timeline, byId, names, markersFor, openDate, setOpenDate, rowRefs }) {
  return (
    <ol className="bm-trans">
      {timeline.map((t, ti) => {
        const open = openDate === t.date;
        const back = t.direction === "backward";
        // How long the deal sat still before this move. The moves cluster at the
        // end, and a row that does not say so hides the acceleration.
        const gap = ti === 0 ? null : mvDaysBetween(timeline[ti - 1].date, t.date);
        const markerCount = (markersFor[t.date] || []).length;
        return (
          <li key={t.date} className={`bm-tran ${back ? "is-back" : ""} ${open ? "is-open" : ""}`}
              ref={(el) => { if (rowRefs) rowRefs.current[t.date] = el; }}>
            <button type="button" className="bm-tran-head" onClick={() => setOpenDate(open ? null : t.date)} aria-expanded={open}>
              <span className="bm-tran-when">
                <span className="bm-tran-date mono">{mvShortDate(t.date)}</span>
                {gap != null && <span className="bm-tran-gap">{gap} days later</span>}
              </span>
              <span className="bm-tran-move">
                <span className="bm-tran-from">{t.from}</span>
                <Icon name={back ? "arrowL" : "arrow"} size={12} />
                <span className="bm-tran-to">{t.to}</span>
              </span>
              <span className="bm-tran-rule">{t.rule}</span>
              <span className="bm-tran-count">
                {markerCount > 0 && (
                  <span className="bm-chip is-marker">
                    <b>{markerCount}</b>{markerCount === 1 ? "marker" : "markers"}
                  </span>
                )}
                {mvSourceCounts(t.window, byId).map((c) => (
                  <span key={c.src} className="bm-chip"><b>{c.n}</b>{c.noun}</span>
                ))}
              </span>
              <Icon name="chevronR" size={12} className="bm-ev-caret" />
            </button>
            {open && (
              <div className="bm-tran-body">
                {(markersFor[t.date] || []).length > 0 && (
                  <div className="bm-why">
                    {markersFor[t.date].map((m) => (
                      <div key={m.id} className="bm-why-row">
                        <span className="bm-why-when mono">{mvShortDate(m.firstDate)}</span>
                        <div className="bm-marker-txt">
                          <b>{MV_MARKER_NAMES[m.id]}</b>
                          <em>{m.detail}</em>
                        </div>
                      </div>
                    ))}
                  </div>
                )}
                <div className="bm-window-label">
                  Everything on the account between {mvShortDate(byId[t.window[0]].timestamp)} and {mvShortDate(t.date)}
                </div>
                {t.window.map((id) => byId[id] ? (
                  <MvRawEvent key={id} ev={byId[id]} causal={t.evidence.indexOf(id) !== -1} />
                ) : null)}
                {t.confirmedBy.length > 0 && (
                  <div className="bm-confirm">
                    Confirmed after the fact by {t.confirmedBy.map((id) => mvEventLabel(byId[id], names)).filter(Boolean).join(", ")}.
                  </div>
                )}
              </div>
            )}
          </li>
        );
      })}
    </ol>
  );
}

// A marker belongs to the move it caused: the first transition on or after the
// day it fired. Listing them separately asks the reader to join them back up.
function mvMarkersByTransition(markers, timeline) {
  const out = {};
  timeline.forEach((t) => { out[t.date] = []; });
  (markers.present || []).forEach((m) => {
    const t = timeline.find((x) => x.date >= m.firstDate) || timeline[timeline.length - 1];
    if (t) out[t.date].push(m);
  });
  Object.keys(out).forEach((k) => out[k].sort((a, b) => (a.firstDate < b.firstDate ? -1 : 1)));
  return out;
}

// ── the seven markers, in the order they actually fired ──────────────────────
// The names a rep would use. Shown inside the move each one caused.
const MV_MARKER_NAMES = {
  M1: "A new senior stakeholder appeared with no contact with us",
  M2: "The champion stopped calling the decision theirs",
  M3: "The exec step was folded into a wider cycle",
  M4: "Reply latency broke the deal's own rolling median",
  M5: "Procurement arrived comparing, not papering",
  M6: "An extended security assessment arrived before any agreement",
  M7: "The buyer side started reading the competitor in public",
};

// ── matched against closed history ───────────────────────────────────────────
function MvMatched({ match, board, historic, setTab, actions }) {
  const [openDeal, setOpenDeal] = useMvState(null);
  const b = match.buckets;
  const bars = [
    { k: "within7", label: "Reached within 7 days", v: b.within7 },
    { k: "d8to14", label: "8 to 14 days", v: b.d8to14 },
    { k: "after14", label: "After 14 days", v: b.after14 },
  ];

  return (
    <>
      <div>
        <div className="bm-shape">
          <div className="bm-shape-n">
            <b className="mono">{match.total}</b>
            <span>deals carried {match.threshold} or more of the same markers</span>
          </div>
          <div className="bm-shape-split">
            <span>{match.won} won</span>
            <span className="bm-shape-lost">{match.lost} lost</span>
          </div>
        </div>

        {/* The win rate is the argument, so it leads each row. The bar is only
            there to make the two decisive numbers comparable at a glance. */}
        <div className="bm-bars">
          {bars.map((row) => (
            <div key={row.k} className={`bm-bar ${match.myBucket === row.k ? "is-here" : ""}`}>
              <b className="bm-bar-rate mono">{row.v.rate}%</b>
              <span className="bm-bar-track">
                <span className="bm-bar-fill" style={{ width: `${row.v.rate}%` }} />
              </span>
              <span className="bm-bar-label">{row.label}</span>
              <span className="bm-bar-n mono">{row.v.won} of {row.v.deals} won</span>
              {match.myBucket === row.k && (
                <span className="bm-bar-you">This deal is on day {match.daysSinceFirstMarker}</span>
              )}
            </div>
          ))}
        </div>

        <div className="bm-sub">Of the {match.lost} that were lost</div>
        <dl className="bm-stats">
          <div>
            <dt className="mono">{match.lostFacts.noContactBeforeDay21} of {match.lost}</dt>
            <dd>never reached the new stakeholder before day 21</dd>
          </div>
          <div>
            <dt className="mono">{match.lostFacts.extendedSecurity} of {match.lost}</dt>
            <dd>got the extended security assessment before any commercial agreement</dd>
          </div>
          <div>
            <dt className="mono">{match.lostFacts.thirdPersonChampion} of {match.lost}</dt>
            <dd>had a champion who started saying "they" about the decision</dd>
          </div>
        </dl>

        <div className="bm-sub">The same sentence, four deals</div>
        <div className="bm-board">
          {board.map((r) => (
            <div key={r.deal_id} className={`bm-board-row is-${r.outcome}`}>
              <span className="bm-board-when mono">{mvShortDate(r.date, true)}</span>
              <span className="bm-board-who">{r.account}<em>{r.speaker}</em></span>
              <q className="bm-board-q">{r.sentence}</q>
              <span className={`bm-board-out is-${r.outcome}`}>
                {r.outcome === "live" ? "on the table" : r.outcome}
                {r.note ? <em>{r.note}</em> : null}
              </span>
            </div>
          ))}
        </div>

        <div className="bm-sub">The closest three, with their raw evidence</div>
        {match.closest.map((s) => {
          const d = s.deal;
          const open = openDeal === d.deal_id;
          const bundle = historic[d.deal_id];
          const events = bundle ? window.MovementEngine.allEvents(bundle) : [];
          return (
            <div key={d.deal_id} className={`bm-match ${open ? "is-open" : ""}`}>
              <button type="button" className="bm-match-head" onClick={() => setOpenDeal(open ? null : d.deal_id)} aria-expanded={open}>
                <span className="bm-match-co">{d.account}</span>
                <span className={`chip ${d.outcome === "won" ? "good" : "warn"}`}>{d.outcome}</span>
                <span className="bm-match-meta mono">
                  {s.overlap.length} of 7 shared · reached the new stakeholder on day {d.days_to_contact_after_first_marker}
                </span>
                <Icon name="chevronR" size={12} className="bm-ev-caret" />
              </button>
              {open && (
                <div className="bm-match-body">
                  {events.length
                    ? events.map((ev) => <MvRawEvent key={ev.event_id} ev={ev} />)
                    : <p className="bm-note">Outcome record only for this deal — no raw evidence was retained.</p>}
                </div>
              )}
            </div>
          );
        })}

        <p className="bm-note bm-control">
          And when it should stay quiet, it does: <b>{match.control.deal.account}</b> went silent for{" "}
          {match.control.deal.champion_silent_days} days over the same August and closed won on its
          original close date. It carried {match.control.markerCount} of the 7 markers, so it never
          entered this set. The threshold is {match.threshold} markers, not one quiet week.
        </p>

        <button type="button" className="btn accent bm-cta" onClick={() => setTab && setTab("ai")}>
          <Icon name="bolt" size={14} /> {actions.length} moves that separated the wins
        </button>
      </div>
    </>
  );
}

// ── a foldable section ───────────────────────────────────────────────────────
// The answer opens; the two bodies of proof stay folded until asked for. A
// folded section still states what is inside it, so closing one does not hide
// the fact that it exists.
function MvSection({ id, title, summary, open, onToggle, children }) {
  return (
    <section className={`dd-section bm-fold ${open ? "is-open" : ""}`} id={id}>
      <button type="button" className="bm-fold-head" onClick={onToggle} aria-expanded={open}>
        <span className="bm-fold-title">{title}</span>
        <span className="bm-fold-summary">{summary}</span>
        <Icon name="chevron" size={14} className="bm-fold-caret" />
      </button>
      {open && <div className="bm-fold-body">{children}</div>}
    </section>
  );
}

// ── the tab ──────────────────────────────────────────────────────────────────
function MovementTab({ deal, setTab }) {
  const [, force] = useMvState(0);
  const [openDate, setOpenDate] = useMvState(null);
  // The answer is the point of the tab, so it is the only one open on arrival.
  // The two bodies of proof are there when asked for.
  const [openSections, setOpenSections] = useMvState({ answer: true, moves: false, history: false });
  const toggle = (k) => setOpenSections((o) => ({ ...o, [k]: !o[k] }));
  const reveal = (k) => setOpenSections((o) => (o[k] ? o : { ...o, [k]: true }));
  const rowRefs = useMvRef({});
  const rootRef = useMvRef(null);

  useMvEffect(() => {
    if (!window.MovementAdapter) return;
    return window.MovementAdapter.subscribe(() => force((n) => n + 1));
  }, []);
  const st = window.MovementAdapter ? window.MovementAdapter.state() : { status: "error", error: "adapter not loaded" };
  const a = deal.movement || (window.MovementAdapter && window.MovementAdapter.analysis());

  const names = useMvMemo(() => (a ? mvNameMap(a) : {}), [a]);
  const byId = useMvMemo(() => {
    const o = {};
    (a ? a.events : []).forEach((e) => { o[e.event_id] = e; });
    return o;
  }, [a]);

  // Clicking a point on the chart opens its transition — which sits below the
  // fold, so take the reader there too. Otherwise the click reads as broken.
  const pickTransition = (date) => {
    setOpenDate((cur) => (cur === date ? cur : date));
    reveal("moves");
    window.requestAnimationFrame(() => {
      const el = rowRefs.current[date];
      if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
    });
    // The row is only in the DOM once the fold has rendered.
    window.setTimeout(() => {
      const el = rowRefs.current[date];
      if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
    }, 60);
  };

  if (!a) {
    return (
      <div className="dd-section" style={{ paddingTop: 8 }}>
        <div className="placeholder-stripe" style={{ minHeight: 180 }}>
          {st.status === "error"
            ? `Raw events could not be read — ${st.error}. Serve the prototype over HTTP (python3 serve.py 8002); fixtures are fetched, not inlined.`
            : "Reading the raw events…"}
        </div>
      </div>
    );
  }

  const back = a.timeline.filter((t) => t.direction === "backward")[0];
  const markerSpan = mvDaysBetween(a.markers.firstMarkerDate, a.markers.present.slice()
    .sort((x, y) => (x.firstDate < y.firstDate ? 1 : -1))[0].firstDate);
  const markersFor = mvMarkersByTransition(a.markers, a.timeline);
  const backCount = a.timeline.filter((t) => t.direction === "backward").length;

  return (
    <div className="bm-tab" ref={rootRef} data-tour="movement-tab">
      <div className="dd-rail-track">
        <section className={`dd-section is-primary bm-head bm-fold ${openSections.answer ? "is-open" : ""}`} id="bm-s-answer">
          <button type="button" className="bm-fold-head is-verdict"
                  onClick={() => toggle("answer")} aria-expanded={openSections.answer}>
            <h3 className="bm-verdict">
              Yes. {a.match.daysSinceBackwards} days ago, on {mvShortDate(back.date)}.
            </h3>
            <Icon name="chevron" size={16} className="bm-fold-caret" />
          </button>
          {openSections.answer && (
          <div className="bm-fold-body">

          {/* The one comparison this product exists to make. Two readings of the
              same deal, side by side, each with the date it was last true. */}
          <div className="bm-contrast">
            <div className="bm-side">
              <span className="bm-side-label">Your CRM says</span>
              <b className="bm-side-val">{a.crm.forecast_cat}</b>
              <span className="bm-side-meta">{a.series.crmStage} · {a.series.crmScore}%</span>
              <span className="bm-side-note">unchanged for {a.series.crmUnchangedDays} days</span>
            </div>
            <span className="bm-vs" aria-hidden="true">vs</span>
            <div className="bm-side is-buyer">
              <span className="bm-side-label">The buyer is</span>
              <b className="bm-side-val">{a.series.state}</b>
              <span className="bm-side-meta">down from {back.from}</span>
              <span className="bm-side-note">
                {backCount} steps back in {a.match.daysSinceBackwards} days
              </span>
            </div>
          </div>

          <MvLines series={a.series} timeline={a.timeline} onPick={pickTransition} activeDate={openDate} />
          </div>
          )}
        </section>

        <MvSection
          id="bm-s-moves"
          title="The proof, on this deal"
          summary={`${a.timeline.length} moves · ${backCount} backwards · ${a.markers.count} of ${a.markers.all.length} markers in ${markerSpan} days`}
          open={openSections.moves}
          onToggle={() => toggle("moves")}
        >
          <MvTransitions timeline={a.timeline} byId={byId} names={names} markersFor={markersFor}
                         openDate={openDate} setOpenDate={setOpenDate} rowRefs={rowRefs} />
        </MvSection>

        <MvSection
          id="bm-s-history"
          title="The proof, from your own closed deals"
          summary={`${a.match.total} deals carried this shape · ${a.match.won} won · ${a.match.lost} lost`}
          open={openSections.history}
          onToggle={() => toggle("history")}
        >
          <MvMatched
            match={a.match}
            board={a.board}
            historic={(window.MovementAdapter.state().bundles || {}).historic || {}}
            actions={a.actions}
            setTab={setTab}
          />
        </MvSection>
      </div>
    </div>
  );
}

window.MovementTab = MovementTab;
