// Analytics over the Jira bug queue. Computed client-side from the same live
// read the triage board uses (open=0 → includes resolved), so there is no extra
// endpoint and no second source of truth.
//
// Design decisions, made deliberately:
//  · P0..P3 is an ORDERED scale, not categorical identity — so it gets a
//    sequential single-hue ramp, not four unrelated hues. The app's SEV chip
//    colours (#B91C1C vs #DC2626) fail CVD separation as a categorical palette
//    (ΔE 7.5 protan), which is fine for a labelled chip and wrong for a chart.
//  · Dark mode is SELECTED from the ramp, not flipped: the darkest step is
//    1.74:1 on the dark surface. Each mode has its own validated steps.
//  · The dark ramp's adjacent ΔE is 9.7 (the 8–12 floor), which is only legal
//    with secondary encoding — hence direct labels on every bar and a table view.
//  · "Median" time-to-resolve, not mean: one 90-day bug shouldn't move it.
//  · Only agent-filed bugs can breach an SLA — nobody promised a customer an
//    update on a bug an engineer opened in Jira.
const { useState: useStateAN, useEffect: useEffectAN, useMemo: useMemoAN } = React;

// Sequential ramp, P0 (most severe) → P3. Each mode has its own steps, validated
// against that mode's surface — dark is SELECTED, never a flip of light: the
// light ramp's darkest step (#7F1D1D) is only 1.74:1 on the dark surface.
//
// Both ramps keep P0 as the most intense red. Ordering dark by "lighter = more"
// would pass the contrast check and still be wrong: P0 would render as pale pink
// next to a vivid P3, inverting how severity reads.
//   light: L 0.396 → 0.711, CVD ΔE 15.3 (pass)
//   dark : contrast all ≥ 3:1, CVD ΔE 9.7 → floor band, so every bar is
//          direct-labeled and a table view exists (secondary encoding).
const RAMP_LIGHT = ["#7F1D1D", "#B91C1C", "#EF4444", "#F87171"];
const RAMP_DARK  = ["#DC2626", "#EF4444", "#F87171", "#FCA5A5"];

function useRamp() {
  const [dark, setDark] = useStateAN(() => document.documentElement.getAttribute("data-theme") === "dark");
  useEffectAN(() => {
    const el = document.documentElement;
    const obs = new MutationObserver(() => setDark(el.getAttribute("data-theme") === "dark"));
    obs.observe(el, { attributes: true, attributeFilter: ["data-theme"] });
    return () => obs.disconnect();
  }, []);
  return dark ? RAMP_DARK : RAMP_LIGHT;
}

// ---- small helpers ---------------------------------------------------------
const DAY = 86400000;
function median(xs) {
  if (!xs.length) return null;
  const s = [...xs].sort((a, b) => a - b);
  const m = Math.floor(s.length / 2);
  return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
}
function humanMs(ms) {
  if (ms == null) return "—";
  const h = ms / 3600000;
  if (h < 1) return Math.max(1, Math.round(ms / 60000)) + "m";
  if (h < 48) return Math.round(h) + "h";
  return Math.round(h / 24) + "d";
}
function weekStart(ts) {
  const d = new Date(ts);
  d.setHours(0, 0, 0, 0);
  d.setDate(d.getDate() - ((d.getDay() + 6) % 7)); // Monday
  return d.getTime();
}
function weekLabel(ts) {
  const d = new Date(ts);
  return `${d.getDate()}/${d.getMonth() + 1}`;
}

// ---- marks -----------------------------------------------------------------
// Horizontal bars: 4px rounded data-end, anchored to the baseline, 2px gap.
function BarRow({ label, value, max, color, sub, onHover }) {
  const pct = max ? Math.max(value > 0 ? 2 : 0, (value / max) * 100) : 0;
  return (
    <div className="an-bar-row"
      onMouseEnter={(e) => onHover && onHover({ label, value, sub, x: e.clientX, y: e.clientY })}
      onMouseLeave={() => onHover && onHover(null)}>
      <span className="an-bar-label">{label}</span>
      <span className="an-bar-track">
        <span className="an-bar-fill" style={{ width: pct + "%", background: color }} />
      </span>
      <span className="an-bar-value">{value}</span>
    </div>
  );
}

// Grouped columns: opened vs resolved per week. Two series → legend, one y-axis.
function WeeklyChart({ weeks, ramp, onHover }) {
  const max = Math.max(1, ...weeks.map((w) => Math.max(w.opened, w.resolved)));
  const H = 132;
  return (
    <div className="an-weekly">
      <div className="an-legend">
        <span className="an-key"><i style={{ background: ramp[1] }} />Opened</span>
        <span className="an-key"><i style={{ background: "var(--good)" }} />Resolved</span>
      </div>
      <div className="an-cols" style={{ height: H }}>
        {weeks.map((w) => (
          <div key={w.ts} className="an-col-group">
            <div className="an-col-bars">
              {[["opened", ramp[1]], ["resolved", "var(--good)"]].map(([k, c]) => (
                <div key={k} className="an-col"
                  style={{ height: Math.round((w[k] / max) * (H - 22)) + "px", background: c, minHeight: w[k] ? 3 : 0 }}
                  onMouseEnter={(e) => onHover({ label: `${weekLabel(w.ts)} · ${k}`, value: w[k], x: e.clientX, y: e.clientY })}
                  onMouseLeave={() => onHover(null)} />
              ))}
            </div>
            <span className="an-col-label">{weekLabel(w.ts)}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

function Tooltip({ tip }) {
  if (!tip) return null;
  return (
    <div className="an-tip" style={{ left: tip.x + 12, top: tip.y + 12 }}>
      <strong>{tip.label}</strong> {tip.value}
      {tip.sub && <em> · {tip.sub}</em>}
    </div>
  );
}

function Card({ title, hint, children }) {
  return (
    <section className="card an-card">
      <div className="an-card-head">
        <h2 className="rs-h">{title}</h2>
        {hint && <p className="rs-sub">{hint}</p>}
      </div>
      {children}
    </section>
  );
}

// ---- dashboard -------------------------------------------------------------
function Analytics() {
  const ramp = useRamp();
  const [bugs, setBugs] = useStateAN([]);
  const [live, setLive] = useStateAN(false);
  const [loading, setLoading] = useStateAN(true);
  const [tip, setTip] = useStateAN(null);
  const [table, setTable] = useStateAN(false);
  const [meta, setMeta] = useStateAN(null);

  useEffectAN(() => {
    window.NS.admin.bugs.meta().then((m) => { if (m.ok) setMeta(m.meta); });
    const load = () => window.NS.admin.bugs.list(false).then((r) => {   // false = include resolved
      setLive(!!r.live); setBugs(r.bugs || []); setLoading(false);
    });
    load();
    window.addEventListener("ns:bugs-changed", load);
    return () => window.removeEventListener("ns:bugs-changed", load);
  }, []);

  const m = useMemoAN(() => {
    const open = bugs.filter((b) => b.statusCategory !== "Done");
    const resolved = bugs.filter((b) => b.resolvedAt);
    const ttr = resolved.map((b) => b.resolvedAt - b.createdAt).filter((x) => x > 0);
    const agentOpen = open.filter((b) => b.fromAgent);
    const breached = agentOpen.filter((b) => window.NS.slaState(b).state === "breached");

    const prios = (meta ? meta.priorities.map((p) => p.name) : ["P0", "P1", "P2", "P3"]);
    const byPriority = prios.map((p) => ({ name: p, count: open.filter((b) => b.priority === p).length }));

    const statuses = (meta ? meta.statuses : []).map((s) => s.name);
    const byStatus = (statuses.length ? statuses : [...new Set(bugs.map((b) => b.status))])
      .map((s) => ({ name: s, count: bugs.filter((b) => b.status === s).length,
                     category: window.NS.categoryOfStatus(s, null) }))
      .filter((s) => s.count > 0);

    const byAssignee = Object.values(open.reduce((acc, b) => {
      const n = b.assignee ? b.assignee.name : "Unassigned";
      (acc[n] = acc[n] || { name: n, count: 0 }).count++;
      return acc;
    }, {})).sort((a, b) => b.count - a.count).slice(0, 8);

    // Last 8 weeks of opened vs resolved.
    const now = weekStart(Date.now());
    const weeks = [];
    for (let i = 7; i >= 0; i--) {
      const ts = now - i * 7 * DAY;
      weeks.push({
        ts,
        opened: bugs.filter((b) => weekStart(b.createdAt) === ts).length,
        resolved: bugs.filter((b) => b.resolvedAt && weekStart(b.resolvedAt) === ts).length,
      });
    }

    const byOrigin = ["agent", "admin", "jira"].map((o) => ({ name: o, count: bugs.filter((b) => b.origin === o).length }));

    const approx = resolved.some((b) => b.resolvedApprox);

    return { open, resolved, approx, ttrMedian: median(ttr), breached, byPriority, byStatus, byAssignee, weeks, byOrigin,
             unassigned: open.filter((b) => !b.assignee).length,
             oldest: open.slice().sort((a, b) => a.createdAt - b.createdAt)[0] || null };
  }, [bugs, meta]);

  if (loading) return <div className="pq-empty">Loading from Jira…</div>;
  if (!live) return <div className="admin-note"><strong>Not connected to Jira.</strong> Analytics reads the live bug queue.</div>;

  const statusColor = (cat) => cat === "done" ? "var(--good)" : cat === "indeterminate" ? "var(--accent)" : "var(--info)";
  const maxPrio = Math.max(1, ...m.byPriority.map((p) => p.count));
  const maxStatus = Math.max(1, ...m.byStatus.map((s) => s.count));
  const maxAssignee = Math.max(1, ...m.byAssignee.map((a) => a.count));

  return (
    <div>
      <div className="page-head">
        <div className="eyebrow">Analytics</div>
        <h1 className="page-title">Bug health</h1>
        <p className="page-sub">
          Live from Jira across every {meta ? meta.issuetype : "Bug"} in <code>{meta ? meta.project : "the project"}</code>.
          Only agent-filed tickets carry an SLA — a bug your team opened in Jira never promised a customer anything.
        </p>
      </div>

      {/* Hero numbers: a single value needs no chart. */}
      <div className="grid grid-4" style={{ marginBottom: 20 }}>
        <div className="card stat"><span className="stat-label">Open bugs</span><span className="stat-value">{m.open.length}</span></div>
        <div className="card stat">
          <span className="stat-label">Median time to resolve{m.approx ? " ≈" : ""}</span>
          <span className="stat-value">{humanMs(m.ttrMedian)}</span>
          <span className="stat-foot" title={m.approx
            ? "Your Done/PRODUCTION. statuses don't set Jira's resolution field, so this is measured from last-updated — an estimate."
            : ""}>
            {m.resolved.length} resolved{m.approx ? " · estimated" : ""}
          </span>
        </div>
        <div className="card stat">
          <span className="stat-label">Past SLA</span>
          <span className="stat-value" style={{ color: m.breached.length ? "var(--sev1)" : "var(--text)" }}>{m.breached.length}</span>
          <span className="stat-foot">agent-filed only</span>
        </div>
        <div className="card stat">
          <span className="stat-label">Unassigned</span>
          <span className="stat-value" style={{ color: m.unassigned ? "var(--sev3)" : "var(--text)" }}>{m.unassigned}</span>
          <span className="stat-foot">of {m.open.length} open</span>
        </div>
      </div>

      <Card title="Opened vs resolved" hint="Last 8 weeks. Bars above the resolved line mean the backlog is growing.">
        <WeeklyChart weeks={m.weeks} ramp={ramp} onHover={setTip} />
      </Card>

      <div className="an-two">
        <Card title="Open by priority" hint="Jira's own priority scheme, ordered P0 → P3.">
          {m.byPriority.map((p, i) => (
            <BarRow key={p.name} label={p.name} value={p.count} max={maxPrio} color={ramp[i] || ramp[ramp.length - 1]} onHover={setTip} />
          ))}
        </Card>

        <Card title="By status" hint="Every status in the Bug workflow, coloured by Jira's status category.">
          {m.byStatus.map((s) => (
            <BarRow key={s.name} label={s.name} value={s.count} max={maxStatus} color={statusColor(s.category)}
              sub={s.category} onHover={setTip} />
          ))}
        </Card>
      </div>

      <div className="an-two">
        <Card title="Open bugs per assignee" hint="Top 8 by open load.">
          {m.byAssignee.map((a) => (
            <BarRow key={a.name} label={a.name} value={a.count} max={maxAssignee}
              color={a.name === "Unassigned" ? "var(--sev3)" : "var(--info)"} onHover={setTip} />
          ))}
        </Card>

        <Card title="Where bugs come from" hint="Support agent · admin portal · straight into Jira.">
          {m.byOrigin.map((o) => (
            <BarRow key={o.name} label={o.name === "agent" ? "Support agent" : o.name === "admin" ? "Admin portal" : "Created in Jira"}
              value={o.count} max={Math.max(1, ...m.byOrigin.map((x) => x.count))}
              color={o.name === "agent" ? ramp[1] : o.name === "admin" ? "var(--accent)" : "var(--info)"} onHover={setTip} />
          ))}
          {m.oldest && (
            <p className="rs-sub an-oldest">
              Oldest open bug: <a href={m.oldest.url} target="_blank" rel="noreferrer">{m.oldest.key}</a>{" "}
              — {Math.round((Date.now() - m.oldest.createdAt) / DAY)} days old.
            </p>
          )}
        </Card>
      </div>

      {/* Colour is never the only channel: a table view carries the same data. */}
      <div className="an-table-toggle">
        <button className="btn btn-ghost btn-sm" onClick={() => setTable((v) => !v)}>
          {table ? "Hide" : "Show"} data as a table
        </button>
      </div>
      {table && (
        <section className="card rs-card">
          <table className="rs-table">
            <thead><tr><th>Priority</th><th>Open</th><th>Status</th><th>Count</th></tr></thead>
            <tbody>
              {m.byPriority.map((p, i) => (
                <tr key={p.name}>
                  <td>{p.name}</td><td>{p.count}</td>
                  <td>{m.byStatus[i] ? m.byStatus[i].name : ""}</td>
                  <td>{m.byStatus[i] ? m.byStatus[i].count : ""}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </section>
      )}

      <Tooltip tip={tip} />
    </div>
  );
}

window.Analytics = Analytics;
