// ============ LEADS TAB ============
// Tenant-scoped CRM-lite view: every conversation where the agent has
// captured a customer name or company shows up here as a "lead". Inline
// edit of lifecycle status + free-text notes; both fields PATCH the server
// and rebroadcast via SSE so other dashboard tabs (and the master-admin
// rollup) stay in sync without a refetch.

const contactsServer = () =>
  (typeof window !== "undefined" && window.CITRUS_CONFIG && window.CITRUS_CONFIG.SERVER_URL) ||
  "http://localhost:3001";

// Merges team members with router-only emails from routing groups.
// Emails already present as team members keep their name; router-only emails
// are added as synthetic entries (name: null → display falls back to email).
function mergeRoutersIntoTeam(members, routingGroups) {
  const seen = new Set((members || []).map((m) => (m.email || "").toLowerCase()));
  const extra = [];
  for (const g of (routingGroups || [])) {
    for (const email of (g.emails || [])) {
      const norm = email.toLowerCase();
      if (!seen.has(norm)) {
        seen.add(norm);
        extra.push({ id: email, email, name: null });
      }
    }
  }
  return [...(members || []), ...extra];
}

// Stage palette — matches the server's VALID_STAGE set in routes/contacts-pipeline.js.
// "contacted" was the pre-migration name for "engaged"; kept as fallback alias
// in the lookup so any legacy rows display correctly until migrated.
const DEAL_STAGES = [
  { id: "new",       label: "New",       bg: "#EEF4FF", fg: "#1F4FB5", bd: "#A6D8FF" },
  { id: "engaged",   label: "Engaged",   bg: "#FFF7ED", fg: "#B45309", bd: "#FCD34D" },
  { id: "qualified", label: "Qualified", bg: "#F5EEFB", fg: "#742855", bd: "#F2C8E0" },
  { id: "quoted",    label: "Quoted",    bg: "#FEF9C3", fg: "#92400E", bd: "#FDE68A" },
  { id: "won",       label: "Won",       bg: "#ECFDF5", fg: "#065F46", bd: "#6EE7B7" },
  { id: "lost",      label: "Lost",      bg: "#FEF2F2", fg: "#9F1239", bd: "#FCA5A5" },
];
const DEAL_STAGE_BY_ID = Object.fromEntries(DEAL_STAGES.map((s) => [s.id, s]));
// Backward-compat alias for pre-migration rows still carrying "contacted"
DEAL_STAGE_BY_ID["contacted"] = DEAL_STAGE_BY_ID["engaged"];

// Lost reason options — must match server VALID_LOST_REASON
const LOST_REASONS = [
  { id: "price",               label: "Price" },
  { id: "timeline",            label: "Timeline" },
  { id: "went_with_competitor",label: "Went with competitor" },
  { id: "went_cold",           label: "Went cold" },
  { id: "other",               label: "Other" },
];

const SUMMARY_STATUS_META = {
  lead:      { label: "Lead",      fg: "#1F4FB5" },
  booked:    { label: "Booked",    fg: "#065F46" },
  closed:    { label: "Closed",    fg: "#6B7280" },
  escalated: { label: "Escalated", fg: "#B45309" },
};

// Classification taxonomy — the agent assigns these per conversation (multi-
// label). Mirrors the server whitelist in server/lead-categories.js: keep the
// ids in sync, never rename one (stored tags + the server enum depend on it).
const CONTACT_CATEGORIES = [
  { id: "spare_parts", label: "Spare parts",           bg: "#EEF4FF", fg: "#1F4FB5", bd: "#A6D8FF" },
  { id: "maintenance", label: "Maintenance / Service", bg: "#FFF7ED", fg: "#B45309", bd: "#FCD34D" },
  { id: "new_project", label: "New project",           bg: "#ECFDF5", fg: "#065F46", bd: "#6EE7B7" },
  { id: "general",     label: "General / Other",       bg: "#F3F4F6", fg: "#4B5563", bd: "#D1D5DB" },
];
const CONTACT_CATEGORY_BY_ID = Object.fromEntries(CONTACT_CATEGORIES.map((c) => [c.id, c]));
const CONTACT_CATEGORY_ORDER = CONTACT_CATEGORIES.map((c) => c.id);

// Small read-only chips for a lead's classification tags. Renders nothing when
// the agent hasn't tagged the conversation yet.
function ContactCategoryTags({ categories }) {
  const cats = Array.isArray(categories) ? categories : [];
  if (!cats.length) return null;
  return (
    <React.Fragment>
      {cats.map((k) => {
        const c = CONTACT_CATEGORY_BY_ID[k];
        if (!c) return null;
        return (
          <span key={k} style={{ fontSize: 10.5, fontWeight: 600, padding: "1px 8px", borderRadius: 999, whiteSpace: "nowrap", color: c.fg, background: c.bg, border: `1px solid ${c.bd}` }}>
            {c.label}
          </span>
        );
      })}
    </React.Fragment>
  );
}

// Contact-log vocabulary — how the operator reached out + how it went. Mirrors
// the server whitelists in routes/contacts-pipeline.js; each maps to an icon/label so
// the activity timeline stays readable.
const CONTACT_LOG_TYPES = [
  { id: "call",    label: "Call",    icon: "📞" },
  { id: "message", label: "Message", icon: "💬" },
  { id: "email",   label: "Email",   icon: "✉️" },
  { id: "meeting", label: "Meeting", icon: "🤝" },
  { id: "note",    label: "Note",    icon: "📝" },
];
const CONTACT_LOG_TYPE_BY_ID = Object.fromEntries(CONTACT_LOG_TYPES.map((t) => [t.id, t]));
const CONTACT_LOG_OUTCOMES = [
  { id: "reached",        label: "Reached" },
  { id: "no_answer",      label: "No answer" },
  { id: "left_message",   label: "Left message" },
  { id: "interested",     label: "Interested" },
  { id: "not_interested", label: "Not interested" },
  { id: "callback",       label: "Wants callback" },
  { id: "scheduled",      label: "Scheduled" },
  { id: "other",          label: "Other" },
];
const CONTACT_LOG_OUTCOME_BY_ID = Object.fromEntries(CONTACT_LOG_OUTCOMES.map((o) => [o.id, o]));

// Pipeline order shown as clickable stages (Lost is offered separately as a
// terminal state, so it's excluded from the linear track).
const DEAL_PIPELINE = ["new", "engaged", "qualified", "quoted", "won"];
// A distinct colour per journey stage (Showed up · Engaged · Qualified · Quoted · Outcome)
const DEAL_STAGE_COLORS = ["#2D6CDF", "#E07B1A", "#8B5CF6", "#B45309", "#0B7A57"];

// AC11: suppress conversion rate when sample is too small to be meaningful.
const MIN_RATE_THRESHOLD = 5;

// AC5 — Priority/temperature badge sourced from the same fields as the daily
// recap email (server/daily-recap.js). Must stay in sync: hot/warm/cold values
// read c.lead.temperature || c.summary?.temperature || c.lead.priority, then
// derive from stage if none is set. Never a separately maintained value.
function leadTemperature(lead) {
  const raw = String(lead.temperature || lead.summaryTemperature || lead.priority || "").toLowerCase();
  if (["hot", "warm", "cold"].includes(raw)) return raw;
  const stage = lead.stage || lead.lifecycleStatus || "new";
  if (["qualified", "won"].includes(stage)) return "hot";
  if (stage === "quoted") return "warm";
  return null;
}
const TEMP_STYLE = {
  hot:  { label: "High",  bg: "#FF4B2B22", fg: "#CC2200", bd: "#FF4B2B55" },
  warm: { label: "Warm",  bg: "#F59E0B22", fg: "#92400E", bd: "#F59E0B55" },
  cold: { label: "Cold",  bg: "#3B82F622", fg: "#1D4ED8", bd: "#3B82F655" },
};

// Staleness threshold for account lifecycle records.
// An account that hasn't moved stages in this many days surfaces as stale.
const ACCOUNT_STALE_DAYS = 7;

function accountStaleDays(account) {
  const hist = Array.isArray(account.stageHistory) ? account.stageHistory : [];
  const lastEntry = hist.length ? hist[hist.length - 1] : null;
  const sinceMs = lastEntry
    ? Date.now() - new Date(lastEntry.enteredAt).getTime()
    : (account.createdAt ? Date.now() - new Date(account.createdAt).getTime() : 0);
  return Math.floor(sinceMs / 86400000);
}

// Strip channel/business prefixes from a raw `from` key for readable display.
function stripChannelPrefix(raw) {
  if (!raw) return raw;
  return String(raw)
    .replace(/^whatsapp:/i, "")
    .replace(/^biz-[^:]+:/i, "")
    .replace(/^tg:/i, "")
    .replace(/^telegram:/i, "");
}

// Canonical contact identity: strips channel prefix, collapses to digits for
// phone numbers (≥7 digits), or lowercases for email/other identifiers.
// Two leads with the same key are the same physical contact.
function normalizeContactKey(from) {
  const s = stripChannelPrefix(from || "");
  const digits = s.replace(/\D/g, "");
  return digits.length >= 7 ? digits : s.toLowerCase();
}

// ---- NEEDS YOUR ATTENTION ZONE ----
// Compact banner: surfaces real conditions only (hot leads, overdue follow-ups,
// escalated conversations). Hidden entirely when nothing needs action.
function NeedsAttentionZone({ leads, accounts, onOpenConversation }) {
  const activeleads = (leads || []).filter((l) => {
    const s = l.stage || l.lifecycleStatus || "new";
    return s !== "won" && s !== "lost";
  });
  const hotLeads = activeleads.filter((l) => leadTemperature(l) === "hot");
  const overdueFollowUps = activeleads.filter((l) => followUpDue(l.nextFollowUp));
  const escalated = activeleads.filter(
    (l) => l.summaryStatus === "escalated" && !hotLeads.some((h) => h.from === l.from)
  );

  const parts = [];
  if (hotLeads.length > 0) parts.push(`${hotLeads.length} hot customer${hotLeads.length !== 1 ? "s" : ""} awaiting reply`);
  if (overdueFollowUps.length > 0) parts.push(`${overdueFollowUps.length} follow-up${overdueFollowUps.length !== 1 ? "s" : ""} overdue`);
  if (escalated.length > 0) parts.push(`${escalated.length} escalated`);

  if (parts.length === 0) return null;

  const firstHot = hotLeads[0] || escalated[0] || overdueFollowUps[0];

  return (
    <div style={{ display: "flex", alignItems: "center", gap: 10, background: "#FFF7ED", border: "1px solid #FCD34D", borderRadius: 10, padding: "10px 14px", marginBottom: 16 }}>
      <span style={{ fontSize: 16, flexShrink: 0 }}>🔥</span>
      <div style={{ flex: 1, minWidth: 0 }}>
        <span style={{ fontSize: 13, fontWeight: 700, color: "#92400E" }}>Needs your attention</span>
        <span style={{ fontSize: 13, color: "#92400E" }}> — {parts.join(" · ")}</span>
      </div>
      {firstHot && (
        <button onClick={() => onOpenConversation && onOpenConversation(firstHot.from)}
          style={{ flexShrink: 0, fontSize: 12, fontWeight: 600, color: "#B45309", background: "none", border: "none", cursor: "pointer", textDecoration: "underline", padding: 0 }}>
          Open →
        </button>
      )}
    </div>
  );
}

// YYYY-MM-DD for an <input type="date"> from an ISO/date string (or "").
function dateInputValue(iso) {
  if (!iso) return "";
  const d = new Date(iso);
  if (Number.isNaN(d.getTime())) return "";
  return d.toISOString().slice(0, 10);
}
// True when a follow-up date is today or in the past (needs attention).
function followUpDue(iso) {
  if (!iso) return false;
  const d = new Date(iso);
  if (Number.isNaN(d.getTime())) return false;
  const today = new Date(); today.setHours(0, 0, 0, 0);
  return d.getTime() <= today.getTime();
}

function ldRelative(iso) {
  if (!iso) return "—";
  const diffMs = Date.now() - new Date(iso).getTime();
  if (Number.isNaN(diffMs)) return "—";
  const min = Math.floor(diffMs / 60000);
  if (min < 1) return "just now";
  if (min < 60) return `${min}m ago`;
  const hr = Math.floor(min / 60);
  if (hr < 24) return `${hr}h ago`;
  const day = Math.floor(hr / 24);
  if (day < 30) return `${day}d ago`;
  return new Date(iso).toLocaleDateString();
}

function contactDisplayName(lead) {
  if (lead.customerName) return lead.customerName;
  if (lead.from && lead.from.startsWith("persona:")) return "Test persona";
  if (lead.from) return lead.from.replace(/^whatsapp:/i, "");
  return "Unknown";
}

function contactPhoneDisplay(lead) {
  if (!lead.from) return null;
  if (lead.from.startsWith("persona:") || lead.from.startsWith("tg:")) return null;
  const digits = lead.from.replace(/^whatsapp:/i, "").replace(/\D/g, "");
  if (!digits) return null;
  return `+${digits}`;
}

function contactMarkdown(lead) {
  const name = contactDisplayName(lead);
  const stageRaw = lead.stage || lead.lifecycleStatus || "new";
  const stage = stageRaw.charAt(0).toUpperCase() + stageRaw.slice(1).replace(/_/g, " ");
  const lines = [`**Contact: ${name}**`];
  if (lead.customerCompany) lines.push(`- Company: ${lead.customerCompany}`);
  const ch = lead.channel || (lead.from ? lead.from.split(":")[0] : null);
  if (ch) lines.push(`- Channel: ${ch.charAt(0).toUpperCase() + ch.slice(1)}`);
  if (lead.agentName) lines.push(`- Agent: ${lead.agentName}`);
  lines.push(`- Stage: ${stage}`);
  if (lead.messageCount) lines.push(`- Messages: ${lead.messageCount}`);
  if (lead.capturedAt) lines.push(`- Captured: ${ldRelative(lead.capturedAt)}`);
  const summary = lead.need || lead.summary || lead.lastMessage;
  if (summary) lines.push(`- Summary: ${summary}`);
  if (lead.notes && lead.notes.trim()) lines.push(`- Notes: ${lead.notes.trim()}`);
  lines.push(`- ID: citrus://contact/${lead.from}`);
  return lines.join("\n");
}

// ── Urgency score (0–99) derived from real priority-engine score or temperature
function contactUrgencyScore(lead) {
  if (lead.urgencyScore && lead.urgencyScore > 0) return Math.min(99, lead.urgencyScore);
  const temp = leadTemperature(lead);
  const base = temp === "hot" ? 78 : temp === "warm" ? 52 : 22;
  const ageH = (Date.now() - new Date(lead.lastMessageAt || lead.capturedAt || 0).getTime()) / 3600000;
  const rec  = Math.max(0, Math.round(14 - ageH * 0.5));
  const msgs = Math.min(7, Math.floor((lead.messageCount || 0) / 2));
  return Math.min(99, base + rec + msgs);
}

function contactLastActivityLabel(lead) {
  const log = Array.isArray(lead.contactLog) ? lead.contactLog : [];
  if (log.length) {
    const last = [...log].sort((a, b) => new Date(b.at || 0) - new Date(a.at || 0))[0];
    const t = CONTACT_LOG_TYPE_BY_ID[last.type];
    return t ? t.label : "Touchpoint";
  }
  return "Touchpoint";
}

function ScoreBars({ score }) {
  const color = score >= 75 ? "#DC2626" : score >= 50 ? "#EA580C" : "#64748B";
  const h = (v) => `${Math.round(v * 13)}px`;
  const bars = [
    score > 70 ? 1 : score > 45 ? 0.7 : 0.3,
    score > 55 ? 0.9 : score > 35 ? 0.6 : 0.25,
    score > 40 ? 0.75 : score > 20 ? 0.45 : 0.18,
    0.3,
  ];
  return (
    <div style={{ display: "flex", alignItems: "flex-end", gap: 2, height: 13, flexShrink: 0 }}>
      {bars.map((v, i) => (
        <div key={i} style={{ width: 4, height: h(v), borderRadius: 1, background: color }} />
      ))}
    </div>
  );
}

const relMs = (ms) => {
  if (!ms) return "";
  const s = Math.round(ms / 1000);
  if (s < 60)   return `${s}s ago`;
  if (s < 3600)  return `${Math.round(s / 60)}m ago`;
  if (s < 86400) return `${Math.round(s / 3600)}h ago`;
  return `${Math.round(s / 86400)}d ago`;
};
const relTs = (iso) => iso ? relMs(Date.now() - new Date(iso).getTime()) : "";

const DEAL_STAGE_LABEL = { new: "New", engaged: "Engaged", qualified: "Qualified", quoted: "Quoted", won: "Won", lost: "Lost", contacted: "Engaged" };

// Safely coerce any fact/summary value to a renderable string.
// facts can arrive as {what, quote, category, ts} objects; summaries can be
// raw objects in legacy data. Never let a bare object reach React render.
function safeStr(val) {
  if (!val) return "";
  if (typeof val === "string") return val;
  if (typeof val === "object") return String(val.what || val.text || val.summary || val.content || "");
  return String(val);
}

// ── Party Quick View panel ────────────────────────────────────────────────────
function ContactQuickView({ lead, onClose, onOpenFull, onChatOpen, onChatClose, chatVisible, agents = [], onPatch, onDelete }) {
  const [profile, setProfile] = useState(null);
  const [catchupBusy, setCatchupBusy] = useState(false);
  const [teamMembers, setTeamMembers] = useState([]);
  const [localStage, setLocalStage] = useState(lead.stage || lead.lifecycleStatus || "new");
  const [localAssignee, setLocalAssignee] = useState(lead.assignedTo || null);
  const [menuOpen, setMenuOpen] = useState(false);
  const [deleteBusy, setDeleteBusy] = useState(false);
  const menuRef = React.useRef(null);
  const [promptOverrides, setPromptOverrides] = useState(Array.isArray(lead.promptOverrides) ? lead.promptOverrides : []);
  const [instrOpen, setInstrOpen] = useState(false);
  const [instrText, setInstrText] = useState("");
  const [instrBusy, setInstrBusy] = useState(false);
  const [memModalOpen, setMemModalOpen] = useState(false);

  useEffect(() => {
    setLocalStage(lead.stage || lead.lifecycleStatus || "new");
    setLocalAssignee(lead.assignedTo || null);
    setPromptOverrides(Array.isArray(lead.promptOverrides) ? lead.promptOverrides : []);
    setInstrOpen(false);
    setInstrText("");
    setMemModalOpen(false);
  }, [lead.from]);

  useEffect(() => {
    Promise.allSettled([
      fetch(`${contactsServer()}/team`).then((r) => r.ok ? r.json() : []),
      fetch(`${contactsServer()}/team/routing`).then((r) => r.ok ? r.json() : []),
    ]).then(([membersRes, routingRes]) => {
      const members = membersRes.status === "fulfilled" && Array.isArray(membersRes.value) ? membersRes.value : [];
      const routing = routingRes.status === "fulfilled" && Array.isArray(routingRes.value) ? routingRes.value : [];
      setTeamMembers(mergeRoutersIntoTeam(members, routing));
    }).catch(() => {});
  }, []);

  const handleStageChange = (e) => {
    const stage = e.target.value;
    setLocalStage(stage);
    setLocalAssignee(null);
    onPatch && onPatch({ stage, assignedTo: null });
  };

  const assigneeKey = localAssignee ? `${localAssignee.type === "agent" ? "a" : "m"}:${localAssignee.id}` : "";

  const handleAssigneeChange = (e) => {
    const val = e.target.value;
    if (!val) {
      setLocalAssignee(null);
      onPatch && onPatch({ assignedTo: null });
      return;
    }
    const [pfx, id] = val.split(":");
    if (pfx === "a") {
      const ag = agents.find((a) => a.id === id);
      if (!ag) return;
      const next = { id: ag.id, name: ag.name, type: "agent" };
      setLocalAssignee(next);
      onPatch && onPatch({ assignedTo: next });
    } else {
      const m = teamMembers.find((m) => m.id === id);
      if (!m) return;
      const next = { id: m.id, name: m.name || m.email, type: "member" };
      setLocalAssignee(next);
      onPatch && onPatch({ assignedTo: next });
    }
  };

  useEffect(() => {
    if (!menuOpen) return;
    const handler = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false); };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [menuOpen]);

  const handleDelete = () => {
    if (deleteBusy) return;
    setDeleteBusy(true);
    setMenuOpen(false);
    fetch(`${contactsServer()}/contacts-pipeline/${encodeURIComponent(lead.from)}`, { method: "DELETE" })
      .then((r) => r.ok ? r.json() : r.json().then((j) => Promise.reject(j)))
      .then(() => { window.toast && window.toast("Contact removed", "good"); onDelete ? onDelete(lead.from) : onClose && onClose(); })
      .catch((err) => { window.toast && window.toast(err.error || "Delete failed", "warn"); setDeleteBusy(false); });
  };

  const handleAddInstruction = () => {
    const text = instrText.trim().slice(0, 400);
    if (!text || instrBusy) return;
    setInstrBusy(true);
    fetch(`${contactsServer()}/contacts-pipeline/${encodeURIComponent(lead.from)}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ addInstruction: { text } }),
    })
      .then((r) => r.ok ? r.json() : r.json().then((j) => Promise.reject(j)))
      .then((updated) => {
        setPromptOverrides(Array.isArray(updated.promptOverrides) ? updated.promptOverrides : []);
        setInstrText("");
        setInstrOpen(false);
        window.toast && window.toast("Instruction added", "good");
      })
      .catch((err) => { window.toast && window.toast(err.error || "Failed to add instruction", "warn"); })
      .finally(() => setInstrBusy(false));
  };

  const handleRemoveInstruction = (id) => {
    fetch(`${contactsServer()}/contacts-pipeline/${encodeURIComponent(lead.from)}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ removeInstruction: { id } }),
    })
      .then((r) => r.ok ? r.json() : r.json().then((j) => Promise.reject(j)))
      .then((updated) => {
        setPromptOverrides(Array.isArray(updated.promptOverrides) ? updated.promptOverrides : []);
      })
      .catch((err) => { window.toast && window.toast(err.error || "Failed to remove", "warn"); });
  };

  const triggerCatchup = () => {
    if (catchupBusy) return;
    setCatchupBusy(true);
    fetch(`${contactsServer()}/conversations/${encodeURIComponent(lead.from)}/catchup`, { method: "POST" })
      .then((r) => r.ok ? r.json() : r.json().then((j) => Promise.reject(j)))
      .then(() => { window.toast && window.toast("Agent sent a catch-up reply", "good"); })
      .catch((err) => { window.toast && window.toast(err.error || "Catch-up failed — agent may still be unavailable", "warn"); })
      .finally(() => setCatchupBusy(false));
  };

  useEffect(() => {
    setProfile(null);
    fetch(`${contactsServer()}/customers/${encodeURIComponent(lead.from)}/memory`)
      .then((r) => r.ok ? r.json() : null)
      .then((d) => setProfile(d?.profile || null))
      .catch(() => {});
  }, [lead.from]);

  const isWon    = localStage === "won";
  const isLost   = localStage === "lost";
  const stageObj = DEAL_STAGE_BY_ID[localStage] || DEAL_STAGE_BY_ID["new"];
  const stageCol = isWon ? "#0B7A57" : isLost ? "#B0234A" : stageObj.fg;

  const initial = (contactDisplayName(lead) || "?")[0].toUpperCase();
  const facts   = Array.isArray(profile?.facts) ? profile.facts : [];
  const score   = contactUrgencyScore(lead);
  const temp    = leadTemperature(lead);
  const ts      = temp ? TEMP_STYLE[temp] : null;

  // Merge all activity sources into one timeline, newest-first
  const rawEvents = [];
  if (Array.isArray(lead.stageHistory)) {
    for (const e of lead.stageHistory) {
      rawEvents.push({ at: e.enteredAt, label: `Moved to ${DEAL_STAGE_LABEL[e.stage] || e.stage}`, accent: true });
    }
  }
  if (Array.isArray(lead.contactLog)) {
    for (const e of lead.contactLog) {
      const type = e.type ? e.type.charAt(0).toUpperCase() + e.type.slice(1) : "Contact";
      const note = e.note ? `: ${e.note.slice(0, 60)}` : "";
      const outcome = e.outcome ? ` · ${e.outcome}` : "";
      rawEvents.push({ at: e.at, label: `${type} logged${note}${outcome}` });
    }
  }
  if (lead.assignedTo?.name) {
    rawEvents.push({ at: lead.stageUpdatedAt || lead.capturedAt, label: `Assigned to ${lead.assignedTo.name}` });
  }
  if (lead.lastMessageAt) {
    rawEvents.push({ at: lead.lastMessageAt, label: "Last chat message", accent: true });
  }
  if (lead.capturedAt) {
    rawEvents.push({ at: lead.capturedAt, label: "Contact captured" });
  }
  rawEvents.sort((a, b) => new Date(b.at || 0) - new Date(a.at || 0));
  const timeline = rawEvents.slice(0, 10).map((e) => ({ ...e, time: relTs(e.at) }));

  const industryLabel = lead.categories?.length ? CONTACT_CATEGORY_BY_ID[lead.categories[0]]?.label : (lead.channel || null);
  const sentiment = (temp === "hot" || temp === "warm") ? "Pos" : temp === "cold" ? "Neg" : "Neu";
  const sentimentCls = temp === "hot" || temp === "warm" ? "cqv-mv-pos" : temp === "cold" ? "cqv-mv-neg" : "cqv-mv-muted";

  return (
    <div className="contact-qv">
      <div className="cqv-sticky">
      {/* Identity block — centered dossier layout */}
      <div className="cqv-identity-block">
        {(onChatOpen || onChatClose) && (
          <button
            className={`cqv-chat-btn${chatVisible ? " active" : ""}`}
            onClick={chatVisible ? onChatClose : onChatOpen}
            title={chatVisible ? "Close conversation" : "Open conversation"}
          >
            {chatVisible ? (
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                <path d="M19 12H5M12 5l-7 7 7 7"/>
              </svg>
            ) : (
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
              </svg>
            )}
          </button>
        )}
        <div ref={menuRef} style={{ position: "absolute", top: 10, right: 36, zIndex: 20 }}>
          <button
            className="cqv-chat-btn"
            style={{ position: "static", padding: "4px 6px", fontSize: 16, lineHeight: 1 }}
            title="More options"
            onClick={() => setMenuOpen((v) => !v)}
          >⋯</button>
          {menuOpen && (
            <div style={{
              position: "absolute", top: "calc(100% + 4px)", right: 0,
              background: "var(--paper)", border: "1px solid var(--border)",
              borderRadius: 8, boxShadow: "0 6px 20px rgba(0,0,0,.12)",
              minWidth: 160, zIndex: 100, overflow: "hidden",
            }}>
              <button
                style={{
                  display: "block", width: "100%", padding: "9px 14px",
                  background: "none", border: "none", cursor: deleteBusy ? "not-allowed" : "pointer",
                  fontSize: 13, color: "#d0302a", textAlign: "left",
                }}
                onMouseEnter={(e) => e.currentTarget.style.background = "rgba(208,48,42,.07)"}
                onMouseLeave={(e) => e.currentTarget.style.background = "none"}
                disabled={deleteBusy}
                onClick={handleDelete}
              >
                {deleteBusy ? "Deleting…" : "Delete Contact"}
              </button>
            </div>
          )}
        </div>
        <button className="contact-qv-close cqv-close-abs" onClick={onClose}>✕</button>
        <div className="cqv-sq-icon">{initial}</div>
        <div className="cqv-company-name">{lead.customerCompany || contactDisplayName(lead)}</div>
        {(() => {
          const phone = contactPhoneDisplay(lead);
          if (!phone) return null;
          return (
            <button
              className="cqv-phone-number"
              onClick={() => { navigator.clipboard.writeText(phone).catch(() => {}); window.toast && window.toast("Number copied", "good"); }}
              title="Click to copy"
            >
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: 5, flexShrink: 0 }}>
                <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 1.18h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.77a16 16 0 0 0 6.29 6.29l.95-.95a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/>
              </svg>
              {phone}
            </button>
          );
        })()}
        {industryLabel && <div className="cqv-company-sub">{industryLabel}</div>}
        <div className="cqv-tag-row">
          {ts && (
            <span className="cqv-stage-badge" style={{ color: ts.fg, borderColor: ts.bd, background: ts.bg }}>
              {temp === "hot" ? "HOT LEAD" : temp === "warm" ? "WARM" : "COLD"}
            </span>
          )}
        </div>
      </div>

      {/* Properties — Status + Assignee */}
      <div className="cqv-section" style={{ paddingTop: 10, paddingBottom: 10 }}>
        <div className="cqv-prop-row">
          <span className="cqv-prop-label">STATUS</span>
          <div className="cqv-prop-sel-wrap">
            <select
              className="cqv-prop-select"
              value={localStage}
              onChange={handleStageChange}
              style={{ color: stageObj.fg, background: stageObj.bg, borderColor: stageObj.bd }}
            >
              {DEAL_STAGES.map((s) => (
                <option key={s.id} value={s.id}>{s.label}</option>
              ))}
            </select>
          </div>
        </div>
        <div className="cqv-prop-row" style={{ borderBottom: "none" }}>
          <span className="cqv-prop-label">ASSIGNEE</span>
          <div className="cqv-prop-sel-wrap">
            <select
              className="cqv-prop-select"
              value={assigneeKey}
              onChange={handleAssigneeChange}
            >
              <option value="">— Unassigned —</option>
              {agents.length > 0 && (
                <optgroup label="Agents">
                  {agents.filter((a) => a.status !== "archived").map((a) => (
                    <option key={a.id} value={`a:${a.id}`}>{a.name}</option>
                  ))}
                </optgroup>
              )}
              {teamMembers.length > 0 && (
                <optgroup label="Team">
                  {teamMembers.map((m) => (
                    <option key={m.id} value={`m:${m.id}`}>{m.name || m.email}</option>
                  ))}
                </optgroup>
              )}
            </select>
          </div>
        </div>
      </div>
      </div>{/* /cqv-sticky */}

      <div className="cqv-body">
      {/* Core Metrics */}
      <div className="cqv-section">
        <div className="cqv-sec-head"><span className="cqv-sec-title">CORE METRICS</span></div>
        <div className="cqv-metrics-grid">
          <div className="cqv-metric-card">
            <div className="cqv-metric-label">PRIORITY</div>
            <div className={`cqv-metric-value cqv-mv-accent`}>{score}%</div>
          </div>
          <div className="cqv-metric-card">
            <div className="cqv-metric-label">SENTIMENT</div>
            <div className={`cqv-metric-value ${sentimentCls}`}>{sentiment}</div>
          </div>
        </div>
      </div>

      {/* AI Instructions */}
      <div className="cqv-section">
        <div className="cqv-sec-head">
          <span className="cqv-sec-title">AGENT INSTRUCTIONS</span>
          {lead.agentName && <span style={{ fontSize: 9, color: "var(--ink-soft)", fontFamily: "var(--mono-font)", letterSpacing: "0.05em", textTransform: "uppercase", marginLeft: "auto" }}>{lead.agentName}</span>}
        </div>
        {promptOverrides.length === 0 ? (
          <div className="cqv-instr-empty">
            {!instrOpen && (
              <>
                <p>{lead.agentName || "The agent"} is following the default brief. Add instructions to tailor behavior for this lead.</p>
                <button className="cqv-instr-add" onClick={() => setInstrOpen(true)}>+ Add an instruction</button>
              </>
            )}
          </div>
        ) : (
          <>
            {promptOverrides.map((ov) => (
              <div key={ov.id} className="cqv-instr-entry">
                <div className="cqv-instr-meta">
                  <span className="cqv-instr-dot">●</span>
                  <span>Added by {ov.author}</span>
                  <span className="cqv-instr-sep">·</span>
                  <span>{relTs(ov.addedAt)}</span>
                  <button className="cqv-instr-remove" title="Remove" onClick={() => handleRemoveInstruction(ov.id)}>×</button>
                </div>
                <div className="cqv-instr-text">"{ov.text}"</div>
              </div>
            ))}
            {!instrOpen && (
              <button className="cqv-instr-add" onClick={() => setInstrOpen(true)}>+ Add another instruction</button>
            )}
          </>
        )}
        {instrOpen && (
          <div style={{ marginTop: 8 }}>
            <textarea
              className="cqv-instr-textarea"
              placeholder="Specific instruction for this lead…"
              maxLength={400}
              value={instrText}
              onChange={(e) => setInstrText(e.target.value)}
              rows={3}
            />
            <div className="cqv-instr-char">{instrText.length}/400</div>
            <div className="cqv-instr-actions">
              <button className="cqv-instr-submit" disabled={instrBusy || !instrText.trim()} onClick={handleAddInstruction}>
                {instrBusy ? "Saving…" : "Save"}
              </button>
              <button className="cqv-instr-cancel" onClick={() => { setInstrOpen(false); setInstrText(""); }}>Cancel</button>
            </div>
          </div>
        )}
      </div>

      {/* Memory Facts */}
      {facts.length > 0 && (
        <div className="cqv-section">
          <div className="cqv-sec-head">
            <span className="cqv-sec-title">MEMORY FACTS</span>
            <button
              className="btn btn-ghost btn-sm"
              style={{ marginLeft: "auto", fontSize: 11, padding: "2px 8px" }}
              onClick={() => setMemModalOpen(true)}
            >See all →</button>
          </div>
          {facts.slice(0, 6).map((f, i) => (
            <div key={i} className="cqv-mem-bullet">
              <span className="cqv-bullet-dot">✦</span>
              <span>{safeStr(f)}</span>
            </div>
          ))}
        </div>
      )}

      {/* Memory modal */}
      {memModalOpen && (
        <div
          style={{ position: "fixed", inset: 0, zIndex: 9999, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,0.4)" }}
          onClick={(e) => { if (e.target === e.currentTarget) setMemModalOpen(false); }}
        >
          <div style={{ background: "var(--paper)", borderRadius: 14, width: 420, maxWidth: "90vw", maxHeight: "80vh", display: "flex", flexDirection: "column", boxShadow: "0 12px 40px rgba(0,0,0,0.22)" }}>
            <div style={{ display: "flex", alignItems: "center", padding: "16px 20px 12px", borderBottom: "1px solid var(--border)" }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontFamily: "var(--display-font)", fontWeight: 600, fontSize: 15 }}>{contactDisplayName(lead)}</div>
                <div style={{ fontSize: 12, color: "var(--ink-soft)", marginTop: 2 }}>{facts.length} memory fact{facts.length !== 1 ? "s" : ""}</div>
              </div>
              <button onClick={() => setMemModalOpen(false)} style={{ background: "none", border: "none", cursor: "pointer", fontSize: 18, color: "var(--ink-soft)", padding: "4px 8px", lineHeight: 1 }}>✕</button>
            </div>
            <div style={{ overflowY: "auto", padding: "12px 20px 20px", flex: 1 }}>
              {facts.map((f, i) => (
                <div key={i} style={{ display: "flex", gap: 8, padding: "8px 0", borderBottom: i < facts.length - 1 ? "1px solid var(--border-soft, var(--border))" : "none", fontSize: 13, lineHeight: 1.5 }}>
                  <span style={{ color: "var(--accent)", fontSize: 10, marginTop: 4, flexShrink: 0 }}>✦</span>
                  <span style={{ color: "var(--ink)" }}>{safeStr(f)}</span>
                </div>
              ))}
            </div>
          </div>
        </div>
      )}

      {/* Recent Activity */}
      <div className="cqv-section">
        <div className="cqv-sec-head"><span className="cqv-sec-title">RECENT ACTIVITY</span></div>
        {timeline.length === 0 ? (
          <div className="cqv-empty-note">No activity logged yet.</div>
        ) : timeline.map((ev, i) => (
          <div key={i} className="cqv-tl-item">
            <div className={`cqv-tl-dot${ev.accent ? " accent" : ""}`} />
            <div>
              <div className="cqv-tl-label">{ev.label}</div>
              {ev.time && <div className="cqv-tl-sub">{ev.time}</div>}
            </div>
          </div>
        ))}
      </div>

      <div style={{ flex: 1, minHeight: 16 }} />
      {lead.lastAgentMsgIsFallback && (
        <button
          className="cqv-footer-btn"
          style={{ background: "color-mix(in oklab, var(--accent) 10%, transparent)", borderColor: "var(--accent)", color: "var(--accent)" }}
          disabled={catchupBusy}
          onClick={triggerCatchup}
        >
          {catchupBusy ? "Sending catch-up…" : "↩ Retry with agent"}
        </button>
      )}
      <button className="cqv-share-btn" onClick={() => { navigator.clipboard.writeText(lead.from).catch(() => {}); window.toast && window.toast("Contact ID copied", "good"); }}>
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
        Share Dossier
      </button>
      </div>{/* /cqv-body */}
    </div>
  );
}

// ── Party Activity View — full two-column chat + dossier ─────────────────────
function ContactActivityView({ lead, threads, agents, onClose, onOpenFull, chatVisible }) {
  const baseThread = (threads || []).find(t => t.id === lead.from || t.customer === lead.from);
  const [localTranscript, setLocalTranscript] = React.useState(() =>
    Array.isArray(baseThread?.transcript) ? baseThread.transcript : []
  );

  React.useEffect(() => {
    const t = (threads || []).find(t => t.id === lead.from || t.customer === lead.from);
    if (t?.transcript) setLocalTranscript(t.transcript);
  }, [threads, lead.from]);

  const agent = (agents || []).find(a => a.id === (baseThread?.agent || lead.agentId));
  const customerName = lead.customerCompany || contactDisplayName(lead);

  const fmtNow = () => {
    const d = new Date();
    const h = d.getHours() % 12 || 12;
    return `${h}:${String(d.getMinutes()).padStart(2, "0")} ${d.getHours() < 12 ? "AM" : "PM"}`;
  };

  return (
    <div className={`cc-chat${chatVisible ? " open" : ""}`}>
      <ConversationPane
        transcript={localTranscript}
        agent={agent}
        customerName={customerName}
        conversationId={lead.from}
        serverUrl={contactsServer}
        headerRight={onClose && <button className="ix-pane-icon" title="Close" onClick={onClose}>×</button>}
        onSendNote={async (text) => {
          setLocalTranscript(prev => [...prev, { from: "internal", text, t: fmtNow(), isNote: true }]);
          try {
            await fetch(`${contactsServer()}/conversations/${encodeURIComponent(lead.from)}/note`, {
              method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text }),
            });
          } catch {}
        }}
        onSendManual={async (text) => {
          setLocalTranscript(prev => [...prev, { from: "you", text, t: fmtNow() }]);
          try {
            const r = await fetch(`${contactsServer()}/conversations/${encodeURIComponent(lead.from)}/reply`, {
              method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ message: text }),
            });
            if (!r.ok) {
              const err = await r.json().catch(() => ({}));
              window.toast && window.toast(err.error || "Send failed", "warn");
            }
          } catch {
            window.toast && window.toast("Send failed", "warn");
          }
        }}
        onSendSuggestion={async (text) => {
          try {
            const r = await fetch(`${contactsServer()}/conversations/${encodeURIComponent(lead.from)}/suggest-to-agent`, {
              method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ suggestion: text }),
            });
            if (!r.ok) window.toast && window.toast("Couldn't send suggestion", "warn");
          } catch {
            window.toast && window.toast("Couldn't send suggestion", "warn");
          }
        }}
        onApproveResult={(approvalId, msgIdx, success) => {
          setLocalTranscript(prev => prev.map((m, i) =>
            i === msgIdx ? { ...m, _draft: !success, _pendingApprovalId: success ? null : approvalId } : m
          ));
        }}
      />
    </div>
  );
}

// ── Lead Record Page ──────────────────────────────────────────────────────────
function ContactRecordPage({ lead, threads = [], agents = [], onBack, onSwitchTab, onPatch }) {
  const [note, setNote] = useState("");
  const [noteSent, setNoteSent] = useState(false);
  const [memProfile, setMemProfile] = useState(null);
  const [teamMembers, setTeamMembers] = useState([]);
  const [localStage, setLocalStage] = useState(lead.stage || lead.lifecycleStatus || "new");
  const [localAssignee, setLocalAssignee] = useState(lead.assignedTo || null);
  const byAgentId = Object.fromEntries((agents || []).map((a) => [a.id, a]));

  useEffect(() => {
    fetch(`${contactsServer()}/customers/${encodeURIComponent(lead.from)}/memory`)
      .then((r) => r.ok ? r.json() : null)
      .then((d) => setMemProfile(d?.profile || null))
      .catch(() => {});
    Promise.allSettled([
      fetch(`${contactsServer()}/team`).then((r) => r.ok ? r.json() : []),
      fetch(`${contactsServer()}/team/routing`).then((r) => r.ok ? r.json() : []),
    ]).then(([membersRes, routingRes]) => {
      const members = membersRes.status === "fulfilled" && Array.isArray(membersRes.value) ? membersRes.value : [];
      const routing = routingRes.status === "fulfilled" && Array.isArray(routingRes.value) ? routingRes.value : [];
      setTeamMembers(mergeRoutersIntoTeam(members, routing));
    }).catch(() => {});
  }, [lead.from]);

  useEffect(() => {
    setLocalStage(lead.stage || lead.lifecycleStatus || "new");
    setLocalAssignee(lead.assignedTo || null);
  }, [lead.from, lead.stage, lead.lifecycleStatus, lead.assignedTo]);

  const handleStageChange = (e) => {
    const stage = e.target.value;
    setLocalStage(stage);
    setLocalAssignee(null);
    onPatch && onPatch({ stage, assignedTo: null });
  };

  const lrAssigneeKey = localAssignee ? `${localAssignee.type === "agent" ? "a" : "m"}:${localAssignee.id}` : "";

  const handleAssigneeChange = (e) => {
    const val = e.target.value;
    if (!val) {
      setLocalAssignee(null);
      onPatch && onPatch({ assignedTo: null });
      return;
    }
    const [pfx, id] = val.split(":");
    if (pfx === "a") {
      const ag = agents.find((a) => a.id === id);
      if (!ag) return;
      const next = { id: ag.id, name: ag.name, type: "agent" };
      setLocalAssignee(next);
      onPatch && onPatch({ assignedTo: next });
    } else {
      const m = teamMembers.find((m) => m.id === id);
      if (!m) return;
      const next = { id: m.id, name: m.name || m.email, type: "member" };
      setLocalAssignee(next);
      onPatch && onPatch({ assignedTo: next });
    }
  };

  // Broadcast entity context for Ask Citrus
  useEffect(() => {
    window.dispatchEvent(new CustomEvent("citrus:entity", {
      detail: { type: "contact", id: lead.from, label: contactDisplayName(lead), tab: "Contacts" },
    }));
    return () => window.dispatchEvent(new CustomEvent("citrus:entity", { detail: null }));
  }, [lead.from]); // eslint-disable-line react-hooks/exhaustive-deps

  // Find matching inbox thread for conversation transcript
  const thread = threads.find((t) => t.id === lead.from || t.customer === lead.from);
  const openAgent = thread ? byAgentId[thread.agent] : null;

  const stageObj = DEAL_STAGE_BY_ID[localStage] || DEAL_STAGE_BY_ID["new"];
  const isWon = localStage === "won";
  const isLost = localStage === "lost";
  const stageLabel = isWon ? "Won" : isLost ? `Lost${lead.lostReason ? " · " + lead.lostReason.replace(/_/g, " ") : ""}` : stageObj.label;

  const facts = Array.isArray(memProfile?.facts) ? memProfile.facts : [];
  const initial = (contactDisplayName(lead) || "?")[0].toUpperCase();

  // Build a system log from what we know
  const syslog = [];
  if (lead.capturedAt) syslog.push({ icon: "◆", label: `Lead captured via ${lead.channel || "conversation"}`, time: ldRelative(lead.capturedAt) });
  const log = Array.isArray(lead.contactLog) ? lead.contactLog : [];
  for (const entry of log.slice().reverse()) {
    const typeInfo = CONTACT_LOG_TYPES ? CONTACT_LOG_TYPES.find((t) => t.id === entry.type) : null;
    const label = typeInfo ? `${typeInfo.icon} ${typeInfo.label}${entry.note ? ` — ${entry.note}` : ""}` : (entry.note || entry.type);
    syslog.push({ icon: typeInfo?.icon || "·", label, time: entry.at ? ldRelative(entry.at) : "" });
  }
  if (lead.stage && lead.stage !== "new") syslog.push({ icon: "→", label: `Stage moved to ${stageLabel}`, time: "" });

  const copyToClipboard = (text) => { try { navigator.clipboard.writeText(text); window.toast && window.toast("Copied", "info"); } catch {} };

  const displayName = contactDisplayName(lead);

  return (
    <div className="ld-record-page">
      {/* ── Main column ── */}
      <div className="ld-record-main">
        {/* Breadcrumb + actions */}
        <div className="ld-record-crumb">
          <button className="ld-record-crumb-back" onClick={onBack}>
            <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><path d="M8 2L4 6l4 4"/></svg>
            Contacts
          </button>
          <span className="ld-record-crumb-sep">/</span>
          <span className="ld-record-crumb-name">{displayName}{lead.customerCompany ? ` (${lead.customerCompany})` : ""}</span>
          <div className="ld-record-crumb-actions">
            <button className="ld-record-crumb-btn" onClick={() => copyToClipboard(`${window.location.origin}${window.location.pathname}?tab=contacts&id=${encodeURIComponent(lead.from)}`)}>
              <svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M5 2H2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V8M7 1h4v4M10 1l-5 5"/></svg>
              Copy URL
            </button>
            <button className="ld-record-crumb-btn" onClick={() => copyToClipboard(`citrus://contact/${lead.from}`)}>
              Copy ID
            </button>
            <button className="ld-record-crumb-btn" onClick={() => copyToClipboard(`${displayName}${lead.customerCompany ? " · " + lead.customerCompany : ""}${lead.from ? "\n" + lead.from : ""}`)}>
              Copy Contact
            </button>
          </div>
        </div>

        {/* Agent Insights */}
        {(lead.need || lead.summary || lead.messageCount) && (
          <div className="ld-record-insights">
            <div className="ld-record-insights-title">
              <svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><circle cx="6" cy="6" r="5"/><path d="M6 4v3M6 8.5v.5"/></svg>
              Agent Insights
            </div>
            <div className="ld-record-insights-grid">
              <div>
                <div className="ld-record-insights-label">Channel</div>
                <div className="ld-record-insights-val">{lead.channel || "—"}</div>
              </div>
              <div>
                <div className="ld-record-insights-label">Messages</div>
                <div className="ld-record-insights-val">{lead.messageCount || 0}</div>
              </div>
              <div>
                <div className="ld-record-insights-label">Captured</div>
                <div className="ld-record-insights-val">{lead.capturedAt ? ldRelative(lead.capturedAt) : "—"}</div>
              </div>
              {lead.need && (
                <div style={{ gridColumn: "1 / -1" }}>
                  <div className="ld-record-insights-label">Pain point</div>
                  <div className="ld-record-insights-val">{lead.need}</div>
                </div>
              )}
              {lead.summary && (
                <div style={{ gridColumn: "1 / -1" }}>
                  <div className="ld-record-insights-label">Summary</div>
                  <div className="ld-record-insights-val">{lead.summary}</div>
                </div>
              )}
            </div>
          </div>
        )}

        {/* Conversation transcript */}
        {thread && (
          <>
            <div className="ld-record-thread-title">Conversation</div>
            <div style={{ padding: "0 20px 4px", display: "flex", flexDirection: "column", gap: 10 }}>
              {(thread.transcript || []).map((m, i) => {
                const isCustomer = m.from === "customer";
                const isYou = m.from === "you";
                const ag = openAgent || { name: lead.agentName || "Agent", palette: { skin: "var(--accent)" } };
                const label = isCustomer ? (thread.customerName || thread.customer) : (isYou ? "You" : ag.name);
                const bg = isCustomer ? null : (isYou ? "var(--ink)" : ag.palette?.skin || "var(--accent)");
                return (
                  <div key={i} className={`ix-msg ix-msg-${isCustomer ? "in" : "out"}`}>
                    <div style={{ fontSize: 11, fontWeight: 600, color: "var(--ink-soft)", marginBottom: 2, padding: "0 6px" }}>{label}</div>
                    <div className="ix-msg-bubble" style={!isCustomer ? { background: bg, color: "#fff" } : null} dir="auto">
                      {m.text}
                    </div>
                    <div className="ix-msg-time">{m.t}</div>
                  </div>
                );
              })}
            </div>
          </>
        )}

        {/* System log */}
        {syslog.length > 0 && (
          <div className="ld-record-syslog">
            <div className="ld-record-syslog-title">
              <svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"><circle cx="6" cy="6" r="5"/><path d="M6 4v2l1.5 1.5"/></svg>
              System Log
            </div>
            {syslog.map((e, i) => (
              <div key={i} className="ld-record-syslog-entry">
                <div className="ld-record-syslog-icon">{e.icon}</div>
                <div>
                  <div className="ld-record-syslog-text">{e.label}</div>
                  {e.time && <div className="ld-record-syslog-time">{e.time}</div>}
                </div>
              </div>
            ))}
          </div>
        )}

        {/* Team note composer */}
        <div style={{ flex: 1 }} />
        <div className="ld-record-note">
          <textarea
            className="ld-record-note-input"
            placeholder="Leave a team note…"
            rows={2}
            value={note}
            onChange={(e) => setNote(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (note.trim()) { setNoteSent(true); setNote(""); setTimeout(() => setNoteSent(false), 2000); } } }}
          />
          <button
            className="ld-record-note-btn"
            onClick={() => { if (note.trim()) { setNoteSent(true); setNote(""); setTimeout(() => setNoteSent(false), 2000); } }}
          >
            {noteSent ? "Saved ✓" : "Post"}
          </button>
        </div>
      </div>

      {/* ── Properties sidebar ── */}
      <div className="ld-record-side">
        <div className="ld-record-side-actions">
          {[
            { icon: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M5 2H2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V8M7 1h4v4M10 1l-5 5"/></svg>, title: "Copy URL", action: () => copyToClipboard(`${window.location.origin}${window.location.pathname}?tab=contacts&id=${encodeURIComponent(lead.from)}`) },
            { icon: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6"><rect x="1" y="3" width="7" height="8" rx="1"/><path d="M4 1h6a1 1 0 0 1 1 1v8"/></svg>, title: "Copy ID", action: () => copyToClipboard(`citrus://contact/${lead.from}`) },
            { icon: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6"><circle cx="6" cy="4" r="2.5"/><path d="M1 10c0-2.5 2-4 5-4s5 1.5 5 4"/></svg>, title: "Open conversation", action: () => { onBack(); setTimeout(() => onSwitchTab && onSwitchTab("inbox", { threadId: lead.from }), 50); } },
          ].map((btn, i) => (
            <button key={i} className="ld-record-side-action" title={btn.title} onClick={btn.action}>{btn.icon}</button>
          ))}
        </div>

        {/* Properties */}
        <div className="ld-record-prop-group">
          <div className="ld-record-prop-group-title">Properties</div>
          <div className="ld-record-prop-row">
            <span className="ld-record-prop-label">Status</span>
            <span className="ld-record-prop-val">
              <select
                className="ld-prop-select"
                value={localStage}
                onChange={handleStageChange}
                style={{ color: stageObj.fg, background: stageObj.bg, borderColor: stageObj.bd }}
              >
                {DEAL_STAGES.map((s) => (
                  <option key={s.id} value={s.id}>{s.label}</option>
                ))}
              </select>
            </span>
          </div>
          <div className="ld-record-prop-row">
            <span className="ld-record-prop-label">Assignee</span>
            <span className="ld-record-prop-val">
              <select
                className="ld-prop-select"
                value={lrAssigneeKey}
                onChange={handleAssigneeChange}
              >
                <option value="">— Unassigned —</option>
                {agents.filter((a) => a.status !== "archived").length > 0 && (
                  <optgroup label="Agents">
                    {agents.filter((a) => a.status !== "archived").map((a) => (
                      <option key={a.id} value={`a:${a.id}`}>{a.name}</option>
                    ))}
                  </optgroup>
                )}
                {teamMembers.length > 0 && (
                  <optgroup label="Team">
                    {teamMembers.map((m) => (
                      <option key={m.id} value={`m:${m.id}`}>{m.name || m.email}</option>
                    ))}
                  </optgroup>
                )}
              </select>
            </span>
          </div>
          {lead.agentName && (
            <div className="ld-record-prop-row">
              <span className="ld-record-prop-label">Agent</span>
              <span className="ld-record-prop-val">{lead.agentName}</span>
            </div>
          )}
          {lead.urgencyScore > 0 && (
            <div className="ld-record-prop-row">
              <span className="ld-record-prop-label">Priority</span>
              <span className="ld-record-prop-val" style={{ color: lead.urgencyScore >= 60 ? "var(--accent)" : "var(--ink)" }}>
                {lead.urgencyScore >= 60 ? "High" : lead.urgencyScore >= 30 ? "Medium" : "Low"} ({lead.urgencyScore})
              </span>
            </div>
          )}
          {lead.customerCompany && (() => {
            const canon = lead.customerCompanyCanonical || lead.customerCompany;
            const co = companies.find((c) => c.name === canon);
            const peerCount = co ? co.count - 1 : 0;
            return (
              <div className="ld-record-prop-row">
                <span className="ld-record-prop-label">Company</span>
                <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
                  <span className="ld-record-prop-val">{lead.customerCompany}</span>
                  {peerCount > 0 && (
                    <button className="ld-company-peer-btn"
                      onClick={() => { setCompanyFilter(canon); setQuickViewLead(null); }}
                      title={`Show all ${co.count} contacts from ${lead.customerCompany}`}>
                      +{peerCount} contact{peerCount !== 1 ? "s" : ""}
                    </button>
                  )}
                </div>
              </div>
            );
          })()}
          {Array.isArray(lead.categories) && lead.categories.length > 0 && (
            <div className="ld-record-prop-row" style={{ alignItems: "flex-start" }}>
              <span className="ld-record-prop-label">Tags</span>
              <div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
                {lead.categories.map((k) => {
                  const c = CONTACT_CATEGORY_BY_ID[k];
                  if (!c) return null;
                  return <span key={k} style={{ fontSize: 10.5, fontWeight: 600, padding: "2px 7px", borderRadius: 999, color: c.fg, background: c.bg, border: `1px solid ${c.bd}` }}>{c.label}</span>;
                })}
              </div>
            </div>
          )}
        </div>

        {/* Memory facts */}
        {facts.length > 0 && (
          <div className="ld-record-prop-group">
            <div className="ld-record-prop-group-title">Memory Facts</div>
            {facts.slice(0, 8).map((f, i) => (
              <div key={i} className="ld-record-fact-item">
                {safeStr(f)}
              </div>
            ))}
          </div>
        )}

        {/* Contact info */}
        <div className="ld-record-prop-group" style={{ marginTop: "auto", paddingTop: 12, borderTop: "1px solid var(--border)" }}>
          <div style={{ fontSize: 12, color: "var(--ink-soft)", marginBottom: 8 }}>{lead.from}</div>
          <button
            className="btn btn-ghost btn-sm"
            style={{ width: "100%", justifyContent: "center", fontSize: 12 }}
            onClick={() => { onBack(); setTimeout(() => onSwitchTab && onSwitchTab("inbox", { threadId: lead.from }), 50); }}
          >
            Open conversation →
          </button>
        </div>
      </div>
    </div>
  );
}

// lockedAgentName: when set (agent-detail embed), pre-filters by agent name
// and hides the OWNER filter chip — the rest of the module works normally.
function ContactsTab({ onSwitchTab, threads = [], agents = [], lockedAgentName = null, tabBarSlot = null }) {
  const [leads, setLeads]         = useState(null);
  const [openRecord, setOpenRecord] = useState(null);     // full-page record
  const [quickViewLead, setQuickViewLead] = useState(null); // lens pane
  const [chatVisible, setChatVisible] = useState(false);   // chat panel open
  const [filter, setFilter]       = useState("all");      // stage filter
  const [ownerFilter, setOwnerFilter] = useState(lockedAgentName || "all");  // agent/owner filter
  const [urgencyFilter, setUrgencyFilter] = useState("all"); // high/medium/low
  const [savingId, setSavingId]   = useState(null);
  const [noteDraft, setNoteDraft] = useState({});
  const [logForm, setLogForm] = useState({});
  const [lostModal, setLostModal] = useState(null);
  const [lostReasonDraft, setLostReasonDraft] = useState("");
  const [accountLostModal, setAccountLostModal] = useState(null);
  const [accountLostReasonDraft, setAccountLostReasonDraft] = useState("");
  const [wonConfirmFrom, setWonConfirmFrom] = useState(null);
  const [accounts, setAccounts]   = useState([]);
  const [contacts, setContacts]   = useState(null);
  const [leadsView, setLeadsView] = useState("pipeline"); // "pipeline" | "customers"
  const [moreMenuFor, setMoreMenuFor] = useState(null);
  const [stagePickerFor, setStagePickerFor] = useState(null);
  const [sort, setSort] = useState("urgency");
  const [companyFilter, setCompanyFilter] = useState("all");
  const [companies, setCompanies] = useState([]);

  const [pendingLens, setPendingLens] = useState(null); // { from, withChat, returnTab }
  const returnTabRef = React.useRef(null);

  // On mount: check for a deferred party-lens open (from Activity page)
  useEffect(() => {
    try {
      const stored = sessionStorage.getItem("citrus_pending_contact_lens");
      if (stored) {
        sessionStorage.removeItem("citrus_pending_contact_lens");
        setPendingLens(JSON.parse(stored));
      }
    } catch {}
  }, []);

  // Live event: fired when Leads is already mounted
  useEffect(() => {
    const handler = (e) => {
      const { from, withChat, returnTab } = e.detail || {};
      if (!from) return;
      setPendingLens({ from, withChat: !!withChat, returnTab: returnTab || null });
    };
    window.addEventListener("citrus-open-contact-lens", handler);
    return () => window.removeEventListener("citrus-open-contact-lens", handler);
  }, []);

  // Apply pendingLens once leads are loaded
  useEffect(() => {
    if (!pendingLens || !leads) return;
    const lead = leads.find((l) => l.from === pendingLens.from);
    if (lead) {
      returnTabRef.current = pendingLens.returnTab || null;
      setQuickViewLead(lead);
      setChatVisible(!!pendingLens.withChat);
    }
    setPendingLens(null);
  }, [pendingLens, leads]);

  // Broadcast quick-view party to Ask Citrus for page context.
  useEffect(() => {
    if (!quickViewLead) {
      window.dispatchEvent(new CustomEvent("citrus:entity", { detail: null }));
      return;
    }
    window.dispatchEvent(new CustomEvent("citrus:entity", {
      detail: { type: "contact", id: quickViewLead.from, label: contactDisplayName(quickViewLead), tab: "Contacts" },
    }));
  }, [quickViewLead]); // eslint-disable-line react-hooks/exhaustive-deps

  // Expose global trigger so inbox, agents page, etc. can open the party lens.
  useEffect(() => {
    window.openContactLens = (lead) => { setChatVisible(false); setQuickViewLead(lead); };
    return () => { if (window.openContactLens) delete window.openContactLens; };
  }, []);

  const openChat = () => setChatVisible(true);
  const closeChat = () => setChatVisible(false);

  const selectLead = (lead) => {
    setChatVisible(false);
    setQuickViewLead(lead);
  };

  const load = () => {
    fetch(`${contactsServer()}/contacts-pipeline`)
      .then((r) => (r.ok ? r.json() : []))
      .then((rows) => setLeads(Array.isArray(rows) ? rows : []))
      .catch(() => setLeads([]));
  };

  useEffect(() => {
    load();
    fetch(`${contactsServer()}/accounts`)
      .then((r) => (r.ok ? r.json() : []))
      .then((rows) => setAccounts(Array.isArray(rows) ? rows : []))
      .catch(() => {});
    fetch(`${contactsServer()}/contacts`)
      .then((r) => (r.ok ? r.json() : []))
      .then((rows) => setContacts(Array.isArray(rows) ? rows : []))
      .catch(() => setContacts([]));
    fetch(`${contactsServer()}/contacts/companies`)
      .then((r) => (r.ok ? r.json() : []))
      .then((rows) => setCompanies(Array.isArray(rows) ? rows : []))
      .catch(() => {});
  }, []);

  const patchAccount = (id, patch) => {
    fetch(`${contactsServer()}/accounts/${encodeURIComponent(id)}`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(patch),
    })
      .then((r) => (r.ok ? r.json() : Promise.reject(r)))
      .then((updated) => setAccounts((prev) => prev.map((a) => a.id === id ? updated : a)))
      .catch(async (r) => {
        const body = r instanceof Response ? await r.json().catch(() => ({})) : {};
        window.toast && window.toast(body.error || "Couldn't update account", "warn");
      });
  };

  // Live updates: when ANY tab (or the server itself) emits a lead_updated,
  // merge into our local state. The app-level SSE listener re-dispatches the
  // event on window so we don't need our own EventSource here.
  useEffect(() => {
    const handler = (e) => {
      const payload = (e && e.detail) || null;
      if (!payload || !payload.from) return;
      setLeads((prev) => {
        if (!prev) return prev;
        const idx = prev.findIndex((l) => l.from === payload.from);
        if (idx === -1) return prev;
        const next = prev.slice();
        next[idx] = {
          ...next[idx],
          stage:      payload.stage      ?? next[idx].stage,
          lostReason: payload.lostReason ?? next[idx].lostReason,
          notes:      payload.notes      ?? next[idx].notes,
          categories: payload.categories ?? next[idx].categories,
          assignedTo: "assignedTo" in payload ? payload.assignedTo : next[idx].assignedTo,
        };
        return next;
      });
      setNoteDraft((d) => {
        if (!(payload.from in d)) return d;
        if (d[payload.from] === payload.notes) return d;
        return { ...d, [payload.from]: payload.notes ?? "" };
      });
    };
    window.addEventListener("citrus-contact-updated", handler);
    return () => window.removeEventListener("citrus-contact-updated", handler);
  }, []);

  // Real-time account stage updates from other users / devices.
  useEffect(() => {
    const handler = (e) => {
      const p = e?.detail;
      if (!p?.id) return;
      setAccounts((prev) => prev.map((a) => a.id === p.id ? { ...a, stage: p.stage } : a));
    };
    window.addEventListener("citrus-account-updated", handler);
    return () => window.removeEventListener("citrus-account-updated", handler);
  }, []);

  // Opens the record page when inbox dispatches "View full record →"
  useEffect(() => {
    const handler = (e) => {
      const { from } = e.detail || {};
      if (!from || !leads) return;
      const lead = leads.find((l) => l.from === from);
      if (lead) setOpenRecord(lead);
    };
    window.addEventListener("citrus-open-contact-record", handler);
    return () => window.removeEventListener("citrus-open-contact-record", handler);
  }, [leads]);

  const patchLead = (from, patch) => {
    setSavingId(from);
    return fetch(`${contactsServer()}/contacts-pipeline/${encodeURIComponent(from)}`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(patch),
    })
      .then((r) => (r.ok ? r.json() : Promise.reject(r)))
      .then((updated) => {
        setLeads((prev) => {
          if (!prev) return prev;
          const idx = prev.findIndex((l) => l.from === from);
          if (idx === -1) return prev;
          const next = prev.slice();
          next[idx] = { ...next[idx], ...updated };
          return next;
        });
      })
      .catch(() => {
        window.toast && window.toast("Couldn't save lead — retrying may help", "warn");
      })
      .finally(() => setSavingId(null));
  };

  const changeStage = (from, stage) => {
    if (stage === "lost") {
      setLostReasonDraft("");
      setLostModal({ from, pendingStage: "lost" });
      return;
    }
    if (stage === "won") {
      setWonConfirmFrom(from);
      return;
    }
    setLeads((prev) => prev && prev.map((l) => l.from === from ? { ...l, stage } : l));
    patchLead(from, { stage });
  };

  const confirmWon = () => {
    if (!wonConfirmFrom) return;
    const from = wonConfirmFrom;
    setLeads((prev) => prev && prev.map((l) => l.from === from ? { ...l, stage: "won" } : l));
    patchLead(from, { stage: "won" })
      .then(() => {
        // Refresh accounts + contacts after won handoff
        fetch(`${contactsServer()}/accounts`)
          .then((r) => r.ok ? r.json() : [])
          .then((rows) => setAccounts(Array.isArray(rows) ? rows : []))
          .catch(() => {});
        fetch(`${contactsServer()}/contacts`)
          .then((r) => r.ok ? r.json() : [])
          .then((rows) => setContacts(Array.isArray(rows) ? rows : []))
          .catch(() => {});
      });
    setWonConfirmFrom(null);
  };

  const confirmLost = () => {
    if (!lostModal || !lostReasonDraft) return;
    const { from } = lostModal;
    setLeads((prev) => prev && prev.map((l) => l.from === from ? { ...l, stage: "lost", lostReason: lostReasonDraft } : l));
    patchLead(from, { stage: "lost", lostReason: lostReasonDraft });
    setLostModal(null);
    setLostReasonDraft("");
  };

  const confirmAccountLost = () => {
    if (!accountLostModal || !accountLostReasonDraft) return;
    const { id } = accountLostModal;
    setAccounts((prev) => prev.map((a) => a.id === id ? { ...a, stage: "lost", lostReason: accountLostReasonDraft } : a));
    patchAccount(id, { stage: "lost", lostReason: accountLostReasonDraft });
    setAccountLostModal(null);
    setAccountLostReasonDraft("");
  };

  // Toggle one classification tag on a lead — lets the operator correct the
  // agent's auto-classification. Keeps taxonomy order so chips stay stable.
  const toggleCategory = (from, key, current) => {
    const set = new Set(Array.isArray(current) ? current : []);
    if (set.has(key)) set.delete(key); else set.add(key);
    const next = CONTACT_CATEGORY_ORDER.filter((k) => set.has(k));
    setLeads((prev) => prev && prev.map((l) => l.from === from ? { ...l, categories: next } : l));
    patchLead(from, { categories: next });
  };

  const onNoteChange = (from, value) => {
    setNoteDraft((d) => ({ ...d, [from]: value }));
  };
  const saveNoteAsLog = (from) => {
    const draft = (noteDraft[from] || "").trim();
    if (!draft) return;
    setNoteDraft((d) => ({ ...d, [from]: "" }));
    patchLead(from, { logContact: { type: "note", note: draft } });
  };

  // Open the conversation in the inbox. Two-step handshake:
  //   1. publish a "citrus-open-thread" event with the from id, which the
  //      inbox listens for to select that specific thread,
  //   2. switch the tab to inbox.
  // Falls back to onSwitchTab + sessionStorage if the inbox listener isn't
  // wired yet (the inbox might mount only after the switch).
  const openConversation = (from) => {
    try {
      sessionStorage.setItem("citrus_pending_inbox_thread", from);
    } catch { /* ignore */ }
    try {
      window.dispatchEvent(new CustomEvent("citrus-open-thread", { detail: { from } }));
    } catch { /* ignore */ }
    if (onSwitchTab) onSwitchTab("inbox");
  };

  const deleteLead = (from) => {
    if (!window.confirm("Delete this lead?\n\nThis removes the captured name, company, stage, notes, and contact history from this conversation. The conversation itself stays in the inbox. Cannot be undone.")) return;
    fetch(`${contactsServer()}/contacts-pipeline/${encodeURIComponent(from)}`, { method: "DELETE" }).catch(() => {});
    setLeads((prev) => prev && prev.filter((l) => l.from !== from));
    window.toast && window.toast("Lead deleted", "warn");
  };

  // Export the currently-shown leads as a CSV that opens directly in Excel.
  // A UTF-8 BOM makes Excel read Arabic names correctly; CRLF line endings keep
  // Excel happy. Exports whatever the active filter shows (All by default).
  const exportCsv = () => {
    const rows = filtered;
    if (!rows.length) return;
    const STAGE = { new: "New", engaged: "Engaged", contacted: "Engaged", qualified: "Qualified", quoted: "Quoted", won: "Won", lost: "Lost" };
    const cols = ["Name", "Company", "Channel", "Agent", "Stage", "Tags", "Messages", "Captured", "Last contact", "Follow up", "Notes", "Contacts logged"];
    const esc = (v) => `"${String(v == null ? "" : v).replace(/"/g, '""')}"`;
    const fmt = (iso) => { if (!iso) return ""; const d = new Date(iso); return Number.isNaN(d.getTime()) ? "" : d.toLocaleDateString(); };
    const lines = [cols.map(esc).join(",")];
    for (const l of rows) {
      lines.push([
        contactDisplayName(l),
        l.customerCompany || "",
        l.channel || "",
        l.agentName || "",
        STAGE[l.stage || l.lifecycleStatus || "new"] || l.stage || "new",
        (Array.isArray(l.categories) ? l.categories : []).map((k) => (CONTACT_CATEGORY_BY_ID[k] ? CONTACT_CATEGORY_BY_ID[k].label : k)).join("; "),
        l.messageCount || 0,
        fmt(l.capturedAt),
        fmt(l.lastMessageAt),
        l.nextFollowUp || "",
        (l.notes || "").replace(/\s+/g, " ").trim(),
        (Array.isArray(l.contactLog) ? l.contactLog : []).map((c) => `${c.type}${c.outcome ? "/" + c.outcome : ""}`).join("; "),
      ].map(esc).join(","));
    }
    const blob = new Blob(["﻿" + lines.join("\r\n")], { type: "text/csv;charset=utf-8;" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `leads-${new Date().toISOString().slice(0, 10)}.csv`;
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 0);
  };

  // ---- Contact log + follow-up ----
  // Open the inline "log a contact" form for a lead, pre-set to a type.
  const openLog   = (from, type) => setLogForm((s) => ({ ...s, [from]: { type, outcome: "", note: "" } }));
  const closeLog  = (from) => setLogForm((s) => { const n = { ...s }; delete n[from]; return n; });
  const updateLog = (from, patch) => setLogForm((s) => ({ ...s, [from]: { ...(s[from] || {}), ...patch } }));
  const saveLog   = (from) => {
    const f = logForm[from];
    if (!f || !f.type) return;
    patchLead(from, { logContact: { type: f.type, outcome: f.outcome || null, note: (f.note || "").trim() } });
    closeLog(from);
    window.toast && window.toast("Contact logged", "good");
  };
  // Set / clear a lead's next follow-up date (YYYY-MM-DD from the date input).
  const setFollowUp = (from, dateStr) => patchLead(from, { nextFollowUp: dateStr || null });

  // One row per contact: same phone (or email) can appear in multiple
  // conversations. Keep the most complete record — name > company > phone —
  // and for ties prefer the most recent lastMessageAt.
  const deduped = useMemo(() => {
    const seen = new Map();
    for (const lead of (leads || [])) {
      const key = normalizeContactKey(lead.from);
      const existing = seen.get(key);
      if (!existing) {
        seen.set(key, lead);
      } else {
        const scoreOf = (l) =>
          (l.customerName ? 4 : 0) +
          (l.customerCompany ? 2 : 0) +
          (new Date(l.lastMessageAt || l.capturedAt || 0).getTime() > 0 ? 1 : 0);
        const newScore = scoreOf(lead);
        const exScore  = scoreOf(existing);
        if (newScore > exScore) {
          seen.set(key, lead);
        } else if (newScore === exScore) {
          const newTs = new Date(lead.lastMessageAt || lead.capturedAt || 0).getTime();
          const exTs  = new Date(existing.lastMessageAt || existing.capturedAt || 0).getTime();
          if (newTs > exTs) seen.set(key, lead);
        }
      }
    }
    return [...seen.values()];
  }, [leads]);

  const filtered = useMemo(() => {
    return deduped.filter((l) => {
      const s = l.stage || l.lifecycleStatus || "new";
      if (filter !== "all") {
        if (filter === "engaged" && s !== "engaged" && s !== "contacted") return false;
        else if (filter !== "engaged" && s !== filter) return false;
      }
      if (ownerFilter !== "all" && (l.assignedTo?.name || l.agentName || "") !== ownerFilter) return false;
      if (urgencyFilter !== "all") {
        const temp = leadTemperature(l);
        if (urgencyFilter === "high"   && temp !== "hot")  return false;
        if (urgencyFilter === "medium" && temp !== "warm") return false;
        if (urgencyFilter === "low"    && temp !== "cold" && temp !== null) return false;
      }
      if (companyFilter !== "all") {
        const lc = l.customerCompanyCanonical || l.customerCompany || "";
        if (lc !== companyFilter) return false;
      }
      return true;
    });
  }, [deduped, filter, ownerFilter, urgencyFilter, companyFilter]);

  const ownerOptions = useMemo(() => {
    const names = new Set();
    for (const l of deduped) if (l.assignedTo?.name) names.add(l.assignedTo.name);
    return [...names].sort();
  }, [deduped]);

  const STAGE_OPTIONS = [
    { id: "all",       label: "All Stages" },
    { id: "new",       label: "New" },
    { id: "engaged",   label: "Engaged" },
    { id: "qualified", label: "Qualified" },
    { id: "quoted",    label: "Quoted" },
    { id: "won",       label: "Won" },
    { id: "lost",      label: "Lost" },
  ];
  const URGENCY_OPTIONS = [
    { id: "all",    label: "All" },
    { id: "high",   label: "High" },
    { id: "medium", label: "Medium" },
    { id: "low",    label: "Low" },
  ];
  const hasActiveFilters = filter !== "all" || ownerFilter !== "all" || urgencyFilter !== "all" || companyFilter !== "all";

  if (leads === null) {
    return <div className="act-feed" style={{ padding: 24, color: "var(--ink-2)" }}>Loading leads…</div>;
  }

  if (openRecord) {
    return (
      <ContactRecordPage
        lead={openRecord}
        threads={threads}
        agents={agents}
        onBack={() => setOpenRecord(null)}
        onSwitchTab={onSwitchTab}
        onPatch={(patch) => {
          patchLead(openRecord.from, patch);
          setOpenRecord((prev) => prev ? { ...prev, ...patch } : prev);
        }}
      />
    );
  }

  // When in Customers view, render it directly (no modals/filters needed)
  if (leadsView === "customers") {
    return (
      <div className={`contacts-page${lockedAgentName ? " contacts-page--embed" : ""}`}>
        <div className="contacts-page-left">
        {tabBarSlot}
        <div className="contacts-page-body">
        <div className="contacts-main">
          <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 20 }}>
            <button onClick={() => setLeadsView("pipeline")}
              style={{ fontSize: 13, fontWeight: 600, padding: "5px 14px", borderRadius: 8, border: "1px solid var(--border)", background: "transparent", color: "var(--ink-2)", cursor: "pointer" }}>
              ← Pipeline
            </button>
            <span style={{ fontFamily: "var(--display-font)", fontWeight: 700, fontSize: 16 }}>Contacts</span>
            {contacts && (
              <span style={{ fontSize: 12, color: "var(--ink-soft)", marginLeft: 4 }}>
                {contacts.filter((c) => (c.deals || []).some((d) => d.stage === "won")).length} won
              </span>
            )}
          </div>
          <CustomersView contacts={contacts} />
        </div>
        </div>
        </div>
      </div>
    );
  }

  return (
  <div className={`contacts-page${lockedAgentName ? " contacts-page--embed" : ""}`}>
    <div className="contacts-page-left">
    {tabBarSlot}
    <div className="contacts-page-body">
    <div className="contacts-main">

      {/* modals */}
      {/* Won confirm modal */}
      {wonConfirmFrom && (
        <div style={{ position: "fixed", inset: 0, zIndex: 9999, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,0.35)" }}
          onClick={(e) => { if (e.target === e.currentTarget) setWonConfirmFrom(null); }}>
          <div style={{ background: "var(--paper)", borderRadius: 14, padding: "24px 28px", width: 320, maxWidth: "90vw", boxShadow: "0 8px 32px rgba(0,0,0,0.18)" }}>
            <div style={{ fontFamily: "var(--display-font)", fontWeight: 700, fontSize: 17, marginBottom: 8 }}>Mark as Won?</div>
            <div style={{ fontSize: 13, color: "var(--ink-2)", marginBottom: 20, lineHeight: 1.5 }}>
              This creates an account record and hands the contact off to the post-sale agent. The lead stage will be locked as Won.
            </div>
            <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
              <button className="btn btn-ghost btn-sm" onClick={() => setWonConfirmFrom(null)}>Cancel</button>
              <button className="btn btn-sm" onClick={confirmWon}
                style={{ background: "#16A34A", color: "#fff", border: "none", fontWeight: 700, padding: "7px 18px", borderRadius: 8, cursor: "pointer" }}>
                ✓ Confirm Won
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Lost-reason modal — shown when operator clicks "Lost". Server rejects
          stage:lost without a lostReason so we gate it client-side too. */}
      {lostModal && (
        <div style={{ position: "fixed", inset: 0, zIndex: 9999, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,0.35)" }}
          onClick={(e) => { if (e.target === e.currentTarget) { setLostModal(null); setLostReasonDraft(""); } }}>
          <div style={{ background: "var(--paper)", borderRadius: 14, padding: "24px 28px", width: 340, maxWidth: "90vw", boxShadow: "0 8px 32px rgba(0,0,0,0.18)" }}>
            <div style={{ fontFamily: "var(--display-font)", fontWeight: 700, fontSize: 17, marginBottom: 6 }}>Why did this lead not convert?</div>
            <div style={{ fontSize: 13, color: "var(--ink-2)", marginBottom: 16 }}>Required to mark as Lost. Helps track where leads fall off.</div>
            <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 20 }}>
              {LOST_REASONS.map((r) => (
                <label key={r.id} style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 12px", borderRadius: 8, border: `1.5px solid ${lostReasonDraft === r.id ? "var(--accent)" : "var(--border)"}`, background: lostReasonDraft === r.id ? "color-mix(in oklab, var(--accent) 8%, transparent)" : "var(--paper)", cursor: "pointer", fontSize: 13 }}>
                  <input type="radio" name="lostReason" value={r.id} checked={lostReasonDraft === r.id} onChange={() => setLostReasonDraft(r.id)} style={{ accentColor: "var(--accent)" }} />
                  {r.label}
                </label>
              ))}
            </div>
            <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
              <button className="btn btn-ghost btn-sm" onClick={() => { setLostModal(null); setLostReasonDraft(""); }}>Cancel</button>
              <button className="btn btn-primary btn-sm" disabled={!lostReasonDraft} onClick={confirmLost}>Confirm Lost</button>
            </div>
          </div>
        </div>
      )}

      {/* Account lost-reason modal — same gate as lead lostModal but for accounts. */}
      {accountLostModal && (
        <div style={{ position: "fixed", inset: 0, zIndex: 9999, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,0.35)" }}
          onClick={(e) => { if (e.target === e.currentTarget) { setAccountLostModal(null); setAccountLostReasonDraft(""); } }}>
          <div style={{ background: "var(--paper)", borderRadius: 14, padding: "24px 28px", width: 340, maxWidth: "90vw", boxShadow: "0 8px 32px rgba(0,0,0,0.18)" }}>
            <div style={{ fontFamily: "var(--display-font)", fontWeight: 700, fontSize: 17, marginBottom: 6 }}>Why was this account lost?</div>
            <div style={{ fontSize: 13, color: "var(--ink-2)", marginBottom: 16 }}>Required to mark as Lost. Tracks where accounts fall off post-sale.</div>
            <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 20 }}>
              {LOST_REASONS.map((r) => (
                <label key={r.id} style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 12px", borderRadius: 8, border: `1.5px solid ${accountLostReasonDraft === r.id ? "var(--accent)" : "var(--border)"}`, background: accountLostReasonDraft === r.id ? "color-mix(in oklab, var(--accent) 8%, transparent)" : "var(--paper)", cursor: "pointer", fontSize: 13 }}>
                  <input type="radio" name="accountLostReason" value={r.id} checked={accountLostReasonDraft === r.id} onChange={() => setAccountLostReasonDraft(r.id)} style={{ accentColor: "var(--accent)" }} />
                  {r.label}
                </label>
              ))}
            </div>
            <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
              <button className="btn btn-ghost btn-sm" onClick={() => { setAccountLostModal(null); setAccountLostReasonDraft(""); }}>Cancel</button>
              <button className="btn btn-primary btn-sm" disabled={!accountLostReasonDraft} onClick={confirmAccountLost}>Confirm Lost</button>
            </div>
          </div>
        </div>
      )}

      {/* ── Parties header ── */}
      <div className="contacts-header">
        <div className="contacts-header-right" style={{ marginLeft: "auto" }}>
          {contacts && contacts.some((c) => (c.deals || []).some((d) => d.stage === "won")) && (
            <button onClick={() => setLeadsView("customers")}
              style={{ fontSize: 12, fontWeight: 600, padding: "5px 12px", borderRadius: 8, border: "1px solid #6EE7B7", background: "#ECFDF5", color: "#065F46", cursor: "pointer", flexShrink: 0 }}>
              Contacts ({contacts.filter((c) => (c.deals || []).some((d) => d.stage === "won")).length})
            </button>
          )}
          <select value={sort} onChange={(e) => setSort(e.target.value)} className="contacts-sort-sel">
            <option value="urgency">Sort: Urgency</option>
            <option value="newest">Sort: Newest</option>
            <option value="oldest">Sort: Oldest</option>
          </select>
          <button className="contacts-export-btn" onClick={exportCsv} disabled={!(leads && leads.length)}>
            ⤓ Export
          </button>
        </div>
      </div>

      {/* ── Filter bar ── */}
      <div className="contacts-filters">
        <div className="contacts-filter-chip">
          <span className="contacts-filter-label">STAGE:</span>
          <select value={filter} onChange={(e) => setFilter(e.target.value)} className="contacts-filter-sel">
            {STAGE_OPTIONS.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
          </select>
        </div>
        {!lockedAgentName && (
          <div className="contacts-filter-chip">
            <span className="contacts-filter-label">OWNER:</span>
            <select value={ownerFilter} onChange={(e) => setOwnerFilter(e.target.value)} className="contacts-filter-sel">
              <option value="all">All Owners</option>
              {ownerOptions.map((n) => <option key={n} value={n}>{n}</option>)}
            </select>
          </div>
        )}
        <div className="contacts-filter-chip">
          <span className="contacts-filter-label">URGENCY:</span>
          <select value={urgencyFilter} onChange={(e) => setUrgencyFilter(e.target.value)} className="contacts-filter-sel">
            {URGENCY_OPTIONS.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
          </select>
        </div>
        {companies.length > 0 && (
          <div className="contacts-filter-chip">
            <span className="contacts-filter-label">COMPANY:</span>
            <select value={companyFilter} onChange={(e) => setCompanyFilter(e.target.value)} className="contacts-filter-sel">
              <option value="all">All Companies</option>
              {companies.map((c) => (
                <option key={c.name} value={c.name}>{c.name} ({c.count})</option>
              ))}
            </select>
          </div>
        )}
        {hasActiveFilters && (
          <button className="contacts-clear-btn" onClick={() => { setFilter("all"); if (!lockedAgentName) setOwnerFilter("all"); setUrgencyFilter("all"); setCompanyFilter("all"); }}>
            Clear Filters
          </button>
        )}
        <span className="contacts-count">{(leads || []).length.toLocaleString()} Relationships Tracked</span>
      </div>

      {/* ── Party Table ── */}
      <div className="ld-table">
        <div className="ld-table-head ld-table-head-v2">
          <div className="ld-th">CONTACT NAME</div>
          <div className="ld-th">CURRENT STAGE</div>
          <div className="ld-th">URGENCY SCORE</div>
          <div className="ld-th">LAST ACTIVITY</div>
          <div className="ld-th">ASSIGNED OWNER</div>
          <div className="ld-th"></div>
        </div>

        {filtered.length === 0 && (
          <div className="kn-empty" style={{ padding: "32px 20px", borderTop: "none" }}>
            {(leads || []).length === 0
              ? "No customers yet. Agents capture contacts automatically as conversations come in."
              : <React.Fragment>
                  No customers match the active filters.{" "}
                  {hasActiveFilters && (
                    <button className="kn-empty-link" onClick={() => { setFilter("all"); setOwnerFilter("all"); setUrgencyFilter("all"); }}>Clear filters</button>
                  )}
                </React.Fragment>
            }
          </div>
        )}

        {(() => {
          const URGENCY_RANK = { hot: 0, warm: 1, cold: 2 };
          const byTime = (l) => new Date(l.lastMessageAt || l.capturedAt || 0).getTime();
          return [...filtered].sort((a, b) => {
            if (sort === "urgency") {
              const sa = contactUrgencyScore(a), sb = contactUrgencyScore(b);
              return sb !== sa ? sb - sa : byTime(b) - byTime(a);
            }
            return sort === "newest" ? byTime(b) - byTime(a) : byTime(a) - byTime(b);
          });
        })().map((lead) => {
          const cur = lead.stage || lead.lifecycleStatus || "new";
          const isWon = cur === "won";
          const isLost = cur === "lost";
          const initial = (contactDisplayName(lead) || "?").trim().charAt(0).toUpperCase() || "?";
          const stageObj = DEAL_STAGE_BY_ID[cur] || DEAL_STAGE_BY_ID["new"];
          const stageLabel = isWon ? "Won" : isLost ? "Lost" : stageObj.label;
          const stageCol = isWon ? "#0B7A57" : isLost ? "#B0234A" : stageObj.fg;
          const stageBg  = isWon ? "#ECFDF5" : isLost ? "#FEF2F2" : stageObj.bg;
          const stageBd  = isWon ? "#6EE7B7" : isLost ? "#FCA5A5" : stageObj.bd;
          const score    = contactUrgencyScore(lead);
          const actLabel = contactLastActivityLabel(lead);
          const isSelected = quickViewLead?.from === lead.from;

          return (
            <div key={lead.from} className={`ld-row ld-row-v2${isSelected ? " is-selected" : ""}`}
              onClick={() => isSelected ? (setQuickViewLead(null), closeChat()) : selectLead(lead)}>
              {/* Party Name */}
              <div className="ld-cell ld-cell-name">
                <div className="ld-row-avatar">{initial}</div>
                <div style={{ minWidth: 0 }}>
                  <div className="ld-row-name" dir="auto">{contactDisplayName(lead)}</div>
                  {lead.customerCompany && (
                    <div className="ld-row-sub ld-row-company" dir="auto"
                      title={`Filter by ${lead.customerCompany}`}
                      onClick={(e) => { e.stopPropagation(); setCompanyFilter(lead.customerCompanyCanonical || lead.customerCompany); }}>
                      {lead.customerCompany}
                    </div>
                  )}
                </div>
              </div>
              {/* Current Stage */}
              <div className="ld-cell">
                <span className="ld-stage-btn" style={{ color: stageCol, background: stageBg, border: `1px solid ${stageBd}`, cursor: "default", pointerEvents: "none" }}>
                  • {stageLabel}
                </span>
              </div>
              {/* Urgency Score */}
              <div className="ld-cell ld-cell-score">
                <span className="ld-score-num" style={{ color: score >= 75 ? "#DC2626" : score >= 50 ? "#EA580C" : "var(--ink-soft)" }}>{score}</span>
                <ScoreBars score={score} />
              </div>
              {/* Last Activity */}
              <div className="ld-cell" style={{ flexDirection: "column", alignItems: "flex-start", gap: 2 }}>
                <span className="ld-cell-soft" style={{ fontSize: 12 }}>{ldRelative(lead.lastMessageAt || lead.capturedAt)}</span>
                <span style={{ fontSize: 11, color: "var(--ink-soft)" }}>{actLabel}</span>
              </div>
              {/* Assigned Owner */}
              <div className="ld-cell ld-cell-soft">{lead.assignedTo?.name || "—"}</div>
            </div>
          );
        })}
      </div>
    </div>

    </div>   {/* closes contacts-page-body */}
    </div>   {/* closes contacts-page-left */}

    {/* ── Lens pane + sliding chat overlay — sibling of contacts-page-left ── */}
    {quickViewLead && (
      <>
        <ContactActivityView
          lead={quickViewLead}
          threads={threads}
          agents={agents}
          onClose={closeChat}
          onOpenFull={() => { setOpenRecord(quickViewLead); setQuickViewLead(null); setChatVisible(false); }}
          chatVisible={chatVisible}
        />
        <ContactQuickView
          lead={quickViewLead}
          onClose={() => {
            const rt = returnTabRef.current;
            returnTabRef.current = null;
            setQuickViewLead(null);
            setChatVisible(false);
            if (rt && onSwitchTab) onSwitchTab(rt);
          }}
          onOpenFull={() => { setOpenRecord(quickViewLead); setQuickViewLead(null); returnTabRef.current = null; }}
          onChatOpen={openChat}
          onChatClose={closeChat}
          chatVisible={chatVisible}
          agents={agents}
          onPatch={(patch) => {
            patchLead(quickViewLead.from, patch);
            setQuickViewLead((prev) => prev ? { ...prev, ...patch } : prev);
          }}
        />
      </>
    )}
  </div>
  );
}

window.ContactsTab = ContactsTab;
window.ContactQuickView = ContactQuickView;
window.ContactActivityView = ContactActivityView;

// ============ PIPELINE FUNNEL (AC3, AC4, AC11) ============
// Shows per-stage counts + conversion rates computed from real stageHistory.
// AC11: suppresses conversion % when stage count < MIN_RATE_THRESHOLD.

const FUNNEL_STAGES = [
  { id: "new",       label: "New",       col: "#2D6CDF" },
  { id: "engaged",   label: "Engaged",   col: "#E07B1A" },
  { id: "qualified", label: "Qualified", col: "#8B5CF6" },
  { id: "quoted",    label: "Quoted",    col: "#B45309" },
  { id: "won",       label: "Won",       col: "#0B7A57" },
];

function avgTimeInStage(leads, stageId) {
  const times = [];
  for (const l of leads) {
    const hist = Array.isArray(l.stageHistory) ? l.stageHistory : [];
    const entryIdx = hist.findIndex((h) => h.stage === stageId);
    if (entryIdx === -1) continue;
    const entry = hist[entryIdx];
    const next = hist[entryIdx + 1];
    const endMs = next ? new Date(next.enteredAt).getTime() : Date.now();
    const startMs = new Date(entry.enteredAt).getTime();
    if (!isNaN(startMs) && !isNaN(endMs) && endMs > startMs) {
      times.push(endMs - startMs);
    }
  }
  if (!times.length) return null;
  const avg = times.reduce((a, b) => a + b, 0) / times.length;
  const hours = avg / 3600000;
  if (hours < 24) return `${Math.round(hours)}h avg`;
  return `${Math.round(hours / 24)}d avg`;
}

function PipelineFunnel({ leads, activeStage, onStageClick }) {
  const stageCounts = {};
  const lostCounts  = {};
  for (const l of leads) {
    const s = l.stage || l.lifecycleStatus || "new";
    stageCounts[s] = (stageCounts[s] || 0) + 1;
    if (s === "lost" && l.lostReason) {
      lostCounts[l.lostReason] = (lostCounts[l.lostReason] || 0) + 1;
    }
  }
  const maxCount = Math.max(...FUNNEL_STAGES.map((s) => stageCounts[s.id] || 0), 1);

  return (
    <div style={{ background: "var(--paper)", border: "1px solid var(--border)", borderRadius: 12, padding: "18px 20px", marginBottom: 18 }}>
      <div style={{ fontFamily: "var(--display-font)", fontWeight: 700, fontSize: 15, marginBottom: 14 }}>Acquisition funnel</div>
      <div style={{ display: "grid", gap: 10 }}>
        {FUNNEL_STAGES.map((stage, i) => {
          const count    = stageCounts[stage.id] || 0;
          const pct      = maxCount > 0 ? (count / maxCount) * 100 : 0;
          const isActive = activeStage === stage.id;
          const prevCount = i > 0 ? (stageCounts[FUNNEL_STAGES[i - 1].id] || 0) : null;
          const convRate = (prevCount !== null && prevCount >= MIN_RATE_THRESHOLD && count >= 0)
            ? Math.round((count / prevCount) * 100) + "%"
            : null;
          const timeLabel = avgTimeInStage(leads, stage.id);
          return (
            <div key={stage.id}>
              {i > 0 && convRate ? (
                <div style={{ fontSize: 11, color: "var(--ink-soft)", paddingLeft: 8, marginBottom: 4 }}>
                  {convRate} converted from {FUNNEL_STAGES[i - 1].label}
                </div>
              ) : i > 0 && (stageCounts[FUNNEL_STAGES[i - 1].id] || 0) < MIN_RATE_THRESHOLD ? (
                <div style={{ fontSize: 11, color: "var(--ink-soft)", paddingLeft: 8, marginBottom: 4 }}>
                  — (too few leads for a reliable rate)
                </div>
              ) : null}
              <button
                onClick={() => onStageClick && onStageClick(stage.id)}
                title={`Filter list to ${stage.label} leads`}
                style={{ display: "flex", alignItems: "center", gap: 12, width: "100%", background: isActive ? `color-mix(in oklab, ${stage.col} 8%, transparent)` : "none", border: isActive ? `1.5px solid color-mix(in oklab, ${stage.col} 40%, transparent)` : "1.5px solid transparent", borderRadius: 8, padding: "4px 6px", cursor: "pointer", textAlign: "left", transition: "background .15s" }}>
                <div style={{ width: 80, fontSize: 12, fontWeight: isActive ? 700 : 600, color: stage.col, flexShrink: 0 }}>{stage.label}</div>
                <div style={{ flex: 1, position: "relative", height: 22, borderRadius: 4, background: "var(--border)" }}>
                  <div style={{ position: "absolute", left: 0, top: 0, height: "100%", width: `${pct}%`, minWidth: count > 0 ? 4 : 0, borderRadius: 4, background: stage.col, opacity: isActive ? 1 : 0.8, transition: "width .4s" }} />
                </div>
                <div style={{ width: 36, fontSize: 13, fontWeight: 700, color: count > 0 ? stage.col : "var(--ink-soft)", textAlign: "right", flexShrink: 0 }}>{count}</div>
                {timeLabel ? <div style={{ width: 60, fontSize: 11, color: "var(--ink-soft)", flexShrink: 0 }}>{timeLabel}</div> : <div style={{ width: 60 }} />}
              </button>
            </div>
          );
        })}
        {/* Lost row */}
        {(stageCounts["lost"] || 0) > 0 && (
          <div>
            {(() => {
              const isActiveLost = activeStage === "lost";
              return (
            <button onClick={() => onStageClick && onStageClick("lost")}
              title="Filter list to Lost leads"
              style={{ display: "flex", alignItems: "center", gap: 12, width: "100%", background: isActiveLost ? "color-mix(in oklab, #B0234A 8%, transparent)" : "none", border: isActiveLost ? "1.5px solid color-mix(in oklab, #B0234A 40%, transparent)" : "1.5px solid transparent", borderRadius: 8, padding: "4px 6px", cursor: "pointer", textAlign: "left" }}>
              <div style={{ width: 80, fontSize: 12, fontWeight: isActiveLost ? 700 : 600, color: "#B0234A", flexShrink: 0 }}>Lost</div>
              <div style={{ flex: 1, position: "relative", height: 22, borderRadius: 4, background: "var(--border)" }}>
                <div style={{ position: "absolute", left: 0, top: 0, height: "100%", width: `${((stageCounts["lost"] || 0) / maxCount) * 100}%`, minWidth: 4, borderRadius: 4, background: "#B0234A", opacity: isActiveLost ? 1 : 0.6 }} />
              </div>
              <div style={{ width: 36, fontSize: 13, fontWeight: 700, color: "#B0234A", textAlign: "right", flexShrink: 0 }}>{stageCounts["lost"] || 0}</div>
              <div style={{ width: 60 }} />
            </button>
              );
            })()}
            {Object.keys(lostCounts).length > 0 && (
              <div style={{ paddingLeft: 92, marginTop: 6, display: "flex", gap: 8, flexWrap: "wrap" }}>
                {Object.entries(lostCounts).sort((a, b) => b[1] - a[1]).map(([reason, n]) => (
                  <span key={reason} style={{ fontSize: 11, padding: "1px 8px", borderRadius: 999, background: "#FEF2F2", color: "#9F1239", border: "1px solid #FCA5A5" }}>
                    {reason.replace(/_/g, " ")}: {n}
                  </span>
                ))}
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

window.PipelineFunnel = PipelineFunnel;

// ============ ACCOUNT KANBAN (account lifecycle view) ============
// ── Customers view — unified contact records with won deals ──────────────────
// One row per contact (person), not per conversation. Shows who has converted.
function CustomersView({ contacts }) {
  if (!contacts) {
    return <div style={{ padding: 32, color: "var(--ink-soft)", fontSize: 13 }}>Loading customers…</div>;
  }

  const customers = contacts
    .filter((c) => (c.deals || []).some((d) => d.stage === "won"))
    .sort((a, b) => {
      const aWon = (a.deals || []).find((d) => d.stage === "won");
      const bWon = (b.deals || []).find((d) => d.stage === "won");
      return new Date(bWon?.createdAt || 0) - new Date(aWon?.createdAt || 0);
    });

  if (customers.length === 0) {
    return (
      <div style={{ padding: "48px 24px", textAlign: "center", color: "var(--ink-soft)", fontSize: 13 }}>
        <div style={{ fontSize: 28, marginBottom: 12 }}>🎉</div>
        <div style={{ fontWeight: 600, color: "var(--ink-2)", marginBottom: 6 }}>No customers yet</div>
        <div>Leads become Contacts when their deal is marked Won.</div>
      </div>
    );
  }

  return (
    <div>
      <div style={{ fontSize: 12, fontWeight: 600, color: "var(--ink-soft)", padding: "0 4px 10px", letterSpacing: "0.06em" }}>
        {customers.length} CUSTOMER{customers.length !== 1 ? "S" : ""}
      </div>
      <div style={{ display: "flex", flexDirection: "column", gap: 1 }}>
        {customers.map((contact) => {
          const wonDeals  = (contact.deals || []).filter((d) => d.stage === "won");
          const activeDeals = (contact.deals || []).filter((d) => !["won", "lost"].includes(d.stage));
          const firstWon  = wonDeals[0];
          const initials  = (contact.name || contact.phone || "?").trim().charAt(0).toUpperCase();
          const phone     = String(contact.phone || "")
            .replace(/^whatsapp:/i, "").replace(/^biz-[^:]+:/i, "").replace(/^tg:/i, "");

          return (
            <div key={contact.id} style={{
              display: "grid",
              gridTemplateColumns: "40px 1fr auto auto",
              alignItems: "center",
              gap: 12,
              padding: "12px 4px",
              borderBottom: "1px solid var(--border)",
            }}>
              {/* Avatar */}
              <div style={{
                width: 36, height: 36, borderRadius: "50%",
                background: "color-mix(in oklab, var(--accent) 15%, var(--paper))",
                color: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center",
                fontWeight: 700, fontSize: 15, flexShrink: 0,
              }}>{initials}</div>

              {/* Name + phone */}
              <div style={{ minWidth: 0 }}>
                <div style={{ fontWeight: 600, fontSize: 14, color: "var(--ink-1)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
                  {contact.name || phone}
                </div>
                {contact.company && (
                  <div style={{ fontSize: 12, color: "var(--ink-soft)", marginTop: 1 }}>{contact.company}</div>
                )}
              </div>

              {/* Active deal badge (if any) */}
              <div style={{ textAlign: "right" }}>
                {activeDeals.length > 0 && (
                  <span style={{
                    fontSize: 11, fontWeight: 600, padding: "2px 8px", borderRadius: 999,
                    background: "#FFF7ED", color: "#B45309", border: "1px solid #FCD34D",
                  }}>
                    + {activeDeals.length} active
                  </span>
                )}
              </div>

              {/* Customer since */}
              <div style={{ fontSize: 12, color: "var(--ink-soft)", whiteSpace: "nowrap", textAlign: "right", minWidth: 80 }}>
                <span style={{
                  display: "inline-block", fontSize: 11, fontWeight: 600, padding: "2px 8px", borderRadius: 999,
                  background: "#ECFDF5", color: "#065F46", border: "1px solid #6EE7B7", marginBottom: 2,
                }}>Won</span>
                <br />
                {firstWon?.createdAt ? new Date(firstWon.createdAt).toLocaleDateString() : "—"}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// Shown in the Leads tab below the funnel when accounts exist.

const ACCOUNT_STAGES = [
  { id: "order_confirmed", label: "Confirmed",   col: "#2D6CDF" },
  { id: "in_progress",     label: "In progress", col: "#E07B1A" },
  { id: "delivered",       label: "Delivered",   col: "#8B5CF6" },
  { id: "paid",            label: "Paid",        col: "#0B7A57" },
];

function AccountKanban({ accounts }) {
  if (!accounts || accounts.length === 0) return null;

  const counts = {};
  for (const s of ACCOUNT_STAGES) counts[s.id] = 0;
  for (const a of accounts) {
    if (counts[a.stage] !== undefined) counts[a.stage]++;
  }

  return (
    <div style={{ background: "var(--paper)", border: "1px solid var(--border)", borderRadius: 12, padding: "18px 20px", marginBottom: 20 }}>
      <div style={{ fontSize: 13, marginBottom: 14 }}>
        <span style={{ fontFamily: "var(--display-font)", fontWeight: 700 }}>Order status</span>
        <span style={{ color: "var(--ink-soft)", marginLeft: 8 }}>— leads that have moved past Won</span>
      </div>
      <div style={{ display: "grid", gridTemplateColumns: `repeat(${ACCOUNT_STAGES.length}, 1fr)`, gap: 10 }}>
        {ACCOUNT_STAGES.map((stage) => (
          <div key={stage.id} style={{ background: `color-mix(in oklab, ${stage.col} 10%, var(--paper))`, border: `1px solid color-mix(in oklab, ${stage.col} 25%, var(--border))`, borderRadius: 10, padding: "14px 16px", textAlign: "center" }}>
            <div style={{ fontSize: 28, fontWeight: 700, color: stage.col, lineHeight: 1.1 }}>{counts[stage.id]}</div>
            <div style={{ fontSize: 12, color: "var(--ink-soft)", marginTop: 4 }}>{stage.label}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

window.AccountKanban = AccountKanban;
