/* global React */
const { useState, useEffect, useMemo, useRef, useCallback } = React;

// ---------- Icons (line icons drawn inline) ----------
const Icon = ({ name, size = 16, className = "" }) => {
  const map = {
    home: <path d="M3 11l9-8 9 8v9a2 2 0 0 1-2 2h-4v-7h-6v7H5a2 2 0 0 1-2-2v-9z" />,
    trend: <path d="M3 17l5-5 4 4 8-8M14 8h7v7" />,
    sparkles: (
      <g>
        <path d="M12 3v6M12 15v6M3 12h6M15 12h6" />
        <path d="M6 6l3 3M15 15l3 3M6 18l3-3M15 9l3-3" />
      </g>
    ),
    bell: <path d="M6 16V11a6 6 0 1 1 12 0v5l1.5 2h-15L6 16zM10 21h4" />,
    gear: (
      <g>
        <circle cx="12" cy="12" r="3" />
        <path d="M19.4 15a7.97 7.97 0 0 0 .1-6l2-1.2-2-3.4-2.3.7a8 8 0 0 0-5.2-3L11.5 0h-3l-.5 2.1a8 8 0 0 0-5.2 3L.5 4.4l-2 3.4 2 1.2a8 8 0 0 0 0 6l-2 1.2 2 3.4 2.3-.7a8 8 0 0 0 5.2 3l.5 2.1h3l.5-2.1a8 8 0 0 0 5.2-3l2.3.7 2-3.4-2-1.2z" transform="translate(0 0)"/>
      </g>
    ),
    book: (
      <g>
        <path d="M4 5a2 2 0 0 1 2-2h12v18H6a2 2 0 0 1-2-2V5z" />
        <path d="M8 7h8M8 11h8M8 15h6" />
      </g>
    ),
    arrow: <path d="M5 12h14M13 6l6 6-6 6" />,
    arrowL: <path d="M19 12H5M11 6l-6 6 6 6" />,
    paperclip: <path d="M21 11l-9 9a5 5 0 0 1-7-7l9-9a3.5 3.5 0 0 1 5 5l-9 9a2 2 0 0 1-3-3l8-8" />,
    mic: (
      <g>
        <rect x="9" y="3" width="6" height="12" rx="3" />
        <path d="M5 11a7 7 0 0 0 14 0M12 18v3M9 21h6" />
      </g>
    ),
    plus: <path d="M12 5v14M5 12h14" />,
    search: (
      <g>
        <circle cx="11" cy="11" r="7" />
        <path d="M20 20l-3.5-3.5" />
      </g>
    ),
    filter: <path d="M3 5h18l-7 9v6l-4-2v-4L3 5z" />,
    grid: (
      <g>
        <rect x="3" y="3" width="7" height="7" rx="1.5" />
        <rect x="14" y="3" width="7" height="7" rx="1.5" />
        <rect x="3" y="14" width="7" height="7" rx="1.5" />
        <rect x="14" y="14" width="7" height="7" rx="1.5" />
      </g>
    ),
    list: <path d="M4 6h16M4 12h16M4 18h16" />,
    sun: (
      <g>
        <circle cx="12" cy="12" r="4" />
        <path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
      </g>
    ),
    moon: <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />,
    close: <path d="M6 6l12 12M18 6L6 18" />,
    expand: <path d="M4 14v6h6M20 10V4h-6M4 20l7-7M20 4l-7 7" />,
    check: <path d="M5 12l5 5L20 7" />,
    chevron: <path d="M6 9l6 6 6-6" />,
    chevronR: <path d="M9 6l6 6-6 6" />,
    chevronL: <path d="M15 6l-6 6 6 6" />,
    mail: (
      <g>
        <rect x="3" y="5" width="18" height="14" rx="2" />
        <path d="M3 7l9 6 9-6" />
      </g>
    ),
    calendar: (
      <g>
        <rect x="3" y="5" width="18" height="16" rx="2" />
        <path d="M3 9h18M8 3v4M16 3v4" />
      </g>
    ),
    note: (
      <g>
        <path d="M5 4h11l4 4v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z" />
        <path d="M8 12h8M8 16h6" />
      </g>
    ),
    bolt: <path d="M13 2L4 14h7l-1 8 9-12h-7l1-8z" />,
    shield: <path d="M12 3l8 3v6c0 5-3.5 8.5-8 9-4.5-.5-8-4-8-9V6l8-3z" />,
    users: (
      <g>
        <circle cx="9" cy="8" r="3.5" />
        <path d="M2.5 20a6.5 6.5 0 0 1 13 0M16 11a3 3 0 0 0 0-6M22 20a5 5 0 0 0-4-4.9" />
      </g>
    ),
    userx: (
      <g>
        <circle cx="9" cy="8" r="3.5" />
        <path d="M2.5 20a6.5 6.5 0 0 1 13 0M17 8l4 4M21 8l-4 4" />
      </g>
    ),
    target: (
      <g>
        <circle cx="12" cy="12" r="9" />
        <circle cx="12" cy="12" r="5" />
        <circle cx="12" cy="12" r="1.5" />
      </g>
    ),
    clock: (
      <g>
        <circle cx="12" cy="12" r="9" />
        <path d="M12 7v5l3.5 2" />
      </g>
    ),
    scale: (
      <g>
        <path d="M12 3v18M6 6h12M8 6l-4 7h8L8 6zM16 6l-4 7h8l-4-7z" />
      </g>
    ),
    route: (
      <g>
        <circle cx="6" cy="6" r="2.5" />
        <circle cx="18" cy="18" r="2.5" />
        <path d="M8.5 6H13a3 3 0 0 1 0 6h-2a3 3 0 0 0 0 6h4.5" />
      </g>
    ),
    handshake: (
      <g>
        <path d="M7 12l3-3 3 3M3 11l4-4 4 4M21 11l-4-4-4 4" />
        <path d="M8 13l4 4 4-4M5 14l5 5a3 3 0 0 0 4 0l5-5" />
      </g>
    ),
    flag: <path d="M5 21V4M5 4h11l-2 4 2 4H5" />,
    sliders: <path d="M4 6h16M4 12h16M4 18h16M9 6v0M15 12v0M7 18v0" />,
    dots: <path d="M5 12h.01M12 12h.01M19 12h.01" />,
    grip: (
      <g fill="currentColor" stroke="none">
        <circle cx="9" cy="6" r="1.2" />
        <circle cx="15" cy="6" r="1.2" />
        <circle cx="9" cy="12" r="1.2" />
        <circle cx="15" cy="12" r="1.2" />
        <circle cx="9" cy="18" r="1.2" />
        <circle cx="15" cy="18" r="1.2" />
      </g>
    ),
    forecast: (
      <g>
        <circle cx="9" cy="11" r="3.5" />
        <path d="M9 4v1.5M9 16.5V18M2 11h1.5M14.5 11H16M4.05 6.05l1.05 1.05M12.9 14.9l1.05 1.05M4.05 15.95l1.05-1.05M12.9 7.1l1.05-1.05" />
        <path d="M14 17h6a2 2 0 0 0 0-4 3.5 3.5 0 0 0-6.5-1.5" />
      </g>
    ),
    chat: (
      <g>
        <path d="M4 5h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H9l-4 3v-3H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z" />
      </g>
    ),
    hash: <path d="M5 9h14M5 15h14M9 4l-2 16M17 4l-2 16" />,
    phone: <path d="M5 4h3l2 5-2.5 1.5a11 11 0 0 0 6 6L15 14l5 2v3a2 2 0 0 1-2 2A15 15 0 0 1 3 6a2 2 0 0 1 2-2z" />,
    device: (
      <g>
        <rect x="6" y="2.5" width="12" height="19" rx="2.5" />
        <path d="M10 5.5h4" />
        <circle cx="12" cy="18.5" r="0.6" fill="currentColor" stroke="none" />
      </g>
    ),
    cols: (
      <g>
        <rect x="3.5" y="5" width="7" height="14" rx="1.5" />
        <rect x="13.5" y="5" width="7" height="14" rx="1.5" />
      </g>
    ),
    colFull: <rect x="3.5" y="5" width="17" height="14" rx="1.5" />,
    copy: (
      <g>
        <rect x="8" y="3" width="12" height="14" rx="2" />
        <path d="M5 7v13a1 1 0 0 0 1 1h11" />
      </g>
    ),
    eye: (
      <g>
        <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z" />
        <circle cx="12" cy="12" r="3" />
      </g>
    ),
  };
  return (
    <svg className={`icon ${className}`} viewBox="0 0 24 24" width={size} height={size}>
      {map[name] || null}
    </svg>
  );
};

// ---------- Sparkline ----------
const Sparkline = ({ data, color = "currentColor", height = 36 }) => {
  if (!data || !data.length) return null;
  const w = 200, h = height, pad = 2;
  const min = Math.min(...data), max = Math.max(...data);
  const r = max - min || 1;
  const pts = data.map((v, i) => {
    const x = (i / (data.length - 1)) * (w - pad * 2) + pad;
    const y = h - pad - ((v - min) / r) * (h - pad * 2);
    return `${x},${y}`;
  }).join(" ");
  const area = `M${pts.split(" ")[0]} L${pts} L${w - pad},${h} L${pad},${h} Z`;
  return (
    <svg className="spark" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{ height }}>
      <defs>
        <linearGradient id="sparkArea" x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity="0.18" />
          <stop offset="100%" stopColor={color} stopOpacity="0" />
        </linearGradient>
      </defs>
      <path className="area" d={area} fill="url(#sparkArea)" />
      <polyline className="line" points={pts} stroke={color} />
    </svg>
  );
};

// ---------- Bar with dual lines (alignment vis) ----------
const AlignmentChart = ({ buyer, seller, height = 160 }) => {
  const w = 600;
  const pad = 16;
  const all = [...buyer, ...seller];
  const min = Math.min(...all);
  const max = Math.max(...all);
  const r = max - min || 1;
  const path = (data) =>
    data.map((v, i) => {
      const x = (i / (data.length - 1)) * (w - pad * 2) + pad;
      const y = height - pad - ((v - min) / r) * (height - pad * 2);
      return `${i === 0 ? "M" : "L"}${x},${y}`;
    }).join(" ");

  return (
    <svg viewBox={`0 0 ${w} ${height}`} width="100%" height={height} preserveAspectRatio="none">
      <defs>
        <linearGradient id="buyerArea" x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%" stopColor="var(--warn)" stopOpacity="0.25" />
          <stop offset="100%" stopColor="var(--warn)" stopOpacity="0" />
        </linearGradient>
      </defs>
      {[0.25, 0.5, 0.75].map((p) => (
        <line key={p} x1={pad} x2={w - pad} y1={height * p} y2={height * p} stroke="var(--border)" strokeDasharray="2 4" />
      ))}
      <path d={`${path(buyer)} L${w - pad},${height} L${pad},${height} Z`} fill="url(#buyerArea)" />
      <path d={path(seller)} fill="none" stroke="var(--text-2)" strokeWidth="1.5" strokeDasharray="4 3" />
      <path d={path(buyer)} fill="none" stroke="var(--warn)" strokeWidth="2" />
    </svg>
  );
};

// ---------- AI dock (composer + thread + starters + slash) ----------
const SLASH_COMMANDS = [
  { cmd: "/draft", hint: "draft an email or message", icon: "mail" },
  { cmd: "/summarize", hint: "summarize this view", icon: "note" },
  { cmd: "/risks", hint: "top risks for this context", icon: "shield" },
];

const startersFor = (context) => {
  const deal = context.activeDeal;
  if (context.route === "addy") {
    return [
      { label: "Which deals need me first?", text: "Which deals need me first?" },
      { label: "Where is buyer proof missing?", text: "Where is buyer proof missing across my queue?" },
      { label: "Draft the next move", text: "Draft the next move for my top deal" },
    ];
  }
  if (deal) {
    return [
      { label: "Why is this stalling?", text: `Why is ${deal.company} stalling?` },
      { label: "Draft re-engagement", text: `/draft re-engagement for ${deal.company}` },
      { label: "Compare proof pattern", text: `Compare ${deal.company} to my strongest buyer-proof deal` },
    ];
  }
  if (context.route === "deals") {
    return [
      { label: "Where is buyer drift highest?", text: "Which stages have the most buyer drift right now?" },
      { label: "What proof did won deals carry?", text: "What buyer proof did my closed-won deals share?" },
      { label: "Where is buyer proof weakest?", text: "/risks" },
    ];
  }
  if (context.route === "playbook") {
    return [
      { label: "Best play this week", text: "Which playbook had the highest win-back this week?" },
      { label: "Stall patterns", text: "What stall patterns are most common right now?" },
      { label: "Suggest a new play", text: "Suggest a new playbook from my recent wins" },
    ];
  }
  if (context.route === "setup") {
    return [
      { label: "Signal quality", text: "Which connections have weak signal quality?" },
      { label: "Signals I'm missing", text: "What signals am I missing right now?" },
      { label: "Permissions check", text: "Check my permissions across tools" },
    ];
  }
  return [
    { label: "What's buyer-unverified today?", text: "Which deals are buyer-unverified right now?" },
    { label: "Where is buyer proof weakest?", text: "/risks for my pipeline" },
    { label: "Draft my forecast review", text: "/draft a weekly forecast update" },
  ];
};

const slashReply = (cmd, context) => {
  const deal = context.activeDeal;
  if (cmd === "/draft") {
    if (deal) return `Draft for ${deal.company}: short, in your voice — open with the missed signal, propose a 20-min slot, end with an ROI question. Want me to fill it in?`;
    if (context.route === "deals") return "Pick a deal and run /draft from there — I'll personalise to that buyer's stage and last touch.";
    return "Tell me who it's for. I can /draft a re-engagement, intro request, or weekly update.";
  }
  if (cmd === "/summarize") {
    if (deal) return `${deal.company} in 3 lines: stage ${deal.stage}, buyer ${deal.buyerScore}% vs seller ${deal.sellerScore}%, momentum ${deal.statusLabel}.`;
    if (context.route === "deals") return "Pipeline in 3 lines: most stages balanced, 4 deals stalled >7 days, 3 missing economic buyer.";
    if (context.route === "playbook") return "Playbooks in 3 lines: 4 active, best win-back is 'Drive verbal commit' at 62%, 'Re-engage cold champion' is the most-used.";
    return `Summarising ${context.routeLabel || "this view"} — give me a sec to pull the right slice.`;
  }
  if (cmd === "/risks") {
    if (deal) return `Top risks on ${deal.company}: economic buyer not engaged, last touch >7 days, momentum trending down.`;
    return "Top portfolio risks: 3 deals missing economic buyer, 4 silent >7 days, Q2 concentrated in 4 accounts.";
  }
  return null;
};

const AIDock = ({ position = "center", compact = false, context = {}, suggestion, onAct, onSend }) => {
  const [val, setVal] = useState("");
  const [thinking, setThinking] = useState(false);
  const [messages, setMessages] = useState([]);
  const [open, setOpen] = useState(!compact);
  const replyTimerRef = useRef(null);
  const taRef = useRef(null);
  const threadRef = useRef(null);
  const activeDeal = context.activeDeal;
  const isAddyRoute = context.route === "addy";
  const contextLabel = activeDeal ? activeDeal.company : isAddyRoute ? "Addy" : context.routeLabel || "Nudge";
  const placeholder = activeDeal
    ? `Ask Addy about ${activeDeal.company}…  (try / for commands)`
    : isAddyRoute
      ? "Ask Addy what to do next…  (try / for commands)"
      : `Ask Addy about ${contextLabel}…  (try / for commands)`;
  const trimmed = val.trim();
  const slashOpen = trimmed.startsWith("/") && !trimmed.includes(" ");
  const slashMatches = slashOpen
    ? SLASH_COMMANDS.filter((c) => c.cmd.startsWith(trimmed.toLowerCase()))
    : [];
  const starters = useMemo(
    () => startersFor(context),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [context.route, activeDeal?.id]
  );

  useEffect(() => {
    if (replyTimerRef.current) {
      clearTimeout(replyTimerRef.current);
      replyTimerRef.current = null;
    }
    setThinking(false);
    setOpen(!compact);
    setMessages([]);
    setVal("");
  }, [compact, context.route, activeDeal?.id]);

  useEffect(() => {
    if (!isAddyRoute || compact || messages.length > 0 || thinking) return;
    setMessages([
      {
        role: "addy",
        text: "I’m on the queue. Ask for the next move, a summary, or a draft and I’ll stay focused on the work that matters.",
        ts: Date.now(),
      },
    ]);
  }, [compact, isAddyRoute, messages.length, thinking]);

  useEffect(() => () => {
    if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
  }, []);

  // Auto-grow textarea up to ~5 lines
  useEffect(() => {
    const ta = taRef.current;
    if (!ta) return;
    ta.style.height = "auto";
    ta.style.height = Math.min(ta.scrollHeight, 120) + "px";
  }, [val]);

  // Auto-scroll thread to bottom when content changes
  useEffect(() => {
    const el = threadRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [messages.length, thinking]);

  const submit = (text) => {
    const q = (text || "").trim();
    if (!q) return;
    setVal("");
    setMessages((m) => [...m, { role: "user", text: q, ts: Date.now() }]);
    setThinking(true);
    onSend && onSend(q);
    if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
    const firstToken = q.split(/\s+/, 1)[0].toLowerCase();
    const slashAns = SLASH_COMMANDS.some((c) => c.cmd === firstToken)
      ? slashReply(firstToken, context)
      : null;
    replyTimerRef.current = setTimeout(() => {
      const a = slashAns || pickReply(q, context);
      setThinking(false);
      setMessages((m) => [...m, { role: "addy", text: a, ts: Date.now() }]);
      replyTimerRef.current = null;
    }, 900);
  };

  const onKeyDown = (e) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      // Tab-complete a single slash match unless user already typed it exactly
      if (slashOpen && slashMatches.length === 1 && trimmed.toLowerCase() !== slashMatches[0].cmd) {
        setVal(slashMatches[0].cmd + " ");
        return;
      }
      submit(val);
    } else if (e.key === "Tab" && slashOpen && slashMatches.length > 0) {
      e.preventDefault();
      setVal(slashMatches[0].cmd + " ");
    } else if (e.key === "Escape") {
      if (val) {
        e.preventDefault();
        setVal("");
      }
    }
  };

  if (compact && !open) {
    return (
      <button
        type="button"
        className="dock-chip"
        onClick={() => setOpen(true)}
        aria-label={`Open Addy for ${contextLabel}`}
      >
        <span className="avatar addy xs">A</span>
        <span>{activeDeal ? `Addy · ${activeDeal.company}` : `Addy · ${contextLabel}`}</span>
        <Icon name="sparkles" size={13} />
      </button>
    );
  }

  const hasThread = messages.length > 0 || thinking;
  const showStarters = !hasThread;

  return (
    <div className={`dock ${position} ${compact ? "compact is-open" : ""}`}>
      {compact && (
        <button type="button" className="dock-close" onClick={() => setOpen(false)} aria-label="Collapse Addy">
          <Icon name="close" size={13} />
        </button>
      )}
      {compact && (
        <div className="dock-context">
          <span className="avatar addy xs">A</span>
          <span>{activeDeal ? `${activeDeal.company} · ${activeDeal.stage}` : contextLabel}</span>
        </div>
      )}

      {hasThread && (
        <div className="dock-thread" ref={threadRef}>
          {messages.map((m, i) => (
            <div key={i} className={`dock-msg ${m.role}`}>
              {m.role === "addy" && <span className="avatar addy xs">A</span>}
              <div className="dock-msg-bubble">{m.text}</div>
            </div>
          ))}
          {thinking && (
            <div className="dock-msg addy">
              <span className="avatar addy xs">A</span>
              <div className="dock-msg-bubble mono dock-msg-thinking">
                <span className="dotty">thinking</span><span className="dot-pulse">…</span>
              </div>
            </div>
          )}
        </div>
      )}

      {showStarters && starters.length > 0 && (
        <div className="dock-starters">
          {starters.map((s, i) => (
            <button
              key={i}
              type="button"
              className="dock-starter"
              onClick={() => submit(s.text)}
              title={s.text}
            >
              <Icon name="bolt" size={12} />
              <span>{s.label}</span>
            </button>
          ))}
        </div>
      )}

      {slashOpen && slashMatches.length > 0 && (
        <div className="dock-slash" role="listbox" aria-label="Slash commands">
          {slashMatches.map((c) => (
            <button
              key={c.cmd}
              type="button"
              className="dock-slash-item"
              onMouseDown={(e) => {
                e.preventDefault();
                setVal(c.cmd + " ");
                taRef.current && taRef.current.focus();
              }}
            >
              <Icon name={c.icon} size={13} />
              <span className="dock-slash-cmd">{c.cmd}</span>
              <span className="dock-slash-hint">{c.hint}</span>
            </button>
          ))}
        </div>
      )}

      <div className="composer">
        <Icon name="paperclip" />
        {suggestion && !hasThread && !slashOpen && (
          <button
            className="composer-suggestion"
            onClick={() => onAct && onAct(suggestion)}
            title={`${suggestion.title} ${suggestion.sub || ""}`}
          >
            <span className="avatar addy xs">A</span>
            <span className="composer-suggestion-text">{suggestion.title}</span>
            <Icon name="arrow" size={12} />
          </button>
        )}
        <textarea
          ref={taRef}
          className="input"
          placeholder={placeholder}
          value={val}
          rows={1}
          onChange={(e) => setVal(e.target.value)}
          onKeyDown={onKeyDown}
        />
        <button className="btn ghost sm" type="button" tabIndex={-1}><Icon name="mic" size={14} /></button>
        <button
          className="send"
          type="button"
          disabled={!trimmed}
          onClick={() => submit(val)}
          aria-label="Send message"
        >
          <Icon name="arrow" size={14} />
        </button>
      </div>
    </div>
  );
};

const REPLY_BANK = [
  "Atlas Logistics is your highest-risk deal — buyer score is at 50% and falling. I drafted a re-engagement email for Sarah; want to review it?",
  "Three deals are stalling for the same reason: missing economic buyer. I can prep intro requests for all of them in one batch.",
  "Acme has the clearest buyer proof. Momentum is high — I'd push for a verbal commit by Friday and prep a mutual close plan.",
  "I noticed your Q2 pipeline is concentrated in 4 accounts. Want me to surface 3 expansion candidates from existing customers?",
  "Last week your meeting → reply rate was 71% (above team avg). Two playbooks worked best — should I codify them?",
];
const pickReply = (q, context = {}) => {
  const deal = context.activeDeal;
  if (deal) {
    const closed = /^closed/i.test(deal.stage || "") || /^closed/i.test(deal.statusLabel || "");
    const lost = /lost/i.test(`${deal.stage || ""} ${deal.statusLabel || ""} ${deal.status || ""}`);
    if (closed && lost) {
      return `${deal.company} is closed lost. I would summarize loss reason, blocker pattern, and what to update in the playbook before moving to any next action.`;
    }
    if (closed) {
      return `${deal.company} is closed won. The useful context is post-sale: QBR prep, reference ask, expansion timing, and handoff risks.`;
    }
    return `${deal.company} is in ${deal.stage}. Buyer ${deal.buyerScore}% vs seller ${deal.sellerScore}% means I should focus on the next action that closes that gap, not generic activity.`;
  }
  if (context.route === "deals") {
    return "In Deal Truth, I can compare seller activity, buyer progression, and the full story behind each forecast state.";
  }
  if (context.route === "setup") {
    return "In Setup, I would focus on connection quality: CRM, calendar, mail, and permissions that determine whether the signals are reliable.";
  }
  if (context.route === "playbook") {
    return "In Playbooks, I would look for repeated stall patterns and suggest where automation helps without burying the rep in busywork.";
  }
  return REPLY_BANK[Math.abs(hash(q)) % REPLY_BANK.length];
};
const hash = (s) => { let h = 0; for (let i = 0; i < s.length; i++) h = (h << 5) - h + s.charCodeAt(i); return h; };

// ---------- Sidebar ----------
const Sidebar = ({ route, setRoute, theme, setTheme, nudgeCount,
                   dashboardView = "ae", setDashboardView,
                   managerSection = "home", setManagerSection,
                   addyState = "idle", addyUnread = 0,
                   expanded = false, onToggleExpanded,
                   profile, workspace, onOpenSettings }) => {
  const isManager = dashboardView === "manager";
  const profileName = profile?.name || "Ridha Mansour";
  const profileFirst = profileName.split(/\s+/)[0] || "Ridha";
  const profileEmail = profile?.email || "ridha@addvocate.ai";
  const profileInitial = (profileName.trim()[0] || "R").toUpperCase();
  const [profileOpen, setProfileOpen] = useState(false);
  const profileRef = useRef(null);
  useEffect(() => {
    if (!profileOpen) return;
    const onClick = (e) => {
      if (profileRef.current && !profileRef.current.contains(e.target)) setProfileOpen(false);
    };
    const onKey = (e) => { if (e.key === "Escape") setProfileOpen(false); };
    document.addEventListener("mousedown", onClick);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onClick);
      document.removeEventListener("keydown", onKey);
    };
  }, [profileOpen]);
  // Addy is no longer a destination — she lives in the logo dots (click
  // them or press ⌘K to summon the spotlight). The "Promises" entry was
  // also removed: the spotlight IS the commitment register. Everything
  // Addy is doing/did/will do is reachable from the logo.
  // Icons map to what the section actually does, not just the route name:
  //   home (AE)        → bolt    : today's action-ranked command center
  //   deals            → eye     : Deal Truth — seeing seller vs buyer truth
  //   analytics        → trend   : drop-off charts and won/lost patterns
  //   home (Manager)   → users   : team cockpit lens
  //   deals (Manager)  → eye     : same Deal Truth lens, across the team
  //   config (Manager) → sliders : forecast model configuration
  // Labels mirror the section's header title verbatim.
  const aeItems = [
    { k: "home",      icon: "bolt",  label: "Today's deal command" },
    { k: "deals",     icon: "eye",   label: "Deal Truth" },
    { k: "analytics", icon: "trend", label: "Analytics" },
  ];
  const managerItems = [
    { k: "home",   icon: "users", label: "Team forecast cockpit" },
    { k: "deals",  icon: "eye",   label: "Deal forecast review" },
    { k: "config", icon: "trend", label: "Analytics" },
  ];
  const primaryItems = isManager ? managerItems : aeItems;
  const handlePrimaryClick = (key) => {
    if (isManager) {
      if (route !== "home") setRoute("home");
      setManagerSection?.(key);
    } else {
      setRoute(key);
    }
  };
  const isPrimaryActive = (key) => {
    if (isManager) return route === "home" && managerSection === key;
    return route === key;
  };
  const switchView = (view) => {
    if (view === dashboardView) return;
    setDashboardView?.(view);
    if (view === "manager") {
      setRoute("home");
      setManagerSection?.("home");
    } else {
      setRoute("home");
    }
  };

  return (
    <div className={`sidebar ${isManager ? "is-manager" : ""} ${expanded ? "is-expanded" : ""}`}>
      {/* Addy lives here. The two dots are her presence — each animates
          per state. Click (or ⌘K) summons the spotlight overlay. */}
      <button
        type="button"
        className="logo"
        data-addy-state={addyState || "idle"}
        aria-label={`Open Addy${addyUnread > 0 ? ` · ${addyUnread} unread` : ""}`}
        title="Addy · ⌘K"
        onClick={() => { if (typeof window.openAddy === "function") window.openAddy(); }}
      >
        <span className="logo-dots" aria-hidden="true">
          <span className="logo-dot logo-dot--1" />
          <span className="logo-dot logo-dot--2" />
          <span className="logo-dot logo-dot--3" />
        </span>
        <span className="logo-wordmark" aria-hidden="true">
          Nudge<sup>®</sup>
        </span>
        {addyUnread > 0 && (
          <span className="logo-badge" aria-hidden="true">{addyUnread}</span>
        )}
      </button>
      {primaryItems.map((i) => (
        <button
          key={i.k}
          className={`nav-btn ${isPrimaryActive(i.k) ? "active" : ""}`}
          title={i.label}
          onClick={() => handlePrimaryClick(i.k)}
        >
          <Icon name={i.icon} />
          <span className="nav-label">{i.label}</span>
        </button>
      ))}
      <div className="spacer" />
      <div className={`sidebar-bottom-controls ${isManager ? "is-manager" : "is-ae"}`}>
        {setDashboardView && (
          <div
            className={`sidebar-mode-switch is-${dashboardView === "manager" ? "mgr" : "ae"}`}
            role="group"
            aria-label="Dashboard mode"
            data-tour="mode-toggle"
          >
            <button
              type="button"
              className={`mode-btn ${dashboardView === "ae" ? "is-active" : ""}`}
              title="AE view"
              aria-label="AE view"
              aria-pressed={dashboardView === "ae"}
              onClick={() => switchView("ae")}
            >
              <Icon name="userx" size={11} />
              <span className="mode-btn-label">AE</span>
            </button>
            <button
              type="button"
              className={`mode-btn ${dashboardView === "manager" ? "is-active" : ""}`}
              title="Manager view"
              aria-label="Manager view"
              aria-pressed={dashboardView === "manager"}
              onClick={() => switchView("manager")}
            >
              <Icon name="users" size={11} />
              <span className="mode-btn-label">MGR</span>
            </button>
          </div>
        )}
        <div
          className={`sidebar-theme-switch is-${theme === "dark" ? "dark" : "light"}`}
          role="group"
          aria-label="Theme"
        >
          <button
            type="button"
            className={`theme-btn ${theme === "dark" ? "is-active" : ""}`}
            title="Dark theme"
            aria-label="Dark theme"
            aria-pressed={theme === "dark"}
            onClick={() => setTheme("dark")}
          >
            <Icon name="moon" size={11} />
            <span className="theme-btn-label">Dark</span>
          </button>
          <button
            type="button"
            className={`theme-btn ${theme === "light" ? "is-active" : ""}`}
            title="Light theme"
            aria-label="Light theme"
            aria-pressed={theme === "light"}
            onClick={() => setTheme("light")}
          >
            <Icon name="sun" size={11} />
            <span className="theme-btn-label">Light</span>
          </button>
        </div>
        <div className="sidebar-profile" ref={profileRef}>
          <button
            type="button"
            className={`sidebar-profile-trigger ${profileOpen ? "is-open" : ""}`}
            title={`Account · ${profileFirst}`}
            aria-label={`Account · ${profileFirst}`}
            aria-haspopup="menu"
            aria-expanded={profileOpen}
            onClick={() => setProfileOpen((v) => !v)}
          >
            <span className="sidebar-profile-avatar" aria-hidden="true">
              {profileInitial}
              <span className="sidebar-profile-status" aria-hidden="true" />
            </span>
            <span className="sidebar-profile-trigger-id">
              <b>{profileFirst}</b>
              <em>{profileEmail}</em>
            </span>
          </button>
          {profileOpen && (
            <div className="sidebar-profile-menu" role="menu" aria-label="Account">
            <div className="sidebar-profile-menu-head">
              <span className="sidebar-profile-avatar lg" aria-hidden="true">{profileInitial}</span>
              <div className="sidebar-profile-menu-id">
                <b>{profileFirst}</b>
                <em>{profileEmail}</em>
                {workspace?.name && <em>{workspace.name}</em>}
              </div>
            </div>
            <button type="button" className="sidebar-profile-menu-item" role="menuitem" onClick={() => setProfileOpen(false)}>
              <Icon name="chat" size={14} /> Help &amp; support
            </button>
            <span className="sidebar-profile-menu-sep" aria-hidden="true" />
            <button type="button" className="sidebar-profile-menu-item is-signout" role="menuitem" onClick={() => setProfileOpen(false)}>
              <Icon name="arrow" size={14} /> Sign out
            </button>
            </div>
          )}
        </div>
      </div>
      {onToggleExpanded && (
        <button
          type="button"
          className="sidebar-toggle"
          title={expanded ? "Collapse sidebar" : "Expand sidebar"}
          aria-label={expanded ? "Collapse sidebar" : "Expand sidebar"}
          aria-pressed={expanded}
          onClick={onToggleExpanded}
        >
          <Icon name={expanded ? "chevronL" : "chevronR"} />
          <span className="nav-label">Collapse</span>
        </button>
      )}
    </div>
  );
};

// ---------- Buyer Forecast ----------
// Daily "weather report" for buyer and seller progression. Shows once on
// the first connection of the day (tracked in localStorage), and can be
// re-opened any time from the sidebar toggle. Mounts/unmounts with a
// scale + fade animation.
const FORECAST_KEY = "nudge.forecast.lastShown";
const todayKey = () => {
  const d = new Date();
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
};
const shouldShowForecastToday = () => {
  try {
    return localStorage.getItem(FORECAST_KEY) !== todayKey();
  } catch (_) {
    return true;
  }
};
const markForecastShown = () => {
  try { localStorage.setItem(FORECAST_KEY, todayKey()); } catch (_) {}
};

const weatherFor = (gap, buyerAhead, delta) => {
  if (gap < 10) return { kind: "sunny", label: "In sync", tone: "good" };
  if (buyerAhead) return { kind: "partly", label: "Buyer ahead", tone: "info" };
  if (gap >= 25 || delta < 0) return { kind: "rainy", label: "Seller ahead", tone: "warn" };
  return { kind: "cloudy", label: "Opening gap", tone: "muted" };
};

const WeatherGlyph = ({ kind, size = 40 }) => {
  const sun = (
    <g>
      <circle cx="22" cy="20" r="6" />
      <path d="M22 9v3M22 28v3M11 20h3M30 20h3M14 12l2 2M28 26l2 2M14 28l2-2M28 14l2-2" />
    </g>
  );
  const cloud = (
    <path d="M14 30h18a5 5 0 0 0 0-10 7 7 0 0 0-13.5-2A5 5 0 0 0 14 30z" />
  );
  const rain = (
    <g>
      <path d="M14 26h18a5 5 0 0 0 0-10 7 7 0 0 0-13.5-2A5 5 0 0 0 14 26z" />
      <path d="M17 30l-2 4M23 30l-2 4M29 30l-2 4" />
    </g>
  );
  const partly = (
    <g>
      <circle cx="16" cy="14" r="5" />
      <path d="M16 5v3M16 20v3M7 14h3M22 14h3M9 7l2 2M21 19l2 2M9 21l2-2M21 9l2-2" />
      <path d="M18 30h14a4 4 0 0 0 0-8 6 6 0 0 0-11-1.5A4 4 0 0 0 18 30z" />
    </g>
  );
  const inner =
    kind === "sunny" ? sun :
    kind === "rainy" ? rain :
    kind === "partly" ? partly :
    cloud;
  return (
    <svg className={`forecast-glyph forecast-glyph-${kind}`} viewBox="0 0 44 40" width={size} height={(size * 40) / 44} aria-hidden>
      {inner}
    </svg>
  );
};

const buildForecast = (deals) => {
  const items = deals
    .filter((d) => !/^closed/i.test(d.stage || ""))
    .map((d) => {
      const series = d.trend && d.trend.length ? d.trend : [d.buyerScore];
      const horizon = Math.min(7, series.length);
      const window = series.slice(-horizon);
      const start = window[0];
      const now = window[window.length - 1];
      const delta = now - start;
      const buyer = d.buyerScore ?? now;
      const seller = d.sellerScore ?? buyer;
      const gap = Math.abs(seller - buyer);
      const buyerAhead = buyer >= seller;
      const leader = gap < 10 ? "In sync" : buyerAhead ? "Buyer ahead" : "Seller ahead";
      const distance = gap < 10
        ? "close path aligned"
        : `${leader.toLowerCase()} by ${gap} pts`;
      return {
        id: d.id,
        company: d.company,
        stage: d.stage,
        buyer,
        seller,
        now,
        delta,
        gap,
        buyerAhead,
        leader,
        distance,
        weather: weatherFor(gap, buyerAhead, delta),
      };
    })
    .sort((a, b) => b.gap - a.gap || Math.abs(b.delta) - Math.abs(a.delta))
    .slice(0, 5);

  const alignedCount = items.filter((it) => it.gap < 10).length;
  const sellerAheadCount = items.filter((it) => !it.buyerAhead && it.gap >= 10).length;
  const buyerAheadCount = items.filter((it) => it.buyerAhead && it.gap >= 10).length;
  let verdict;
  if (sellerAheadCount >= 2) {
    verdict = { kind: "rainy", title: "Seller out ahead", sub: `${sellerAheadCount} deals where seller work is ahead of buyer commitment` };
  } else if (alignedCount >= 3) {
    verdict = { kind: "sunny", title: "Progression aligned", sub: `${alignedCount} deals moving on the same path to close` };
  } else if (buyerAheadCount > sellerAheadCount) {
    verdict = { kind: "partly", title: "Buyer-led motion", sub: `${buyerAheadCount} buyers are closer to the target than seller activity` };
  } else {
    verdict = { kind: "cloudy", title: "Mixed distance", sub: `${alignedCount} aligned, ${sellerAheadCount} seller-ahead, ${buyerAheadCount} buyer-ahead` };
  }
  return { items, verdict, alignedCount, sellerAheadCount, buyerAheadCount };
};

const BuyerForecast = ({ open, onClose, deals = [], onOpenDeal }) => {
  const [render, setRender] = useState(open);
  const [closing, setClosing] = useState(false);

  useEffect(() => {
    if (open) {
      setRender(true);
      setClosing(false);
      return undefined;
    }
    if (render) {
      setClosing(true);
      const t = setTimeout(() => {
        setRender(false);
        setClosing(false);
      }, 320);
      return () => clearTimeout(t);
    }
    return undefined;
  }, [open, render]);

  useEffect(() => {
    if (!open) return undefined;
    const onKey = (e) => { if (e.key === "Escape") onClose && onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose]);

  const forecast = useMemo(() => buildForecast(deals), [deals]);
  if (!render) return null;

  const today = new Date().toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric" });

  return (
    <div
      className={`forecast-backdrop ${closing ? "is-closing" : "is-opening"}`}
      role="dialog"
      aria-modal="true"
      aria-labelledby="forecast-title"
      onClick={onClose}
    >
      <div className={`forecast-card tone-${forecast.verdict.kind}`} onClick={(e) => e.stopPropagation()}>
        <button type="button" className="forecast-close" onClick={onClose} aria-label="Dismiss forecast">
          <Icon name="close" size={14} />
        </button>
        <div className="forecast-eyebrow">
          <span>Today's progression forecast</span>
          <span className="mono sub">{today}</span>
        </div>
        <div className="forecast-hero">
          <WeatherGlyph kind={forecast.verdict.kind} size={64} />
          <div className="forecast-hero-text">
            <h2 id="forecast-title" className="forecast-title">{forecast.verdict.title}</h2>
            <p className="forecast-sub">{forecast.verdict.sub}</p>
          </div>
        </div>
        <ul className="forecast-list">
          {forecast.items.map((it) => {
            const sign = it.delta > 0 ? "+" : "";
            const bandLeft = Math.min(it.buyer, it.seller);
            const bandWidth = Math.abs(it.seller - it.buyer);
            return (
              <li key={it.id}>
                <button
                  type="button"
                  className={`forecast-row tone-${it.weather.tone}`}
                  onClick={() => { onOpenDeal && onOpenDeal(it.id); onClose && onClose(); }}
                  title={`Open ${it.company}`}
                >
                  <WeatherGlyph kind={it.weather.kind} size={28} />
                  <div className="forecast-row-text">
                    <div className="forecast-row-top">
                      <div>
                        <div className="forecast-row-co">{it.company}</div>
                        <div className="forecast-row-meta">{it.stage} · {it.distance}</div>
                      </div>
                      <div className={`forecast-row-delta tone-${it.weather.tone}`}>
                        <span>{sign}{it.delta}</span>
                        <em>buyer 7d</em>
                      </div>
                    </div>
                    <div className="forecast-path" aria-hidden="true">
                      <span className="forecast-path-gap" style={{ left: `${bandLeft}%`, width: `${bandWidth}%` }} />
                      <span className="forecast-path-marker buyer" style={{ left: `${it.buyer}%` }}>
                        <b>B</b>
                        <em>{it.buyer}</em>
                      </span>
                      <span className="forecast-path-marker seller" style={{ left: `${it.seller}%` }}>
                        <b>S</b>
                        <em>{it.seller}</em>
                      </span>
                      <span className="forecast-path-target">Target</span>
                    </div>
                  </div>
                </button>
              </li>
            );
          })}
          {forecast.items.length === 0 && (
            <li className="forecast-empty">No open deals to forecast yet.</li>
          )}
        </ul>
        <div className="forecast-foot">
          <span className="muted">Buyer and seller share one path to close. The forecast shows distance on that path.</span>
          <button type="button" className="btn sm" onClick={onClose}>Got it</button>
        </div>
      </div>
    </div>
  );
};

// ---------- Evidence drawer ----------
const sourceMeta = (source = "") => {
  const key = source.toLowerCase();
  if (/salesforce|crm/.test(key)) return { label: "Salesforce", mark: "SF", tone: "salesforce", slug: "salesforce", color: "00A1E0" };
  if (/gong|call/.test(key)) return { label: "Gong", mark: "G", tone: "gong", slug: "gong", color: "8039DF" };
  if (/slack/.test(key)) return { label: "Slack", mark: "S", tone: "slack", slug: "slack", color: "4A154B" };
  if (/gmail|mail|email|inbox/.test(key)) return { label: "Gmail", mark: "M", tone: "gmail", slug: "gmail", color: "EA4335" };
  if (/calendar|meet/.test(key)) return { label: "Calendar", mark: "C", tone: "calendar", slug: "googlecalendar", color: "4285F4" };
  if (/linkedin/.test(key)) return { label: "LinkedIn", mark: "in", tone: "linkedin", slug: "linkedin", color: "0A66C2" };
  if (/outreach|sequence/.test(key)) return { label: "Outreach", mark: "O", tone: "outreach", slug: null };
  return { label: source || "Source", mark: "N", tone: "nudge", slug: null };
};

const SourceLogo = ({ source, size = "md" }) => {
  const meta = sourceMeta(source);
  const [failed, setFailed] = useState(false);
  if (meta.slug && !failed) {
    return (
      <span className={`source-logo ${size} ${meta.tone} is-brand`} title={meta.label} aria-label={meta.label}>
        <img
          src={`https://cdn.simpleicons.org/${meta.slug}/${meta.color}`}
          alt=""
          onError={() => setFailed(true)}
        />
      </span>
    );
  }
  return (
    <span className={`source-logo ${size} ${meta.tone}`} title={meta.label} aria-label={meta.label}>
      {meta.mark}
    </span>
  );
};

const latestActivity = (deal, kind) => (deal?.activity || []).find((item) => item.kind === kind);
const latestMeeting = (deal, past = true) => (deal?.meetings || []).find((meeting) => !!meeting.isPast === past) || (deal?.meetings || [])[0];

const buildRecommendationEvidence = ({ recommendation, deal, draft }) => {
  const n = recommendation || {};
  const text = `${n.kind || ""} ${n.title || ""} ${n.body || ""} ${n.sub || ""} ${n.action || ""}`.toLowerCase();
  const email = latestActivity(deal, "email");
  const meeting = latestMeeting(deal, true);
  const nextMeeting = latestMeeting(deal, false);
  const diag = (typeof DEAL_DIAGNOSIS !== "undefined" && deal?.id) ? DEAL_DIAGNOSIS[deal.id] : null;
  const sources = [
    {
      source: "Salesforce",
      label: "CRM state",
      time: deal?.closeDate || "Recent",
      detail: deal ? `${deal.stage} · ${deal.arr || `$${Math.round((deal.value || 0) / 1000)}K`} · ${deal.statusLabel || "Open"}` : "Deal record matched from CRM.",
      proof: diag?.what || n.sub || "Current stage, amount, close date, and status are the base forecast record.",
    },
  ];

  if (n.evidence) {
    sources.push({
      source: n.evidence.source || "LinkedIn",
      label: n.evidence.label || "Signal detected",
      time: n.evidence.time || "Recent",
      detail: n.evidence.detail || n.title || "External signal matched",
      proof: n.evidence.proof || n.sub || "Addy matched this external signal against the active deal context.",
    });
  }

  if (/economic buyer|cfo|finance|decision-maker|stakeholder/.test(text)) {
    const missing = (deal?.stakeholders || []).find((s) => s.missing) || (deal?.stakeholders || []).find((s) => /cfo|finance|procurement|legal/i.test(s.role));
    sources.push({
      source: n.automation?.source || "LinkedIn",
      label: "Stakeholder match",
      time: "Live enrichment",
      detail: n.automation?.foundContact
        ? `${n.automation.foundContact.name} · ${n.automation.foundContact.role}`
        : missing
          ? `${missing.name} · ${missing.role}`
          : "Likely finance owner identified",
      proof: n.automation?.foundContact?.signal || "Addy matched the missing buyer role against the account map and recommended an intro path.",
    });
  }

  if (/silent|engagement|reply|unopened|proposal|follow-up|commitment/.test(text)) {
    sources.push({
      source: "Gmail",
      label: "Thread signal",
      time: email?.time || n.time || "Recent",
      detail: email ? email.title : n.sub || "Recent buyer engagement changed",
      proof: email?.body || n.body || "Thread activity indicates the buyer has not created matching momentum.",
    });
  }

  if (/meeting|calendar|timeline|close plan|workshop|commit/.test(text) || nextMeeting) {
    sources.push({
      source: "Calendar",
      label: nextMeeting && !nextMeeting.isPast ? "Upcoming meeting" : "Meeting history",
      time: (nextMeeting && !nextMeeting.isPast ? nextMeeting.date : meeting?.date) || "Recent",
      detail: (nextMeeting && !nextMeeting.isPast ? nextMeeting.title : meeting?.title) || "Calendar path checked",
      proof: (nextMeeting && !nextMeeting.isPast ? nextMeeting.goal : meeting?.debrief?.summary) || "Addy checked whether the next buyer commitment exists on the calendar.",
    });
  }

  if (meeting?.debrief && (/call|meeting|legal|redline|security|budget|qbr|expansion|reference/.test(text) || sources.length < 3)) {
    sources.push({
      source: "Gong",
      label: "Call evidence",
      time: meeting.date,
      detail: meeting.debrief.sentiment || meeting.title,
      proof: meeting.debrief.transcriptExcerpt || meeting.debrief.summary,
    });
  }

  if (/playbook|escalat|manager|legal|slack|internal/.test(text) || sources.length < 4) {
    sources.push({
      source: "Slack",
      label: "Team context",
      time: "Today",
      detail: n.kind || "Internal coordination",
      proof: diag?.fix || n.action || "No owned internal next step is recorded yet, so Addy is turning the signal into an assignable action.",
    });
  }

  if (draft?.workflowSource && !sources.some((item) => item.source === draft.workflowSource)) {
    sources.push({
      source: draft.workflowSource,
      label: "Automation source",
      time: "Live enrichment",
      detail: draft.workflowSummary || `${draft.recipientName} · ${draft.recipientRole}`,
      proof: draft.contactSignal || "The workflow source supplied the enriched contact and role evidence for this action.",
    });
  }

  return sources.slice(0, 5);
};

const EvidenceDrawer = ({ open, onClose, recommendation, deal, draft }) => {
  const drawerRef = useRef(null);
  const titleId = `evidence-title-${deal?.id || "feed"}-${(recommendation?.kind || recommendation?.title || "recommendation").toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;

  useEffect(() => {
    if (!open) return undefined;
    const previous = document.activeElement;
    const onKey = (e) => { if (e.key === "Escape") onClose && onClose(); };
    window.addEventListener("keydown", onKey);
    setTimeout(() => drawerRef.current?.focus(), 0);
    return () => {
      window.removeEventListener("keydown", onKey);
      previous?.focus?.();
    };
  }, [open, onClose]);

  if (!open) return null;

  const sources = buildRecommendationEvidence({ recommendation, deal, draft });
  const confidence = Math.min(96, 68 + (sources.length * 6) + (deal?.gap ? Math.min(10, Math.abs(deal.gap) / 4) : 0));
  const primary = recommendation?.title || recommendation?.kind || "Addy recommendation";
  const explanation = recommendation?.body || recommendation?.sub || (deal && (DEAL_DIAGNOSIS[deal.id]?.why || deal.summary));

  return (
    <div className="evidence-back" onClick={onClose}>
      <aside
        ref={drawerRef}
        className="evidence-drawer"
        onClick={(e) => e.stopPropagation()}
        data-dialog-root="true"
        role="dialog"
        aria-modal="true"
        aria-labelledby={titleId}
        tabIndex={-1}
      >
        <header className="evidence-head">
          <div>
            <span className="eyebrow">Evidence</span>
            <h2 id={titleId}>{primary}</h2>
            {deal && <p>{deal.company} · {deal.stage} · {deal.statusLabel}</p>}
          </div>
          <button type="button" className="btn sm ghost" onClick={onClose} aria-label="Close evidence">
            <Icon name="close" size={14} />
          </button>
        </header>

        <div className="evidence-confidence">
          <span>Confidence</span>
          <b>{Math.round(confidence)}%</b>
          <i><span style={{ width: `${confidence}%` }} /></i>
        </div>

        {explanation && (
          <section className="evidence-summary">
            <span className="eyebrow">Why Addy surfaced this</span>
            <p>{explanation}</p>
          </section>
        )}

        <section className="evidence-sources" aria-label="Evidence sources">
          {sources.map((item, i) => {
            const meta = sourceMeta(item.source);
            return (
              <article key={`${item.source}-${i}`} className="evidence-source">
                <SourceLogo source={item.source} />
                <div>
                  <div className="evidence-source-top">
                    <b>{item.label}</b>
                    <span>{meta.label} · {item.time}</span>
                  </div>
                  <strong>{item.detail}</strong>
                  <p>{item.proof}</p>
                </div>
              </article>
            );
          })}
        </section>

        <footer className="evidence-foot">
          <button type="button" className="btn ghost" onClick={onClose}>Dismiss</button>
          <button type="button" className="btn accent" onClick={onClose}>
            Use recommendation <Icon name="arrow" size={13} />
          </button>
        </footer>
      </aside>
    </div>
  );
};

const EvidenceStrip = ({ recommendation, deal, draft, limit = 4, compact = false }) => {
  const [visible, setVisible] = useState(false);
  const [openIndex, setOpenIndex] = useState(null);
  const sources = buildRecommendationEvidence({ recommendation, deal, draft }).slice(0, limit);
  if (!sources.length) return null;
  const selected = openIndex !== null ? sources[openIndex] : null;

  return (
    <div className={`evidence-strip ${compact ? "is-compact" : ""}`}>
      <button
        type="button"
        className="evidence-disclosure"
        onClick={() => {
          setVisible((next) => {
            if (next) setOpenIndex(null);
            return !next;
          });
        }}
        aria-expanded={visible}
        aria-label={visible ? "Hide evidence" : "Show evidence"}
        title={visible ? "Hide evidence" : "Show evidence"}
      >
        i
      </button>
      {visible && (
        <>
          <div className="evidence-strip-row" aria-label="Evidence sources">
            {sources.map((item, i) => {
              const meta = sourceMeta(item.source);
              const active = openIndex === i;
              return (
                <button
                  key={`${item.source}-${i}`}
                  type="button"
                  className={`evidence-chip ${active ? "is-active" : ""}`}
                  onClick={() => setOpenIndex(active ? null : i)}
                  aria-expanded={active}
                >
                  <SourceLogo source={item.source} size="sm" />
                  <span>
                    <b>{meta.label}</b>
                    <em>{item.label}</em>
                  </span>
                </button>
              );
            })}
          </div>
          {selected && (
            <div className="evidence-inline-proof">
              <div className="evidence-inline-head">
                <SourceLogo source={selected.source} size="sm" />
                <span>{sourceMeta(selected.source).label} · {selected.time}</span>
              </div>
              <b>{selected.detail}</b>
              <p>{selected.proof}</p>
            </div>
          )}
        </>
      )}
    </div>
  );
};

const SEVERITY_IMPACT_WEIGHT = { warn: 1.0, amber: 0.65, info: 0.35, good: 0.1 };
const computeGapImpact = (gap, deal) => {
  const weight = SEVERITY_IMPACT_WEIGHT[gap?.severity] ?? 0.5;
  return Math.round((deal?.value || 0) * weight);
};
const formatImpactDollars = (dollars) => {
  if (!dollars) return "";
  if (dollars >= 1_000_000) return `$${(dollars / 1_000_000).toFixed(dollars >= 10_000_000 ? 0 : 1)}M`;
  if (dollars >= 1_000) return `$${Math.round(dollars / 1000)}K`;
  return `$${dollars}`;
};
const sortGapsByImpact = (gaps = [], deal) => gaps
  .map((n, originalIndex) => ({ n, originalIndex, impact: computeGapImpact(n, deal) }))
  .sort((a, b) => b.impact - a.impact || a.originalIndex - b.originalIndex);
const STATUS_IMPACT_WEIGHT = { "needs-attention": 1.0, "at-risk": 0.85, "amber": 0.55, "healthy": 0.2 };
const computeDealPipelineImpact = (deal) => {
  if (!deal) return 0;
  if (/closed/i.test(deal.stage || "") || /closed/i.test(deal.statusLabel || "")) return 0;
  const gaps = deal.nudges || [];
  if (gaps.length) {
    return Math.max(0, ...gaps.map((g) => computeGapImpact(g, deal)));
  }
  const weight = STATUS_IMPACT_WEIGHT[deal.status] ?? 0.5;
  return Math.round((deal.value || 0) * weight);
};
const sortDealsByPipelineImpact = (deals = []) => [...deals].sort(
  (a, b) => computeDealPipelineImpact(b) - computeDealPipelineImpact(a),
);

const trackNudgeEvent = (eventName, payload = {}) => {
  if (window.__NUDGE_DEBUG_EVENTS__) console.info("[nudge:event]", eventName, payload);
};

const evidenceText = (value) => {
  if (value == null) return "";
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
  if (Array.isArray(value)) return value.map(evidenceText).join(" ");
  if (typeof value === "object") return Object.values(value).map(evidenceText).join(" ");
  return "";
};
const hasTextMatch = (items = [], re) => items.some((item) => re.test(evidenceText(item)));
const defenseStatus = (value) => (
  value === "present" ? "Present" :
  value === "partial" ? "Partial" :
  value === "missing" ? "Missing" :
  "Unknown"
);
const firstName = (name = "") => name.trim().split(/\s+/)[0] || "";
const check = (status, detail, source) => ({ status, detail, source });
const latestMatching = (items = [], re) => items.find((item) => re.test(evidenceText(item)));
const activeDecisionOwner = (stakeholders = []) => stakeholders.find((s) =>
  !s.missing && (s.power >= 5 || /cfo|cro|ceo|coo|cto|founder|chief|decision|economic/i.test(`${s.role || ""} ${s.name || ""}`))
);
const fallbackChampion = (stakeholders = []) => stakeholders.find((s) => !s.missing && s.power >= 4 && s.support >= 4);

// ---------- Lens helpers (Deal Truth / Seller / Buyer) ----------
// Pillars are ordered to match the on-screen coverage strip.
const MEDDPICC_PILLARS = [
  { code: "M",  label: "Metrics",          re: /\b(roi|metric|kpi|payback|business case|value driver|cost saving|numbers|quantif)/i },
  { code: "E",  label: "Economic buyer",   re: /\b(cfo|chief financial|economic buyer|signing authority|budget approval|procurement signoff|finance)\b/i },
  { code: "DC", label: "Decision criteria",re: /\b(criteria|evaluation|eval checklist|requirements|comparison|compare|scorecard|fit)\b/i },
  { code: "DP", label: "Decision process", re: /\b(decision process|timeline|close date|mutual action|next step|approval path|stages|milestone)\b/i },
  { code: "PP", label: "Paper process",    re: /\b(redline|legal|procurement|contract|msa|paper|signing|dpa|security review)\b/i },
  { code: "IP", label: "Identify pain",    re: /\b(pain|challenge|issue|problem|bottleneck|friction|stall|risk|blocker)\b/i },
  { code: "CH", label: "Champion",         re: /\b(champion|advocate|mobilizer|intro|introduce|coach)\b/i },
  { code: "CO", label: "Competition",      re: /\b(competitor|displacement|linnworks|northline|battlecard|vs\.?|versus|evaluating)\b/i },
];

const SELLER_ACTORS = /^(you|ridha|addy)$/i;
const isSellerActor = (actor = "") => SELLER_ACTORS.test(actor.trim());
const isMixedActor = (actor = "") => /\+/.test(actor) && /you|ridha/i.test(actor);

function activitySide(item, deal) {
  const actor = item.actor || "";
  if (item.kind === "addy") return "ai";
  if (isMixedActor(actor)) return "mixed";
  if (isSellerActor(actor)) return "seller";
  // Match against any stakeholder name (or champion fallback).
  const buyerNames = [(deal?.champion || ""), ...(deal?.stakeholders || []).map((s) => s.name)].filter(Boolean);
  const isBuyer = buyerNames.some((n) => actor.toLowerCase().includes(n.toLowerCase()));
  return isBuyer ? "buyer" : "seller";
}

function getActivityMeddpicc(item, deal) {
  const text = `${item.title || ""} ${item.body || ""} ${item.tag || ""} ${item.actor || ""}`;
  const codes = new Set();
  MEDDPICC_PILLARS.forEach((p) => { if (p.re.test(text)) codes.add(p.code); });
  // Champion match by name from this specific deal — catches "Sarah Chen reposted ..."
  const champ = deal?.champion;
  if (champ && new RegExp(`\\b${firstName(champ)}\\b`, "i").test(text)) codes.add("CH");
  return Array.from(codes);
}

// Try to extract a buyer commitment from a buyer-side activity body. If we
// can't parse a clean commitment, fall back to the title.
function extractBuyerCommitment(item) {
  const body = item.body || "";
  // Look for "agreed to ...", "confirmed ...", "will ...", "by <date> ..."
  const patterns = [
    /agreed (?:to |on |that )([^.]+)/i,
    /confirmed (?:that |budget )?([^.]+)/i,
    /committed to ([^.]+)/i,
    /will ([^.]+? by [^.]+)/i,
    /(?:i'?ll|we'?ll) ([^.]+)/i,
  ];
  for (const re of patterns) {
    const m = body.match(re);
    if (m) return m[1].trim().replace(/\s+/g, " ");
  }
  return item.title || "Engagement";
}

// Partition activity into seller/buyer/mixed/ai with derived fields.
function partitionLensActivity(deal) {
  const items = (deal?.activity || []).map((it) => {
    const side = activitySide(it, deal);
    return {
      ...it,
      side,
      meddpicc: getActivityMeddpicc(it, deal),
    };
  });
  const seller = items.filter((it) => it.side === "seller" || it.side === "ai" || it.side === "mixed");
  const buyer  = items.filter((it) => it.side === "buyer" || it.side === "mixed").map((it) => ({
    ...it,
    commitment: extractBuyerCommitment(it),
  }));
  // Coverage = unique pillars touched by seller-side activity.
  const sellerCoverage = new Set();
  seller.forEach((it) => it.meddpicc.forEach((c) => sellerCoverage.add(c)));
  const coveredPillars = MEDDPICC_PILLARS.map((p) => ({ ...p, covered: sellerCoverage.has(p.code) }));
  return { seller, buyer, coveredPillars };
}

// Multi-stakeholder champion classification for the buyer lens.
// Returns one entry per engaged stakeholder, plus an `expansion` count
// indicating how many internal stakeholders the buyer has pulled in beyond
// the primary champion.
function classifyChampions(deal) {
  const stakeholders = (deal?.stakeholders || []).filter((s) => !s.missing);
  if (stakeholders.length === 0) return { roles: [], expansion: 0, primaryName: null };

  const championName = deal?.champion;
  const ranked = [...stakeholders].sort(
    (a, b) => ((b.power || 0) * 2 + (b.support || 0)) - ((a.power || 0) * 2 + (a.support || 0))
  );

  // Primary champion: explicit name match wins; otherwise top-ranked engaged stakeholder
  // whose support is high enough to count.
  let primary = championName ? ranked.find((s) => s.name === championName) : null;
  if (!primary) primary = ranked.find((s) => (s.support || 0) >= 4) || ranked[0];

  const roles = ranked.map((s) => {
    const isPrimary = s === primary;
    const power = s.power || 0;
    const support = s.support || 0;
    let category = "supporter";
    let move = "Keep informed";
    if (isPrimary) {
      category = "primary-champion";
      move = "Protect — keep tight on the close plan";
    } else if (power >= 3 && support >= 4) {
      category = "co-champion";
      move = "Amplify — co-own the next milestone";
    } else if (support >= 4 && power < 3) {
      category = "mobilizer";
      move = "Equip — give them ammo to expand internally";
    } else if (power >= 4 && support <= 3) {
      category = "sponsor";
      move = "Convert — earn deeper support before signoff";
    } else if (power >= 3 && support <= 2) {
      category = "blocker-risk";
      move = "Address — surface concerns directly";
    }
    return { ...s, category, move, isPrimary };
  });

  const expansion = roles.filter((r) => !r.isPrimary).length;
  return { roles, expansion, primaryName: primary?.name || null };
}

// Follow-through: did the buyer's stated commitment actually translate into
// downstream activity? Pulls 1-2 distinctive nouns from the commitment text
// and scans activity items recorded *after* the commitment for a topical
// match. Activity arrays are reverse-chronological in the seed data, so
// "later in time" means "earlier index" in the array.
const COMMITMENT_STOPWORDS = /^(the|a|an|to|on|in|at|by|for|with|of|and|or|that|this|will|can|could|would|should|i|we|our|their|next|some|any|been|done|have|has|had|been|us|them|so|but|if|as)$/i;
function commitmentTopics(commitment = "") {
  const words = String(commitment).toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 4 && !COMMITMENT_STOPWORDS.test(w));
  // Dedupe, keep insertion order, cap at 3 tokens.
  return Array.from(new Set(words)).slice(0, 3);
}
function findFollowThrough(commitment, item, allItems = []) {
  const topics = commitmentTopics(commitment);
  if (topics.length === 0) return null;
  const idx = allItems.indexOf(item);
  if (idx <= 0) return null;
  // Activity array is reverse-chronological. Items "later in time" sit at
  // smaller indices. We want activity newer than `item` — slice(0, idx).
  const newer = allItems.slice(0, idx);
  for (const candidate of newer) {
    if (candidate === item) continue;
    const haystack = `${candidate.title || ""} ${candidate.body || ""}`.toLowerCase();
    const matchedTopic = topics.find((t) => haystack.includes(t));
    if (matchedTopic) {
      return { matched: true, item: candidate, topic: matchedTopic };
    }
  }
  return { matched: false, topics };
}

const CHAMPION_ROLE_LABEL = {
  "primary-champion": "Primary champion",
  "co-champion": "Co-champion",
  "mobilizer": "Mobilizer",
  "sponsor": "Executive sponsor",
  "blocker-risk": "Blocker risk",
  "supporter": "Supporter",
};
const CHAMPION_ROLE_TONE = {
  "primary-champion": "good",
  "co-champion": "good",
  "mobilizer": "info",
  "sponsor": "amber",
  "blocker-risk": "warn",
  "supporter": "muted",
};

const getDealDefense = (deal) => {
  if (!deal) {
    return {
      state: "Unknown",
      tone: "unknown",
      reason: "No deal context available.",
      recommendedAction: "Add evidence before defending this deal.",
      buyerNextStep: "unknown",
      decisionOwner: "unknown",
      approvalPath: "unknown",
      evidence: "unknown",
      riskLabel: "Buyer proof unknown",
      buyerName: "",
      checks: {},
      evidenceItems: [],
    };
  }

  const nudges = deal.nudges || [];
  const activity = deal.activity || [];
  const meetings = deal.meetings || [];
  const stakeholders = deal.stakeholders || [];
  const text = `${deal.summary || ""} ${evidenceText(nudges)} ${evidenceText(activity)} ${evidenceText(meetings)}`;
  const diag = (typeof DEAL_DIAGNOSIS !== "undefined" && deal?.id) ? DEAL_DIAGNOSIS[deal.id] : null;
  const sellerAhead = (deal.sellerScore || 0) - (deal.buyerScore || 0);
  const signedGap = deal.gap ?? ((deal.buyerScore || 0) - (deal.sellerScore || 0));
  const absGap = Math.abs(signedGap);
  const externalSignal = nudges.find((n) => n.evidence);
  const championLeft = /changed jobs|left|former champion|moved to/i.test(text);
  const competitorSignal = /competitor|linnworks|reposted/i.test(text);
  const silenceSignal = /no buyer engagement|72h|quiet|silent|proposal unopened|hasn't opened|stalled between calls/i.test(text);
  const missingOwnerSignal = /economic buyer|decision-maker|cfo not|cfo missing|stakeholders|no confirmed internal owner|no owner/i.test(text) ||
    stakeholders.some((s) => s.missing && /cfo|economic|decision|operations|finance/i.test(s.role || ""));
  const approvalGapSignal = /approval path|approval|procurement|legal|redline|finance review/i.test(text) && /missing|unknown|not|pending|stalled|absent|contingent/i.test(text);

  const upcomingMeeting = meetings.find((m) => !m.isPast);
  const pastMeetingWithNextStep = meetings.find((m) => m.isPast && (m.debrief?.nextSteps || m.intel?.talkingPoints));
  const buyerActivity = activity.find((a) => /buyer|milestone/i.test(a.tag || "") || /replied|confirmed|signed|asked|agreed|accepted|approved/i.test(`${a.title} ${a.body}`));
  const decisionOwnerRecord = activeDecisionOwner(stakeholders);
  const championRecord = fallbackChampion(stakeholders);
  const missingOwner = stakeholders.find((s) => s.missing && /cfo|economic|decision|finance|operations/i.test(s.role || ""));
  const approvalSignal = latestMatching([...activity, ...meetings], /approval|procurement|legal|security|signoff|redline|budget|signed|countersigned/i);
  const positiveEvidence = buyerActivity || upcomingMeeting || approvalSignal;

  const buyerNextStepCheck = upcomingMeeting
    ? check("present", `${upcomingMeeting.title} · ${upcomingMeeting.date}`, "Calendar")
    : pastMeetingWithNextStep
      ? check("partial", `${pastMeetingWithNextStep.title} produced next steps, but no future meeting is booked`, "Gong")
      : silenceSignal
        ? check("missing", "No current buyer-owned next step after latest signal", "Gmail")
        : check("unknown", "No dated buyer-owned next step found", "CRM");

  const decisionOwnerCheck = championLeft
    ? check("missing", `${deal.champion} changed jobs; replacement owner not confirmed`, "LinkedIn")
    : decisionOwnerRecord
      ? check("present", `${decisionOwnerRecord.name} · ${decisionOwnerRecord.role}`, "CRM")
      : championRecord
        ? check("partial", `${championRecord.name} is supportive, but signing authority is not proven`, "CRM")
        : missingOwner
          ? check("missing", `${missingOwner.role} is marked missing from stakeholder map`, "CRM")
          : missingOwnerSignal
            ? check("missing", "Decision owner not confirmed in account map", "CRM")
            : check("unknown", "No power/contact evidence found", "CRM");

  const approvalPathCheck = approvalSignal && approvalGapSignal
    ? check("partial", `${approvalSignal.title || "Approval signal"} exists, but path is still contingent or pending`, /meeting/i.test(approvalSignal.kind || "") ? "Gong" : "CRM")
    : approvalGapSignal
      ? check("missing", "Approval path is flagged as unknown or pending", "CRM")
      : approvalSignal
        ? check("present", `${approvalSignal.title || approvalSignal.dealName || "Approval signal"} · ${approvalSignal.time || approvalSignal.date || "recent"}`, /meeting/i.test(approvalSignal.kind || "") ? "Gong" : "CRM")
      : check("unknown", "No approval, security, legal, or procurement evidence found", "CRM");

  const evidenceCheck = externalSignal
    ? check("present", `${externalSignal.evidence.source}: ${externalSignal.evidence.detail}`, externalSignal.evidence.source)
    : positiveEvidence
      ? check("present", `${(buyerActivity || approvalSignal || upcomingMeeting).title || "Buyer signal"} · ${(buyerActivity || approvalSignal || upcomingMeeting).time || (buyerActivity || approvalSignal || upcomingMeeting).date || "recent"}`, "CRM")
      : sellerAhead >= 20 || silenceSignal
        ? check("missing", `Seller score leads buyer by ${Math.max(0, sellerAhead)} pts with no matching buyer proof`, "CRM")
        : check("unknown", "No recent buyer-side evidence found", "CRM");

  const buyerNextStep = buyerNextStepCheck.status;
  const decisionOwner = decisionOwnerCheck.status;
  const approvalPath = approvalPathCheck.status;
  const evidence = evidenceCheck.status;
  const missingCount = [buyerNextStep, decisionOwner, approvalPath, evidence].filter((v) => v === "missing").length;
  const presentCount = [buyerNextStep, decisionOwner, approvalPath, evidence].filter((v) => v === "present").length;
  const severeRisk = championLeft || (sellerAhead >= 25 && buyerNextStep !== "present") || (competitorSignal && sellerAhead >= 20);

  const evidenceItems = [
    {
      source: "CRM",
      label: "Buyer/seller score",
      detail: `${deal.stage} · buyer ${deal.buyerScore}% vs seller ${deal.sellerScore}% · ${absGap} pt gap`,
      tone: absGap >= 25 ? "warn" : absGap >= 10 ? "amber" : "good",
    },
    externalSignal?.evidence && {
      source: externalSignal.evidence.source,
      label: externalSignal.title,
      detail: externalSignal.evidence.detail,
      proof: externalSignal.evidence.proof,
      tone: "warn",
    },
    buyerActivity && {
      source: buyerActivity.kind === "meeting" ? "Gong" : "Gmail",
      label: buyerActivity.title,
      detail: `${buyerActivity.actor} · ${buyerActivity.time}`,
      proof: buyerActivity.body,
      tone: /confirmed|approved|signed|agreed/i.test(`${buyerActivity.title} ${buyerActivity.body}`) ? "good" : "neutral",
    },
    upcomingMeeting && {
      source: "Calendar",
      label: "Next buyer touch",
      detail: `${upcomingMeeting.title} · ${upcomingMeeting.date}`,
      proof: upcomingMeeting.goal || "Future buyer meeting is on the calendar.",
      tone: "good",
    },
    decisionOwnerCheck.status !== "unknown" && {
      source: decisionOwnerCheck.source,
      label: "Decision coverage",
      detail: decisionOwnerCheck.detail,
      tone: decisionOwnerCheck.status === "present" ? "good" : decisionOwnerCheck.status === "partial" ? "amber" : "warn",
    },
    approvalPathCheck.status !== "unknown" && {
      source: approvalPathCheck.source,
      label: "Approval path",
      detail: approvalPathCheck.detail,
      tone: approvalPathCheck.status === "present" ? "good" : approvalPathCheck.status === "partial" ? "amber" : "warn",
    },
  ].filter(Boolean).slice(0, 6);

  let state = "Weak";
  let tone = "weak";
  let reason = `Buyer proof is incomplete: ${missingCount} required ${missingCount === 1 ? "signal is" : "signals are"} missing.`;
  if (championLeft) {
    state = "Unverified";
    tone = "unverified";
    reason = `LinkedIn shows ${deal.champion} changed jobs, so this deal no longer has a confirmed active champion.`;
  } else if (competitorSignal) {
    state = "Unverified";
    tone = "unverified";
    reason = `${deal.champion} is engaging with competitor content while buyer momentum trails seller activity by ${Math.max(0, sellerAhead)} pts.`;
  } else if (missingCount >= 2 || severeRisk) {
    state = "Unverified";
    tone = "unverified";
    reason = `This forecast is not defensible yet: ${[buyerNextStepCheck, decisionOwnerCheck, approvalPathCheck, evidenceCheck].filter((c) => c.status === "missing").map((c) => c.detail).slice(0, 2).join("; ")}.`;
  } else if (presentCount >= 3 && sellerAhead < 15 && !severeRisk) {
    state = "Verified";
    tone = "verified";
    reason = `Defensible: ${presentCount} buyer-proof signals are present and the score gap is ${absGap} pts.`;
  } else if (buyerNextStep === "missing") {
    reason = "No buyer-owned next step is confirmed from calendar or activity data.";
  }

  const primaryNudge = nudges[0];
  const recommendedAction = primaryNudge?.executeLabel || primaryNudge?.action
    ? `${primaryNudge.executeLabel || primaryNudge.action}: ${primaryNudge.title}.`
    : buyerNextStep === "missing" || buyerNextStep === "unknown"
      ? "Confirm decision owner, approval path, and a dated buyer-owned next step."
      : evidence === "missing"
        ? "Add buyer evidence before defending the forecast."
        : decisionOwner === "missing"
          ? "Confirm the decision owner before manager review."
          : "Use the evidence below to defend why the deal is still real.";

  const riskLabel = championLeft
    ? "Champion changed jobs"
    : competitorSignal
      ? "Competitor signal"
      : buyerNextStep === "missing"
    ? "No buyer-owned next step"
    : evidence === "missing"
      ? "No evidence"
      : decisionOwner === "missing"
        ? "No decision owner"
        : "Seller-active, buyer-unverified";

  return {
    state,
    tone,
    reason,
    recommendedAction,
    buyerNextStep,
    decisionOwner,
    approvalPath,
    evidence,
    riskLabel,
    buyerName: firstName(deal.champion),
    checks: {
      buyerNextStep: buyerNextStepCheck,
      decisionOwner: decisionOwnerCheck,
      approvalPath: approvalPathCheck,
      evidence: evidenceCheck,
    },
    evidenceItems,
  };
};

const isBuyerUnverifiedDeal = (deal) => {
  if (!deal || /closed/i.test(`${deal.stage || ""} ${deal.statusLabel || ""}`)) return false;
  const defense = getDealDefense(deal);
  return defense.state !== "Verified";
};

// Progress permission — answers "is this deal allowed to progress based on
// buyer proof?". Reuses the existing getDealDefense checks so we don't
// invent a parallel scoring system. Closed deals return null so the UI can
// skip rendering the indicator (the question doesn't apply post-close).
const PROGRESS_PROOF_LABELS = {
  buyerNextStep: "buyer-owned next step",
  decisionOwner: "decision owner",
  approvalPath:  "approval path",
  evidence:      "buyer evidence",
};
const getProgressPermission = (deal) => {
  if (!deal) return null;
  if (/closed/i.test(`${deal.stage || ""} ${deal.statusLabel || ""}`)) return null;

  const defense = getDealDefense(deal);
  // Labels are deliberately proof-anchored ("Proof complete/partial/missing")
  // so the chip reads as a buyer-proof judgment, not a second risk badge —
  // it sits alongside deal.statusLabel without colliding on wording.
  const stateMap = {
    Verified:   { state: "allowed",  label: "Proof complete", fullLabel: "Buyer proof is complete", icon: "check"  },
    Weak:       { state: "at_risk",  label: "Proof partial",  fullLabel: "Buyer proof is partial",  icon: "flag"   },
    Unverified: { state: "blocked",  label: "Proof missing",  fullLabel: "Buyer proof is missing",  icon: "shield" },
  };
  const base = stateMap[defense.state] || stateMap.Weak;

  const missingProof = Object.entries(defense.checks || {})
    .filter(([, c]) => c && c.status === "missing")
    .map(([key]) => PROGRESS_PROOF_LABELS[key])
    .filter(Boolean);

  // Reason copy — anchored on the buyer-proof question, distinct from the
  // health-style language used by the status badge.
  let reason;
  if (base.state === "allowed") {
    reason = "Buyer-owned next step and supporting proof are in place — this stage is earned.";
  } else if (base.state === "at_risk") {
    reason = "Some buyer proof is present, but key signals are still weak for this stage.";
  } else {
    reason = "Critical buyer proof is missing — this stage is not earned yet.";
  }

  return {
    state: base.state,
    label: base.label,
    fullLabel: base.fullLabel,
    icon: base.icon,
    reason,
    missingProof,
    nextMove: defense.recommendedAction || null,
  };
};

const EvidenceQuote = ({ recommendation, deal, draft, className = "" }) => {
  const all = buildRecommendationEvidence({ recommendation, deal, draft });
  const primary = all.find((e) => e.source !== "Salesforce") || all[0];
  if (!primary || !primary.proof) return null;
  return (
    <div className={`evidence-quote ${className}`}>
      <SourceLogo source={primary.source} size="sm" />
      <span className="evidence-quote-time">{primary.time}</span>
      <span className="evidence-quote-text">“{primary.proof}”</span>
    </div>
  );
};

// ---------- Top brand row ----------
// Three dots in progressive opacity (the brand mark) + Nudge wordmark.
// Matches the printed brand sheet: light → medium → solid, reading like a
// signal building up.
const Brand = () => (
  <div className="brand">
    <span className="brand-mark" aria-hidden="true">
      <span className="brand-mark-dot brand-mark-dot--1" />
      <span className="brand-mark-dot brand-mark-dot--2" />
      <span className="brand-mark-dot brand-mark-dot--3" />
    </span>
    <span className="brand-wordmark">
      Nudge<sup>®</sup>
    </span>
  </div>
);

// ---------- Ambient background ----------
function ParallaxDots() {
  return (
    <div className="ambient" aria-hidden="true">
      <div className="ambient-vignette" />
    </div>
  );
}

// ---------- Progress gate (deal-room indicator) ----------
// Single small chip in the deal-room header that answers "is this deal
// allowed to progress based on buyer proof?". Click toggles a tiny popover
// with the reason and any missing proof. Hidden on closed deals.
function ProgressGate({ deal, className = "" }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  const permission = getProgressPermission(deal);

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

  if (!permission) return null;

  return (
    <span className={`progress-gate state-${permission.state} ${className}`} ref={ref}>
      <button
        type="button"
        className="progress-gate-chip"
        onClick={() => setOpen((v) => !v)}
        aria-expanded={open}
        aria-haspopup="dialog"
        aria-label={`${permission.fullLabel} — show details`}
        title={permission.fullLabel}
      >
        <Icon name={permission.icon} size={11} />
        <span>{permission.label}</span>
      </button>
      {open && (
        <div className="progress-gate-pop" role="dialog" aria-label={permission.fullLabel}>
          <div className="progress-gate-pop-h">
            <Icon name={permission.icon} size={12} />
            <b>{permission.fullLabel}</b>
          </div>
          <p className="progress-gate-pop-r">{permission.reason}</p>
          {permission.missingProof.length > 0 && (
            <p className="progress-gate-pop-m">
              <span>Missing</span>{permission.missingProof.join(", ")}
            </p>
          )}
        </div>
      )}
    </span>
  );
}

const DealCategories = (() => {
  const gapCategories = [
    { k: "champion-drift", label: "Champion drift", icon: "userx", short: "Champion", hint: "The internal owner is quiet, left, or looking elsewhere." },
    { k: "stakeholder-missing", label: "Missing stakeholder", icon: "users", short: "Stakeholder", hint: "A buyer, blocker, or signer is not in the deal." },
    { k: "process-blocker", label: "Process blocker", icon: "scale", short: "Process", hint: "Legal, security, procurement, or approval flow is stuck." },
    { k: "value-proof", label: "Value proof gap", icon: "target", short: "Value proof", hint: "The buyer has not validated ROI, urgency, or success proof." },
    { k: "timing-risk", label: "Timing risk", icon: "clock", short: "Timing", hint: "The buyer timeline is slipping or unconfirmed." },
  ];

  const actionCategories = [
    { k: "re-engage", label: "Re-engage", icon: "chat", short: "Re-engage", hint: "Restart the buyer conversation with relevant context." },
    { k: "map-stakeholders", label: "Map stakeholders", icon: "route", short: "Map people", hint: "Find the missing champion, blocker, or economic buyer." },
    { k: "unblock-process", label: "Unblock process", icon: "scale", short: "Unblock", hint: "Move legal, security, procurement, or approvals forward." },
    { k: "prove-value", label: "Prove value", icon: "trend", short: "Prove value", hint: "Send proof, ROI, or a business-case asset." },
    { k: "close-expand", label: "Close or expand", icon: "handshake", short: "Close/expand", hint: "Convert momentum into commit, QBR, or expansion." },
  ];

  const gapByDeal = {
    atlas: "champion-drift",
    global: "champion-drift",
    techstart: "process-blocker",
    acme: "timing-risk",
    startupco: "stakeholder-missing",
    horizon: "value-proof",
    northstar: "value-proof",
    aurora: "stakeholder-missing",
    pacific: "champion-drift",
    rooted: "stakeholder-missing",
    bluefish: "timing-risk",
    ironbridge: "process-blocker",
  };

  const actionByDeal = {
    atlas: "re-engage",
    global: "map-stakeholders",
    techstart: "unblock-process",
    acme: "close-expand",
    startupco: "map-stakeholders",
    horizon: "close-expand",
    northstar: "prove-value",
    aurora: "map-stakeholders",
    pacific: "re-engage",
    rooted: "prove-value",
    bluefish: "re-engage",
    ironbridge: "unblock-process",
  };

  const find = (items, key, fallbackKey) =>
    items.find((item) => item.k === key) || items.find((item) => item.k === fallbackKey) || items[0];

  const textFor = (deal = {}) => {
    const nudgeText = (deal.nudges || [])
      .map((nudge) => `${nudge.kind || ""} ${nudge.title || ""} ${nudge.sub || ""} ${nudge.action || ""}`)
      .join(" ");
    return `${deal.summary || ""} ${deal.topGap || ""} ${deal.suggestedManagerAction || ""} ${deal.managerNextAction || ""} ${deal.stage || ""} ${deal.statusLabel || ""} ${nudgeText}`.toLowerCase();
  };

  const inferGapKey = (deal = {}) => {
    if (gapByDeal[deal.sourceDealId || deal.id]) return gapByDeal[deal.sourceDealId || deal.id];
    const text = textFor(deal);
    if (/champion|quiet|silent|left|changed job|repost|competitor/.test(text)) return "champion-drift";
    if (/cfo|economic buyer|decision-maker|stakeholder|signer|owner|finance/.test(text)) return "stakeholder-missing";
    if (/legal|redline|security|procurement|compliance|approval/.test(text)) return "process-blocker";
    if (/close|date|timeline|timing|slip|scheduled/.test(text)) return "timing-risk";
    return "value-proof";
  };

  const inferActionKey = (deal = {}) => {
    if (actionByDeal[deal.sourceDealId || deal.id]) return actionByDeal[deal.sourceDealId || deal.id];
    const text = textFor(deal);
    if (/identify|lookup|find|intro|stakeholder|cfo|economic buyer|decision-maker/.test(text)) return "map-stakeholders";
    if (/legal|redline|security|procurement|compliance|approval|unblock/.test(text)) return "unblock-process";
    if (/commit|close|qbr|expansion|reference/.test(text)) return "close-expand";
    if (/proof|roi|business case|value|case study/.test(text)) return "prove-value";
    return "re-engage";
  };

  function getGapCategory(deal) {
    return find(gapCategories, inferGapKey(deal), "value-proof");
  }

  function getActionCategory(deal) {
    return find(actionCategories, inferActionKey(deal), "re-engage");
  }

  function DealCategoryChip({ category, type, compact = false }) {
    if (!category) return null;
    const prefix = type === "gap" ? "Gap" : "Action";
    return (
      <span
        className={`deal-cat-chip deal-cat-${type} ${compact ? "is-compact" : ""}`}
        title={category.label}
        aria-label={`${prefix}: ${category.label}. ${category.hint}`}
      >
        <Icon name={category.icon} size={compact ? 12 : 13} />
        <span className="deal-cat-label" aria-hidden="true">{category.label}</span>
      </span>
    );
  }

  function DealCategoryStrip({ deal, gapCategory, actionCategory, compact = false, className = "" }) {
    const gap = gapCategory || (deal ? getGapCategory(deal) : null);
    const action = actionCategory || (deal ? getActionCategory(deal) : null);
    if (!gap && !action) return null;
    return (
      <div className={`deal-cat-strip ${compact ? "is-compact" : ""} ${className}`} aria-label="Deal categories">
        {gap && <DealCategoryChip category={gap} type="gap" compact={compact} />}
        {action && <DealCategoryChip category={action} type="action" compact={compact} />}
      </div>
    );
  }

  return {
    gapCategories,
    actionCategories,
    getGapCategory,
    getActionCategory,
    DealCategoryChip,
    DealCategoryStrip,
  };
})();

// ---------- Channel layer (tiny) ----------
// Two small components: a static "via {channel}" chip for compact surfaces,
// and a segmented switch the user can use to pick the channel themselves.
// The action stays the same — only the channel (and the draft length/style)
// varies.
function ChannelChip({ channel, label, className = "" }) {
  if (!channel) return null;
  const name = label || (window.CHANNEL_LABEL && window.CHANNEL_LABEL[channel]) || channel;
  const icon = (window.CHANNEL_ICON && window.CHANNEL_ICON[channel]) || "mail";
  return (
    <span className={`ch-chip ${className}`}>
      <Icon name={icon} size={11} />
      via {name}
    </span>
  );
}

function ChannelSwitch({ value, options, onChange, className = "" }) {
  if (!options || options.length === 0) return null;
  return (
    <div className={`ch-switch ${className}`}>
      <span className="ch-switch-l">Send via</span>
      <div className="ch-switch-options" role="radiogroup">
        {options.map((ch) => {
          const label = (window.CHANNEL_LABEL && window.CHANNEL_LABEL[ch]) || ch;
          const icon  = (window.CHANNEL_ICON  && window.CHANNEL_ICON[ch])  || "mail";
          const on = ch === value;
          return (
            <button
              key={ch}
              type="button"
              role="radio"
              aria-checked={on}
              className={`ch-pill ${on ? "is-on" : ""}`}
              onClick={() => onChange && onChange(ch)}
            >
              <Icon name={icon} size={11} />
              {label}
            </button>
          );
        })}
      </div>
    </div>
  );
}

Object.assign(window, {
  Icon, Sparkline, AlignmentChart, AIDock, Sidebar, Brand, ParallaxDots, WeatherGlyph,
  SourceLogo, EvidenceDrawer, EvidenceStrip, EvidenceQuote, buildRecommendationEvidence,
  computeGapImpact, formatImpactDollars, sortGapsByImpact,
  computeDealPipelineImpact, sortDealsByPipelineImpact,
  trackNudgeEvent, getDealDefense, isBuyerUnverifiedDeal, defenseStatus,
  BuyerForecast, shouldShowForecastToday, markForecastShown,
  NUDGE_GAP_CATEGORIES: DealCategories.gapCategories,
  NUDGE_ACTION_CATEGORIES: DealCategories.actionCategories,
  getDealGapCategory: DealCategories.getGapCategory,
  getDealActionCategory: DealCategories.getActionCategory,
  DealCategoryChip: DealCategories.DealCategoryChip,
  DealCategoryStrip: DealCategories.DealCategoryStrip,
  ChannelChip, ChannelSwitch,
  getProgressPermission, ProgressGate,
  MEDDPICC_PILLARS, getActivityMeddpicc, partitionLensActivity,
  classifyChampions, CHAMPION_ROLE_LABEL, CHAMPION_ROLE_TONE,
  findFollowThrough, commitmentTopics, activitySide,
});
