// ---------- Contacts (caller-history CRM, Angkasa request Aug 2026) ----------
// Read side is a pure join computed server-side by customerHistoryService.js
// over callLogStore/ticketStore/kbGapStore/consentStore — this view has no
// data of its own beyond the two small writes (notes, tags) that genuinely
// have nowhere else to live. Follows the same Card/Btn/Field/Badge + voxApi
// conventions TicketsView (ops.jsx) already uses.

function crmVerifiedTone(verified) {
  return verified ? "ok" : "warn";
}

// Best-effort Malaysian phone display — falls back to a plain "+digits"
// string when the length doesn't match a recognizable mobile/landline shape,
// so an unusual number never renders blank or throws.
function crmFormatPhone(digits = "") {
  const d = String(digits || "");
  if (!d) return "—";
  if (d.startsWith("60")) {
    const rest = d.slice(2);
    if (rest.length === 9) return `+60 ${rest.slice(0, 2)}-${rest.slice(2, 5)} ${rest.slice(5)}`;
    if (rest.length === 10) return `+60 ${rest.slice(0, 3)}-${rest.slice(3, 6)} ${rest.slice(6)}`;
  }
  return `+${d}`;
}

function crmInitials(name = "") {
  const parts = String(name || "").trim().split(/\s+/).filter(Boolean);
  const initials = parts.slice(0, 2).map((w) => w[0]?.toUpperCase() || "").join("");
  return initials || "?";
}

// Server-side export streams a real CSV file, but the route needs a Bearer
// token — a plain <a href> navigation can't carry that header, so this
// fetches it as an authenticated blob and triggers the save itself, same
// idea as the client-built CSV exports already elsewhere in this app
// (dashboard.jsx's exportCurrentCsv), just sourced from the server route
// instead of already-loaded rows.
async function downloadContactsCsv() {
  let authHeader = {};
  try {
    const s = JSON.parse(localStorage.getItem("voxpro.session") || "null");
    if (s && s.token) authHeader = { Authorization: `Bearer ${s.token}` };
  } catch {}
  const res = await fetch("/crm/contacts/export/csv", { headers: authHeader });
  if (!res.ok) { alert("Export failed"); return; }
  const blob = await res.blob();
  const a = document.createElement("a");
  a.href = URL.createObjectURL(blob);
  a.download = `contacts-${new Date().toISOString().slice(0, 10)}.csv`;
  a.click();
  URL.revokeObjectURL(a.href);
}

function ContactsListView({ lang, onOpen }) {
  const t = makeT(lang);
  const [rows, setRows] = React.useState([]);
  const [summary, setSummary] = React.useState(null);
  const [trend, setTrend] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [q, setQ] = React.useState("");
  const [filter, setFilter] = React.useState("all");
  const [selected, setSelected] = React.useState(() => new Set());
  const [bulkTagInput, setBulkTagInput] = React.useState("");
  const [applyingBulkTag, setApplyingBulkTag] = React.useState(false);

  async function load(query, activeFilter) {
    setLoading(true);
    try {
      const params = new URLSearchParams();
      if (query) params.set("q", query);
      if (activeFilter && activeFilter !== "all") params.set("filter", activeFilter);
      const data = await voxApiOptional(`/crm/contacts?${params.toString()}`, { contacts: [], summary: null });
      setRows(data.contacts || []);
      setSummary(data.summary || null);
    } finally {
      setLoading(false);
    }
  }

  React.useEffect(() => {
    const handle = setTimeout(() => load(q, filter), 250);
    return () => clearTimeout(handle);
  }, [q, filter]);

  function toggleSelected(phoneDigits) {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(phoneDigits)) next.delete(phoneDigits);
      else next.add(phoneDigits);
      return next;
    });
  }

  function toggleSelectAll() {
    setSelected((prev) => (prev.size === rows.length ? new Set() : new Set(rows.map((r) => r.phoneDigits))));
  }

  async function applyBulkTag() {
    const tag = bulkTagInput.trim();
    if (!tag || selected.size === 0) return;
    setApplyingBulkTag(true);
    try {
      await voxApi("/crm/contacts/bulk-tags", {
        method: "POST",
        body: JSON.stringify({ phones: [...selected], tag }),
      });
      setBulkTagInput("");
      setSelected(new Set());
      await load(q, filter);
    } catch (e) {
      alert(e.message || t("crm.saveFailed"));
    } finally {
      setApplyingBulkTag(false);
    }
  }

  // Repeat-caller-rate trend loads once independently of search/filter — it
  // describes the whole caller base over time, same reasoning as `summary`.
  React.useEffect(() => {
    voxApiOptional("/crm/insights/caller-trend?weeks=12", { trend: [] }).then((data) => {
      setTrend(Array.isArray(data.trend) ? data.trend : []);
    });
  }, []);

  const repeatRateTile = React.useMemo(() => {
    if (!trend.length) return null;
    const latest = trend[trend.length - 1];
    const prior = trend.length > 1 ? trend[trend.length - 2] : null;
    const deltaVal = prior ? Math.round((latest.repeatRatePct - prior.repeatRatePct) * 10) / 10 : 0;
    return {
      label: t("crm.stat.repeatRate"),
      value: `${latest.repeatRatePct}%`,
      delta: `${deltaVal >= 0 ? "+" : ""}${deltaVal}%`,
      spark: trend.map((w) => w.repeatRatePct),
    };
  }, [trend, t]);

  const filters = [
    { id: "all", label: t("crm.filter.all") },
    { id: "verified", label: t("crm.filter.verified") },
    { id: "unverified", label: t("crm.filter.unverified") },
    { id: "kb-gap", label: t("crm.filter.kbGap") },
    { id: "vip", label: t("crm.filter.vip") },
    { id: "at-risk", label: t("crm.filter.atRisk") },
  ];

  return (
    <div className="view-stack">
      <div className="row" style={{ justifyContent: "space-between", alignItems: "flex-start", marginBottom: 16, flexWrap: "wrap", gap: 12 }}>
        <div>
          <div className="strong" style={{ fontSize: 18 }}>{t("crm.title")}</div>
          <div className="muted text-12 mt-1">{t("crm.subtitle")}</div>
        </div>
        <Btn variant="ghost" size="sm" icon="download" onClick={downloadContactsCsv}>{t("crm.exportCsv")}</Btn>
      </div>

      {summary && (
        <div className="g-4 mb-3">
          <div className="stat">
            <div className="stat-label">{t("crm.stat.total")}</div>
            <div className="stat-value">{summary.total}</div>
          </div>
          <div className="stat">
            <div className="stat-label">{t("crm.stat.consentCoverage")}</div>
            <div className="stat-value">{summary.consentCoveragePct}%</div>
          </div>
          <div className="stat">
            <div className="stat-label">{t("crm.stat.atRiskCount")}</div>
            <div className="stat-value" style={summary.atRiskCount > 0 ? { color: "var(--err)" } : undefined}>{summary.atRiskCount}</div>
          </div>
          <div className="stat">
            <div className="stat-label">{t("crm.stat.totalSpend")}</div>
            <div className="stat-value">{fmtRM(summary.totalSpendMyr)}</div>
          </div>
        </div>
      )}

      {repeatRateTile && (
        <Card title={t("crm.chart.callerTrend")} subtitle={t("crm.stat.repeatRate")} className="mb-3">
          <div className="row gap-4" style={{ alignItems: "center", flexWrap: "wrap" }}>
            <div style={{ minWidth: 120 }}>
              <div className="stat-value" style={{ fontSize: 28 }}>{repeatRateTile.value}</div>
              <span className={`stat-delta ${repeatRateTile.delta.startsWith("-") ? "down" : ""}`}>
                {repeatRateTile.delta.startsWith("-") ? "↓" : "↑"} {repeatRateTile.delta} {t("crm.stat.vsLastWeek")}
              </span>
            </div>
            <div style={{ flex: 1, minWidth: 240 }}>
              <LineChart data={repeatRateTile.spark} height={90} />
            </div>
          </div>
        </Card>
      )}

      <div className="row gap-3 mb-3" style={{ flexWrap: "wrap", alignItems: "center" }}>
        <div style={{ position: "relative", maxWidth: 360, flex: 1, minWidth: 240 }}>
          <Icon name="search" style={{ position: "absolute", left: 10, top: "50%", transform: "translateY(-50%)", width: 14, height: 14, color: "var(--c-text-4)" }} />
          <input
            className="fld"
            style={{ paddingLeft: 30 }}
            placeholder={t("crm.searchPlaceholder")}
            value={q}
            onChange={(e) => setQ(e.target.value)}
          />
        </div>
      </div>

      <div className="row gap-2 mb-3" style={{ flexWrap: "wrap" }}>
        {filters.map((f) => (
          <button
            key={f.id}
            className="chip"
            style={filter === f.id ? { background: "var(--a-soft)", color: "var(--a)", borderColor: "var(--a-line)", fontWeight: 600 } : undefined}
            onClick={() => setFilter(f.id)}
          >
            {f.label}
          </button>
        ))}
      </div>

      {selected.size > 0 && (
        <div className="row gap-2 mb-3" style={{ alignItems: "center", padding: "8px 12px", background: "var(--a-soft)", border: "1px solid var(--a-line)", borderRadius: 8 }}>
          <span className="text-12" style={{ fontWeight: 600 }}>{t("crm.bulkTag.selectedCount", { count: selected.size })}</span>
          <input
            className="fld"
            style={{ maxWidth: 180, height: 28 }}
            placeholder={t("crm.bulkTag.placeholder")}
            value={bulkTagInput}
            onChange={(e) => setBulkTagInput(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") applyBulkTag(); }}
            disabled={applyingBulkTag}
          />
          <Btn variant="primary" size="sm" onClick={applyBulkTag} disabled={applyingBulkTag || !bulkTagInput.trim()}>{t("crm.bulkTag.apply")}</Btn>
          <Btn variant="ghost" size="sm" onClick={() => setSelected(new Set())}>{t("crm.bulkTag.clear")}</Btn>
        </div>
      )}

      <Card>
        <div className="tbl-wrap">
          <table className="tbl">
            <thead>
              <tr>
                <th style={{ width: 32 }}>
                  <input type="checkbox" checked={rows.length > 0 && selected.size === rows.length} onChange={toggleSelectAll} onClick={(e) => e.stopPropagation()} />
                </th>
                <th>{t("crm.table.name")}</th>
                <th>{t("crm.table.phone")}</th>
                <th>{t("crm.table.lastCall")}</th>
                <th>{t("crm.table.totalCalls")}</th>
                <th>{t("crm.table.repeatedQs")}</th>
                <th>{t("crm.table.kbGap")}</th>
                <th>{t("crm.table.spend")}</th>
                <th>{t("crm.table.tags")}</th>
              </tr>
            </thead>
            <tbody>
              {rows.length === 0 && !loading && (
                <tr><td colSpan={9} style={{ textAlign: "center", color: "var(--c-text-4)", padding: 20 }}>{t("crm.noneMatch")}</td></tr>
              )}
              {rows.map((row) => (
                <tr key={row.phoneDigits} style={{ cursor: "pointer" }} onClick={() => onOpen(row.phoneDigits)}>
                  <td onClick={(e) => e.stopPropagation()}>
                    <input type="checkbox" checked={selected.has(row.phoneDigits)} onChange={() => toggleSelected(row.phoneDigits)} />
                  </td>
                  <td>
                    <div className="row gap-2" style={{ alignItems: "center" }}>
                      <span className="text-12" style={{ color: row.name ? "var(--c-text)" : "var(--c-text-4)" }}>
                        {row.name || t("crm.unnamedCaller")}
                      </span>
                      <Badge tone={crmVerifiedTone(row.verified)}>{row.verified ? t("crm.badge.verified") : t("crm.badge.unverified")}</Badge>
                      {row.atRisk && <Badge tone="err">{t("crm.badge.atRisk")}</Badge>}
                    </div>
                  </td>
                  <td className="mono text-11">{crmFormatPhone(row.phoneDigits)}</td>
                  <td className="text-12 muted">{fmtDate(row.lastCallAt)}</td>
                  <td className="mono text-11">{row.totalCalls}</td>
                  <td>{row.repeatedQuestionCount > 0 ? <Badge tone="accent">{row.repeatedQuestionCount}</Badge> : <span className="muted">—</span>}</td>
                  <td>{row.hasOpenKbGap ? <Badge tone="err">{t("crm.badge.open")}</Badge> : <span className="muted">—</span>}</td>
                  <td className="mono text-11">{fmtRM(row.totalCostMyr)}</td>
                  <td>
                    {row.tags.length
                      ? row.tags.map((tag) => <span key={tag} className="chip" style={{ marginRight: 4 }}>{tag}</span>)
                      : <span className="muted">—</span>}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </Card>
    </div>
  );
}

function ContactProfileView({ lang, phone, onBack }) {
  const t = makeT(lang);
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [notFound, setNotFound] = React.useState(false);
  const [noteText, setNoteText] = React.useState("");
  const [savingNote, setSavingNote] = React.useState(false);
  const [tagInput, setTagInput] = React.useState("");
  const [editingTags, setEditingTags] = React.useState(false);
  const [savingTags, setSavingTags] = React.useState(false);
  const [expandedCallId, setExpandedCallId] = React.useState(null);
  const [editingName, setEditingName] = React.useState(false);
  const [nameInput, setNameInput] = React.useState("");
  const [savingName, setSavingName] = React.useState(false);
  const [editingAssign, setEditingAssign] = React.useState(false);
  const [assignInput, setAssignInput] = React.useState("");
  const [savingAssign, setSavingAssign] = React.useState(false);
  const [showSmsModal, setShowSmsModal] = React.useState(false);
  const [smsText, setSmsText] = React.useState("");
  const [sendingSms, setSendingSms] = React.useState(false);
  const [showMergeModal, setShowMergeModal] = React.useState(false);
  const [mergeTarget, setMergeTarget] = React.useState("");
  const [merging, setMerging] = React.useState(false);

  // Every value here already exists on data.calls[] (aiAnalysis, duration,
  // assistantName) — this is presentation over data the profile already
  // fetched, not a new backend call. Hook must run every render (React
  // rules), so it's declared before the loading/notFound early returns
  // below and just no-ops on an empty array until data loads.
  const callInsights = React.useMemo(() => {
    const calls = data?.calls || [];
    const oldestFirst = [...calls].reverse();
    const withDuration = calls.filter((c) => Number.isFinite(c.durationSeconds));
    const avgDurationSeconds = withDuration.length
      ? Math.round(withDuration.reduce((sum, c) => sum + c.durationSeconds, 0) / withDuration.length)
      : 0;
    const assistantCounts = new Map();
    calls.forEach((c) => {
      const name = c.assistantName || "—";
      assistantCounts.set(name, (assistantCounts.get(name) || 0) + 1);
    });
    return {
      sentimentDots: oldestFirst.map((c) => ({ id: c.id, sentiment: c.aiAnalysis?.sentiment || null, at: c.startedAt })),
      avgDurationSeconds,
      byAssistant: [...assistantCounts.entries()].sort((a, b) => b[1] - a[1]),
    };
  }, [data]);

  // Merges calls/notes/tickets/PTP commitments into one chronological feed
  // instead of four separate cards — same source data as those cards,
  // just re-sorted by date. Capped so a caller with hundreds of calls
  // doesn't turn this into another full call-history list.
  const timeline = React.useMemo(() => {
    if (!data) return [];
    const items = [];
    (data.calls || []).forEach((c) => items.push({
      key: `call-${c.id}`, kind: "call", at: c.startedAt || c.updatedAt,
      label: t("crm.timeline.call", { outcome: c.endedReason || "—" }),
    }));
    (data.notes || []).forEach((n) => items.push({
      key: `note-${n.id}`, kind: "note", at: n.at,
      label: t("crm.timeline.note", { author: n.authorName }),
    }));
    (data.tickets || []).forEach((tk) => items.push({
      key: `ticket-${tk.id}`, kind: "ticket", at: tk.createdAt,
      label: t("crm.timeline.ticket", { id: tk.id, status: tk.status }),
    }));
    (data.ptpCommitments || []).forEach((p) => items.push({
      key: `ptp-${p.logId}`, kind: "ptp", at: p.expectedPaymentDate,
      label: t("crm.timeline.ptp", { status: p.status }),
    }));
    return items
      .filter((i) => i.at)
      .sort((a, b) => new Date(b.at) - new Date(a.at))
      .slice(0, 20);
  }, [data, lang]);

  async function load() {
    setLoading(true);
    setNotFound(false);
    try {
      const res = await voxApi(`/crm/contacts/${encodeURIComponent(phone)}`);
      setData(res.contact);
    } catch (e) {
      setNotFound(true);
    } finally {
      setLoading(false);
    }
  }

  React.useEffect(() => { load(); }, [phone]);

  async function addNote() {
    if (!noteText.trim()) return;
    setSavingNote(true);
    try {
      await voxApi(`/crm/contacts/${encodeURIComponent(phone)}/notes`, {
        method: "POST",
        body: JSON.stringify({ text: noteText.trim() }),
      });
      setNoteText("");
      await load();
    } catch (e) {
      alert(e.message || t("crm.saveFailed"));
    } finally {
      setSavingNote(false);
    }
  }

  async function saveTags(nextTags) {
    setSavingTags(true);
    try {
      await voxApi(`/crm/contacts/${encodeURIComponent(phone)}/tags`, {
        method: "POST",
        body: JSON.stringify({ tags: nextTags }),
      });
      await load();
    } catch (e) {
      alert(e.message || t("crm.saveFailed"));
    } finally {
      setSavingTags(false);
    }
  }

  function addTag() {
    const tag = tagInput.trim();
    setTagInput("");
    setEditingTags(false);
    if (!tag) return;
    const next = Array.from(new Set([...(data?.tags || []), tag]));
    saveTags(next);
  }

  function removeTag(tag) {
    const next = (data?.tags || []).filter((x) => x !== tag);
    saveTags(next);
  }

  async function saveName() {
    const next = nameInput.trim();
    setEditingName(false);
    if (next === (data?.name || "")) return;
    setSavingName(true);
    try {
      await voxApi(`/crm/contacts/${encodeURIComponent(phone)}/name`, {
        method: "POST",
        body: JSON.stringify({ name: next }),
      });
      await load();
    } catch (e) {
      alert(e.message || t("crm.saveFailed"));
    } finally {
      setSavingName(false);
    }
  }

  async function saveAssign(nextName) {
    setSavingAssign(true);
    try {
      await voxApi(`/crm/contacts/${encodeURIComponent(phone)}/assign`, {
        method: "POST",
        body: JSON.stringify(nextName ? { id: nextName, name: nextName } : {}),
      });
      await load();
    } catch (e) {
      alert(e.message || t("crm.saveFailed"));
    } finally {
      setSavingAssign(false);
    }
  }

  async function sendSms() {
    if (!smsText.trim()) return;
    setSendingSms(true);
    try {
      await voxApi("/sms/send", {
        method: "POST",
        body: JSON.stringify({ to: `+${data.phoneDigits}`, message: smsText.trim() }),
      });
      setShowSmsModal(false);
      setSmsText("");
      await load();
    } catch (e) {
      alert(e.message || t("crm.saveFailed"));
    } finally {
      setSendingSms(false);
    }
  }

  async function mergeIntoTarget() {
    const target = mergeTarget.trim();
    if (!target) return;
    if (!confirm(t("crm.merge.confirm", { target }))) return;
    setMerging(true);
    try {
      await voxApi("/crm/contacts/merge", {
        method: "POST",
        body: JSON.stringify({ from: data.phoneDigits, into: target }),
      });
      setShowMergeModal(false);
      setMergeTarget("");
      onBack();
    } catch (e) {
      alert(e.message || t("crm.saveFailed"));
    } finally {
      setMerging(false);
    }
  }

  async function requestDeletion() {
    if (!confirm(t("crm.confirmDeletion"))) return;
    try {
      await voxApi("/pdpa/dispose", { method: "POST", body: JSON.stringify({ identifier: phone, mode: "anonymize" }) });
      alert(t("crm.deletionRequested"));
      onBack();
    } catch (e) {
      alert(e.message || t("crm.saveFailed"));
    }
  }

  const backBtn = (
    <button className="btn btn-ghost btn-sm" onClick={onBack} style={{ marginBottom: 14 }}>
      <Icon name="arrR" style={{ transform: "rotate(180deg)" }} />{t("crm.back")}
    </button>
  );

  if (loading) {
    return <div className="view-stack"><div className="muted text-12">{t("crm.loading")}</div></div>;
  }
  if (notFound || !data) {
    return <div className="view-stack">{backBtn}<div className="muted text-12">{t("crm.notFound")}</div></div>;
  }

  return (
    <div className="view-stack">
      {backBtn}

      <div className="card card-pad-lg mb-4">
        <div className="row" style={{ justifyContent: "space-between", alignItems: "flex-start", gap: 20, flexWrap: "wrap" }}>
          <div className="row gap-3" style={{ alignItems: "flex-start" }}>
            <div style={{
              width: 48, height: 48, borderRadius: "50%", flex: "none",
              background: "linear-gradient(135deg, #b8e8e2, #8fd4cc)",
              display: "grid", placeItems: "center", fontSize: 16, fontWeight: 600,
            }}>
              {crmInitials(data.name)}
            </div>
            <div>
              <div className="row gap-2" style={{ alignItems: "center", flexWrap: "wrap" }}>
                {editingName ? (
                  <input
                    autoFocus
                    className="fld"
                    style={{ width: 200, height: 26, fontSize: 14, padding: "4px 8px" }}
                    value={nameInput}
                    onChange={(e) => setNameInput(e.target.value)}
                    onKeyDown={(e) => { if (e.key === "Enter") saveName(); if (e.key === "Escape") setEditingName(false); }}
                    onBlur={saveName}
                    disabled={savingName}
                  />
                ) : (
                  <span
                    className="strong"
                    style={{ fontSize: 16, cursor: "pointer" }}
                    title={t("crm.editName")}
                    onClick={() => { setNameInput(data.name || ""); setEditingName(true); }}
                  >
                    {data.name || t("crm.unnamedCaller")} <span style={{ opacity: 0.4, display: "inline-flex" }}><Icon name="edit" size={11} /></span>
                  </span>
                )}
                <Badge tone={crmVerifiedTone(data.verified)}>{data.verified ? t("crm.badge.verified") : t("crm.badge.unverified")}</Badge>
                {data.consent ? <Badge tone="ok">{t("crm.consentRecorded")}</Badge> : <Badge>{t("crm.noConsent")}</Badge>}
                {data.atRisk && <Badge tone="err">{t("crm.badge.atRisk")}</Badge>}
              </div>
              <div className="mono text-12 muted mt-1">{crmFormatPhone(data.phoneDigits)}</div>
              <div className="text-11 muted mt-2">
                {t("crm.metaLine", { firstSeen: fmtDate(data.firstSeenAt), lastCall: fmtDate(data.lastCallAt), count: data.totalCalls })}
                {" · "}{t("crm.stat.totalSpend")}: {fmtRM(data.totalCostMyr)}
                {" · "}{t("crm.stat.avgDuration")}: {fmtDuration(callInsights.avgDurationSeconds)}
                {data.smsActivityCount > 0 && <>{" · "}{t("crm.stat.channels.sms", { count: data.smsActivityCount })}</>}
              </div>
              {data.atRisk && (
                <div className="text-11 mt-2" style={{
                  color: "var(--err)", background: "var(--err-soft)",
                  border: "1px solid rgba(198,48,64,0.25)", borderRadius: 8,
                  padding: "6px 10px", display: "inline-block",
                }}>
                  <strong>{t("crm.atRisk.why")}:</strong> {t(`crm.atRisk.reason.${data.atRiskReason}`)}
                </div>
              )}
              {callInsights.byAssistant.length > 1 && (
                <div className="text-11 muted mt-2">
                  {t("crm.stat.byAssistant")}: {callInsights.byAssistant.map(([name, count]) => `${name} (${count})`).join(" · ")}
                </div>
              )}
              {callInsights.sentimentDots.length > 0 && (
                <div className="row gap-1 mt-2" style={{ alignItems: "center" }} title={t("crm.stat.sentimentTrend")}>
                  <span className="text-11 muted" style={{ marginRight: 4 }}>{t("crm.stat.sentimentTrend")}:</span>
                  {callInsights.sentimentDots.map((d) => (
                    <span
                      key={d.id}
                      title={`${fmtDate(d.at)} · ${d.sentiment || "—"}`}
                      style={{
                        width: 8, height: 8, borderRadius: "50%", display: "inline-block",
                        background: d.sentiment === "positive" ? "var(--ok)"
                          : d.sentiment === "negative" ? "var(--err)"
                          : d.sentiment === "neutral" ? "var(--c-text-4)"
                          : "var(--c-line-2)",
                      }}
                    />
                  ))}
                </div>
              )}
              <div className="row gap-2 mt-3" style={{ flexWrap: "wrap", alignItems: "center" }}>
                {data.tags.map((tag) => (
                  <span key={tag} className="chip" style={{ cursor: "pointer" }} title="Click to remove" onClick={() => removeTag(tag)}>{tag} ✕</span>
                ))}
                {editingTags ? (
                  <input
                    autoFocus
                    className="fld"
                    style={{ width: 140, height: 26, fontSize: 11.5, padding: "4px 8px" }}
                    value={tagInput}
                    placeholder={t("crm.tagsPlaceholder")}
                    onChange={(e) => setTagInput(e.target.value)}
                    onKeyDown={(e) => { if (e.key === "Enter") addTag(); if (e.key === "Escape") { setEditingTags(false); setTagInput(""); } }}
                    onBlur={addTag}
                    disabled={savingTags}
                  />
                ) : (
                  <button className="chip" style={{ borderStyle: "dashed", color: "var(--c-text-3)" }} onClick={() => setEditingTags(true)}>
                    <Icon name="plus" style={{ width: 10, height: 10 }} />{t("crm.addTag")}
                  </button>
                )}
              </div>
              <div className="row gap-2 mt-2" style={{ alignItems: "center" }}>
                <span className="text-11 muted">{t("crm.assignedTo")}:</span>
                {editingAssign ? (
                  <input
                    autoFocus
                    className="fld"
                    style={{ width: 140, height: 24, fontSize: 11.5, padding: "3px 8px" }}
                    value={assignInput}
                    placeholder={t("crm.assignPlaceholder")}
                    onChange={(e) => setAssignInput(e.target.value)}
                    onKeyDown={(e) => {
                      if (e.key === "Enter") { setEditingAssign(false); saveAssign(assignInput.trim()); }
                      if (e.key === "Escape") setEditingAssign(false);
                    }}
                    onBlur={() => { setEditingAssign(false); saveAssign(assignInput.trim()); }}
                    disabled={savingAssign}
                  />
                ) : (
                  <span
                    className="text-11"
                    style={{ cursor: "pointer", color: data.assignedTo ? "var(--c-text)" : "var(--c-text-4)" }}
                    onClick={() => { setAssignInput(data.assignedTo?.name || ""); setEditingAssign(true); }}
                  >
                    {data.assignedTo?.name || t("crm.unassigned")} <span style={{ opacity: 0.4, display: "inline-flex" }}><Icon name="edit" size={9} /></span>
                  </span>
                )}
              </div>
            </div>
          </div>
          <div className="row gap-2" style={{ flexWrap: "wrap" }}>
            <Btn variant="ghost" size="sm" icon="message" onClick={() => setShowSmsModal(true)}>{t("crm.sendSms")}</Btn>
            <Btn variant="ghost" size="sm" icon="layers" onClick={() => setShowMergeModal(true)}>{t("crm.merge.action")}</Btn>
            <button className="btn" style={{ borderColor: "rgba(198,48,64,0.35)", color: "var(--err)" }} onClick={requestDeletion}>
              <Icon name="trash" />{t("crm.requestDeletion")}
            </button>
          </div>
        </div>
      </div>

      {showSmsModal && (
        <div className="modal-back" onClick={() => setShowSmsModal(false)}>
          <div className="modal" onClick={(e) => e.stopPropagation()}>
            <div className="modal-head">
              <div className="card-title">{t("crm.sendSms")} — {crmFormatPhone(data.phoneDigits)}</div>
              <Btn variant="ghost" size="sm" icon="x" onClick={() => setShowSmsModal(false)} />
            </div>
            <div className="modal-body">
              <Field label={t("crm.sms.messageLabel")}>
                <Textarea value={smsText} onChange={(e) => setSmsText(e.target.value)} placeholder={t("crm.sms.placeholder")} style={{ minHeight: 90 }} />
              </Field>
            </div>
            <div className="modal-foot">
              <Btn variant="ghost" onClick={() => setShowSmsModal(false)}>{t("crm.cancel")}</Btn>
              <Btn variant="primary" onClick={sendSms} disabled={sendingSms || !smsText.trim()}>{sendingSms ? t("crm.sending") : t("crm.sendSms")}</Btn>
            </div>
          </div>
        </div>
      )}

      {showMergeModal && (
        <div className="modal-back" onClick={() => setShowMergeModal(false)}>
          <div className="modal" onClick={(e) => e.stopPropagation()}>
            <div className="modal-head">
              <div className="card-title">{t("crm.merge.action")}</div>
              <Btn variant="ghost" size="sm" icon="x" onClick={() => setShowMergeModal(false)} />
            </div>
            <div className="modal-body">
              <div className="text-12 muted mb-3">{t("crm.merge.explain")}</div>
              <Field label={t("crm.merge.targetLabel")}>
                <Input value={mergeTarget} onChange={(e) => setMergeTarget(e.target.value)} placeholder={t("crm.merge.targetPlaceholder")} />
              </Field>
            </div>
            <div className="modal-foot">
              <Btn variant="ghost" onClick={() => setShowMergeModal(false)}>{t("crm.cancel")}</Btn>
              <Btn variant="primary" onClick={mergeIntoTarget} disabled={merging || !mergeTarget.trim()}>{merging ? t("crm.merge.merging") : t("crm.merge.action")}</Btn>
            </div>
          </div>
        </div>
      )}

      <div style={{ display: "grid", gridTemplateColumns: "1.6fr 1fr", gap: 20, alignItems: "start" }}>
        <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
          {timeline.length > 0 && (
            <Card title={t("crm.section.timeline")} subtitle={t("crm.timeline.subtitle")}>
              {timeline.map((item) => (
                <div key={item.key} className="row gap-2" style={{ padding: "7px 0", borderBottom: "1px solid var(--c-line)", alignItems: "center" }}>
                  <span style={{
                    width: 6, height: 6, borderRadius: "50%", flex: "none",
                    background: item.kind === "ptp" ? "var(--warn)" : item.kind === "ticket" ? "var(--err)" : item.kind === "note" ? "var(--a)" : "var(--c-text-4)",
                  }} />
                  <span className="text-11" style={{ flex: 1 }}>{item.label}</span>
                  <span className="text-11 muted">{fmtDate(item.at)}</span>
                </div>
              ))}
            </Card>
          )}

          <Card title={t("crm.section.callHistory")} subtitle={`${data.totalCalls}`}>
            {data.calls.length === 0 && <div className="muted text-12">{t("crm.noneMatch")}</div>}
            {data.calls.map((call) => (
              <div key={call.id} style={{ padding: "10px 0", borderBottom: "1px solid var(--c-line)" }}>
                <div className="row" style={{ justifyContent: "space-between", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
                  <div>
                    <div className="text-12 strong">{fmtDate(call.startedAt || call.updatedAt)}</div>
                    <div className="text-11 muted mt-1">
                      {fmtDuration(call.durationSeconds)}
                      {call.aiAnalysis?.sentiment ? ` · ${call.aiAnalysis.sentiment}` : ""}
                      {call.aiAnalysis?.qaScore != null ? ` · QA ${call.aiAnalysis.qaScore}/5` : ""}
                    </div>
                  </div>
                  <div className="row gap-2" style={{ alignItems: "center" }}>
                    <Badge tone={outcomeTone(call.endedReason)}>{call.endedReason || "—"}</Badge>
                    <button
                      className="text-11"
                      style={{ color: "var(--a)", background: "none", border: 0, cursor: "pointer" }}
                      onClick={() => setExpandedCallId((prev) => (prev === call.id ? null : call.id))}
                    >
                      {t("crm.viewTranscript")} {expandedCallId === call.id ? "▲" : "▼"}
                    </button>
                  </div>
                </div>
                {expandedCallId === call.id && (
                  <div style={{ marginTop: 8, padding: 10, background: "var(--c-bg)", borderRadius: 8 }}>
                    {(call.turns || []).length === 0 && <div className="text-11 muted">—</div>}
                    {(call.turns || []).map((turn, i) => (
                      <div key={i} className="text-11 mb-2">
                        {turn.user && (
                          <div>
                            <span className="mono" style={{ fontWeight: 700, color: "var(--c-text-2)" }}>{t("crm.transcript.caller")}:</span>{" "}
                            {turn.user}
                          </div>
                        )}
                        {turn.assistant && (
                          <div className="mt-1">
                            <span className="mono" style={{ fontWeight: 700, color: "var(--a)" }}>{call.assistantName || t("crm.transcript.ai")}:</span>{" "}
                            {turn.assistant}
                          </div>
                        )}
                      </div>
                    ))}
                  </div>
                )}
              </div>
            ))}
          </Card>
        </div>

        <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
          {data.ptpCommitments && data.ptpCommitments.length > 0 && (
            <Card title={t("crm.section.ptp")} subtitle={`${data.ptpCommitments.length}`}>
              {data.ptpCommitments.map((p) => (
                <div key={p.logId} style={{ padding: "8px 0", borderBottom: "1px solid var(--c-line)" }}>
                  <div className="row gap-2" style={{ alignItems: "center", justifyContent: "space-between" }}>
                    <span className="text-12 strong">{fmtDate(p.expectedPaymentDate)}</span>
                    <Badge tone={p.status === "confirmed" ? "ok" : p.status === "broken" ? "err" : "warn"}>{p.status}</Badge>
                  </div>
                  {p.balance != null && <div className="text-11 muted mt-1">{t("crm.ptp.balance")}: {fmtRM(p.balance)}</div>}
                  {p.akaunNumber && <div className="text-11 muted mono">{t("crm.ptp.akaun")}: {p.akaunNumber}</div>}
                  {p.campaignName && <div className="text-11 muted">{p.campaignName}</div>}
                </div>
              ))}
            </Card>
          )}

          <Card title={t("crm.section.tickets")}>
            {data.tickets.length === 0 && <div className="muted text-12">{t("crm.noTickets")}</div>}
            {data.tickets.map((tk) => (
              <div key={tk.id} style={{ padding: "8px 0", borderBottom: "1px solid var(--c-line)" }}>
                <div className="mono text-12 strong">{tk.id}</div>
                <div className="row gap-2 mt-1">
                  <Badge tone={ticketPriorityTone(tk.priority)}>{tk.priority}</Badge>
                  <Badge tone={ticketStatusTone(tk.status)}>{tk.status}</Badge>
                </div>
                <div className="text-11 muted mt-1">{tk.subject}</div>
              </div>
            ))}
          </Card>

          <Card title={t("crm.section.notes")}>
            <div className="row gap-2 mb-3">
              <input
                className="fld"
                placeholder={t("crm.notesPlaceholder")}
                value={noteText}
                onChange={(e) => setNoteText(e.target.value)}
                onKeyDown={(e) => { if (e.key === "Enter") addNote(); }}
                disabled={savingNote}
              />
              <Btn variant="primary" size="sm" onClick={addNote} disabled={savingNote}>{t("crm.addNote")}</Btn>
            </div>
            {data.notes.length === 0 && <div className="muted text-12">{t("crm.noNotes")}</div>}
            {data.notes.map((note) => (
              <div key={note.id} style={{ padding: "8px 0", borderBottom: "1px solid var(--c-line)" }}>
                <div className="text-12">{note.text}</div>
                <div className="text-11 muted mt-1">
                  — {note.authorName}{note.authorRole ? ` (${note.authorRole})` : ""} · {fmtDate(note.at)}
                </div>
              </div>
            ))}
          </Card>

          <Card title={t("crm.section.repeatedQuestions")}>
            {data.repeatedQuestions.length === 0 && <div className="muted text-12">{t("crm.noRepeats")}</div>}
            {data.repeatedQuestions.map((q) => (
              <div key={q.normalized} style={{ padding: "10px 0", borderBottom: "1px solid var(--c-line)" }}>
                <div className="text-12 mb-1">&ldquo;{q.raw}&rdquo;</div>
                <div className="row gap-2" style={{ alignItems: "center", flexWrap: "wrap" }}>
                  <Badge tone="accent">{t("crm.askedTimes", { count: q.count })}</Badge>
                  <span className="text-11 muted">{q.dates.map((d) => fmtDate(d)).join(" · ")}</span>
                </div>
                {q.kbGap && (
                  <div className="text-11 mt-2" style={{
                    color: "var(--err)", background: "var(--err-soft)",
                    border: "1px solid rgba(198,48,64,0.25)", borderRadius: 8,
                    padding: "5px 9px", display: "inline-block",
                  }}>
                    {t("crm.alsoKbGap", { count: q.kbGap.callerCount })}
                  </div>
                )}
              </div>
            ))}
          </Card>
        </div>
      </div>
    </div>
  );
}

function ContactsView({ lang = "en" } = {}) {
  const [selectedPhone, setSelectedPhone] = React.useState(null);
  if (selectedPhone) {
    return <ContactProfileView lang={lang} phone={selectedPhone} onBack={() => setSelectedPhone(null)} />;
  }
  return <ContactsListView lang={lang} onOpen={setSelectedPhone} />;
}

window.ContactsView = ContactsView;
