/* global React, Icon */
// ─────────────────────────────────────────────────────────────────────────────
// DATA COVERAGE — how much trustworthy signal Nudge actually holds on a deal.
//
// Nudge's read is only as sharp as the data feeding it. This module scores, for a
// single deal, the QUALITY of that data — not just how much, but how fresh, how
// two-sided (buyer-confirmed vs. rep-logged), how broadly across the committee and
// the channels, and how deeply captured (recorded calls, not just notes). The
// result is one tier — Rich / Solid / Sparse — backed by a transparent, multi-
// dimensional report you can open on demand.
//
// Two principles hold the algorithm together:
//   1. Quality ≠ quantity. A deal whose only "signal" is the rep's own logging is a
//      thin read however many touches it has; buyer replies and recorded calls are
//      what make a read trustworthy.
//   2. Graceful degradation. The rich fields (MEDDPICC, commitments, warmth,
//      EXECUTION_CONTEXT) live on only a handful of deals. Every dimension has a
//      baseline from always-present fields; rare fields only ENRICH or add a bonus —
//      they never penalise a deal for not modelling them.
//
// Framing matters: this describes our INPUTS, not the deal's health or the
// reliability of our insight. A Sparse deal isn't a disclaimer — it's a prompt:
// feed Nudge two more signals and the read sharpens. Distinct axis from
// forecast-engine's dealFeatures (which scores deal health/risk).
// ─────────────────────────────────────────────────────────────────────────────

const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
const mean = (arr) => (arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0);
const gradeOf = (s) => (s >= 70 ? "strong" : s >= 45 ? "ok" : s >= 20 ? "thin" : "none");

// Loose recency read: timestamps here are relative strings ("This week", "2 days
// ago", "Mar 14"). We only need a coarse fresh→stale bucket; unparseable dates are
// treated as neutral so a real (non-relative) date never gets punished.
function recencyScore100(activity) {
  if (!activity || !activity.length) return { score: 0, when: null };
  const when = activity[0].time || "";
  const t = String(when).toLowerCase();
  const num = t.match(/(\d+)\s*day/);
  let score;
  if (num) {
    const d = Number(num[1]);
    score = d <= 2 ? 100 : d <= 4 ? 82 : d <= 10 ? 52 : 26;
  } else if (/today|just now|hour|\bmin\b|this morning|yesterday/.test(t)) score = 100;
  else if (/this week|days? ago|last week/.test(t)) score = 80;
  else if (/weeks? ago|last month|month/.test(t)) score = 40;
  else score = 60;
  return { score, when };
}

// Coverage expectation rises with the deal's stage. One touch and one contact is
// normal in Discovery, but the same data in Negotiation is genuinely thin. Tiering
// against this expectation flags deals under-covered *for where they are*, not just
// early deals that haven't had time to gather data.
function coverageExpectation(stage) {
  const s = String(stage || "").toLowerCase();
  if (s.includes("negotiation") || s.includes("evaluation"))
    return { level: 3, sparseBelow: 46, richAbove: 72, label: stage };
  if (s.includes("proposal") || s.includes("demo"))
    return { level: 2, sparseBelow: 38, richAbove: 66, label: stage };
  if (s.includes("closed won"))
    return { level: 2, sparseBelow: 34, richAbove: 62, label: "Closed Won" };
  if (s.includes("closed")) // closed lost — a post-mortem read on what we ever gathered
    return { level: 2, sparseBelow: 38, richAbove: 66, label: "Closed Lost" };
  return { level: 1, sparseBelow: 26, richAbove: 58, label: stage || "Discovery" }; // discovery / qualification
}

// ── The scorer ────────────────────────────────────────────────────────────────
// Seven weighted dimensions (weights sum to 100) + a Pega-only methodology bonus.
// Each dimension is 0–100 with an explicit evidence string and an improvement hint.
function dealDataCoverage(deal) {
  const activity = Array.isArray(deal && deal.activity) ? deal.activity : [];
  const stakeholders = Array.isArray(deal && deal.stakeholders) ? deal.stakeholders : [];
  const meetings = Array.isArray(deal && deal.meetings) ? deal.meetings : [];
  const ec =
    typeof window !== "undefined" && window.EXECUTION_CONTEXT && deal
      ? window.EXECUTION_CONTEXT[deal.id]
      : null;

  // 1 ── VOLUME — how much activity we hold.
  const touches = activity.length;
  const volume = {
    key: "volume",
    label: "Activity volume",
    weight: 16,
    score: clamp(Math.round((touches / 8) * 100), 0, 100),
    detail: touches === 0 ? "no activity logged" : `${touches} touch${touches === 1 ? "" : "es"} logged`,
    hint: "Sync recent emails & calls into the deal",
  };

  // 2 ── RECENCY — is the latest signal still warm?
  const rec = recencyScore100(activity);
  const recency = {
    key: "recency",
    label: "Recency",
    weight: 12,
    score: rec.score,
    detail: rec.when ? `last signal ${rec.when}` : "no recent signal",
    hint: "A fresh touch keeps the read current",
  };

  // 3 ── COMMITTEE COVERAGE — breadth across the buying committee.
  const mapped = stakeholders.filter((s) => s && !s.missing).length;
  const missing = stakeholders.filter((s) => s && s.missing).length;
  let committeeScore = clamp((mapped / 3) * 100 - missing * 12, 0, 100);
  const warmths = stakeholders.filter((s) => s && !s.missing && typeof s.warmth === "number" && s.warmth > 0).map((s) => s.warmth);
  if (warmths.length) committeeScore = clamp(Math.round(committeeScore * 0.6 + mean(warmths) * 0.4), 0, 100);
  const committee = {
    key: "committee",
    label: "Committee coverage",
    weight: 16,
    score: Math.round(committeeScore),
    detail:
      mapped === 0
        ? "no contacts mapped"
        : missing > 0
          ? `${mapped} mapped · ${missing} role${missing === 1 ? "" : "s"} missing`
          : `${mapped} stakeholder${mapped === 1 ? "" : "s"} mapped`,
    hint: mapped <= 1 ? "Map the rest of the buying committee" : "Fill the missing decision roles",
  };

  // 4 ── CAPTURE DEPTH — recorded calls beat notes beat nothing.
  const debriefs = meetings.filter((m) => m && m.debrief);
  const transcripts = debriefs.filter((m) => m.debrief && m.debrief.transcriptExcerpt);
  const videos = debriefs.filter((m) => m.debrief && m.debrief.videoUrl);
  const notes = activity.filter((a) => a && a.kind === "note").length;
  const depth = {
    key: "depth",
    label: "Capture depth",
    weight: 16,
    score: clamp(debriefs.length * 28 + transcripts.length * 10 + videos.length * 6 + Math.min(notes, 2) * 6, 0, 100),
    detail:
      debriefs.length === 0
        ? notes > 0
          ? `${notes} note${notes === 1 ? "" : "s"}, no recorded calls`
          : "no calls captured"
        : `${debriefs.length} call${debriefs.length === 1 ? "" : "s"} captured${transcripts.length ? ` · ${transcripts.length} transcript${transcripts.length === 1 ? "" : "s"}` : ""}`,
    hint: "Record or log your next call",
  };

  // 5 ── CHANNEL DIVERSITY — corroboration across mediums (AI/Addy excluded).
  const kinds = new Set(activity.map((a) => a && a.kind).filter((k) => k && k !== "ai" && k !== "addy"));
  let channelN = kinds.size;
  if (ec && ec.contacts) {
    const ch = new Set();
    Object.values(ec.contacts).forEach((c) => {
      const f = (c && c.channelFrequency) || {};
      Object.keys(f).forEach((k) => {
        if (f[k] === "high" || f[k] === "medium") ch.add(k);
      });
    });
    channelN = Math.max(channelN, ch.size);
  }
  const channels = {
    key: "channels",
    label: "Channel diversity",
    weight: 12,
    score: channelN <= 0 ? 0 : channelN === 1 ? 40 : channelN === 2 ? 72 : 100,
    detail: channelN === 0 ? "no channels" : `${channelN} channel${channelN === 1 ? "" : "s"}`,
    hint: "Engage on a second channel (email + call)",
  };

  // 6 ── TWO-SIDED / BUYER-CONFIRMED — the core quality signal.
  const total = activity.length;
  const buyerTouches = activity.filter((a) => a && (a.tag === "buyer" || a.tag === "milestone")).length;
  let twoScore = 0;
  if (total > 0) {
    const share = buyerTouches / total;
    twoScore = Math.round(share * 70 + (Math.min(buyerTouches, 3) / 3) * 30);
  }
  const commitments = Array.isArray(deal && deal.commitments) ? deal.commitments : [];
  if (commitments.length) {
    const bu = commitments.filter((c) => c.direction === "buyer-to-us").length;
    const us = commitments.filter((c) => c.direction === "us-to-buyer").length;
    if (bu > 0 && us > 0) twoScore = Math.min(100, twoScore + 12); // mutual commitments = strong two-sided proof
  }
  const twoSided = {
    key: "twoSided",
    label: "Buyer-confirmed",
    weight: 16,
    score: clamp(twoScore, 0, 100),
    detail:
      buyerTouches === 0
        ? "no buyer-side signal"
        : `${buyerTouches} buyer signal${buyerTouches === 1 ? "" : "s"} of ${total} touch${total === 1 ? "" : "es"}`,
    hint: "Get a buyer reply or a booked next step",
  };

  // 7 ── TRAJECTORY LEGIBILITY — do we have a trend, not just a snapshot?
  const trendLen = Array.isArray(deal && deal.trend) ? deal.trend.length : 0;
  const events = Array.isArray(deal && deal.scoreEvents) ? deal.scoreEvents.length : 0;
  const traj = {
    key: "trajectory",
    label: "Trajectory",
    weight: 12,
    score: clamp((trendLen >= 9 ? 70 : trendLen >= 6 ? 45 : trendLen > 0 ? 25 : 0) + (events > 0 ? 30 : 0), 0, 100),
    detail: trendLen > 0 ? `${trendLen}-point trend${events ? ` · ${events} events` : ""}` : "no history",
    hint: "Trajectory builds as activity accrues",
  };

  const dimensions = [volume, recency, committee, depth, channels, twoSided, traj].map((d) => ({
    ...d,
    grade: gradeOf(d.score),
  }));

  // Methodology bonus — Pega only; additive, so non-Pega deals are never penalised.
  let methodBonus = 0;
  const med = deal && deal.meddpicc;
  if (med && typeof med === "object") {
    const vals = Object.values(med).filter((n) => typeof n === "number");
    if (vals.length) methodBonus += (mean(vals) / 100) * 6;
  }
  const roleSet = new Set(stakeholders.map((s) => s && s.meddpicc).filter(Boolean));
  if (roleSet.size >= 4) methodBonus += 2;
  methodBonus = clamp(methodBonus, 0, 8);

  const weighted = dimensions.reduce((sum, d) => sum + d.score * d.weight, 0) / 100;
  const score = clamp(Math.round(weighted + methodBonus), 0, 100);

  const exp = coverageExpectation(deal && deal.stage);
  const tier = score >= exp.richAbove ? "rich" : score < exp.sparseBelow ? "sparse" : "solid";
  const label = tier === "rich" ? "Rich" : tier === "solid" ? "Solid" : "Sparse";

  // Sharpen-list — the highest-leverage thin dimensions, only when not Rich.
  const sharpen =
    tier === "rich"
      ? []
      : dimensions
          .filter((d) => d.grade === "thin" || d.grade === "none")
          .sort((a, b) => b.weight * (100 - b.score) - a.weight * (100 - a.score))
          .slice(0, 2)
          .map((d) => ({ label: d.label, action: d.hint }));

  const summary = `${label} read · ${score}/100`;

  return { tier, label, score, stage: exp.label, dimensions, sharpen, summary };
}

// ── Chip (cards) ──────────────────────────────────────────────────────────────
// A tiny tier dot + a small caps word. Quiet enough to recede on a busy card,
// clear enough to be unmistakable. Non-interactive (cards are themselves clickable);
// the score + tier ride along in the tooltip.
function DataCoverageChip({ deal, compact = false }) {
  const cov = React.useMemo(() => dealDataCoverage(deal), [deal]);
  return (
    <span
      className={`data-cov-tag is-${cov.tier} ${compact ? "is-compact" : ""}`}
      title={cov.summary}
      aria-label={`Data coverage: ${cov.summary}`}
    >
      <span className="data-cov-dot" aria-hidden="true" />
      <span className="data-cov-word">{cov.label}</span>
    </span>
  );
}

// ── Tag + report (deal detail) ─────────────────────────────────────────────────
// The subtle tag, now a button: click it to open the full quality report — the
// seven dimensions with score bars + evidence, and the sharpen prompts. Follows the
// repo's established disclosure idiom (ref + outside-click + Escape), mirroring
// ProgressGate / AddyTrace.
function DataCoverageTag({ deal }) {
  const cov = React.useMemo(() => dealDataCoverage(deal), [deal]);
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);

  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => {
      if (ref.current && !ref.current.contains(e.target)) setOpen(false);
    };
    const onKey = (e) => {
      if (e.key === "Escape") setOpen(false);
    };
    document.addEventListener("mousedown", onDoc);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDoc);
      document.removeEventListener("keydown", onKey);
    };
  }, [open]);

  return (
    <span className={`data-cov-gate is-${cov.tier}`} ref={ref}>
      <button
        type="button"
        className={`data-cov-tag is-${cov.tier} is-button`}
        onClick={(e) => {
          e.stopPropagation();
          setOpen((v) => !v);
        }}
        aria-expanded={open}
        aria-haspopup="dialog"
        title={`${cov.summary} — click for detail`}
      >
        <span className="data-cov-dot" aria-hidden="true" />
        <span className="data-cov-word">{cov.label} read</span>
        <Icon name="chevron" size={11} className="data-cov-chev" />
      </button>
      {open && (
        <div className="data-cov-pop" role="dialog" aria-label="Data coverage detail">
          <div className="data-cov-pop-h">
            <span className={`data-cov-tag is-${cov.tier}`}>
              <span className="data-cov-dot" aria-hidden="true" />
              <span className="data-cov-word">{cov.label} read</span>
            </span>
            <span className="data-cov-pop-score">
              {cov.score}<i>/100</i>
            </span>
          </div>
          <p className="data-cov-pop-sub">Signal Nudge holds on this {cov.stage} deal</p>
          <ul className="data-cov-pop-dims">
            {cov.dimensions.map((d) => (
              <li key={d.key} className={`grade-${d.grade}`}>
                <span className="dcd-label">{d.label}</span>
                <span className="dcd-bar">
                  <i style={{ width: `${d.score}%` }} />
                </span>
                <span className="dcd-detail">{d.detail}</span>
              </li>
            ))}
          </ul>
          {cov.sharpen.length > 0 && (
            <div className="data-cov-pop-fix">
              <span className="dcd-fix-h">
                <Icon name="sparkles" size={12} /> Sharpen this read
              </span>
              <ul>
                {cov.sharpen.map((s, i) => (
                  <li key={i}>{s.action}</li>
                ))}
              </ul>
            </div>
          )}
        </div>
      )}
    </span>
  );
}

window.dealDataCoverage = dealDataCoverage;
window.DataCoverageChip = DataCoverageChip;
window.DataCoverageTag = DataCoverageTag;
