/* ============================================================
   Metric hover context — shared by the Drivers page and FBO Detail.
   Gives any FBC row or owner row a hover popover with:
     • the raw current metric value (+ YoY where we have prior data)
     • a year-to-date weekly trend sparkline
     • the entity's rank among all owners (or all FBCs) in the system
   All values come straight through the MM.* accessors / per-week
   driver components (o.dvw2026) — no data is fabricated here.
   ============================================================ */
const { useState: useStateMH, useRef: useRefMH, useLayoutEffect: useLayoutEffectMH } = React;

/* ---- per-driver metadata: how to read the raw value + weekly series ---- */
const DRIVER_META = {
  conversion: {
    label: "Estimate close rate", higherBetter: true, kind: "pts",
    raw: (o) => o.conv2026, prior: (o) => o.conv2025,
    fmt: (v) => v == null ? "—" : v.toFixed(1) + "%",
    weekly: (dvw, i) => {
      const ne = dvw.numEst[i], ec = dvw.estClose[i];
      return (typeof ne === "number" && ne > 0 && typeof ec === "number") ? ec / ne * 100 : null;
    }
  },
  frequency: {
    label: "Monthly cleans / customer", higherBetter: true, kind: "num",
    raw: (o) => o.freq2026, prior: (o) => o.freq2025,
    fmt: (v) => v == null ? "—" : v.toFixed(2),
    weekly: (dvw, i) => typeof dvw.cleansPerCust[i] === "number" ? dvw.cleansPerCust[i] : null
  },
  retention: {
    label: "Annualized 13-wk churn", higherBetter: false, kind: "pts",
    raw: (o) => o.churn2026, prior: (o) => o.churn2025,
    fmt: (v) => v == null ? "—" : v.toFixed(1) + "%",
    // dvw churn is stored as a fraction (0..1); ×100 to read as a percent
    weekly: (dvw, i) => typeof dvw.churn[i] === "number" ? dvw.churn[i] * 100 : null
  },
  engagement: {
    label: "FranConnect contacts YTD", higherBetter: true, kind: "int",
    raw: (o) => o.contactsYtd, prior: () => null,
    fmt: (v) => v == null ? "—" : Math.round(v) + " / 10",
    weekly: null
  },
  nps: {
    label: "Franchisee NPS rating", higherBetter: true, kind: "rating",
    raw: (o) => o.npsRating, prior: () => null,
    fmt: (v) => v == null ? "—" : v + "/10",
    weekly: null
  }
};

const isNumMH = (v) => typeof v === "number" && !isNaN(v);

/* ---- per-FBC aggregate raw value for a driver (matches fbcDrivers) ---- */
function fbcRawValue(f, driver) {
  switch (driver) {
    case "conversion": return f.conversion;
    case "frequency": return f.frequency;
    case "retention": return f.churn;
    case "engagement": return f.engAll;
    case "nps": return f.npsSpring;
    default: return null;
  }
}

/* ---- owner weekly series (YTD) for the sparkline ---- */
function ownerWeeklySeries(o, driver) {
  const meta = DRIVER_META[driver];
  if (!meta.weekly || !o.dvw2026) return [];
  const cw = MM.config.currentWeek, out = [];
  for (let i = 0; i < cw; i++) out.push(meta.weekly(o.dvw2026, i));
  return out;
}

/* ---- FBC weekly series (YTD): mean across the FBC's owners ---- */
function fbcWeeklySeries(fbcId, driver) {
  const meta = DRIVER_META[driver];
  if (!meta.weekly) return [];
  const cw = MM.config.currentWeek, pool = MM.owners.filter((o) => o.fbcId === fbcId), out = [];
  for (let i = 0; i < cw; i++) {
    let s = 0, n = 0;
    pool.forEach((o) => {
      if (o.dvw2026) { const v = meta.weekly(o.dvw2026, i); if (isNumMH(v)) { s += v; n += 1; } }
    });
    out.push(n ? s / n : null);
  }
  return out;
}

/* ---- whole-system weekly series (YTD): mean across every owner ---- */
function systemWeeklySeries(driver) {
  const meta = DRIVER_META[driver];
  if (!meta.weekly) return [];
  const cw = MM.config.currentWeek, pool = MM.owners, out = [];
  for (let i = 0; i < cw; i++) {
    let s = 0, n = 0;
    pool.forEach((o) => {
      if (o.dvw2026) { const v = meta.weekly(o.dvw2026, i); if (isNumMH(v)) { s += v; n += 1; } }
    });
    out.push(n ? s / n : null);
  }
  return out;
}

/* ---- rank among all owners (or all FBCs) in the system ---- */
function rankOf(entity, driver, kind) {
  const meta = DRIVER_META[driver];
  let pool, me;
  if (kind === "fbc") {
    pool = MM.fbcDrivers().map((f) => fbcRawValue(f, driver));
    me = fbcRawValue(entity.full || entity, driver);
  } else {
    pool = MM.owners.map((o) => meta.raw(o));
    me = meta.raw(entity);
  }
  if (!isNumMH(me)) return null;
  const vals = pool.filter(isNumMH);
  if (!vals.length) return null;
  const better = vals.filter((v) => meta.higherBetter ? v > me : v < me).length;
  return { rank: better + 1, total: vals.length, me: me };
}

/* ---- assemble everything the popover shows ---- */
function metricInfo(driver, kind, entity) {
  const meta = DRIVER_META[driver];
  let raw, prior, series, name;
  if (kind === "fbc") {
    const full = entity.full || entity;
    raw = fbcRawValue(full, driver);
    prior = null;
    series = fbcWeeklySeries(full.id, driver);
    name = full.name;
  } else {
    raw = meta.raw(entity);
    prior = meta.prior(entity);
    series = ownerWeeklySeries(entity, driver);
    name = entity.contact;
  }
  const rank = rankOf(entity, driver, kind);
  const tone = rank ? (() => { const q = rank.rank / rank.total; return q <= 0.34 ? "great" : q <= 0.66 ? "average" : "opportunity"; })() : "info";
  let yoyText = null, yoyTone = null;
  if (isNumMH(raw) && isNumMH(prior)) {
    const d = raw - prior, good = meta.higherBetter ? d >= 0 : d <= 0;
    const mag = driver === "frequency" ? Math.abs(d).toFixed(2) : Math.abs(d).toFixed(1);
    const unit = meta.kind === "pts" ? " pts" : "";
    yoyText = (d > 0 ? "▲ " : d < 0 ? "▼ " : "▬ ") + mag + unit + " YoY";
    yoyTone = good ? "good" : "bad";
  }
  return {
    name: name, label: meta.label,
    rawText: meta.fmt(raw), series: series, tone: tone,
    yoyText: yoyText, yoyTone: yoyTone, higherBetter: meta.higherBetter,
    rankText: rank ? ("#" + rank.rank + " of " + rank.total + (kind === "fbc" ? " FBCs" : " owners")) : "Not enough data to rank",
    hasSpark: meta.weekly != null
  };
}

/* ---- tiny inline sparkline ---- */
function Sparkline({ values, tone }) {
  const pts = (values || []).map((v, i) => ({ v, i })).filter((p) => isNumMH(p.v));
  if (pts.length < 2) return <span className="mp-spark-empty">Weekly trend not tracked for this metric</span>;
  const W = 236, H = 44, pad = 5;
  const xs = pts.map((p) => p.i), ys = pts.map((p) => p.v);
  const minX = Math.min.apply(null, xs), maxX = Math.max.apply(null, xs);
  let minY = Math.min.apply(null, ys), maxY = Math.max.apply(null, ys);
  if (minY === maxY) { minY -= 1; maxY += 1; }
  const sx = (i) => pad + (i - minX) / ((maxX - minX) || 1) * (W - 2 * pad);
  const sy = (v) => H - pad - (v - minY) / ((maxY - minY) || 1) * (H - 2 * pad);
  const d = pts.map((p, k) => (k ? "L" : "M") + sx(p.i).toFixed(1) + " " + sy(p.v).toFixed(1)).join(" ");
  const col = "var(--mly-" + (tone || "info") + ")";
  const last = pts[pts.length - 1];
  return (
    <svg className="mp-spark" viewBox={"0 0 " + W + " " + H} preserveAspectRatio="none" aria-hidden="true">
      <path d={d} fill="none" stroke={col} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
      <circle cx={sx(last.i)} cy={sy(last.v)} r="2.8" fill={col} />
    </svg>);
}

/* ---- the floating popover (positioned by the row via useHoverPop) ---- */
function MetricPopover({ pos, driver, kind, entity }) {
  if (!pos) return null;
  const info = metricInfo(driver, kind, entity);
  return (
    <span className={"metric-pop fixed" + (pos.below ? " below" : "")} role="tooltip"
    style={{ left: pos.left + "px", top: pos.top + "px" }}>
      <span className="mp-head">
        <span className="mp-name">{info.name}</span>
        <span className="mp-rank">{info.rankText}</span>
      </span>
      <span className="mp-label">{info.label}</span>
      <span className="mp-figs">
        <span className={"mp-raw tone-" + info.tone}>{info.rawText}</span>
        {info.yoyText && <span className={"mp-yoy " + info.yoyTone}>{info.yoyText}</span>}
      </span>
      <Sparkline values={info.series} tone={info.tone} />
      <span className="mp-foot">{info.hasSpark ? "Year-to-date weekly trend" : "Point-in-time metric"}</span>
    </span>);
}

/* ---- hover-state hook: gives a row its open flag, position + handlers ---- */
function useHoverPop() {
  const [open, setOpen] = useStateMH(false);
  const [pos, setPos] = useStateMH(null);
  const ref = useRefMH(null);
  useLayoutEffectMH(() => {
    if (!open || !ref.current) return;
    function place() {
      const r = ref.current.getBoundingClientRect();
      const vw = window.innerWidth, cardW = 256, cardH = 168;
      const below = r.top < cardH + 28;
      let cx = r.left + r.width / 2;
      cx = Math.max(cardW / 2 + 10, Math.min(vw - cardW / 2 - 10, cx));
      setPos({ left: cx, top: below ? r.bottom + 12 : r.top - 12, below: below });
    }
    place();
    window.addEventListener("scroll", place, true);
    window.addEventListener("resize", place);
    return () => {
      window.removeEventListener("scroll", place, true);
      window.removeEventListener("resize", place);
    };
  }, [open]);
  const bind = {
    ref: ref,
    onMouseEnter: () => setOpen(true),
    onMouseLeave: () => setOpen(false),
    onFocus: () => setOpen(true),
    onBlur: () => setOpen(false)
  };
  return { open, pos, bind };
}

Object.assign(window, {
  DRIVER_META, fbcRawValue, ownerWeeklySeries, fbcWeeklySeries, systemWeeklySeries,
  rankOf, metricInfo, Sparkline, MetricPopover, useHoverPop
});
