// Thread list: header with the view picker + filters, then the rows
const channelIcon = (channel, type) => {
  if (channel === "call") {
    if (type === "missed") return { icon: I.phoneMissed, color: "#D92D20" };
    if (type === "outbound") return { icon: I.phoneOut, color: "#667085" };
    return { icon: I.phoneIn, color: "#12B76A" };
  }
  if (channel === "note") return { icon: I.note, color: "#F79009" };
  return { icon: I.chat, color: "#004CE6" };
};

// ─── Last-interaction preview ────────────────────────────────────────────
// Walks the thread's message timeline (window.THREAD_MESSAGES + any
// session-appended messages) and returns a structured preview describing
// the most recent real interaction (skipping date dividers).
//
//   { kind: "call" | "sms" | "whatsapp" | "voice" | "note",
//     text: string,                // line shown after the icon
//     status?: "sent" | "delivered" | "read",   // outbound msg ticks
//     dir?: "in" | "out",
//     callType?: "inbound" | "outbound" | "missed" }
const buildLastInteractionPreview = (thread) => {
  const live = window.__appendedMessages && window.__appendedMessages[thread.id] || [];
  const base = window.THREAD_MESSAGES && window.THREAD_MESSAGES[thread.id] || [];
  const all = [...base, ...live];
  // Find the most recent real interaction. Dividers aren't messages, and an
  // event marker records something that happened *to* the conversation — the
  // row should still preview the last thing actually said.
  let last = null;
  for (let i = all.length - 1; i >= 0; i--) {
    if (all[i].kind && all[i].kind !== "date" && all[i].kind !== "event") {last = all[i];break;}
  }
  if (!last) return null;

  // Calls — show only direction + duration ("Inbound call · 02:14")
  if (last.kind === "call") {
    const callType = last.status === "missed" ?
    "missed" :
    last.dir === "out" ? "outbound" : "inbound";
    const label = callType === "missed" ?
    "Missed call" :
    callType === "outbound" ? "Outbound call" : "Inbound call";
    const dur = last.duration ? ` · ${last.duration}` : "";
    return { kind: "call", text: `${label}${dur}`, callType };
  }

  // Voice note — treat like a message preview
  if (last.kind === "voice") {
    return {
      kind: "voice",
      dir: last.dir === "out" ? "out" : "in",
      status: last.status || "delivered",
      text: `Voice note${last.duration ? ` · ${last.duration}` : ""}`
    };
  }

  // Internal notes — "Note: <preview>"
  if (last.kind === "note") {
    return { kind: "note", text: `${last.text || ""}` };
  }

  // SMS / WhatsApp / generic message bubble
  if (last.kind === "sms") {
    const isWA = last.channel === "whatsapp";
    return {
      kind: isWA ? "whatsapp" : "sms",
      dir: last.dir === "out" ? "out" : "in",
      status: last.scheduled ? "scheduled" : last.status || "delivered",
      text: last.text || ""
    };
  }

  // Unknown kind — fall back to its text/title if any
  return { kind: last.kind, text: last.text || last.title || "" };
};

// Outbound message ticks (single = sent, double grey = delivered, double blue = read).
const StatusTicks = ({ status }) => {
  // Queued to go out, not yet sent — a clock rather than any tick.
  if (status === "scheduled") {
    return <I.clock size={13} stroke="#98A2B3" style={{ flexShrink: 0 }} />;
  }
  if (status === "sent") {
    return <I.check size={13} stroke="#98A2B3" style={{ flexShrink: 0 }} />;
  }
  if (status === "read") {
    return <I.checkDouble size={13} stroke="#1570EF" style={{ flexShrink: 0 }} />;
  }
  // Default: delivered (double grey ticks)
  return <I.checkDouble size={13} stroke="#98A2B3" style={{ flexShrink: 0 }} />;
};

const ThreadListHeader = ({ view, onViewChange, status, onStatusChange, viewCounts, query, onQueryChange, showCollapseToggle, categoriesCollapsed, onToggleCategories, onStartNew }) => {
  const [dropdown, setDropdown] = useState(null); // views | status | filterMenu | phones | agents | channels | bulk | sd
  const rootRef = useRef(null);

  // Close any open dropdown on outside click
  useEffect(() => {
    if (!dropdown) return;
    const close = (e) => {if (rootRef.current && !rootRef.current.contains(e.target)) setDropdown(null);};
    document.addEventListener("mousedown", close);
    return () => document.removeEventListener("mousedown", close);
  }, [dropdown]);

  // Views the user pinned in Settings → Organize conversations. Re-read on
  // change so the picker updates without a reload.
  const readViews = () => window.jcEnabledViews ? window.jcEnabledViews() : [{ id: "all", label: "All" }];
  const [enabledViews, setEnabledViews] = useState(readViews);
  useEffect(() => {
    const onChange = () => setEnabledViews(readViews());
    window.addEventListener("jc:convoViewsChanged", onChange);
    window.addEventListener("jc:profileChanged", onChange);
    return () => {
      window.removeEventListener("jc:convoViewsChanged", onChange);
      window.removeEventListener("jc:profileChanged", onChange);
    };
  }, []);
  const currentView = enabledViews.find((v) => v.id === view) || enabledViews[0] || { id: "all", label: "All" };

  // Phone lines are a filter group like any other now, so they carry the flag
  // and number as option metadata rather than needing their own pill.
  const lines = window.PHONE_LINES || [];
  const PHONE_OPTIONS = lines.map((l) => ({ value: l.id, label: l.name, sub: l.phone, flag: l.flag }));

  // Multi-select filters (each holds an array of selected IDs)
  const AGENTS = ["Ayush Sharma", "Priya Patel", "Miguel Torres", "Jamie Ortiz", "Renee Huang", "Unassigned"];
  const CHANNELS = ["SMS/MMS", "Calls", "Notes", "WhatsApp", "Group SMS"];
  const BULK_CAMPAIGNS = ["Q2 Product Launch", "Renewal Push", "Spring Promo", "Webinar Invite", "Back-in-stock Alert"];
  // Contact-level tags applied via the contact record (CRM/Convo). Used to slice the inbox by
  // customer segment, lifecycle stage, or qualitative attributes that travel with the contact.
  const CONTACT_TAGS = ["VIP", "Enterprise", "SMB", "Trial", "Paying customer", "Churned", "Hot lead", "At risk", "Advocate", "Do not contact"];
  // Topics — subjects end customers frequently bring up on calls (auto-tagged from transcripts)
  const TOPICS = ["Pricing", "Cancellation", "Refund request", "Billing issue", "Feature request", "Onboarding", "Integration help", "Bug report", "Renewal", "Competitor mention", "Demo request", "Outage"];
  // Participating reps — anyone who took part in the conversation, not just the
  // assignee. Seeded from the real roster so the filter names match the people
  // the app already knows, padded out to a full 15-strong team.
  const PARTICIPATING_REPS = [
  ...(window.AGENTS || []).map((a) => a.name),
  "Dana Whitfield", "Owen Baptiste", "Priyanka Rao", "Marcus Feld",
  "Ines Delgado", "Theo Lindqvist", "Nadia Haddad"];


  const GROUPS = [
  { key: "phones", label: "Phone numbers", icon: I.phone, options: PHONE_OPTIONS },
  { key: "agents", label: "Assigned to", icon: I.user, options: AGENTS },
  { key: "reps", label: "Participants", icon: I.users, options: PARTICIPATING_REPS },
  { key: "channels", label: "Channels", icon: I.chat, options: CHANNELS },
  { key: "topics", label: "Topics", icon: I.hash, options: TOPICS },
  { key: "bulk", label: "Campaigns", icon: I.megaphone, options: BULK_CAMPAIGNS },
  { key: "tags", label: "Tags", icon: I.tag, options: CONTACT_TAGS }];

  // Derived from GROUPS so adding a filter doesn't mean remembering to update
  // the empty state, the clear-all, and the active count separately.
  const emptySel = () => GROUPS.reduce((acc, g) => ({ ...acc, [g.key]: [] }), {});

  const [sel, setSel] = useState(emptySel);

  const toggleSelValue = (groupKey, v) => {
    setSel((prev) => {
      const cur = prev[groupKey] || [];
      return { ...prev, [groupKey]: cur.includes(v) ? cur.filter((x) => x !== v) : [...cur, v] };
    });
  };
  const clearGroup = (groupKey) => setSel((prev) => ({ ...prev, [groupKey]: [] }));
  const totalActive = GROUPS.reduce((n, g) => n + (sel[g.key] || []).length, 0);


  return (
    <div ref={rootRef} style={{ padding: "12px 14px 10px", borderBottom: "1px solid #E4E7EC", background: "#FFFFFF" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
        {showCollapseToggle &&
        <button
          onClick={onToggleCategories}
          title={categoriesCollapsed ? "Show inbox categories" : "Hide inbox categories"}
          style={{
            ...iconBtn,
            width: 26, height: 26, flexShrink: 0,
            background: categoriesCollapsed ? "#EFF8FF" : "#FFFFFF",
            border: `1px solid ${categoriesCollapsed ? "#84CAFF" : "#E4E7EC"}`
          }}>
            {categoriesCollapsed ?
          <I.chevRight size={14} stroke="#004CE6" /> :
          <I.chevLeft size={14} stroke="#667085" />}
          </button>
        }
        <div className="t-h-l-semi" style={{ fontSize: 15, flex: 1, fontWeight: 600 }}>Overview</div>
        <button onClick={onStartNew}
        style={{
          display: "inline-flex", alignItems: "center", gap: 6, height: 32, padding: "0 12px",
          borderRadius: 4, border: "1px solid #004CE6", background: "#FFFFFF", color: "#004CE6",
          fontSize: 14, fontWeight: 600, cursor: "pointer", flexShrink: 0, fontFamily: "inherit"
        }}
        onMouseEnter={(e) => e.currentTarget.style.background = "#EFF8FF"}
        onMouseLeave={(e) => e.currentTarget.style.background = "#FFFFFF"}>
          <I.plus size={14} stroke="#004CE6" /> Start New
        </button>
      </div>

      {/* Search input with filter icon on the right */}
      <div style={{ position: "relative", marginBottom: 10, display: "flex", alignItems: "center", gap: 8 }}>
        <div style={{ position: "relative", flex: 1 }}>
          <I.search size={14} stroke="#98A2B3" style={{ position: "absolute", left: 12, top: "50%", transform: "translateY(-50%)" }} />
          <input
            value={query} onChange={(e) => onQueryChange(e.target.value)}
            placeholder="Search name, number or keyword"
            style={{
              width: "100%", height: 34, padding: "0 12px 0 34px", borderRadius: 8, border: "1px solid #D0D5DD",
              fontSize: 14, color: "#101828", background: "#FFFFFF", outline: "none", fontFamily: "inherit"
            }} />
          
        </div>
        <div style={{ position: "relative", flexShrink: 0 }}>
          <button
            onClick={() => setDropdown(dropdown === "filterMenu" ? null : "filterMenu")}
            title="Filter conversations"
            style={{
              width: 34, height: 34, display: "inline-flex", alignItems: "center", justifyContent: "center",
              borderRadius: 8, border: `1px solid ${dropdown === "filterMenu" || totalActive > 0 ? "#84CAFF" : "#D0D5DD"}`,
              background: dropdown === "filterMenu" || totalActive > 0 ? "#EFF8FF" : "#FFFFFF",
              cursor: "pointer", padding: 0, position: "relative"
            }}>
            <I.filter size={16} stroke={dropdown === "filterMenu" || totalActive > 0 ? "#004CE6" : "#475467"} />
            {totalActive > 0 &&
            <span style={{
              position: "absolute", top: -4, right: -4, minWidth: 16, height: 16, padding: "0 4px",
              borderRadius: 500, background: "#004CE6", color: "#FFFFFF", fontSize: 10, fontWeight: 700,
              display: "inline-flex", alignItems: "center", justifyContent: "center",
              border: "1.5px solid #FFFFFF"
            }}>{totalActive}</span>
            }
          </button>
          {dropdown === "filterMenu" &&
          <div style={{
            position: "absolute", top: "calc(100% + 6px)", right: 0, zIndex: 25,
            minWidth: 210, background: "#FFFFFF", border: "1px solid #E4E7EC", borderRadius: 6,
            boxShadow: "0 8px 24px rgba(16,24,40,.08), 0 2px 6px rgba(16,24,40,.04)", padding: 4
          }}>
              {GROUPS.map((g) => {
              const groupCount = sel[g.key].length;
              const GIcon = g.icon;
              return (
                <div key={g.key}
                onClick={() => setDropdown(g.key)}
                style={{
                  display: "flex", alignItems: "center", gap: 10, padding: "8px 10px", borderRadius: 4,
                  cursor: "pointer", fontSize: 13, color: "#344054", fontWeight: 500
                }}
                onMouseEnter={(e) => e.currentTarget.style.background = "#F9FAFB"}
                onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}>
                    <GIcon size={14} stroke="#667085" />
                    <span style={{ flex: 1 }}>{g.label}</span>
                    {groupCount > 0 &&
                  <span style={{
                    fontSize: 10, fontWeight: 600, background: "#EFF8FF", color: "#004CE6",
                    borderRadius: 500, padding: "1px 6px", minWidth: 18, textAlign: "center"
                  }}>{groupCount}</span>
                  }
                    <I.chevRight size={13} stroke="#98A2B3" />
                  </div>);

            })}
              {totalActive > 0 &&
            <>
                  <div style={{ height: 1, background: "#E4E7EC", margin: "4px 4px" }} />
                  <div
                onClick={() => {setSel(emptySel());setDropdown(null);}}
                style={{
                  display: "flex", alignItems: "center", gap: 8, padding: "8px 10px", borderRadius: 4,
                  cursor: "pointer", fontSize: 12, color: "#B42318", fontWeight: 500
                }}
                onMouseEnter={(e) => e.currentTarget.style.background = "#FEF3F2"}
                onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}>
                    <I.x size={12} stroke="#B42318" /> Clear all filters
                  </div>
                </>
            }
            </div>
          }

          {/* Sub-menu (checkbox list) for the selected group */}
          {GROUPS.map((g) =>
          dropdown === g.key &&
          <CheckboxDropdown key={g.key}
          title={g.label}
          options={g.options}
          selected={sel[g.key]}
          onToggle={(v) => toggleSelValue(g.key, v)}
          onClear={() => clearGroup(g.key)}
          onBack={() => setDropdown("filterMenu")}
          onClose={() => setDropdown(null)} />


          )}
        </div>
      </div>

      {/* Primary chips: the pinned-view picker, then the Open/Closed status
          dropdown. Applied filters live in the filter menu only — they are
          deliberately not mirrored here as pills. */}
      <div style={{ display: "flex", gap: 6, marginBottom: 8, flexWrap: "wrap", rowGap: 6, position: "relative", minHeight: "32px" }} data-comment-anchor="ec8a856fd4-div-270-7">
        <ViewPill
          current={currentView}
          count={viewCounts ? viewCounts[currentView.id] : 0}
          views={enabledViews}
          counts={viewCounts || {}}
          open={dropdown === "views"}
          onToggle={() => setDropdown(dropdown === "views" ? null : "views")}
          onPick={(id) => {setDropdown(null);if (onViewChange) onViewChange(id);}}
          onCustomize={() => {
            setDropdown(null);
            if (window.jcOpenConvoSettings) window.jcOpenConvoSettings();
          }} />


        <StatusPill
          value={status}
          open={dropdown === "status"}
          onToggle={() => setDropdown(dropdown === "status" ? null : "status")}
          onPick={(id) => {setDropdown(null);if (onStatusChange) onStatusChange(id);}} />

      </div>
    </div>);

};

// Status toggle pill — Open / Closed.
// Split interaction: clicking the LABEL activates this pill as the selected filter
// (mutually exclusive with the "All" pill); clicking the CHEVRON opens the dropdown
// to switch between Open and Closed.
// The single always-selected views pill — replaces the old All + Open/Closed
// pair. Label + count is the current view; the chevron opens the picker built
// from whatever the user pinned in Organize conversations.
const ViewPill = ({ current, count, views, counts, open, onToggle, onPick, onCustomize }) => {
  return (
    <div style={{ position: "relative" }}>
      <button
        onClick={onToggle}
        aria-haspopup="menu"
        aria-expanded={open}
        style={{
          display: "inline-flex", alignItems: "center", gap: 6, padding: "0 10px 0 12px",
          borderRadius: 8,
          border: `1px solid ${open ? "#004CE6" : "#84CAFF"}`,
          background: "#EFF8FF", color: "#004CE6",
          fontSize: 14, fontWeight: 500, cursor: "pointer", whiteSpace: "nowrap",
          fontFamily: "inherit", height: 32
        }}>
        {current.label}
        <span style={{
          fontSize: 11, fontWeight: 600,
          background: "rgba(0,76,230,0.12)", color: "#004CE6",
          borderRadius: 500, padding: "1px 6px", minWidth: 18, textAlign: "center"
        }}>{count || 0}</span>
        <span style={{ display: "inline-flex", transform: open ? "rotate(180deg)" : "none", transition: "transform .15s ease" }}>
          <I.chevDown size={14} stroke="#004CE6" />
        </span>
      </button>
      {open &&
      <div role="menu" style={{
        position: "absolute", top: "calc(100% + 6px)", left: 0, zIndex: 30,
        minWidth: 232, background: "#FFFFFF", border: "1px solid #E4E7EC", borderRadius: 8,
        boxShadow: "0 12px 28px rgba(16,24,40,.12)", padding: 4
      }}>
          {views.map((v) => {
          const on = v.id === current.id;
          return (
            <div key={v.id} role="menuitemradio" aria-checked={on}
            onClick={() => onPick(v.id)}
            style={{
              display: "flex", alignItems: "center", gap: 8, padding: "8px 10px",
              borderRadius: 6, cursor: "pointer", fontSize: 13,
              color: on ? "#004CE6" : "#344054", fontWeight: on ? 600 : 500,
              background: on ? "#EFF8FF" : "transparent"
            }}
            onMouseEnter={(e) => {if (!on) e.currentTarget.style.background = "#F9FAFB";}}
            onMouseLeave={(e) => {if (!on) e.currentTarget.style.background = "transparent";}}>
                <span style={{ flex: 1 }}>{v.label}</span>
                <span style={{
                fontSize: 11, fontWeight: 600,
                background: on ? "rgba(0,76,230,0.12)" : "#F2F4F7",
                color: on ? "#004CE6" : "#667085",
                borderRadius: 500, padding: "1px 6px", minWidth: 18, textAlign: "center"
              }}>{counts[v.id] || 0}</span>
              </div>);

        })}
          <div style={{ height: 1, background: "#E4E7EC", margin: "4px 4px" }} />
          <div role="menuitem"
          onClick={onCustomize}
          style={{
            display: "flex", alignItems: "center", gap: 8, padding: "8px 10px",
            borderRadius: 6, cursor: "pointer", fontSize: 13, color: "#004CE6", fontWeight: 500
          }}
          onMouseEnter={(e) => e.currentTarget.style.background = "#F9FAFB"}
          onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}>
            <I.settings size={13} stroke="#004CE6" /> Customize
          </div>
        </div>
      }
    </div>);

};

// Open / Closed status dropdown — the second half of the old views pill.
//
// Deliberately plainer than ViewPill: no count badge, and no blue "active"
// treatment even when narrowed to Open or Closed. Status is a lens on whatever
// view is selected rather than a filter in its own right, so it reads as
// chrome. Default and hover are the only two states. It is also independent of
// Organize conversations — the user cannot un-pin it.
// Only the two real states. A combined "both" option would duplicate the All
// view sitting immediately to its left.
const STATUS_OPTIONS = [
{ id: "open", label: "Open" },
{ id: "closed", label: "Closed" }];

const StatusPill = ({ value, open, onToggle, onPick }) => {
  const current = STATUS_OPTIONS.find((o) => o.id === value) || STATUS_OPTIONS[0];
  return (
    <div style={{ position: "relative" }}>
      <button
        onClick={onToggle}
        aria-haspopup="menu"
        aria-expanded={open}
        style={{
          display: "inline-flex", alignItems: "center", gap: 6, padding: "0 10px 0 12px",
          borderRadius: 8, border: "1px solid #E4E4E7",
          background: open ? "#F9FAFB" : "#FFFFFF", color: "#344054",
          fontSize: 14, fontWeight: 500, cursor: "pointer", whiteSpace: "nowrap",
          fontFamily: "inherit", height: 32
        }}
        onMouseEnter={(e) => e.currentTarget.style.background = "#F9FAFB"}
        onMouseLeave={(e) => e.currentTarget.style.background = open ? "#F9FAFB" : "#FFFFFF"}>
        {current.label}
        <span style={{ display: "inline-flex", transform: open ? "rotate(180deg)" : "none", transition: "transform .15s ease" }}>
          <I.chevDown size={14} stroke="#667085" />
        </span>
      </button>
      {open &&
      <div role="menu" style={{
        position: "absolute", top: "calc(100% + 6px)", left: 0, zIndex: 30,
        minWidth: 176, background: "#FFFFFF", border: "1px solid #E4E7EC", borderRadius: 8,
        boxShadow: "0 12px 28px rgba(16,24,40,.12)", padding: 4
      }}>
          {STATUS_OPTIONS.map((o) => {
          const on = o.id === current.id;
          return (
            <div key={o.id} role="menuitemradio" aria-checked={on}
            onClick={() => onPick(o.id)}
            style={{
              display: "flex", alignItems: "center", gap: 8, padding: "8px 10px",
              borderRadius: 6, cursor: "pointer", fontSize: 13,
              color: "#344054", fontWeight: on ? 600 : 500
            }}
            onMouseEnter={(e) => e.currentTarget.style.background = "#F9FAFB"}
            onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}>
                <span style={{ flex: 1 }}>{o.label}</span>
                {on && <I.check size={13} stroke="#667085" />}
              </div>);

        })}
        </div>
      }
    </div>);

};

// Multi-select checkbox sub-menu (opens to the right of the main filter menu root)
//
// Options are plain strings for most groups. Phone numbers pass objects —
// { value, label, sub, flag } — so a line can show its flag and number without
// needing a second component; search covers both lines of the row.
const CheckboxDropdown = ({ title, options, selected, onToggle, onClear, onBack, onClose }) => {
  const [q, setQ] = useState("");
  const norm = (o) => typeof o === "string" ? { value: o, label: o } : o;
  const needle = q.toLowerCase();
  const filtered = options.map(norm).filter((o) =>
  o.label.toLowerCase().includes(needle) || (o.sub || "").toLowerCase().includes(needle)
  );
  return (
    <div style={{
      position: "absolute", top: "calc(100% + 6px)", right: 0, zIndex: 30,
      width: 260, background: "#FFFFFF", border: "1px solid #E4E7EC", borderRadius: 6,
      boxShadow: "0 12px 28px rgba(16,24,40,.12)", padding: 4, display: "flex", flexDirection: "column"
    }}>
      <div style={{
        display: "flex", alignItems: "center", gap: 6, padding: "6px 6px 6px 4px",
        borderBottom: "1px solid #F2F4F7", marginBottom: 4
      }}>
        <button onClick={onBack} title="Back" style={{
          width: 24, height: 24, borderRadius: 4, border: "none", background: "transparent",
          display: "inline-flex", alignItems: "center", justifyContent: "center", cursor: "pointer"
        }}
        onMouseEnter={(e) => e.currentTarget.style.background = "#F2F4F7"}
        onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}>
          <I.chevLeft size={14} stroke="#667085" />
        </button>
        <span style={{ fontSize: 13, fontWeight: 600, color: "#101828", flex: 1 }}>{title}</span>
        {selected.length > 0 &&
        <button onClick={onClear} style={{
          fontSize: 11, color: "#004CE6", fontWeight: 500, background: "transparent",
          border: "none", padding: "2px 6px", borderRadius: 4, cursor: "pointer"
        }}>Clear</button>
        }
      </div>
      <div style={{ position: "relative", padding: "0 4px 4px" }}>
        <I.search size={12} stroke="#98A2B3" style={{ position: "absolute", left: 12, top: "50%", transform: "translateY(-50%)" }} />
        <input value={q} onChange={(e) => setQ(e.target.value)}
        placeholder={`Search ${title.toLowerCase()}`}
        style={{
          width: "100%", height: 28, padding: "0 8px 0 28px", borderRadius: 4,
          border: "1px solid #E4E7EC", fontSize: 12, outline: "none", fontFamily: "inherit"
        }} />
      </div>
      <div style={{ maxHeight: 32 * 6, overflowY: "auto", padding: "2px 0" }}>
        {filtered.map((opt) => {
          const on = selected.includes(opt.value);
          return (
            <div key={opt.value} onClick={() => onToggle(opt.value)}
            style={{
              display: "flex", alignItems: "center", gap: 10, padding: "7px 10px",
              borderRadius: 4, cursor: "pointer", fontSize: 13, color: "#344054"
            }}
            onMouseEnter={(e) => e.currentTarget.style.background = "#F9FAFB"}
            onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}>
              <span style={{
                width: 14, height: 14, borderRadius: 3,
                border: `1.5px solid ${on ? "#004CE6" : "#D0D5DD"}`,
                background: on ? "#004CE6" : "#FFFFFF",
                display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0
              }}>
                {on && <I.check size={10} stroke="#FFFFFF" />}
              </span>
              {opt.flag && <span style={{ fontSize: 14, lineHeight: 1, flexShrink: 0 }}>{opt.flag}</span>}
              <span style={{ flex: 1, minWidth: 0 }}>
                <span style={{ display: "block", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{opt.label}</span>
                {opt.sub && <span style={{ display: "block", fontSize: 10, color: "#667085", marginTop: 1 }}>{opt.sub}</span>}
              </span>
            </div>);

        })}
        {filtered.length === 0 &&
        <div style={{ fontSize: 12, color: "#98A2B3", padding: "10px 12px", textAlign: "center" }}>No matches</div>
        }
      </div>
    </div>);

};

// Pill-shaped dropdown used for the primary row (Status + Phone line)
const ToggleDropdown = ({ label, icon: IconC, value, active, open, onToggle, options, selectedId, onPick, scrollable }) => {
  return (
    <div style={{ position: "relative" }}>
      <button onClick={onToggle}
      style={{
        display: "inline-flex", alignItems: "center", gap: 5, padding: "4px 10px",
        borderRadius: 500,
        border: `1px solid ${open ? "#004CE6" : active ? "#A4BCFD" : "#E4E7EC"}`,
        background: active ? "#EFF8FF" : "#FFFFFF",
        color: active ? "#004CE6" : "#344054",
        fontSize: 12, fontWeight: 500, cursor: "pointer", whiteSpace: "nowrap"
      }}>
        {IconC && <IconC size={12} stroke={active ? "#004CE6" : "#667085"} />}
        {!active && <span style={{ color: "#98A2B3" }}>{label}:</span>}
        <span style={{ fontWeight: 600 }}>{value}</span>
        <I.chevDown size={11} stroke={active ? "#004CE6" : "#98A2B3"} />
      </button>
      {open &&
      <div style={{
        position: "absolute", top: "calc(100% + 4px)", left: 0, minWidth: 200, zIndex: 20,
        background: "#FFFFFF", border: "1px solid #E4E7EC", borderRadius: 6,
        boxShadow: "0 8px 16px rgba(48,49,51,0.10)", padding: 4,
        maxHeight: scrollable ? 36 * 5 + 8 : "none",
        overflowY: scrollable ? "auto" : "visible"
      }}>
          {options.map((o) => {
          const sel = o.id === selectedId;
          return (
            <div key={o.id} onClick={() => onPick(o)} style={{
              padding: "7px 10px", borderRadius: 4, fontSize: 12,
              background: sel ? "#EFF8FF" : "transparent",
              color: sel ? "#004CE6" : "#344054",
              cursor: "pointer", fontWeight: sel ? 600 : 400,
              display: "flex", alignItems: "center", gap: 6
            }}
            onMouseEnter={(e) => {if (!sel) e.currentTarget.style.background = "#F9FAFB";}}
            onMouseLeave={(e) => {if (!sel) e.currentTarget.style.background = "transparent";}}>
                {sel ? <I.check size={12} stroke="#004CE6" /> : <span style={{ width: 12, display: "inline-block" }} />}
                <span style={{ flex: 1 }}>{o.label}</span>
                {typeof o.count === "number" && o.count > 0 &&
              <span style={{
                fontSize: 10, fontWeight: 600, background: "#F2F4F7",
                color: "#667085", borderRadius: 500, padding: "1px 5px", minWidth: 16, textAlign: "center"
              }}>{o.count}</span>
              }
              </div>);

        })}
        </div>
      }
    </div>);

};

const DropdownChip = ({ label, value, options, open, onToggle, onPick, icon: IconC }) => {
  const isDefault = value.startsWith("All");
  return (
    <div style={{ position: "relative" }}>
      <button onClick={onToggle}
      style={{
        display: "inline-flex", alignItems: "center", gap: 5, padding: "4px 8px 4px 8px",
        borderRadius: 4, border: `1px solid ${open ? "#004CE6" : isDefault ? "#E4E7EC" : "#A4BCFD"}`,
        background: isDefault ? "#FFFFFF" : "#EEF4FF",
        color: isDefault ? "#667085" : "#3538CD",
        fontSize: 11, fontWeight: 500, cursor: "pointer", whiteSpace: "nowrap"
      }}>
        <IconC size={12} stroke={isDefault ? "#667085" : "#3538CD"} />
        <span style={{ color: "#98A2B3" }}>{label}:</span>
        <span style={{ fontWeight: 600 }}>{isDefault ? "Any" : value}</span>
        <I.chevDown size={11} stroke={isDefault ? "#98A2B3" : "#3538CD"} />
      </button>
      {open &&
      <div style={{
        position: "absolute", top: "calc(100% + 4px)", left: 0, minWidth: 180, zIndex: 20,
        background: "#FFFFFF", border: "1px solid #E4E7EC", borderRadius: 6,
        boxShadow: "0 0 1px rgba(48,49,51,0.05), 0 8px 16px rgba(48,49,51,0.10)",
        padding: 4
      }}>
          {options.map((o) => {
          const sel = o === value;
          return (
            <div key={o} onClick={() => onPick(o)} style={{
              padding: "7px 10px", borderRadius: 4, fontSize: 12,
              background: sel ? "#EFF8FF" : "transparent",
              color: sel ? "#004CE6" : "#344054",
              cursor: "pointer", fontWeight: sel ? 600 : 400,
              display: "flex", alignItems: "center", gap: 6
            }}
            onMouseEnter={(e) => {if (!sel) e.currentTarget.style.background = "#F9FAFB";}}
            onMouseLeave={(e) => {if (!sel) e.currentTarget.style.background = "transparent";}}>
                {sel && <I.check size={12} stroke="#004CE6" />}
                {!sel && <span style={{ width: 12, display: "inline-block" }} />}
                {o}
              </div>);

        })}
        </div>
      }
    </div>);

};

const iconBtn = {
  width: 26, height: 26, display: "inline-flex", alignItems: "center", justifyContent: "center",
  borderRadius: 4, border: "1px solid #E4E7EC", background: "#FFFFFF", cursor: "pointer", padding: 0
};

const ThreadRow = ({ thread, active, onClick, currentUserId, exiting, rowLines = "r3" }) => {
  // A conversation marked unread by hand has no count — just the dot.
  const isUnread = thread.unread > 0 || !!thread.__dotUnread;
  const mine = thread.assignee?.id === currentUserId;
  const hasSavedContact = !!thread.isContact;
  // Simplified list: show tags on at most 3 conversations
  const showTags = !!thread.showTags && thread.tags && thread.tags.length > 0;
  /* The two-row list is this row minus the last event — the preview line, its
     channel glyph and its delivery ticks. The labels stay, so the second row
     is the tags: what the conversation *is* rather than what was last said in
     it, on the argument that the thread is one click away and the labels are
     what you scan a list of eighty by. */
  const twoRow = rowLines === "r2";
  // When the contact isn't saved, surface the phone number as the primary label
  // — falling back to the name, since a conversation started from a typed name
  // has no number yet and would otherwise render a blank row.
  const primaryLabel = (hasSavedContact ? thread.name : thread.phone) || thread.name;

  // Force this row to re-render when a session message is appended in this
  // thread, so the preview reflects the just-sent message.
  const [, setTick] = useState(0);
  useEffect(() => {
    const onAppend = (e) => {
      if (e.detail && e.detail.threadId === thread.id) setTick((t) => t + 1);
    };
    window.addEventListener("convo:appendMessage", onAppend);
    return () => window.removeEventListener("convo:appendMessage", onAppend);
  }, [thread.id]);

  const preview = buildLastInteractionPreview(thread) || {
    kind: thread.channel,
    text: thread.preview || "",
    callType: thread.type
  };
  const isCall = preview.kind === "call";
  const isMsg = preview.kind === "sms" || preview.kind === "whatsapp" || preview.kind === "voice";
  const isNote = preview.kind === "note";
  const isOut = isMsg && preview.dir === "out";

  // Leading icon — different glyph per kind
  let LeadIcon = null;
  let leadColor = "#98A2B3";
  if (isCall) {
    const ci = channelIcon("call", preview.callType);
    LeadIcon = ci.icon;
    leadColor = ci.color;
  } else if (preview.kind === "whatsapp") {
    LeadIcon = I.whatsapp || I.chat;leadColor = "#25D366";
  } else if (isNote) {
    LeadIcon = I.note;leadColor = "#B54708";
  }
  // SMS rows get no leading icon — keeps the list calm; only calls/notes/WA stand out.

  /* Named rather than written twice: it sits on the event line in the
     three-row row and on the tag line in the two-row one, and the two must not
     drift into being two different badges. */
  const unreadEl = !isUnread ? null :
  thread.__dotUnread ?
  <span style={{
    width: 8, height: 8, borderRadius: 500,
    background: "#004CE6", flexShrink: 0
  }} /> :
  <div style={{
    minWidth: 18, height: 18, borderRadius: 500,
    background: "#004CE6", color: "#FFFFFF", fontSize: 10, fontWeight: 600,
    display: "flex", alignItems: "center", justifyContent: "center", padding: "0 5px", flexShrink: 0
  }}>{thread.unread}</div>;

  return (
    <div onClick={onClick}
    className={`convo-row${exiting ? " convo-row-exit" : ""}`}
    style={{
      padding: "16px 14px", borderBottom: "1px solid #F2F4F7", cursor: "pointer",
      background: active ? "#EFF8FF" : isUnread ? "#FCFDFE" : "#FFFFFF",
      borderLeft: active ? "3px solid #004CE6" : "3px solid transparent",
      display: "flex", gap: 10, alignItems: "flex-start", position: "relative"
    }}
    onMouseEnter={(e) => {if (!active) e.currentTarget.style.background = "#F9FAFB";}}
    onMouseLeave={(e) => {if (!active) e.currentTarget.style.background = isUnread ? "#FCFDFE" : "#FFFFFF";}}>
      
      {/* 24px in the two-row row: a 40px avatar is taller than the two lines
          beside it, so it sets the row's height instead of the content doing
          it and the initials end up floating opposite a gap. */}
      <div style={{
        width: twoRow ? 24 : 40, height: twoRow ? 24 : 40,
        borderRadius: 500, background: "#EFF8FF", color: "#175CD3",
        display: "flex", alignItems: "center", justifyContent: "center",
        fontSize: twoRow ? 10 : 13, fontWeight: 600,
        flexShrink: 0
      }}>{thread.avatar}</div>

      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 2 }}>
          <span style={{
            fontSize: 14, fontWeight: isUnread ? 600 : 500, color: "#101828",
            overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1
          }}>{primaryLabel}</span>
          <span style={{ fontSize: 11, color: "#98A2B3", fontWeight: 400, flexShrink: 0 }}>
            {thread.time}
          </span>
        </div>
        {twoRow ?
        /* The tags are the second row now, with the unread count on the end of
           it. Rendered when there is either — a conversation with no tags and
           nothing unread is a one-line row, which is right: there is nothing
           else true about it. */
        (showTags || isUnread) &&
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 4 }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            {showTags && <ThreadTagRow tags={thread.tags} marginTop={0} />}
          </div>
          {unreadEl}
        </div> :
        <>
        <div style={{ display: "flex", alignItems: "flex-end", gap: 8 }}>
          <div style={{
            flex: 1, minWidth: 0,
            display: "flex", alignItems: "center", gap: 6,
            fontSize: 12, color: isUnread ? "#344054" : "#667085", lineHeight: 1.4
          }}>
            {LeadIcon && <LeadIcon size={12} stroke={isNote ? "#98A2B3" : leadColor} style={{ flexShrink: 0 }} />}
            {isOut &&
            <span style={{ display: "inline-flex", alignItems: "center", flexShrink: 0 }}>
                <StatusTicks status={preview.status} />
              </span>
            }
            {isNote &&
            <span style={{
              fontSize: 12, fontWeight: 500, color: "#101828",
              flexShrink: 0
            }}>Note</span>
            }
            <span style={{
              flex: 1, minWidth: 0,
              fontStyle: isCall ? "normal" : "normal",
              color: isCall && preview.callType === "missed" ? "#B42318" : isUnread ? "#344054" : "#667085",
              fontWeight: isCall && preview.callType === "missed" ? 500 : "inherit",
              whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"
            }}>{preview.text}</span>
          </div>
          {unreadEl}
        </div>
        {showTags && <ThreadTagRow tags={thread.tags} />}
        </>
        }
      </div>
    </div>);

};

// Tag colours are derived from the label so a given tag always looks the same
// wherever it appears, without hand-maintaining a colour map.
const TAG_PALETTE = [
{ fg: "#B54708", bd: "#FEDF89" }, // amber
{ fg: "#5925DC", bd: "#D9D6FE" }, // violet
{ fg: "#175CD3", bd: "#B2DDFF" }, // blue
{ fg: "#027A48", bd: "#A6F4C5" }, // green
{ fg: "#C11574", bd: "#FCCEEE" }, // pink
{ fg: "#344054", bd: "#D0D5DD" }] // slate
;
const tagStyleFor = (label) => {
  let h = 0;
  for (let i = 0; i < label.length; i++) h = (h * 31 + label.charCodeAt(i)) >>> 0;
  return TAG_PALETTE[h % TAG_PALETTE.length];
};

// Third row of a conversation: up to three tags, then a +N overflow chip.
const ThreadTagRow = ({ tags, max = 3, marginTop = 8 }) => {
  const shown = tags.slice(0, max);
  const extra = tags.length - shown.length;
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 6, marginTop, flexWrap: "nowrap", overflow: "hidden" }}>
      {shown.map((t) => {
        const c = tagStyleFor(t);
        return (
          <span key={t} title={t} style={{
            flexShrink: 1, minWidth: 0,
            height: 20, boxSizing: "border-box",
            padding: "0 8px", borderRadius: 6,
            border: `1px solid ${c.bd}`, color: c.fg, background: "#FFFFFF",
            fontSize: 11, fontWeight: 500, lineHeight: "18px",
            whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"
          }}>{t}</span>);

      })}
      {extra > 0 &&
      <span title={tags.slice(max).join(", ")} style={{
        flexShrink: 0, height: 20, boxSizing: "border-box",
        padding: "0 7px", borderRadius: 6,
        background: "#F2F4F7", color: "#475467",
        fontSize: 11, fontWeight: 600, lineHeight: "20px"
      }}>+{extra}</span>
      }
    </div>);

};

// Which threads each view shows. Mirrors the descriptions users read in
// Settings → Conversation Settings → Organize conversations.
const filterByView = (threads, viewId, currentUserId) => {
  switch (viewId) {
    case "unread":     return threads.filter((t) => t.unread > 0);
    case "open":       return threads.filter((t) => t.status !== "closed");
    case "closed":     return threads.filter((t) => t.status === "closed");
    case "assigned":   return threads.filter((t) => t.assignee && t.assignee.id === currentUserId && t.status !== "closed");
    case "unassigned": return threads.filter((t) => !t.assignee && t.status !== "closed");
    case "drafts":     return threads.filter((t) => !!t.draft);
    case "all":
    default:           return threads;
  }
};
window.filterByView = filterByView;

/* Nothing in the account yet. Reuses the shared .gp-empty block so this reads
   as the same kind of empty as every other page's. */
const EmptyThreadListState = () => (
  <div className="gp-empty" style={{ padding: "56px 20px" }}>
    <span className="gp-empty-ic">
      <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor"
      strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
        <circle cx="12" cy="7" r="4" />
      </svg>
    </span>
    <h3 className="gp-empty-title">No conversations yet</h3>
    <p className="gp-empty-sub">
      Add a contact first — then you can start a conversation with them over SMS,
      WhatsApp or a call.
    </p>
    {/* Link rather than a bordered button: this sends you to Contacts, it
        doesn't add anything from here. */}
    <button type="button" className="gp-empty-cta gp-empty-cta--link"
    onClick={() => window.jcGoToPage && window.jcGoToPage("contacts")}>
      Add a contact
    </button>
  </div>);


const ThreadList = ({ threads, selected, onSelect, view, onViewChange, status = "open", onStatusChange, currentUser, query, onQueryChange, showCollapseToggle, categoriesCollapsed, onToggleCategories, onStartNew, exitingId, rowLines = "r3", listRef }) => {
  // View first, then the Open/Closed lens narrows what it returned.
  let filtered = filterByView(filterByView(threads, view, currentUser.id), status, currentUser.id);


  // Apply query
  if (query?.trim()) {
    const q = query.toLowerCase();
    filtered = filtered.filter((t) =>
    t.name.toLowerCase().includes(q) ||
    t.phone.toLowerCase().includes(q) ||
    (t.preview || "").toLowerCase().includes(q)
    );
  }

  // Publish the order on screen so panels that only receive a single thread —
  // the contact sheet's CRM widget — can tell where it sits in the list.
  window.__visibleThreadOrder = filtered.map((t) => t.id);

  // When a conversation's status change drops it out of this view, keep the row
  // on screen for its exit animation. In views it still belongs to — All, most
  // obviously — there's nothing to animate and the row simply stays put.
  const lastOrder = useRef([]);
  const exitingThread = exitingId && !filtered.some((t) => t.id === exitingId) ?
  threads.find((t) => t.id === exitingId) : null;
  useEffect(() => {
    if (!exitingThread) lastOrder.current = filtered.map((t) => t.id);
  });
  const rows = filtered.slice();
  if (exitingThread) {
    const at = lastOrder.current.indexOf(exitingId);
    rows.splice(at < 0 ? 0 : at, 0, exitingThread);
  }

  // Count per view so the picker can show how many each holds. The status lens
  // is applied here too — otherwise a badge would promise 20 conversations
  // while the list below it, narrowed to Closed, shows three.
  const statusScoped = filterByView(threads, status, currentUser.id);
  const viewCounts = {};
  (window.JC_CONVO_VIEWS || []).forEach((v) => {
    viewCounts[v.id] = filterByView(statusScoped, v.id, currentUser.id).length;
  });
  const handleStartNew = () => {
    if (onStartNew) { onStartNew(); return; }
    if (window.showAppToast) window.showAppToast("Start a new conversation", "ok");
  };
  return (
    <div ref={listRef} className="convo-list-col" style={{
      width: 430, borderRight: "1px solid #E4E7EC", background: "#FFFFFF",
      display: "flex", flexDirection: "column", flexShrink: 0
    }}>
      <ThreadListHeader view={view} onViewChange={onViewChange}
      status={status} onStatusChange={onStatusChange}
      viewCounts={viewCounts}
      query={query || ""} onQueryChange={onQueryChange}
      showCollapseToggle={showCollapseToggle}
      categoriesCollapsed={categoriesCollapsed}
      onToggleCategories={onToggleCategories}
      onStartNew={handleStartNew} />
      <div style={{ flex: 1, overflowY: "auto" }} data-comment-anchor="35b5e6404d-div-883-7">
        {rows.length === 0 ?
        /* Two different empties. A brand-new account has nothing to converse
           with yet, so it points at the step before this one — add a contact.
           A filter that excludes everything is the ordinary case and keeps its
           one-liner, since the fix is to widen the filter, not add anything. */
        window.jcIsEmptyProfile && window.jcIsEmptyProfile() ?
        <EmptyThreadListState /> :
        <div style={{ padding: 40, textAlign: "center", color: "#98A2B3", fontSize: 12 }}>
            No conversations match
          </div> :
        rows.map((t) =>
        <ThreadRow key={t.id} thread={t} active={selected === t.id}
        onClick={() => onSelect(t.id)} currentUserId={currentUser.id}
        exiting={t.id === exitingId} rowLines={rowLines} />
        )}
      </div>
    </div>);

};

window.ThreadList = ThreadList;