// MovementExplainer — window.MovementExplainer
//
// The product's core narrative surface: not displaying numbers, but EXPLAINING
// them. One component, mounted on every analytical page, driven entirely by page
// context. There is no per-page logic here and no tenant branching anywhere —
// which lines are in scope is a function of page_type (server-side), and the
// wording is a function of the tenant's own profile vocabulary.
//
// Each card answers three questions in the owner's language:
//   what moved      → the headline, with the dollar magnitude in bold
//   what drove it   → the detail, tying in the driver or the account
//   what it means   → one conservative "what to consider" line
//
// THE ATTRIBUTION BADGE IS THE HONEST BIT. It is computed deterministically on
// the server (movements.ts) and it governs what the narration was ALLOWED to
// claim — it is not a confidence score the model assigned itself:
//
//   🟢 attributed   a driver decomposes the movement, or one GL account dominates it
//   🟡 partial      part of it is explained; the rest explicitly is not
//   ⚪ unattributed no cause is established — and the card says exactly that,
//                   rather than offering a plausible-sounding guess
//
// Every number on a card is drillable, and "Drill in →" opens the same universal
// hierarchical DrillPanel (line ▸ GL account ▸ transactions) the rest of the
// platform uses. Nothing here is a dead end.
//
// Props: { tenantId, pageType, comparison?, periodStart?, periodEnd?, title?, compact? }

(function () {
  const R = window.React;
  if (!R) return;
  const { useState, useEffect } = R;
  const h = R.createElement;

  const INK = "#0F1520", MUTE = "#4B5563", LINE = "#E4E8F0";
  const POS = "#059669", NEG = "#DC2626";
  const MONO = "'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace";

  const BADGE = {
    attributed: { dot: "🟢", label: "Attributed", tone: "#0f9d76", bg: "rgba(16,185,129,.08)",
      hint: "A driver decomposes this movement, or a single GL account accounts for most of it." },
    partial: { dot: "🟡", label: "Partly explained", tone: "#b8860b", bg: "rgba(184,146,30,.10)",
      hint: "One account carries part of the movement; the rest is spread across others." },
    unattributed: { dot: "⚪", label: "Not attributed", tone: "#6B7280", bg: "rgba(107,114,128,.08)",
      hint: "The movement is diffuse and no driver applies — no cause is established by the data." },
  };

  const money = (v) => {
    const F = window.PerduraFormat;
    if (F && F.money) return F.money(v, { compact: true });
    const a = Math.abs(Math.round(v || 0));
    return (v < 0 ? "−$" : "$") + (a >= 1000 ? (a / 1000).toFixed(1) + "K" : a.toLocaleString());
  };
  const signed = (v) => (v >= 0 ? "+" : "−") + money(Math.abs(v)).replace(/^[−+]/, "");

  // ── Drill: the same universal hierarchical panel the platform uses ─────────
  // Line ▸ GL account ▸ transactions. We pass the movement's own account codes and
  // its exact date window, so the drill lands on precisely the rows that produced
  // the number on the card — not a re-derived approximation of them.
  function drill(mv, label, win) {
    if (!window.openDrillPanel || !mv) return;
    window.openDrillPanel({
      name: label || mv.label,
      category: "Movement",
      pageType: "movement_explainer",
      codes: (mv.drill && mv.drill.codes) || [],
      start: (win && win.start) || (mv.drill && mv.drill.start) || null,
      end: (win && win.end) || (mv.drill && mv.drill.end) || null,
      back: null,
      backLabel: "Back",
    });
  }

  // A number that leads somewhere. Every figure on a card is one of these — a
  // reader who doubts a number can always get to the transactions behind it.
  function Fig({ children, onClick, title, color, strong }) {
    return h("span", {
      onClick, title: title || "Show the transactions behind this number",
      className: onClick ? "drillable-label" : undefined,
      style: {
        fontFamily: MONO, fontWeight: strong ? 800 : 600,
        color: color || INK, cursor: onClick ? "pointer" : "default",
        textDecoration: onClick ? "underline dotted rgba(0,0,0,.25)" : "none",
        textUnderlineOffset: 3,
      },
    }, children);
  }

  function Skeleton({ n }) {
    // Skeletons, not a spinner: the shape of the answer is already known, so the
    // page doesn't reflow when it lands.
    return h("div", { style: { display: "flex", flexDirection: "column", gap: 10 } },
      Array.from({ length: n || 3 }).map((_, i) =>
        h("div", {
          key: i,
          style: {
            border: "1px solid " + LINE, borderRadius: 10, padding: 14,
            background: "#fff", opacity: 1 - i * 0.18,
          },
        },
          h("div", { style: { height: 12, width: "58%", borderRadius: 4, background: "#EEF1F6", marginBottom: 9 } }),
          h("div", { style: { height: 10, width: "88%", borderRadius: 4, background: "#F3F5F9", marginBottom: 6 } }),
          h("div", { style: { height: 10, width: "72%", borderRadius: 4, background: "#F3F5F9" } }),
        )));
  }

  function Card({ mv, ex, cmpLabel }) {
    const st = mv.attribution_status;
    const b = BADGE[st] || BADGE.unattributed;
    const dec = mv.driver_decomposition;
    // Favourable/adverse is a property of the LINE (a cost falling is good, revenue
    // falling is not) — computed on the server, never guessed from the sign.
    const good = mv.favourable;

    return h("div", {
      style: {
        border: "1px solid " + LINE, borderLeft: "3px solid " + b.tone,
        borderRadius: 10, padding: "14px 16px", background: "#fff",
      },
    },
      // Header: line + magnitude + badge
      h("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 12, marginBottom: 8, flexWrap: "wrap" } },
        h("div", { style: { flex: 1, minWidth: 220 } },
          h("div", { style: { fontSize: 14, fontWeight: 800, color: INK, lineHeight: 1.35 } },
            (ex && ex.headline) || mv.label),
        ),
        h("div", { style: { display: "flex", alignItems: "center", gap: 10 } },
          h(Fig, {
            strong: true, color: good ? POS : NEG,
            onClick: () => drill(mv, mv.label),
            title: "Change vs " + cmpLabel + " — show the transactions behind it",
          }, signed(mv.delta)),
          h("span", {
            title: b.hint + "  —  " + mv.attribution_reason,
            style: {
              fontSize: 10, fontWeight: 700, color: b.tone, background: b.bg,
              border: "1px solid " + b.tone + "33", borderRadius: 20,
              padding: "3px 9px", whiteSpace: "nowrap", cursor: "help",
            },
          }, b.dot + " " + b.label),
        ),
      ),

      // The numbers, each drillable.
      h("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", fontSize: 11.5, color: MUTE, marginBottom: ex ? 9 : 0 } },
        h("span", null, "Now ",
          h(Fig, { onClick: () => drill(mv, mv.label), color: INK }, money(mv.current))),
        h("span", null, cmpLabel + " ",
          h(Fig, {
            color: INK,
            onClick: () => drill(mv, mv.label + " — " + cmpLabel,
              { start: null, end: null }),
          }, money(mv.comparison))),
        mv.delta_pct != null && h("span", null, "Change ",
          h("span", { style: { fontFamily: MONO, fontWeight: 700, color: good ? POS : NEG } },
            (mv.delta >= 0 ? "+" : "−") + Math.abs(mv.delta_pct * 100).toFixed(1) + "%")),
        // Volume vs rate. This is the number the reader could not have worked out
        // for themselves, so it gets stated plainly. The two sum to the total
        // exactly — the server refuses to publish a split that doesn't.
        dec && h("span", { title: "Volume + Rate = the total change, exactly. Volume is the part explained by " + dec.driver_label.toLowerCase() + " moving; Rate is the part explained by the " + dec.per + " figure moving." },
          "Volume ",
          h(Fig, { color: INK }, signed(dec.volume_effect)),
          " · Rate ",
          h(Fig, { color: INK }, signed(dec.rate_effect))),
        mv.outlier && mv.outlier.is_outlier && h("span", {
          title: "This period sits " + Math.abs(mv.outlier.z).toFixed(1) + "σ " + mv.outlier.dir
            + " this line's own 12-month average — unusual for this line, judged against its own history.",
          style: { color: "#b8860b", fontWeight: 700 },
        }, "⚠ Unusual vs its own history"),
      ),

      // Narration.
      ex && h("div", { style: { fontSize: 12.5, color: "#374151", lineHeight: 1.55 } },
        ex.detail && h("div", { style: { marginBottom: 6 } }, ex.detail),
        ex.what_to_consider && h("div", {
          style: {
            fontSize: 12, color: INK, background: "#F7F8FB",
            borderRadius: 6, padding: "7px 10px", marginTop: 4,
          },
        }, h("b", null, "Worth considering: "), ex.what_to_consider),
      ),

      // When there is no narration (synthesis unavailable), the deterministic facts
      // still stand on their own — the panel degrades to the math, never to nothing.
      !ex && h("div", { style: { fontSize: 12, color: MUTE, fontStyle: "italic" } },
        mv.attribution_reason),

      h("div", { style: { marginTop: 10, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 } },
        h("span", { style: { fontSize: 10.5, color: "#9AA3B2" } },
          st === "unattributed" ? "No cause established by the data" : mv.attribution_reason),
        h("button", {
          onClick: () => drill(mv, mv.label),
          style: {
            background: "none", border: "none", padding: 0, color: "#1C4ED8",
            fontSize: 11.5, fontWeight: 700, cursor: "pointer", whiteSpace: "nowrap",
          },
        }, "Drill in →"),
      ),
    );
  }

  function MovementExplainer(props) {
    const tenantId = props.tenantId;
    const pageType = props.pageType;
    const comparison = props.comparison || "prior_period";
    const [phase, setPhase] = useState("loading"); // loading | done | empty | hidden
    const [res, setRes] = useState(null);

    useEffect(() => {
      let alive = true;
      if (!tenantId || !pageType || !window.PerduraSynthesis || !window.PerduraSynthesis.fetchMovements) {
        setPhase("hidden"); return;
      }
      setPhase("loading");
      window.PerduraSynthesis.fetchMovements(tenantId, pageType, {
        comparison,
        period_start: props.periodStart,
        period_end: props.periodEnd,
      }).then((r) => {
        if (!alive) return;
        if (!r || r.error) { setPhase("hidden"); return; }
        setRes(r);
        setPhase((r.movements && r.movements.length) ? "done" : "empty");
      }).catch(() => { if (alive) setPhase("hidden"); });
      return () => { alive = false; };
    }, [tenantId, pageType, comparison, props.periodStart, props.periodEnd]);

    if (phase === "hidden") return null;

    const title = props.title || "What moved, and why";

    const shell = (body, sub) => h("div", {
      style: {
        border: "1px solid " + LINE, borderRadius: 12, background: "#fff",
        boxShadow: "0 1px 6px rgba(13,32,64,0.06)", padding: 16, marginTop: 16,
      },
    },
      h("div", { style: { marginBottom: 12 } },
        h("div", { style: { fontSize: 10, fontWeight: 800, letterSpacing: ".08em", textTransform: "uppercase", color: "#6475a0" } },
          "Movement Explainer"),
        h("div", { style: { fontSize: 15, fontWeight: 800, color: INK, marginTop: 2 } }, title),
        sub && h("div", { style: { fontSize: 11.5, color: MUTE, marginTop: 3 } }, sub),
      ),
      body);

    if (phase === "loading") return shell(h(Skeleton, { n: 3 }), "Computing the period's material movements…");

    const cmpLabel = (res && res.comparison_period && res.comparison_period.label) || "the prior period";

    // Honest empty state. "Nothing moved materially" is a real, useful answer —
    // not a failure to be papered over.
    if (phase === "empty") {
      return shell(
        h("div", { style: { padding: "18px 4px", fontSize: 13, color: MUTE } },
          res && res.empty_reason === "no_data"
            ? "Awaiting underlying data — no transactions to compare yet."
            : "No material movements this period vs " + cmpLabel + "."),
        res && res.period ? res.period.label + " vs " + cmpLabel : null);
    }

    const exBy = {};
    (res.explanations || []).forEach((e) => { exBy[e.movement_id] = e; });

    const sub = res.period.label + " vs " + cmpLabel
      + " · ranked by dollar impact"
      + (res.counts && res.counts.unattributed
        ? " · " + res.counts.unattributed + " movement" + (res.counts.unattributed === 1 ? "" : "s") + " with no established cause"
        : "");

    return shell(
      h("div", { style: { display: "flex", flexDirection: "column", gap: 10 } },
        res.summary && h("div", {
          style: { fontSize: 12.5, color: "#374151", lineHeight: 1.55, paddingBottom: 4 },
        }, res.summary),
        res.movements.map((mv) =>
          h(Card, { key: mv.id, mv, ex: exBy[mv.id], cmpLabel })),
      ),
      sub);
  }

  window.MovementExplainer = MovementExplainer;
})();
