// ---------- Dashboard + Analytics + Billing ----------

const StatusRow = ({ label, status, detail }) => (
  <div className="row gap-3" style={{ justifyContent: "space-between" }}>
    <div className="row gap-2">
      <span style={{ width: 6, height: 6, borderRadius: 3, background: status === "ok" ? "var(--ok)" : status === "warn" ? "var(--warn)" : "var(--err)", boxShadow: `0 0 8px ${status === "ok" ? "var(--ok)" : status === "warn" ? "var(--warn)" : "var(--err)"}` }} />
      <span className="text-12">{label}</span>
    </div>
    <span className="mono text-11 muted">{detail}</span>
  </div>
);

// Per-client stats derived from call logs
function buildClientStats(client, callLogs) {
  const logs = callLogs.filter(l =>
    (l.customerId && l.customerId === client.id) ||
    (l.customerName && l.customerName.toLowerCase() === (client.name || "").toLowerCase())
  );
  const totalCalls = logs.length;
  const totalRevenue = logs.reduce((s, l) => s + (Number(l.sellingMyr) || 0), 0);
  const totalMinutes = logs.reduce((s, l) => s + (Number(l.durationSeconds) || 0), 0) / 60;
  const campaigns = new Set(logs.map(l => l.campaignName).filter(Boolean));
  const lastCall = logs[0]?.startedAt || null;
  const outcomes = logs.reduce((acc, l) => {
    const o = l.endedReason || "completed";
    acc[o] = (acc[o] || 0) + 1;
    return acc;
  }, {});
  return { totalCalls, totalRevenue, totalMinutes, campaigns: campaigns.size, lastCall, outcomes };
}

const ClientCard = ({ client, stats, onClick }) => {
  const initials = (client.name || "?").slice(0, 2).toUpperCase();
  const atLimit = client.monthlyMinuteLimit > 0 && stats.totalMinutes >= client.monthlyMinuteLimit;
  const pctUsed = client.monthlyMinuteLimit > 0
    ? Math.min(100, (stats.totalMinutes / client.monthlyMinuteLimit) * 100)
    : null;
  return (
    <div className="card" style={{ padding: 16, cursor: "pointer" }} onClick={onClick}>
      <div className="row gap-3 mb-3">
        <div className="agent-avatar" style={{ width: 36, height: 36, fontSize: 13, borderRadius: 9, flexShrink: 0 }}>{initials}</div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="strong text-13 truncate">{client.name}</div>
          <div className="muted text-11 truncate">{client.company || client.email || "—"}</div>
        </div>
        <Badge tone={atLimit ? "warn" : "ok"} dot>{atLimit ? "at limit" : "active"}</Badge>
      </div>

      <div className="g-2" style={{ gap: 8, marginBottom: 12 }}>
        <div className="stat" style={{ padding: "8px 10px" }}>
          <div className="stat-label" style={{ fontSize: 10 }}>Revenue</div>
          <div className="stat-value" style={{ fontSize: 15 }}>{fmtRM(stats.totalRevenue)}</div>
        </div>
        <div className="stat" style={{ padding: "8px 10px" }}>
          <div className="stat-label" style={{ fontSize: 10 }}>Calls</div>
          <div className="stat-value" style={{ fontSize: 15 }}>{stats.totalCalls.toLocaleString()}</div>
        </div>
        <div className="stat" style={{ padding: "8px 10px" }}>
          <div className="stat-label" style={{ fontSize: 10 }}>Minutes</div>
          <div className="stat-value" style={{ fontSize: 15 }}>{stats.totalMinutes.toFixed(1)}</div>
        </div>
        <div className="stat" style={{ padding: "8px 10px" }}>
          <div className="stat-label" style={{ fontSize: 10 }}>Campaigns</div>
          <div className="stat-value" style={{ fontSize: 15 }}>{stats.campaigns}</div>
        </div>
      </div>

      {pctUsed !== null && (
        <div style={{ marginBottom: 8 }}>
          <div className="row" style={{ justifyContent: "space-between", fontSize: 10, marginBottom: 4 }}>
            <span className="muted">Monthly usage</span>
            <span className="mono">{stats.totalMinutes.toFixed(0)} / {client.monthlyMinuteLimit} min</span>
          </div>
          <div style={{ height: 4, background: "var(--c-surface-2)", borderRadius: 2, overflow: "hidden" }}>
            <div style={{ width: `${pctUsed}%`, height: "100%", background: atLimit ? "var(--warn)" : "var(--a)", borderRadius: 2 }} />
          </div>
        </div>
      )}

      {client.assignedAssistantIds?.length > 0 && (
        <div className="row" style={{ justifyContent: "space-between", fontSize: 11 }}>
          <span className="muted">Assistants</span>
          <span className="mono">{client.assignedAssistantIds.length}</span>
        </div>
      )}
      {stats.lastCall && (
        <div className="row mt-1" style={{ justifyContent: "space-between", fontSize: 11 }}>
          <span className="muted">Last call</span>
          <span className="mono" style={{ color: "var(--c-text-4)" }}>{fmtDate(stats.lastCall)}</span>
        </div>
      )}
    </div>
  );
};

function dashContainRate(logs) {
  if (!logs.length) return null;
  const ok = logs.filter(l => {
    const r = String(l.endedReason || "").toLowerCase();
    return !r.includes("transfer") && !r.includes("fail") && !r.includes("error") && !r.includes("voicemail");
  });
  return Math.round((ok.length / logs.length) * 100);
}

const OUTCOME_COLORS = ["var(--a)", "var(--ok)", "var(--warn)", "var(--err)"];
const RANGE_MS = { "24h": 86400000, "7d": 604800000, "30d": 2592000000, "90d": 7776000000 };
const RANGE_LABELS = { "24h": "Last 24h", "7d": "Last 7 days", "30d": "Last 30 days", "90d": "Last 90 days" };

const DashboardView = ({ mode, callLogs = [], assistants = [], clients = [], allowedViews }) => {
  const isClient = mode === "client";
  const canLaunchCampaign = !isClient || allowedViews?.has("blast");
  const [range, setRange] = React.useState("30d");

  const nowRef = React.useMemo(() => new Date(), []);

  const { logsInRange, logsPrevPeriod } = React.useMemo(() => {
    const ms = RANGE_MS[range] || RANGE_MS["30d"];
    const cut1 = nowRef.getTime() - ms;
    const cut2 = nowRef.getTime() - ms * 2;
    return {
      logsInRange: callLogs.filter(l => new Date(l.startedAt || 0).getTime() >= cut1),
      logsPrevPeriod: callLogs.filter(l => {
        const t = new Date(l.startedAt || 0).getTime();
        return t >= cut2 && t < cut1;
      }),
    };
  }, [callLogs, nowRef, range]);

  const totalRevenue = React.useMemo(
    () => clients.reduce((s, c) => s + buildClientStats(c, callLogs).totalRevenue, 0),
    [clients, callLogs]
  );

  // KPI: Calls
  const callsNow = logsInRange.length;
  const callsPrev = logsPrevPeriod.length;
  const callsDeltaNum = callsPrev > 0 ? (callsNow - callsPrev) / callsPrev * 100 : null;
  const callsDeltaStr = callsDeltaNum != null
    ? (callsDeltaNum >= 0 ? `+${callsDeltaNum.toFixed(1)}%` : `${callsDeltaNum.toFixed(1)}%`)
    : callsNow > 0 ? "new" : "—";

  // KPI: Avg latency
  const { latencyNow, latencyPrev } = React.useMemo(() => {
    function avgLat(logs) {
      const vals = logs.flatMap(l => (l.turns || []).map(t => Number(t.latencyMs)).filter(v => v > 0));
      return vals.length ? Math.round(vals.reduce((s, v) => s + v, 0) / vals.length) : null;
    }
    return { latencyNow: avgLat(logsInRange), latencyPrev: avgLat(logsPrevPeriod) };
  }, [logsInRange, logsPrevPeriod]);
  const latencyStr = latencyNow != null ? `${latencyNow}ms` : "—";
  const latencyDelta = latencyNow != null && latencyPrev != null ? latencyNow - latencyPrev : null;
  const latencyDeltaStr = latencyDelta != null ? (latencyDelta <= 0 ? `${latencyDelta}ms` : `+${latencyDelta}ms`) : "—";

  // KPI: Containment
  const containNow = React.useMemo(() => dashContainRate(logsInRange), [logsInRange]);
  const containPrev = React.useMemo(() => dashContainRate(logsPrevPeriod), [logsPrevPeriod]);
  const containStr = containNow != null ? `${containNow}%` : "—";
  const containDelta = containNow != null && containPrev != null ? containNow - containPrev : null;
  const containDeltaStr = containDelta != null ? (containDelta >= 0 ? `+${containDelta}%` : `${containDelta}%`) : "—";

  // KPI: Spend
  const spendNow = React.useMemo(() => logsInRange.reduce((s, l) => s + (Number(l.sellingMyr) || 0), 0), [logsInRange]);
  const spendPrev = React.useMemo(() => logsPrevPeriod.reduce((s, l) => s + (Number(l.sellingMyr) || 0), 0), [logsPrevPeriod]);
  const spendDelta = spendNow - spendPrev;
  const spendDeltaStr = spendDelta >= 0 ? `+${fmtRM(spendDelta)}` : fmtRM(spendDelta);

  // Client KPI: avg duration
  const avgDurNow = React.useMemo(() => {
    const durs = logsInRange.map(l => Number(l.durationSeconds)).filter(v => v > 0);
    return durs.length ? Math.round(durs.reduce((s, v) => s + v, 0) / durs.length) : null;
  }, [logsInRange]);
  const avgDurPrev = React.useMemo(() => {
    const durs = logsPrevPeriod.map(l => Number(l.durationSeconds)).filter(v => v > 0);
    return durs.length ? Math.round(durs.reduce((s, v) => s + v, 0) / durs.length) : null;
  }, [logsPrevPeriod]);
  const avgDurStr = avgDurNow != null ? fmtDuration(avgDurNow) : "—";
  const avgDurDelta = avgDurNow != null && avgDurPrev != null ? avgDurNow - avgDurPrev : null;
  const avgDurDeltaStr = avgDurDelta != null ? (avgDurDelta <= 0 ? `${avgDurDelta}s` : `+${avgDurDelta}s`) : "—";

  // Client: monthly usage + balance
  const thisMonthStart = React.useMemo(() => new Date(nowRef.getFullYear(), nowRef.getMonth(), 1).getTime(), [nowRef]);
  const monthlyMinsUsed = React.useMemo(() =>
    callLogs.filter(l => new Date(l.startedAt || 0).getTime() >= thisMonthStart)
      .reduce((s, l) => s + (Number(l.durationSeconds) || 0), 0) / 60,
    [callLogs, thisMonthStart]);

  const myClientProfile = React.useMemo(() => {
    if (!isClient) return null;
    const s = getSession();
    return clients.find(c =>
      c.id === s?.clientId ||
      String(c.email || "").toLowerCase() === String(s?.email || "").toLowerCase()
    ) || null;
  }, [isClient, clients]);

  const minuteLimit = myClientProfile?.monthlyMinuteLimit || 0;
  const minsLeft = minuteLimit > 0 ? Math.max(0, minuteLimit - monthlyMinsUsed) : null;
  const pctUsed = minuteLimit > 0 ? Math.min(100, (monthlyMinsUsed / minuteLimit) * 100) : null;
  const allTimeSpent = React.useMemo(() => callLogs.reduce((s, l) => s + (Number(l.sellingMyr) || 0), 0), [callLogs]);

  // Sparklines — 20 equal-time buckets across selected range
  const makeSparkCounts = React.useCallback((logs) => {
    const ms = RANGE_MS[range] || RANGE_MS["30d"];
    const end = nowRef.getTime();
    const start = end - ms;
    const bucketMs = ms / 20;
    const buckets = Array(20).fill(0);
    logs.forEach(l => {
      if (!l.startedAt) return;
      const t = new Date(l.startedAt).getTime();
      const idx = Math.min(19, Math.floor((t - start) / bucketMs));
      if (idx >= 0) buckets[idx]++;
    });
    return buckets;
  }, [range, nowRef]);

  const makeSparkAvg = React.useCallback((logs, getValue) => {
    const ms = RANGE_MS[range] || RANGE_MS["30d"];
    const end = nowRef.getTime();
    const start = end - ms;
    const bucketMs = ms / 20;
    const sums = Array(20).fill(0);
    const counts = Array(20).fill(0);
    logs.forEach(l => {
      if (!l.startedAt) return;
      const t = new Date(l.startedAt).getTime();
      const idx = Math.min(19, Math.floor((t - start) / bucketMs));
      if (idx < 0) return;
      const v = getValue(l);
      if (v != null && !isNaN(v)) { sums[idx] += v; counts[idx]++; }
    });
    return sums.map((s, i) => counts[i] ? s / counts[i] : 0);
  }, [range, nowRef]);

  const callsSpark = React.useMemo(() => makeSparkCounts(logsInRange), [logsInRange, makeSparkCounts]);

  const latencySpark = React.useMemo(() => makeSparkAvg(logsInRange, l => {
    const vals = (l.turns || []).map(t => Number(t.latencyMs)).filter(v => v > 0);
    return vals.length ? vals.reduce((s, v) => s + v, 0) / vals.length : null;
  }), [logsInRange, makeSparkAvg]);

  const containSpark = React.useMemo(() => {
    const ms = RANGE_MS[range] || RANGE_MS["30d"];
    const end = nowRef.getTime(); const start = end - ms; const bucketMs = ms / 20;
    const total = Array(20).fill(0); const ok = Array(20).fill(0);
    logsInRange.forEach(l => {
      if (!l.startedAt) return;
      const t = new Date(l.startedAt).getTime();
      const idx = Math.min(19, Math.floor((t - start) / bucketMs));
      if (idx < 0) return;
      total[idx]++;
      const r = String(l.endedReason || "").toLowerCase();
      if (!r.includes("transfer") && !r.includes("fail") && !r.includes("error") && !r.includes("voicemail")) ok[idx]++;
    });
    return total.map((n, i) => n ? Math.round(ok[i] / n * 100) : 0);
  }, [logsInRange, nowRef, range]);

  const spendSpark = React.useMemo(() => makeSparkAvg(logsInRange, l => Number(l.sellingMyr) || 0), [logsInRange, makeSparkAvg]);
  const durSpark = React.useMemo(() => makeSparkAvg(logsInRange, l => l.durationSeconds ? Number(l.durationSeconds) : null), [logsInRange, makeSparkAvg]);

  // Volume chart — hourly for 24h, daily otherwise
  const volumeData = React.useMemo(() => {
    if (range === "24h") {
      const buckets = Array(24).fill(0);
      logsInRange.forEach(l => { if (l.startedAt) buckets[new Date(l.startedAt).getHours()]++; });
      const h = nowRef.getHours();
      return [...buckets.slice(h + 1), ...buckets.slice(0, h + 1)];
    }
    const days = { "7d": 7, "30d": 30, "90d": 90 }[range] || 30;
    const ms = RANGE_MS[range] || RANGE_MS["30d"];
    const start = nowRef.getTime() - ms;
    const dayMs = ms / days;
    const buckets = Array(days).fill(0);
    logsInRange.forEach(l => {
      if (!l.startedAt) return;
      const t = new Date(l.startedAt).getTime();
      const idx = Math.min(days - 1, Math.floor((t - start) / dayMs));
      if (idx >= 0) buckets[idx]++;
    });
    return buckets;
  }, [logsInRange, nowRef, range]);

  const volumeLabels = React.useMemo(() => {
    if (range === "24h") {
      const curHour = nowRef.getHours();
      return Array.from({ length: 24 }, (_, i) => {
        const hr = (curHour + 1 + i) % 24;
        return (i % 6 === 0 || i === 23) ? `${String(hr).padStart(2, "0")}:00` : "";
      });
    }
    const days = { "7d": 7, "30d": 30, "90d": 90 }[range] || 30;
    const showEvery = days <= 7 ? 1 : days <= 30 ? 5 : 15;
    return Array.from({ length: days }, (_, i) => {
      if (i % showEvery !== 0 && i !== days - 1) return "";
      const d = new Date(nowRef.getTime() - (days - 1 - i) * 86400000);
      return `${d.getMonth() + 1}/${d.getDate()}`;
    });
  }, [range, nowRef]);

  // Outcome breakdown
  const outcomePairs = React.useMemo(() => {
    const map = {};
    logsInRange.forEach(l => { const k = l.endedReason || "completed"; map[k] = (map[k] || 0) + 1; });
    return Object.entries(map).sort((a, b) => b[1] - a[1]).slice(0, 4);
  }, [logsInRange]);

  const totalOutcomes = logsInRange.length || 1;
  const recentLogs = callLogs.slice(0, 6);
  const activeAssistants = assistants.filter(a => a.status !== "paused");

  const activityFeed = React.useMemo(() => callLogs.slice(0, 5).map(l => {
    const ago = l.startedAt ? Math.round((Date.now() - new Date(l.startedAt).getTime()) / 60000) : null;
    return { ...l, agoStr: ago == null ? "—" : ago < 60 ? `${ago}m` : `${Math.round(ago / 60)}h` };
  }), [callLogs]);

  const chartTitle = range === "24h" ? "Call volume — last 24 hours" : `Call volume — ${RANGE_LABELS[range]}`;
  const chartSub = range === "24h" ? "Hourly, rotated to current hour" : "Daily breakdown";

  const adminKpis = [
    { label: `Calls (${range})`,  value: callsNow.toLocaleString(), delta: callsDeltaStr,  spark: callsSpark },
    { label: "Avg latency",       value: latencyStr,                delta: latencyDeltaStr, spark: latencySpark },
    { label: "Containment",       value: containStr,                delta: containDeltaStr, spark: containSpark },
    { label: `Spend (${range})`,  value: fmtRM(spendNow),          delta: spendDeltaStr,   spark: spendSpark, deltaBad: true },
  ];

  const clientKpis = [
    { label: `Calls (${range})`,  value: callsNow.toLocaleString(), delta: callsDeltaStr,  spark: callsSpark },
    { label: `Spent (${range})`,  value: fmtRM(spendNow),          delta: spendDeltaStr,   spark: spendSpark, deltaBad: true },
    { label: "Avg duration",      value: avgDurStr,                 delta: avgDurDeltaStr,  spark: durSpark },
    { label: "Balance left",
      value: minsLeft != null ? `${Math.round(minsLeft)} min` : "—",
      delta: minuteLimit > 0 ? `of ${minuteLimit} min/mo` : "No limit set",
      spark: [] },
  ];

  const kpis = isClient ? clientKpis : adminKpis;

  return (
    <div>
      <div className="ph">
        <div>
          <h1 className="ph-title">{isClient ? "Welcome back" : "Operations overview"}</h1>
          <div className="ph-subtitle">
            {isClient
              ? "Here's how your voice agents performed."
              : `Realtime view across ${assistants.length} assistant${assistants.length !== 1 ? "s" : ""}.`}
          </div>
        </div>
        <div className="ph-actions">
          <div className="row gap-1">
            {["24h", "7d", "30d", "90d"].map(r => (
              <Btn key={r} variant={range === r ? "primary" : "ghost"} size="sm" onClick={() => setRange(r)}>{r}</Btn>
            ))}
          </div>
          <Btn variant="ghost" icon="download" size="sm">Export</Btn>
          {canLaunchCampaign && <Btn variant="primary" icon="plus" size="sm" onClick={() => window.__navigate && window.__navigate("blast")}>New campaign</Btn>}
        </div>
      </div>

      {isClient && (
        <div style={{ marginBottom: 20, padding: "14px 20px", background: "var(--c-surface)", borderRadius: 12, border: "1px solid var(--c-line-2)", display: "flex", gap: 32, flexWrap: "wrap", alignItems: "center" }}>
          <div>
            <div className="muted text-11" style={{ marginBottom: 3 }}>Total calls (all time)</div>
            <div style={{ fontSize: 22, fontWeight: 700, letterSpacing: "-0.02em" }}>{callLogs.length.toLocaleString()}</div>
          </div>
          <div>
            <div className="muted text-11" style={{ marginBottom: 3 }}>Total spent (all time)</div>
            <div style={{ fontSize: 22, fontWeight: 700, letterSpacing: "-0.02em" }}>{fmtRM(allTimeSpent)}</div>
          </div>
          {minuteLimit > 0 && (
            <div style={{ flex: 1, minWidth: 200 }}>
              <div className="row" style={{ justifyContent: "space-between", fontSize: 11, marginBottom: 5 }}>
                <span className="muted">Minutes used this month</span>
                <span className="mono strong">{Math.round(monthlyMinsUsed)} / {minuteLimit} min</span>
              </div>
              <div style={{ height: 6, background: "var(--c-surface-2)", borderRadius: 3, overflow: "hidden" }}>
                <div style={{ width: `${pctUsed}%`, height: "100%", background: pctUsed > 85 ? "var(--warn)" : "var(--a)", borderRadius: 3, transition: "width 0.4s" }} />
              </div>
            </div>
          )}
        </div>
      )}

      {!isClient && clients.length > 0 && (
        <div style={{ marginBottom: 22 }}>
          <div className="ph" style={{ marginBottom: 12, paddingBottom: 0 }}>
            <div>
              <div className="row gap-3" style={{ alignItems: "center", marginBottom: 4 }}>
                <div className="card-title" style={{ fontSize: 15 }}>Client accounts</div>
                <div className="muted text-11">{clients.length} client{clients.length !== 1 ? "s" : ""}</div>
              </div>
              <div className="row gap-2" style={{ alignItems: "baseline" }}>
                <span style={{ fontSize: 28, fontWeight: 700, letterSpacing: "-0.02em" }}>{fmtRM(totalRevenue)}</span>
                <span className="muted text-13">total revenue</span>
              </div>
            </div>
            <div className="ph-actions">
              <Btn variant="ghost" size="sm" icon="users" onClick={() => window.__navigate && window.__navigate("clients")}>Manage clients</Btn>
            </div>
          </div>
          <div className="g-3">
            {clients.map(client => (
              <ClientCard key={client.id} client={client} stats={buildClientStats(client, callLogs)} onClick={() => window.__navigate && window.__navigate("clients")} />
            ))}
          </div>
        </div>
      )}

      <div className="stat-grid">
        {kpis.map(k => <StatTile key={k.label} k={k} />)}
      </div>

      <div className="g-2-1" style={{ marginBottom: 18 }}>
        <Card title={chartTitle} subtitle={chartSub}
          action={<span className="chip"><span style={{ width: 6, height: 6, borderRadius: 3, background: "var(--a)", display: "inline-block" }} /> Live</span>}>
          <div className="chart-wrap">
            <LineChart data={volumeData} labels={volumeLabels} />
          </div>
        </Card>

        <Card title="Outcome breakdown" subtitle={`${callsNow} call${callsNow !== 1 ? "s" : ""} · ${RANGE_LABELS[range]}`}>
          <div className="row" style={{ justifyContent: "center", padding: "8px 0 4px" }}>
            <Donut size={160} label={callsNow.toLocaleString()} sub={`${range} calls`}
              segments={outcomePairs.length > 0
                ? outcomePairs.map(([, v], i) => ({ value: v, color: OUTCOME_COLORS[i % 4] }))
                : [{ value: 1, color: "var(--c-line-2)" }]
              } />
          </div>
          <div className="col gap-2 mt-3">
            {outcomePairs.length === 0 && <div className="muted text-11">No calls in this window yet.</div>}
            {outcomePairs.map(([label, count], i) => (
              <div key={label} className="row" style={{ justifyContent: "space-between", fontSize: 12 }}>
                <span className="row gap-2 muted">
                  <span style={{ width: 8, height: 8, borderRadius: 2, background: OUTCOME_COLORS[i % 4] }} />
                  {label}
                </span>
                <span className="mono">{Math.round(count / totalOutcomes * 100)}%</span>
              </div>
            ))}
          </div>
        </Card>
      </div>

      <div className="g-2-1" style={{ marginBottom: 18 }}>
        <Card title="Recent calls" subtitle="Latest calls across all assistants"
          action={<Btn variant="ghost" size="sm" onClick={() => window.__navigate && window.__navigate("logs")}>View all →</Btn>}>
          <div className="tbl-wrap" style={{ margin: "-18px" }}>
            <table className="tbl">
              <thead>
                <tr><th>Time</th><th>Contact</th><th>Assistant</th><th>Outcome</th><th className="right">Duration</th><th className="right">Cost</th></tr>
              </thead>
              <tbody>
                {recentLogs.length === 0 && (
                  <tr><td colSpan={6} style={{ textAlign: "center", color: "var(--c-text-4)", padding: 20 }}>No calls yet</td></tr>
                )}
                {recentLogs.map(l => {
                  const contactName = l.customerName || l.contactName || l.contact || "Unknown";
                  const contactNum = l.contact || l.to || "—";
                  const assistantName = l.assistantName || l.assistant || "—";
                  const outcome = l.endedReason || l.outcome || "completed";
                  const dur = l.durationSeconds != null ? fmtDuration(l.durationSeconds) : (l.duration || "—");
                  const cost = l.sellingMyr != null ? fmtRM(l.sellingMyr) : (l.cost || "—");
                  const atStr = l.startedAt ? fmtDate(l.startedAt) : (l.at || "—");
                  return (
                    <tr key={l.id || l.callId}>
                      <td className="mono" style={{ fontSize: 11 }}>{atStr}</td>
                      <td>
                        <div className="strong">{contactName}</div>
                        <div className="mono" style={{ fontSize: 10.5, color: "var(--c-text-4)" }}>{contactNum}</div>
                      </td>
                      <td>{assistantName}</td>
                      <td><Badge tone={outcomeTone(outcome)} dot>{outcome}</Badge></td>
                      <td className="mono right">{dur}</td>
                      <td className="mono right strong">{cost}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </Card>

        <Card title="Top assistants" subtitle={`By call volume · ${RANGE_LABELS[range]}`}>
          <div className="col gap-3">
            {activeAssistants.length === 0 && <div className="muted text-11">No active assistants yet.</div>}
            {activeAssistants.slice(0, 5).map(a => {
              const callCount = logsInRange.filter(l => l.assistantId === a.id || l.assistantName === a.name).length;
              const maxCalls = Math.max(...activeAssistants.map(x => logsInRange.filter(l => l.assistantId === x.id || l.assistantName === x.name).length), 1);
              const totalCalls = callLogs.filter(l => l.assistantId === a.id || l.assistantName === a.name).length;
              return (
                <div key={a.id} className="row gap-3" style={{ alignItems: "center" }}>
                  <div className="agent-avatar" style={{ width: 30, height: 30, fontSize: 11, borderRadius: 8 }}>
                    {(a.name || "?").slice(0, 2).toUpperCase()}
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div className="row" style={{ justifyContent: "space-between" }}>
                      <span className="text-12 strong">{a.name}</span>
                      <span className="mono text-11 muted">{callCount} <span style={{ opacity: 0.5 }}>/ {totalCalls} total</span></span>
                    </div>
                    <div style={{ height: 3, background: "var(--c-surface-2)", borderRadius: 2, marginTop: 4, overflow: "hidden" }}>
                      <div style={{ width: `${(callCount / maxCalls) * 100}%`, height: "100%", background: "var(--a)" }} />
                    </div>
                  </div>
                </div>
              );
            })}
          </div>
        </Card>
      </div>

      <div className="g-3 dev-only">
        <Card title="System health">
          <div className="col gap-3">
            <StatusRow label="LiveKit gateway" status="ok" detail="active" />
            <StatusRow label="YTL ILMU ASR" status="ok" detail="connected" />
            <StatusRow label="OpenAI TTS" status="ok" detail="ready" />
            <StatusRow label="Asterisk AMI" status="warn" detail="check config" />
            <StatusRow label="Mesolitica TTS" status="ok" detail="local" />
            <StatusRow label="SIP Trunk · alienvoip" status="ok" detail="registered" />
          </div>
        </Card>

        <Card title="Provider spend" subtitle="Last 24 hours, RM">
          <div className="col gap-3">
            {[
              { label: "OpenAI", v: 84.20, pct: 44 },
              { label: "YTL ILMU", v: 38.10, pct: 20 },
              { label: "ElevenLabs", v: 32.80, pct: 17 },
              { label: "Mesolitica", v: 18.90, pct: 10 },
              { label: "LiveKit egress", v: 18.40, pct: 9 },
            ].map(r => (
              <div key={r.label}>
                <div className="row" style={{ justifyContent: "space-between" }}>
                  <span className="text-12">{r.label}</span>
                  <span className="mono text-11">RM {r.v.toFixed(2)}</span>
                </div>
                <div style={{ height: 4, background: "var(--c-surface-2)", borderRadius: 2, marginTop: 4 }}>
                  <div style={{ width: `${r.pct}%`, height: "100%", background: r.label === "OpenAI" ? "var(--a)" : "var(--c-elev-2)", borderRadius: 2 }} />
                </div>
              </div>
            ))}
          </div>
        </Card>

        <Card title="Recent activity" action={<Btn variant="ghost" size="sm" onClick={() => window.__navigate && window.__navigate("logs")}>All →</Btn>}>
          <div className="col gap-3" style={{ fontSize: 12 }}>
            {activityFeed.length === 0 && <div className="muted text-11">No recent activity.</div>}
            {activityFeed.map((l, i) => (
              <div key={l.id || i} className="row gap-3" style={{ alignItems: "flex-start" }}>
                <div className="mono text-11 muted" style={{ width: 30, flex: "none", marginTop: 2 }}>{l.agoStr}</div>
                <div style={{ flex: 1 }}>
                  <span className="strong">{l.assistantName || "Agent"}</span>{" "}
                  <span className="muted">called</span>{" "}
                  {l.customerName || l.contact || "contact"}{" "}
                  {l.endedReason && <Badge tone={outcomeTone(l.endedReason)} dot>{l.endedReason}</Badge>}
                </div>
              </div>
            ))}
          </div>
        </Card>
      </div>
    </div>
  );
};

// ---------- Analytics ----------
const LegacyAnalyticsView = ({ callLogs = [] }) => {
  const [tab, setTab] = React.useState("overview");
  return (
    <div>
      <div className="ph">
        <div>
          <h1 className="ph-title">Analytics</h1>
          <div className="ph-subtitle">Deep dive into call patterns, latency, costs, and assistant performance.</div>
        </div>
        <div className="ph-actions">
          <Segmented value="7d" onChange={() => {}} options={[
            { value: "24h", label: "24h" }, { value: "7d", label: "7d" },
            { value: "30d", label: "30d" }, { value: "90d", label: "90d" },
          ]} />
          <Btn variant="ghost" icon="filter" size="sm">Filters</Btn>
          <Btn variant="ghost" icon="download" size="sm">Export</Btn>
        </div>
      </div>

      <Tabs tabs={[
        { id: "overview", label: "Overview" },
        { id: "perf", label: "Performance" },
        { id: "cost", label: "Cost" },
      ]} active={tab} onSelect={setTab} />

      <div className="stat-grid">
        <StatTile k={{ label: "Total calls", value: callLogs.length.toLocaleString(), delta: "+22.4%", spark: [4,5,6,5,7,8,9,11,10,12,13,14,15,16,17,18,19,21,23,24] }} />
        <StatTile k={{ label: "Avg duration", value: "2:14", delta: "-0:08", spark: [22,20,21,19,18,18,17,16,15,16,15,14,15,14,13,13,12,13,12,11] }} />
        <StatTile k={{ label: "First-call resolve", value: "67.4%", delta: "+4.2%", spark: [55,58,57,60,62,61,63,65,64,66,67,66,67,68,67,67,68,68,67,67] }} />
        <StatTile k={{ label: "Containment", value: "78.2%", delta: "+3.1%", spark: [60,62,64,63,65,68,67,70,71,72,73,72,74,75,76,77,77,78,78,78] }} />
      </div>

      <Card title="Calls per hour" subtitle="Hourly volume — last 7 days"
        action={<div className="row gap-2"><Badge tone="accent" dot>This week</Badge><Badge dot>Previous</Badge></div>}>
        <div className="chart-wrap" style={{ height: 260 }}>
          <BarChart height={260} data={[42,38,32,28,21,18,14,18,52,84,124,168,184,162,148,138,142,164,142,118,86,72,58,48]}
            labels={["00","","02","","04","","06","","08","","10","","12","","14","","16","","18","","20","","22",""]} />
        </div>
      </Card>

      <div className="g-2 mt-4">
        <Card title="Latency by stage" subtitle="Median, last 7d">
          <div className="col gap-3">
            {[
              { stage: "VAD detect", ms: 82, pct: 18 },
              { stage: "ASR (ILMU)", ms: 312, pct: 55 },
              { stage: "LLM stream", ms: 246, pct: 44 },
              { stage: "TTS first byte", ms: 148, pct: 28 },
              { stage: "Egress to caller", ms: 64, pct: 12 },
            ].map(r => (
              <div key={r.stage}>
                <div className="row" style={{ justifyContent: "space-between" }}>
                  <span className="text-12">{r.stage}</span>
                  <span className="mono text-11">{r.ms}ms</span>
                </div>
                <div style={{ height: 4, background: "var(--c-surface-2)", borderRadius: 2, marginTop: 4 }}>
                  <div style={{ width: `${r.pct}%`, height: "100%", background: "var(--a)", borderRadius: 2 }} />
                </div>
              </div>
            ))}
          </div>
        </Card>

        <Card title="Language mix" subtitle="Of detected user utterances">
          <div className="row" style={{ justifyContent: "center", padding: 8 }}>
            <Donut size={140} label="9.1K" sub="utterances"
              segments={[
                { value: 58, color: "var(--a)" },
                { value: 31, color: "var(--info)" },
                { value: 8, color: "var(--ok)" },
                { value: 3, color: "var(--warn)" },
              ]} />
          </div>
          <div className="col gap-2 mt-2">
            {[
              { c: "var(--a)", l: "Bahasa Malaysia", v: "58%" },
              { c: "var(--info)", l: "English", v: "31%" },
              { c: "var(--ok)", l: "Mandarin", v: "8%" },
              { c: "var(--warn)", l: "Tamil", v: "3%" },
            ].map(r => (
              <div key={r.l} className="row" style={{ justifyContent: "space-between", fontSize: 12 }}>
                <span className="row gap-2 muted"><span style={{ width: 8, height: 8, borderRadius: 2, background: r.c }} /> {r.l}</span>
                <span className="mono">{r.v}</span>
              </div>
            ))}
          </div>
        </Card>
      </div>
    </div>
  );
};

const ANALYTICS_LAYOUT_KEY = "voxpro.analytics.layout.v1";
const ANALYTICS_PRESET_KEY = "voxpro.analytics.presets.v1";
// Widget orders arranged for clean 3-col grid (no gaps):
// Admin:  [PTP hero×3] [volume×3] [outcomes|industries|assistants] [clients|structured×2] [numericTrends×3] [categorical×3]
// Client: [PTP hero×3] [volume×3] [outcomes|structured×2] [numericTrends×3] [categorical×3]
const ADMIN_WIDGET_ORDER = ["collectionsPtp", "volume", "outcomes", "industries", "assistants", "clients", "structured", "structuredNumeric", "structuredCategorical"];
const CLIENT_WIDGET_ORDER = ["collectionsPtp", "volume", "outcomes", "structured", "structuredNumeric", "structuredCategorical"];
const DEFAULT_WIDGET_ORDER = ADMIN_WIDGET_ORDER;
const ANALYTICS_MAX_COLUMNS = 3;
const ANALYTICS_MIN_CARD_WIDTH = 280;
const ANALYTICS_GRID_GAP = 16;
const ANALYTICS_DEFAULT_WIDGET_SIZES = {
  collectionsPtp: { cols: 3, rows: 1 },      // hero — always full-width
  collectionsTimeline: { cols: 1, rows: 1 },
  volume: { cols: 3, rows: 1 },
  outcomes: { cols: 1, rows: 1 },
  industries: { cols: 1, rows: 1 },
  assistants: { cols: 1, rows: 1 },
  clients: { cols: 1, rows: 1 },
  structured: { cols: 2, rows: 1 },
  structuredNumeric: { cols: 3, rows: 1 },
  structuredCategorical: { cols: 3, rows: 1 }, // full-width categorical breakdown
};

function analyticsSeriesFromLogs(logs = [], bucket = "day") {
  const grouped = new Map();
  logs.forEach((log) => {
    const date = new Date(log.startedAt || log.updatedAt || Date.now());
    const key = bucket === "hour"
      ? `${String(date.getHours()).padStart(2, "0")}:00`
      : `${date.getMonth() + 1}/${date.getDate()}`;
    grouped.set(key, (grouped.get(key) || 0) + 1);
  });
  return [...grouped.entries()].slice(-12);
}

function topBreakdown(logs = [], getKey, limit = 5) {
  return Object.entries(logs.reduce((acc, item) => {
    const key = getKey(item) || "Unassigned";
    acc[key] = (acc[key] || 0) + 1;
    return acc;
  }, {}))
    .sort((a, b) => b[1] - a[1])
    .slice(0, limit);
}

function moveItem(list, fromId, toId) {
  if (!fromId || !toId || fromId === toId) return list;
  const next = [...list];
  const fromIndex = next.indexOf(fromId);
  const toIndex = next.indexOf(toId);
  if (fromIndex < 0 || toIndex < 0) return list;
  const [moved] = next.splice(fromIndex, 1);
  next.splice(toIndex, 0, moved);
  return next;
}

function normalizeWidgetOrder(order = [], defaults = []) {
  const base = Array.isArray(order) ? order.filter(Boolean) : [];
  const known = new Set(defaults);
  const deduped = [...new Set(base)].filter(id => known.has(id));
  defaults.forEach(id => {
    if (!deduped.includes(id)) deduped.push(id);
  });
  return deduped;
}

function resolveAnalyticsColumnCount(width) {
  if (!Number.isFinite(width) || width <= 0) return 1;
  if (width >= 900) return 3;
  if (width >= 560) return 2;
  return 1;
}

function getDefaultWidgetSize(widgetId) {
  return ANALYTICS_DEFAULT_WIDGET_SIZES[widgetId] || { cols: 1, rows: 1 };
}

function clampWidgetSize(size = {}, columnCount = 1) {
  return {
    cols: Math.max(1, Math.min(columnCount || 1, Number(size.cols) || 1)),
    rows: Math.max(1, Math.min(3, Number(size.rows) || 1)),
  };
}

function getResolvedWidgetSize(widgetId, widgetSizes = {}, columnCount = 1) {
  const configured = widgetSizes?.[widgetId] || getDefaultWidgetSize(widgetId);
  const clamped = clampWidgetSize(configured, columnCount);
  if (columnCount === 1) return { cols: 1, rows: clamped.rows };
  return clamped;
}

function buildAnalyticsLayoutPlan(order = [], widgetSizes = {}, columnCount = 1) {
  const plan = {};
  order.forEach((widgetId) => {
    const size = getResolvedWidgetSize(widgetId, widgetSizes, columnCount);
    plan[widgetId] = { cols: size.cols };
  });
  return plan;
}

function normalizeAnalyticsText(value = "") {
  return String(value || "").trim().toLowerCase();
}

function buildOrganizationScope(targetClient, clients = []) {
  if (!targetClient) return null;
  const organizationKey = normalizeAnalyticsText(targetClient.organizationKey || targetClient.company || targetClient.name);
  const organizationName = String(targetClient.organizationName || targetClient.company || targetClient.name || "").trim();
  const relatedClients = organizationKey
    ? clients.filter((client) => normalizeAnalyticsText(client.organizationKey || client.company || client.name) === organizationKey)
    : [targetClient];
  const clientIds = new Set(relatedClients.map((client) => String(client.id || "").trim()).filter(Boolean));
  const clientLabels = new Set(
    relatedClients
      .flatMap((client) => [client.organizationName, client.company, client.name])
      .map(normalizeAnalyticsText)
      .filter(Boolean)
  );
  if (organizationName) clientLabels.add(normalizeAnalyticsText(organizationName));
  return {
    organizationKey,
    organizationName,
    clientIds,
    clientLabels,
  };
}

function logMatchesOrganizationScope(log, scope) {
  if (!scope) return true;
  const logOrganizationKey = normalizeAnalyticsText(log.organizationKey);
  if (scope.organizationKey && logOrganizationKey && logOrganizationKey === scope.organizationKey) {
    return true;
  }
  const logClientId = String(log.clientId || log.customerId || "").trim();
  if (logClientId && scope.clientIds.has(logClientId)) {
    return true;
  }
  const labels = [
    normalizeAnalyticsText(log.organizationName),
    normalizeAnalyticsText(log.clientName),
    normalizeAnalyticsText(log.customerName),
  ].filter(Boolean);
  return labels.some((label) => scope.clientLabels.has(label));
}

function AnalyticsWidget({
  widgetId,
  title,
  subtitle,
  badge,
  action,
  onDragStart,
  onDragOver,
  onDrop,
  onResizeStart,
  placement,
  sizeLabel,
  children,
}) {
  return (
    <div
      draggable={Boolean(onDragStart)}
      onDragStart={onDragStart}
      onDragOver={onDragOver}
      onDrop={onDrop}
      style={{
        minWidth: 0,
        position: "relative",
        gridColumn: `span ${Math.min(placement?.cols || 1, 3)}`,
      }}
    >
      <Card
        title={title}
        subtitle={subtitle}
        action={
          <div className="row gap-2" style={{ alignItems: "center" }}>
            {badge && <Badge tone="accent">{badge}</Badge>}
            {action}
            {sizeLabel && <span className="muted text-11">{sizeLabel}</span>}
            {onDragStart && <span className="muted" title="Drag to reorder"><Icon name="drag" /></span>}
          </div>
        }
      >
        {children}
      </Card>
      {onResizeStart && (
        <div
          onMouseDown={(e) => onResizeStart && onResizeStart(widgetId, e)}
          title="Drag to resize"
          style={{
            position: "absolute",
            right: 8,
            bottom: 8,
            width: 14,
            height: 14,
            cursor: "nwse-resize",
            borderRight: "2px solid var(--c-text-4)",
            borderBottom: "2px solid var(--c-text-4)",
            opacity: 0.6,
          }}
        />
      )}
    </div>
  );
}

const AnalyticsView = ({ mode, callLogs = [], assistants = [], clients = [] }) => {
  const session = React.useMemo(() => getSession(), []);
  const isClient = mode === "client";
  const matchedClient = React.useMemo(() => {
    if (session?.clientId) {
      return clients.find(client => client.id === session.clientId) || null;
    }
    const sessionOrgKey = String(session?.organizationKey || "").trim().toLowerCase();
    if (sessionOrgKey) {
      return clients.find(client => String(client.organizationKey || "").trim().toLowerCase() === sessionOrgKey) || null;
    }
    return null;
  }, [clients, session]);
  const organizationOptions = React.useMemo(() => {
    const seen = new Set();
    return clients.reduce((list, client) => {
      const key = normalizeAnalyticsText(client.organizationKey || client.company || client.name);
      if (!key || seen.has(key)) return list;
      seen.add(key);
      list.push({
        id: client.id,
        organizationKey: key,
        label: client.organizationName || client.company || client.name || "Unnamed organization",
      });
      return list;
    }, []).sort((a, b) => a.label.localeCompare(b.label));
  }, [clients]);
  const defaultWidgetOrder = React.useMemo(() => (isClient ? CLIENT_WIDGET_ORDER : ADMIN_WIDGET_ORDER), [isClient]);
  const isAdminOrgEditor = session?.role === "admin" && isClient;
  const showWorkspaceClientFilter = session?.role === "admin" && !isAdminOrgEditor;
  const [range, setRange] = React.useState("30d");
  const [filterIndustry, setFilterIndustry] = React.useState("");
  const [filterAssistant, setFilterAssistant] = React.useState("");
  const [filterClient, setFilterClient] = React.useState("");
  const [tab, setTab] = React.useState("overview");
  const [draggingId, setDraggingId] = React.useState("");
  const [widgetOrder, setWidgetOrder] = React.useState(defaultWidgetOrder);
  const [widgetSizes, setWidgetSizes] = React.useState({});
  const [presetName, setPresetName] = React.useState("");
  const [selectedPreset, setSelectedPreset] = React.useState("");
  const [numericField, setNumericField] = React.useState("");
  const [categoricalField, setCategoricalField] = React.useState("");
  const [layoutLoaded, setLayoutLoaded] = React.useState(false);
  const [profileLocked, setProfileLocked] = React.useState(false);
  const [lockSaving, setLockSaving] = React.useState(false);
  const [selectedOrgId, setSelectedOrgId] = React.useState("");
  const [reportingConfig, setReportingConfig] = React.useState({ allowedStructuredFields: [] });
  const [reportingSaving, setReportingSaving] = React.useState(false);
  const [gridColumnCount, setGridColumnCount] = React.useState(1);
  const layoutSaveTimeout = React.useRef(null);
  const gridRef = React.useRef(null);
  const targetOrgClient = React.useMemo(() => {
    if (session?.role === "client") return matchedClient;
    if (!isAdminOrgEditor || !selectedOrgId) return null;
    return clients.find(client => client.id === selectedOrgId) || null;
  }, [clients, isAdminOrgEditor, matchedClient, selectedOrgId, session]);
  const targetClientId = targetOrgClient?.id || "";
  const organizationScope = React.useMemo(
    () => buildOrganizationScope(targetOrgClient, clients),
    [clients, targetOrgClient]
  );
  const editingOrganizationLabel = targetOrgClient?.organizationName || targetOrgClient?.company || targetOrgClient?.name || "";

  React.useEffect(() => {
    if (!isAdminOrgEditor || selectedOrgId || organizationOptions.length === 0) return;
    setSelectedOrgId(organizationOptions[0].id);
  }, [isAdminOrgEditor, organizationOptions, selectedOrgId]);

  React.useEffect(() => {
    setLayoutLoaded(false);
    if (isAdminOrgEditor && !targetClientId) {
      setProfileLocked(false);
      setReportingConfig({ allowedStructuredFields: [] });
      setWidgetOrder(normalizeWidgetOrder(defaultWidgetOrder, defaultWidgetOrder));
      setWidgetSizes({});
      setLayoutLoaded(true);
      return undefined;
    }
    const path = targetClientId && session?.role === "admin"
      ? `/analytics-presets?clientId=${encodeURIComponent(targetClientId)}`
      : "/analytics-presets";
    voxApiOptional(path, null)
      .then((data) => {
        const scoped = data?.layout || null;
        setProfileLocked(Boolean(data?.locked));
        setReportingConfig(data?.reportingConfig && typeof data.reportingConfig === "object"
          ? data.reportingConfig
          : { allowedStructuredFields: [] });
        const presets = data?.presets && typeof data.presets === "object" ? Object.keys(data.presets) : [];
        if (scoped) {
          setWidgetOrder(normalizeWidgetOrder(scoped.widgetOrder, defaultWidgetOrder));
          setWidgetSizes(scoped.widgetSizes && typeof scoped.widgetSizes === "object" ? scoped.widgetSizes : {});
          if (showWorkspaceClientFilter) {
            setFilterClient(scoped.filterClient || "");
          } else {
            setFilterClient("");
          }
          setFilterIndustry(scoped.filterIndustry || "");
          setFilterAssistant(scoped.filterAssistant || "");
          setRange(scoped.range || "30d");
          setTab(scoped.tab || "overview");
          setNumericField(scoped.numericField || "");
          setCategoricalField(scoped.categoricalField || "");
        } else {
          setWidgetOrder(normalizeWidgetOrder(defaultWidgetOrder, defaultWidgetOrder));
          setWidgetSizes({});
        }
        if (!selectedPreset && presets.length === 0) {
          setSelectedPreset("");
        }
      })
      .catch(() => {
        setProfileLocked(false);
        setReportingConfig({ allowedStructuredFields: [] });
        setWidgetOrder(normalizeWidgetOrder(defaultWidgetOrder, defaultWidgetOrder));
        setWidgetSizes({});
      })
      .finally(() => setLayoutLoaded(true));
  }, [defaultWidgetOrder, isAdminOrgEditor, isClient, session, showWorkspaceClientFilter, targetClientId]);

  // Auto-apply client's industry as default filter when no saved preset has set one
  React.useEffect(() => {
    if (!layoutLoaded || !isClient || !matchedClient?.industry) return;
    setFilterIndustry(prev => prev || matchedClient.industry);
  }, [layoutLoaded, isClient, matchedClient?.industry]);

  React.useEffect(() => {
    if (!layoutLoaded || (profileLocked && session?.role !== "admin")) return;
    if (isAdminOrgEditor && !targetClientId) return;
    const payload = {
      widgetOrder,
      widgetSizes,
      filterIndustry,
      filterAssistant,
      filterClient,
      range,
      tab,
      numericField,
      categoricalField,
    };
    if (layoutSaveTimeout.current) clearTimeout(layoutSaveTimeout.current);
    layoutSaveTimeout.current = setTimeout(() => {
      voxApiOptional("/analytics-presets/layout", null, {
        method: "PUT",
        body: JSON.stringify({
          ...payload,
          targetClientId: session?.role === "admin" ? targetClientId : "",
        }),
      });
    }, 250);
    return () => {
      if (layoutSaveTimeout.current) clearTimeout(layoutSaveTimeout.current);
    };
  }, [categoricalField, filterAssistant, filterClient, filterIndustry, isAdminOrgEditor, layoutLoaded, numericField, profileLocked, range, session, tab, targetClientId, widgetOrder, widgetSizes]);

  React.useEffect(() => {
    if (isClient && (matchedClient || session?.companyName)) {
      setFilterClient(matchedClient?.company || matchedClient?.name || session?.companyName || "");
    }
  }, [isClient, matchedClient, session]);

  React.useEffect(() => {
    const node = gridRef.current;
    if (!node) return undefined;

    function updateColumns() {
      const nextCount = Math.min(
        ANALYTICS_MAX_COLUMNS,
        Math.max(1, resolveAnalyticsColumnCount(node.clientWidth))
      );
      setGridColumnCount(nextCount);
    }

    updateColumns();

    if (typeof ResizeObserver !== "function") {
      window.addEventListener("resize", updateColumns);
      return () => window.removeEventListener("resize", updateColumns);
    }

    const observer = new ResizeObserver(() => updateColumns());
    observer.observe(node);
    return () => observer.disconnect();
  }, []);

  const scopedCallLogs = React.useMemo(() => {
    if (isAdminOrgEditor && !organizationScope) return [];
    if (!organizationScope) return callLogs;
    return callLogs.filter((log) => logMatchesOrganizationScope(log, organizationScope));
  }, [callLogs, isAdminOrgEditor, organizationScope]);

  const industries = React.useMemo(() => [...new Set([
    ...scopedCallLogs.map(l => l.industry).filter(Boolean),
    ...assistants.map(a => a.industry).filter(Boolean),
    ...clients.map(c => c.industry).filter(Boolean),
  ])].sort(), [scopedCallLogs, assistants, clients]);

  const assistantNames = React.useMemo(() => [...new Set([
    ...scopedCallLogs.map(l => l.assistantName).filter(Boolean),
    ...assistants.map(a => a.name).filter(Boolean),
  ])].sort(), [scopedCallLogs, assistants]);

  const clientNames = React.useMemo(() => [...new Set([
    ...scopedCallLogs.map(l => l.clientName || l.organizationName || l.customerName).filter(Boolean),
    ...clients.map(c => c.company || c.name).filter(Boolean),
  ])].sort(), [scopedCallLogs, clients]);

  const rangeFiltered = React.useMemo(() => {
    const now = Date.now();
    const days = { "24h": 1, "7d": 7, "30d": 30, "90d": 90 }[range] || 30;
    const cutoff = now - (days * 24 * 60 * 60 * 1000);
    return scopedCallLogs.filter(log => new Date(log.startedAt || log.updatedAt || 0).getTime() >= cutoff);
  }, [scopedCallLogs, range]);

  const filtered = React.useMemo(() => {
    return rangeFiltered.filter(log => {
      if (filterIndustry && log.industry !== filterIndustry) return false;
      if (filterAssistant && log.assistantName !== filterAssistant) return false;
      if (showWorkspaceClientFilter && filterClient && (log.clientName || log.customerName || "") !== filterClient) return false;
      return true;
    });
  }, [rangeFiltered, filterIndustry, filterAssistant, filterClient, showWorkspaceClientFilter]);

  const allStructuredFields = React.useMemo(() => {
    const counts = {};
    scopedCallLogs.forEach((log) => {
      Object.keys(log.structuredOutput || {}).forEach((key) => {
        counts[key] = (counts[key] || 0) + 1;
      });
    });
    return Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([key]) => key);
  }, [scopedCallLogs]);

  const activeAllowedStructuredFields = React.useMemo(() => {
    const configured = Array.isArray(reportingConfig?.allowedStructuredFields)
      ? reportingConfig.allowedStructuredFields.map((value) => String(value || "").trim()).filter(Boolean)
      : [];
    if (configured.length === 0) return allStructuredFields;
    const configuredSet = new Set(configured);
    return allStructuredFields.filter((field) => configuredSet.has(field));
  }, [allStructuredFields, reportingConfig]);

  const allowedStructuredFieldSet = React.useMemo(
    () => new Set(activeAllowedStructuredFields),
    [activeAllowedStructuredFields]
  );

  const overview = React.useMemo(() => {
    const totalCalls = filtered.length;
    const totalRevenue = filtered.reduce((sum, l) => sum + Number(l.sellingMyr || 0), 0);
    const totalDuration = filtered.reduce((sum, l) => sum + Number(l.durationSeconds || 0), 0);
    const structuredCount = filtered.filter((l) => {
      const keys = Object.keys(l.structuredOutput || {});
      return keys.some((key) => allowedStructuredFieldSet.has(key));
    }).length;
    const successful = filtered.filter(l => {
      const reason = String(l.endedReason || l.outcome || "").toLowerCase();
      return !reason.includes("failed") && !reason.includes("busy") && !reason.includes("voicemail");
    }).length;
    return {
      totalCalls,
      totalRevenue,
      avgDuration: totalCalls ? Math.round(totalDuration / totalCalls) : 0,
      structuredRate: totalCalls ? Math.round((structuredCount / totalCalls) * 100) : 0,
      completionRate: totalCalls ? Math.round((successful / totalCalls) * 100) : 0,
    };
  }, [allowedStructuredFieldSet, filtered]);

  const volumeSeries = React.useMemo(() => analyticsSeriesFromLogs(filtered, range === "24h" ? "hour" : "day"), [filtered, range]);
  const outcomeBreakdown = React.useMemo(() => topBreakdown(filtered, l => l.endedReason || l.outcome || "unknown", 5), [filtered]);
  const industryBreakdown = React.useMemo(() => topBreakdown(filtered, l => l.industry || "Unassigned", 6), [filtered]);
  const assistantBreakdown = React.useMemo(() => topBreakdown(filtered, l => l.assistantName || "Unknown", 6), [filtered]);
  const clientBreakdown = React.useMemo(() => {
    return Object.entries(filtered.reduce((acc, l) => {
      const key = l.clientName || l.customerName || "Unassigned";
      if (!acc[key]) acc[key] = { calls: 0, revenue: 0 };
      acc[key].calls += 1;
      acc[key].revenue += Number(l.sellingMyr || 0);
      return acc;
    }, {}))
      .sort((a, b) => b[1].calls - a[1].calls)
      .slice(0, 5);
  }, [filtered]);
  const structuredBreakdown = React.useMemo(() => {
    const counts = {};
    filtered.forEach(l => {
      Object.keys(l.structuredOutput || {}).forEach(key => {
        if (!allowedStructuredFieldSet.has(key)) return;
        counts[key] = (counts[key] || 0) + 1;
      });
    });
    return Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 6);
  }, [allowedStructuredFieldSet, filtered]);

  const numericStructuredFields = React.useMemo(() => {
    const counts = {};
    filtered.forEach(log => {
      Object.entries(log.structuredOutput || {}).forEach(([key, value]) => {
        if (!allowedStructuredFieldSet.has(key)) return;
        const num = Number(value);
        if (value !== "" && value != null && Number.isFinite(num)) {
          counts[key] = (counts[key] || 0) + 1;
        }
      });
    });
    return Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([key]) => key);
  }, [allowedStructuredFieldSet, filtered]);

  const categoricalStructuredFields = React.useMemo(() => {
    const counts = {};
    filtered.forEach(log => {
      Object.entries(log.structuredOutput || {}).forEach(([key, value]) => {
        if (!allowedStructuredFieldSet.has(key)) return;
        if (value == null || value === "") return;
        if (typeof value === "boolean") {
          counts[key] = (counts[key] || 0) + 1;
          return;
        }
        const num = Number(value);
        if (!Number.isFinite(num) || String(value).trim() === "") {
          counts[key] = (counts[key] || 0) + 1;
        }
      });
    });
    return Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([key]) => key);
  }, [allowedStructuredFieldSet, filtered]);

  React.useEffect(() => {
    if (!numericField || !numericStructuredFields.includes(numericField)) {
      setNumericField(numericStructuredFields[0] || "");
    }
  }, [numericField, numericStructuredFields]);

  React.useEffect(() => {
    if (!categoricalField || !categoricalStructuredFields.includes(categoricalField)) {
      setCategoricalField(categoricalStructuredFields[0] || "");
    }
  }, [categoricalField, categoricalStructuredFields]);

  const numericFieldSeries = React.useMemo(() => {
    if (!numericField) return [];
    return filtered
      .map(log => ({
        label: fmtDate(log.startedAt || log.updatedAt || "") || "—",
        value: Number(log.structuredOutput?.[numericField]),
      }))
      .filter(item => Number.isFinite(item.value))
      .slice(-12);
  }, [filtered, numericField]);

  const categoricalFieldBreakdown = React.useMemo(() => {
    if (!categoricalField) return [];
    return Object.entries(filtered.reduce((acc, log) => {
      const raw = log.structuredOutput?.[categoricalField];
      if (raw == null || raw === "") return acc;
      const key = typeof raw === "boolean" ? (raw ? "Yes" : "No") : String(raw);
      acc[key] = (acc[key] || 0) + 1;
      return acc;
    }, {})).sort((a, b) => b[1] - a[1]).slice(0, 6);
  }, [filtered, categoricalField]);

  const maxIndustry = Math.max(...industryBreakdown.map(([, count]) => count), 1);
  const maxAssistant = Math.max(...assistantBreakdown.map(([, count]) => count), 1);
  const maxStructured = Math.max(...structuredBreakdown.map(([, count]) => count), 1);

  const maxCategoricalField = Math.max(...categoricalFieldBreakdown.map(([, count]) => count), 1);

  // Collections-specific analytics
  const collectionsStats = React.useMemo(() => {
    const today = new Date(); today.setHours(0,0,0,0);
    const ptpLogs = filtered.filter(l => {
      const d = l.expected_payment_date || l.structuredOutput?.expected_payment_date;
      return d && String(d).trim();
    });
    const totalBalance = ptpLogs.reduce((s, l) => {
      const b = Number(l.balance ?? l.structuredOutput?.balance);
      return s + (Number.isFinite(b) ? b : 0);
    }, 0);
    const avgBalance = ptpLogs.length > 0 ? totalBalance / ptpLogs.length : 0;
    const ptpRate = filtered.length > 0 ? (ptpLogs.length / filtered.length) * 100 : 0;

    // Group by payment timeline bucket
    const buckets = { overdue: 0, today: 0, tomorrow: 0, this_week: 0, next_week: 0, later: 0 };
    const bucketBalance = { overdue: 0, today: 0, tomorrow: 0, this_week: 0, next_week: 0, later: 0 };
    ptpLogs.forEach(l => {
      const raw = l.expected_payment_date || l.structuredOutput?.expected_payment_date;
      const d = new Date(raw); d.setHours(0,0,0,0);
      const diffDays = Math.round((d - today) / 86400000);
      const bal = Number(l.balance ?? l.structuredOutput?.balance) || 0;
      let bucket;
      if (diffDays < 0) bucket = "overdue";
      else if (diffDays === 0) bucket = "today";
      else if (diffDays === 1) bucket = "tomorrow";
      else if (diffDays <= 7) bucket = "this_week";
      else if (diffDays <= 14) bucket = "next_week";
      else bucket = "later";
      buckets[bucket] += 1;
      bucketBalance[bucket] += bal;
    });

    return { ptpCount: ptpLogs.length, ptpRate, totalBalance, avgBalance, buckets, bucketBalance };
  }, [filtered]);

  const isCollections = filterIndustry === "collections" ||
    (filtered.length > 3 && filtered.filter(l => l.industry === "collections").length / filtered.length > 0.5);

  const layoutPlan = React.useMemo(
    () => buildAnalyticsLayoutPlan(widgetOrder, widgetSizes, gridColumnCount),
    [widgetOrder, widgetSizes, gridColumnCount]
  );

  function widgetChrome(id) {
    return (
      <div className="row gap-1">
        <span className="muted text-11">{(widgetSizes[id]?.cols || 1)}×{(widgetSizes[id]?.rows || 1)}</span>
      </div>
    );
  }

  function handleResizeStart(id, event) {
    event.preventDefault();
    event.stopPropagation();
    const startX = event.clientX;
    const initial = getResolvedWidgetSize(id, widgetSizes, gridColumnCount);

    function onMove(moveEvent) {
      const dx = moveEvent.clientX - startX;
      const gridWidth = gridRef.current?.clientWidth || 0;
      const colWidth = gridColumnCount > 0
        ? (gridWidth - (Math.max(gridColumnCount - 1, 0) * ANALYTICS_GRID_GAP)) / gridColumnCount
        : ANALYTICS_MIN_CARD_WIDTH;
      const nextCols = Math.max(1, Math.min(gridColumnCount, initial.cols + Math.round(dx / colWidth)));
      setWidgetSizes(prev => ({ ...prev, [id]: { cols: nextCols, rows: 1 } }));
    }

    function onUp() {
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
    }

    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
  }

  function resetLayout() {
    if (profileLocked && session?.role !== "admin") return;
    setWidgetOrder(defaultWidgetOrder);
    setWidgetSizes({});
  }

  function savePreset() {
    if (profileLocked && session?.role !== "admin") return;
    if (isAdminOrgEditor && !targetClientId) return;
    const name = presetName.trim();
    if (!name) return;
    voxApiOptional("/analytics-presets/presets", null, {
      method: "POST",
      body: JSON.stringify({
        name,
        targetClientId: session?.role === "admin" ? targetClientId : "",
        layout: {
          widgetOrder,
          widgetSizes,
          filterIndustry,
          filterAssistant,
          filterClient,
          range,
          tab,
          numericField,
          categoricalField,
        },
      }),
    }).then(() => {
      setSelectedPreset(name);
      setPresetName("");
    });
  }

  const [serverPresets, setServerPresets] = React.useState([]);

  React.useEffect(() => {
    if (isAdminOrgEditor && !targetClientId) {
      setServerPresets([]);
      return;
    }
    const path = targetClientId && session?.role === "admin"
      ? `/analytics-presets?clientId=${encodeURIComponent(targetClientId)}`
      : "/analytics-presets";
    voxApiOptional(path, null).then((data) => {
      setServerPresets(Object.keys(data?.presets || {}).sort());
    });
  }, [isAdminOrgEditor, selectedPreset, session, targetClientId]);

  function applyPreset(name) {
    if (!name) return;
    if (isAdminOrgEditor && !targetClientId) return;
    const path = targetClientId && session?.role === "admin"
      ? `/analytics-presets?clientId=${encodeURIComponent(targetClientId)}`
      : "/analytics-presets";
    voxApiOptional(path, null).then((data) => {
      const preset = data?.presets?.[name];
      if (!preset) return;
      setWidgetOrder(normalizeWidgetOrder(preset.widgetOrder, defaultWidgetOrder));
      setWidgetSizes(preset.widgetSizes || {});
      setFilterIndustry(preset.filterIndustry || "");
      setFilterAssistant(preset.filterAssistant || "");
      if (session?.role === "admin") setFilterClient(preset.filterClient || "");
      setRange(preset.range || "30d");
      setTab(preset.tab || "overview");
      setNumericField(preset.numericField || "");
      setCategoricalField(preset.categoricalField || "");
      setSelectedPreset(name);
    });
  }

  function deletePreset() {
    if (profileLocked && session?.role !== "admin") return;
    if (!selectedPreset) return;
    if (isAdminOrgEditor && !targetClientId) return;
    const path = targetClientId && session?.role === "admin"
      ? `/analytics-presets/presets/${encodeURIComponent(selectedPreset)}?clientId=${encodeURIComponent(targetClientId)}`
      : `/analytics-presets/presets/${encodeURIComponent(selectedPreset)}`;
    voxApiOptional(path, null, {
      method: "DELETE",
    }).then(() => {
      setSelectedPreset("");
      setServerPresets(prev => prev.filter(name => name !== selectedPreset));
    });
  }

  function exportCurrentCsv() {
    if (!filtered.length) return;
    const keys = new Set(["startedAt", "industry", "clientName", "assistantName", "endedReason", "durationSeconds", "sellingMyr"]);
    filtered.forEach(log => Object.keys(log.structuredOutput || {}).forEach(key => {
      if (allowedStructuredFieldSet.has(key)) keys.add(key);
    }));
    const headers = [...keys];
    const csv = [
      headers.join(","),
      ...filtered.map(log => headers.map(key => JSON.stringify(log[key] ?? log.structuredOutput?.[key] ?? "")).join(",")),
    ].join("\n");
    const a = document.createElement("a");
    a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
    a.download = `analytics-${Date.now()}.csv`;
    a.click();
  }

  async function toggleProfileLock() {
    if (session?.role !== "admin" || !targetClientId) return;
    setLockSaving(true);
    try {
      const result = await voxApi("/analytics-presets/lock", {
        method: "PUT",
        body: JSON.stringify({
          targetClientId,
          locked: !profileLocked,
        }),
      });
      setProfileLocked(Boolean(result?.locked));
    } catch (error) {
      console.error("[analytics] failed to toggle lock", error.message);
    } finally {
      setLockSaving(false);
    }
  }

  function toggleAllowedStructuredField(field) {
    setReportingConfig((prev) => {
      const current = Array.isArray(prev?.allowedStructuredFields) ? prev.allowedStructuredFields : [];
      const next = new Set(current);
      if (next.has(field)) {
        next.delete(field);
      } else {
        next.add(field);
      }
      return {
        ...prev,
        allowedStructuredFields: [...next].sort((a, b) => a.localeCompare(b)),
      };
    });
  }

  async function saveReportingRules() {
    if (session?.role !== "admin" || !targetClientId) return;
    setReportingSaving(true);
    try {
      const result = await voxApi("/analytics-presets/reporting-config", {
        method: "PUT",
        body: JSON.stringify({
          targetClientId,
          reportingConfig,
        }),
      });
      setReportingConfig(result?.reportingConfig && typeof result.reportingConfig === "object"
        ? result.reportingConfig
        : { allowedStructuredFields: [] });
    } catch (error) {
      console.error("[analytics] failed to save reporting config", error.message);
    } finally {
      setReportingSaving(false);
    }
  }

  const canEditLayout = session?.role === "admin" || !profileLocked;
  const hasOrgSelected = Boolean(targetClientId);
  const selectedStructuredFieldCount = Array.isArray(reportingConfig?.allowedStructuredFields)
    ? reportingConfig.allowedStructuredFields.length
    : 0;
  const widgetDragProps = React.useCallback((id) => {
    if (!canEditLayout) {
      return {
        onDragStart: undefined,
        onDragOver: undefined,
        onDrop: undefined,
        onResizeStart: undefined,
      };
    }
    return {
      onDragStart: () => setDraggingId(id),
      onDragOver: (e) => e.preventDefault(),
      onDrop: () => {
        setDraggingId("");
        setWidgetOrder(prev => moveItem(prev, draggingId, id));
      },
      onResizeStart: handleResizeStart,
    };
  }, [canEditLayout, draggingId, handleResizeStart]);

  const widgets = {
    volume: (
      <AnalyticsWidget widgetId="volume" title="Call volume" subtitle={`Live call trend for ${range}`} badge={range.toUpperCase()} action={null} placement={layoutPlan.volume} sizeLabel={`${layoutPlan.volume?.cols || 1}×1`} {...widgetDragProps("volume")}>
        <div className="chart-wrap" style={{ height: 240 }}>
          <BarChart height={240} data={volumeSeries.map(([, value]) => value)} labels={volumeSeries.map(([label]) => label)} />
        </div>
      </AnalyticsWidget>
    ),
    outcomes: (
      <AnalyticsWidget widgetId="outcomes" title="Outcome breakdown" subtitle="Most common call endings in the current slice" badge={`${outcomeBreakdown.length} outcomes`} action={null} placement={layoutPlan.outcomes} sizeLabel={`${layoutPlan.outcomes?.cols || 1}×1`} {...widgetDragProps("outcomes")}>
        <div className="row" style={{ justifyContent: "center", padding: "6px 0 12px" }}>
          <Donut size={150} label={overview.totalCalls.toLocaleString()} sub="calls" segments={outcomeBreakdown.map(([, value], idx) => ({ value, color: ["var(--a)", "var(--ok)", "var(--warn)", "var(--err)", "var(--info)"][idx % 5] }))} />
        </div>
        <div className="col gap-2">
          {outcomeBreakdown.map(([label, count], idx) => (
            <div key={label} className="row" style={{ justifyContent: "space-between", fontSize: 12 }}>
              <span className="row gap-2 muted"><span style={{ width: 8, height: 8, borderRadius: 2, background: ["var(--a)", "var(--ok)", "var(--warn)", "var(--err)", "var(--info)"][idx % 5] }} />{label}</span>
              <span className="mono">{count}</span>
            </div>
          ))}
        </div>
      </AnalyticsWidget>
    ),
    industries: (
      <AnalyticsWidget widgetId="industries" title="Industry mix" subtitle="Volume by industry" badge={filterIndustry || "All industries"} action={null} placement={layoutPlan.industries} sizeLabel={`${layoutPlan.industries?.cols || 1}×1`} {...widgetDragProps("industries")}>
        <div className="col gap-3">
          {industryBreakdown.map(([label, count]) => (
            <div key={label}>
              <div className="row" style={{ justifyContent: "space-between" }}><span className="text-12">{label}</span><span className="mono text-11">{count}</span></div>
              <div style={{ height: 5, background: "var(--c-surface-2)", borderRadius: 999, marginTop: 4 }}><div style={{ width: `${(count / maxIndustry) * 100}%`, height: "100%", background: "var(--a)", borderRadius: 999 }} /></div>
            </div>
          ))}
          {industryBreakdown.length === 0 && <div className="muted text-11">No industry-tagged calls in this range.</div>}
        </div>
      </AnalyticsWidget>
    ),
    assistants: (
      <AnalyticsWidget widgetId="assistants" title="Assistant leaderboard" subtitle="Top assistants by call volume" badge={filterAssistant || "All assistants"} action={null} placement={layoutPlan.assistants} sizeLabel={`${layoutPlan.assistants?.cols || 1}×1`} {...widgetDragProps("assistants")}>
        <div className="col gap-3">
          {assistantBreakdown.map(([label, count]) => (
            <div key={label}>
              <div className="row" style={{ justifyContent: "space-between" }}><span className="text-12">{label}</span><span className="mono text-11">{count}</span></div>
              <div style={{ height: 5, background: "var(--c-surface-2)", borderRadius: 999, marginTop: 4 }}><div style={{ width: `${(count / maxAssistant) * 100}%`, height: "100%", background: "var(--ok)", borderRadius: 999 }} /></div>
            </div>
          ))}
        </div>
      </AnalyticsWidget>
    ),
    clients: (
      <AnalyticsWidget widgetId="clients" title={isClient ? "Your account" : "Customer accounts"} subtitle="Customer-specific reporting from the current dataset" badge={filterClient || "All clients"} action={null} placement={layoutPlan.clients} sizeLabel={`${layoutPlan.clients?.cols || 1}×1`} {...widgetDragProps("clients")}>
        <div className="col gap-3">
          {clientBreakdown.map(([label, stats]) => (
            <div key={label} style={{ paddingBottom: 8, borderBottom: "1px solid var(--c-line)" }}>
              <div className="row" style={{ justifyContent: "space-between", marginBottom: 4 }}><span className="text-12 strong">{label}</span><span className="mono text-11">{stats.calls} calls</span></div>
              <div className="row" style={{ justifyContent: "space-between", fontSize: 11 }}><span className="muted">Revenue</span><span className="mono">{fmtRM(stats.revenue)}</span></div>
            </div>
          ))}
          {clientBreakdown.length === 0 && <div className="muted text-11">No client-linked logs in this range.</div>}
        </div>
      </AnalyticsWidget>
    ),
    structured: (
      <AnalyticsWidget widgetId="structured" title="Structured outputs" subtitle="Most populated extracted fields across customers" badge={`${overview.structuredRate}% coverage`} action={null} placement={layoutPlan.structured} sizeLabel={`${layoutPlan.structured?.cols || 1}×1`} {...widgetDragProps("structured")}>
        <div className="col gap-3">
          {structuredBreakdown.map(([label, count]) => (
            <div key={label}>
              <div className="row" style={{ justifyContent: "space-between" }}><span className="text-12 mono">{label}</span><span className="mono text-11">{count}</span></div>
              <div style={{ height: 5, background: "var(--c-surface-2)", borderRadius: 999, marginTop: 4 }}><div style={{ width: `${(count / maxStructured) * 100}%`, height: "100%", background: "var(--warn)", borderRadius: 999 }} /></div>
            </div>
          ))}
          {structuredBreakdown.length === 0 && <div className="muted text-11">No structured-output fields captured yet.</div>}
        </div>
      </AnalyticsWidget>
    ),
    structuredNumeric: (
      <AnalyticsWidget widgetId="structuredNumeric" title="Structured numeric trends" subtitle="Numeric extracted fields for the current customer slice" badge={numericField || "No numeric field"} action={null} placement={layoutPlan.structuredNumeric} sizeLabel={`${layoutPlan.structuredNumeric?.cols || 1}×1`} {...widgetDragProps("structuredNumeric")}>
        <Field label="Numeric field">
          <Select value={numericField} onChange={e => setNumericField(e.target.value)}>
            <option value="">Select field</option>
            {numericStructuredFields.map(field => <option key={field} value={field}>{field}</option>)}
          </Select>
        </Field>
        {numericFieldSeries.length > 0 ? (
          <div className="chart-wrap" style={{ height: 240, marginTop: 10 }}>
            <LineChart data={numericFieldSeries.map(item => item.value)} height={240} color="var(--warn)" />
          </div>
        ) : (
          <div className="muted text-11" style={{ paddingTop: 10 }}>No numeric structured values available for this selection.</div>
        )}
      </AnalyticsWidget>
    ),
    structuredCategorical: (
      <AnalyticsWidget widgetId="structuredCategorical" title="Structured categorical mix" subtitle="Breakdown of customer-specific extracted values" badge={categoricalField || "No categorical field"} action={null} placement={layoutPlan.structuredCategorical} sizeLabel={`${layoutPlan.structuredCategorical?.cols || 1}×1`} {...widgetDragProps("structuredCategorical")}>
        <Field label="Categorical field">
          <Select value={categoricalField} onChange={e => setCategoricalField(e.target.value)}>
            <option value="">Select field</option>
            {categoricalStructuredFields.map(field => <option key={field} value={field}>{field}</option>)}
          </Select>
        </Field>
        {categoricalFieldBreakdown.length > 0 ? (
          <div className="col gap-3 mt-3">
            {categoricalFieldBreakdown.map(([label, count]) => (
              <div key={label}>
                <div className="row" style={{ justifyContent: "space-between" }}>
                  <span className="text-12">{label}</span>
                  <span className="mono text-11">{count}</span>
                </div>
                <div style={{ height: 5, background: "var(--c-surface-2)", borderRadius: 999, marginTop: 4 }}>
                  <div style={{ width: `${(count / maxCategoricalField) * 100}%`, height: "100%", background: "var(--info)", borderRadius: 999 }} />
                </div>
              </div>
            ))}
          </div>
        ) : (
          <div className="muted text-11" style={{ paddingTop: 10 }}>No categorical structured values available for this selection.</div>
        )}
      </AnalyticsWidget>
    ),
    collectionsPtp: (
      <AnalyticsWidget widgetId="collectionsPtp" title="Promise to Pay" subtitle="Customers who committed to a payment date during collections calls" badge={`${collectionsStats.ptpRate.toFixed(1)}% PTP rate`} action={null} placement={layoutPlan.collectionsPtp} sizeLabel={`${layoutPlan.collectionsPtp?.cols || 1}×1`} {...widgetDragProps("collectionsPtp")}>
        {collectionsStats.ptpCount === 0 ? (
          <div className="muted text-11" style={{ padding: "16px 0" }}>No promise-to-pay data. Run a collections campaign to see results here.</div>
        ) : (
          <div style={{ display: "grid", gridTemplateColumns: "auto 1fr auto", gap: 36, alignItems: "center" }}>
            {/* PTP rate ring */}
            <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>
              <svg width="148" height="148" viewBox="0 0 148 148">
                <circle cx="74" cy="74" r="56" fill="none" stroke="var(--c-surface-2)" strokeWidth="14" />
                <circle cx="74" cy="74" r="56" fill="none" stroke="var(--ok)" strokeWidth="14"
                  strokeDasharray={`${(collectionsStats.ptpRate / 100) * 351.86} 351.86`}
                  strokeLinecap="round"
                  transform="rotate(-90 74 74)"
                />
                <text x="74" y="68" textAnchor="middle" fontSize="26" fontWeight="800" fill="var(--c-text)" fontFamily="inherit">{collectionsStats.ptpRate.toFixed(1)}%</text>
                <text x="74" y="87" textAnchor="middle" fontSize="11" fill="var(--c-text-3)" fontFamily="inherit">of {filtered.length} calls</text>
              </svg>
              <span style={{ fontSize: 10, color: "var(--c-text-3)" }}>Goal: 100% PTP</span>
            </div>
            {/* Key stats */}
            <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
              <div>
                <div style={{ fontSize: 11, color: "var(--c-text-3)", marginBottom: 4 }}>Customers Committed</div>
                <div style={{ fontSize: 44, fontWeight: 800, color: "var(--ok)", letterSpacing: "-0.03em", lineHeight: 1 }}>{collectionsStats.ptpCount}</div>
                <div style={{ fontSize: 11, color: "var(--c-text-3)", marginTop: 5 }}>{collectionsStats.ptpRate.toFixed(1)}% conversion · {filtered.length} contacted</div>
              </div>
              <div className="g-2" style={{ gap: 10 }}>
                <div className="stat" style={{ padding: "10px 14px" }}>
                  <div className="stat-label" style={{ fontSize: 10 }}>Pending to Collect</div>
                  <div className="stat-value" style={{ fontSize: 20, color: "var(--warn)" }}>{fmtRM(collectionsStats.totalBalance)}</div>
                </div>
                <div className="stat" style={{ padding: "10px 14px" }}>
                  <div className="stat-label" style={{ fontSize: 10 }}>Avg per Customer</div>
                  <div className="stat-value" style={{ fontSize: 20 }}>{fmtRM(collectionsStats.avgBalance)}</div>
                </div>
              </div>
              <div>
                <div style={{ height: 8, background: "var(--c-surface-2)", borderRadius: 4, overflow: "hidden" }}>
                  <div style={{ width: `${Math.min(100, collectionsStats.ptpRate)}%`, height: "100%", background: "linear-gradient(90deg, var(--ok), var(--a))", borderRadius: 4, transition: "width 0.5s" }} />
                </div>
                <div className="row mt-1" style={{ justifyContent: "space-between", fontSize: 10, color: "var(--c-text-3)" }}>
                  <span>0%</span><span>PTP target: 100%</span>
                </div>
              </div>
            </div>
            {/* Inline payment timeline */}
            <div style={{ minWidth: 230 }}>
              <div style={{ fontSize: 12, fontWeight: 600, color: "var(--c-text-2)", marginBottom: 14 }}>Payment timeline</div>
              <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
                {[
                  { key: "overdue", label: "Overdue", color: "var(--err)" },
                  { key: "today", label: "Today", color: "var(--warn)" },
                  { key: "tomorrow", label: "Tomorrow", color: "var(--warn)" },
                  { key: "this_week", label: "This week", color: "var(--a)" },
                  { key: "next_week", label: "Next week", color: "var(--a)" },
                  { key: "later", label: "Later", color: "var(--c-text-3)" },
                ].filter(b => collectionsStats.buckets[b.key] > 0).map(({ key, label, color }) => (
                  <div key={key}>
                    <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 4 }}>
                      <span style={{ fontSize: 11, color }}>{label}</span>
                      <div style={{ display: "flex", gap: 10 }}>
                        <span className="mono" style={{ fontSize: 11 }}>{collectionsStats.buckets[key]}</span>
                        <span className="mono muted" style={{ fontSize: 11 }}>{fmtRM(collectionsStats.bucketBalance[key])}</span>
                      </div>
                    </div>
                    <div style={{ height: 6, background: "var(--c-surface-2)", borderRadius: 3, overflow: "hidden" }}>
                      <div style={{ width: `${(collectionsStats.buckets[key] / collectionsStats.ptpCount) * 100}%`, height: "100%", background: color, borderRadius: 3 }} />
                    </div>
                  </div>
                ))}
              </div>
            </div>
          </div>
        )}
      </AnalyticsWidget>
    ),
    collectionsTimeline: (
      <AnalyticsWidget widgetId="collectionsTimeline" title="Payment timeline" subtitle="When customers promised to pay" badge={`${collectionsStats.ptpCount} promises`} action={null} placement={layoutPlan.collectionsTimeline} sizeLabel={`${layoutPlan.collectionsTimeline?.cols || 1}×1`} {...widgetDragProps("collectionsTimeline")}>
        {collectionsStats.ptpCount === 0 ? (
          <div className="muted text-11">No payment dates captured yet.</div>
        ) : (
          <div className="col gap-3">
            {[
              { key: "overdue", label: "Overdue", color: "var(--err)" },
              { key: "today", label: "Today", color: "var(--warn)" },
              { key: "tomorrow", label: "Tomorrow", color: "var(--warn)" },
              { key: "this_week", label: "This week", color: "var(--a)" },
              { key: "next_week", label: "Next week", color: "var(--a)" },
              { key: "later", label: "Later", color: "var(--c-text-3)" },
            ].filter(b => collectionsStats.buckets[b.key] > 0).map(({ key, label, color }) => (
              <div key={key}>
                <div className="row" style={{ justifyContent: "space-between", marginBottom: 3 }}>
                  <span className="text-12" style={{ color }}>{label}</span>
                  <div className="row gap-3">
                    <span className="mono text-11">{collectionsStats.buckets[key]} calls</span>
                    <span className="mono text-11 muted">{fmtRM(collectionsStats.bucketBalance[key])}</span>
                  </div>
                </div>
                <div style={{ height: 5, background: "var(--c-surface-2)", borderRadius: 999 }}>
                  <div style={{ width: `${(collectionsStats.buckets[key] / collectionsStats.ptpCount) * 100}%`, height: "100%", background: color, borderRadius: 999 }} />
                </div>
              </div>
            ))}
          </div>
        )}
      </AnalyticsWidget>
    ),
  };

  return (
    <div>
      <div className="ph">
        <div>
          <h1 className="ph-title">Analytics</h1>
          <div className="ph-subtitle">
            {isClient
              ? "Your saved analytics layout, customer-specific structured output charts, and reporting presets."
              : "Configurable reporting by assistant, customer, industry, and structured output fields."}
          </div>
          {isAdminOrgEditor && (
            <div className="row gap-2 mt-2" style={{ flexWrap: "wrap" }}>
              <Badge tone="accent">Editing org</Badge>
              <span className="text-12" style={{ color: "var(--c-text-2)" }}>
                {editingOrganizationLabel || "Choose an organization to configure its reporting dashboard."}
              </span>
            </div>
          )}
        </div>
        <div className="ph-actions">
          {session?.role === "admin" && targetClientId && !isAdminOrgEditor && (
            <Btn variant="ghost" size="sm" onClick={toggleProfileLock}>
              {lockSaving ? "Saving…" : profileLocked ? "Unlock org layout" : "Lock org layout"}
            </Btn>
          )}
          <Segmented value={range} onChange={setRange} options={[{ value: "24h", label: "24h" }, { value: "7d", label: "7d" }, { value: "30d", label: "30d" }, { value: "90d", label: "90d" }]} />
          <Btn variant="ghost" icon="download" size="sm" onClick={exportCurrentCsv}>Export</Btn>
          <Btn variant="ghost" size="sm" onClick={resetLayout} disabled={!canEditLayout}>Reset layout</Btn>
        </div>
      </div>

      {isAdminOrgEditor && (
        <Card
          title="Organization dashboard controls"
          subtitle="Choose which organization you are editing, lock its analytics dashboard, and control exactly which structured-output fields appear in reporting."
          className="mb-3"
          action={
            <div className="row gap-2" style={{ flexWrap: "wrap", justifyContent: "flex-end" }}>
              <Badge tone={profileLocked ? "warn" : "ok"}>{profileLocked ? "Locked" : "Unlocked"}</Badge>
              <Btn variant="ghost" size="sm" onClick={toggleProfileLock} disabled={!hasOrgSelected || lockSaving}>
                {lockSaving ? "Saving..." : profileLocked ? "Unlock this organization dashboard" : "Lock this organization dashboard"}
              </Btn>
            </div>
          }
        >
          <div className="g-2" style={{ alignItems: "start" }}>
            <Field label="Editing organization">
              <Select value={selectedOrgId} onChange={e => setSelectedOrgId(e.target.value)}>
                <option value="">Choose organization</option>
                {organizationOptions.map((option) => (
                  <option key={option.organizationKey} value={option.id}>{option.label}</option>
                ))}
              </Select>
            </Field>
            <Field label="Allowed structured reporting fields" help="Select exactly which bot structured-output fields this organization can access in analytics. Leave every box unchecked to keep all current fields visible.">
              <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 10 }}>
                <Btn variant="ghost" size="sm" onClick={() => setReportingConfig(prev => ({ ...prev, allowedStructuredFields: [...allStructuredFields] }))} disabled={!hasOrgSelected || allStructuredFields.length === 0}>Allow all current fields</Btn>
                <Btn variant="ghost" size="sm" onClick={() => setReportingConfig(prev => ({ ...prev, allowedStructuredFields: [] }))} disabled={!hasOrgSelected}>Show all fields</Btn>
                <Btn variant="ghost" size="sm" onClick={saveReportingRules} disabled={!hasOrgSelected || reportingSaving}>{reportingSaving ? "Saving..." : "Save field rules"}</Btn>
              </div>
              <div className="row gap-2 mb-2" style={{ flexWrap: "wrap" }}>
                <Badge tone="info">{selectedStructuredFieldCount ? `${selectedStructuredFieldCount} fields explicitly allowed` : "All structured fields currently visible"}</Badge>
                {editingOrganizationLabel && <Badge tone="default">{editingOrganizationLabel}</Badge>}
              </div>
              <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
                {allStructuredFields.length === 0 && <span className="muted text-11">No structured fields have been captured for this organization yet.</span>}
                {allStructuredFields.map((field) => {
                  const checked = (reportingConfig.allowedStructuredFields || []).includes(field);
                  return (
                    <label
                      key={field}
                      className="chip"
                      style={{
                        display: "inline-flex",
                        alignItems: "center",
                        gap: 8,
                        cursor: "pointer",
                        borderColor: checked ? "var(--a-line)" : "var(--c-line)",
                        background: checked ? "rgba(141, 139, 255, 0.12)" : undefined,
                      }}
                    >
                      <input type="checkbox" checked={checked} onChange={() => toggleAllowedStructuredField(field)} />
                      <span className="mono text-11">{field}</span>
                    </label>
                  );
                })}
              </div>
            </Field>
          </div>
        </Card>
      )}

      {profileLocked && (
        <div style={{ marginBottom: 12, padding: "10px 14px", borderRadius: 10, border: "1px solid var(--c-line)", background: "var(--c-surface-2)", fontSize: 12, color: "var(--c-text-2)" }}>
          {session?.role === "admin"
            ? "This organization dashboard is locked for client users. You can still edit it as developer/admin."
            : "This organization dashboard has been locked by the developer. You can view it, but layout changes will not be saved."}
        </div>
      )}

      <Tabs tabs={[{ id: "overview", label: "Overview" }, { id: "performance", label: "Performance" }, { id: "customer", label: "Customer reporting" }]} active={tab} onSelect={setTab} />

      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 14, alignItems: "center" }}>
        {industries.length > 0 && <Select value={filterIndustry} onChange={e => setFilterIndustry(e.target.value)} style={{ width: 180 }}><option value="">All industries</option>{industries.map(name => <option key={name} value={name}>{name}</option>)}</Select>}
        {assistantNames.length > 0 && <Select value={filterAssistant} onChange={e => setFilterAssistant(e.target.value)} style={{ width: 180 }}><option value="">All assistants</option>{assistantNames.map(name => <option key={name} value={name}>{name}</option>)}</Select>}
        {showWorkspaceClientFilter && clientNames.length > 0 && <Select value={filterClient} onChange={e => setFilterClient(e.target.value)} style={{ width: 200 }}><option value="">All clients</option>{clientNames.map(name => <option key={name} value={name}>{name}</option>)}</Select>}
        {(filterIndustry || filterAssistant || (showWorkspaceClientFilter && filterClient)) && <Btn variant="ghost" size="sm" onClick={() => { setFilterIndustry(""); setFilterAssistant(""); if (showWorkspaceClientFilter) setFilterClient(""); }}>Clear filters</Btn>}
      </div>

      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 14, alignItems: "center" }}>
        <Select value={selectedPreset} onChange={e => { setSelectedPreset(e.target.value); applyPreset(e.target.value); }} style={{ width: 220 }}>
          <option value="">Saved presets</option>
          {serverPresets.map(name => <option key={name} value={name}>{name}</option>)}
        </Select>
        <Input value={presetName} onChange={e => setPresetName(e.target.value)} placeholder={isAdminOrgEditor ? "Save org dashboard preset" : isClient ? "Save your layout preset" : "Save workspace preset"} style={{ width: 220 }} />
        <Btn variant="ghost" size="sm" onClick={savePreset} disabled={!canEditLayout}>Save preset</Btn>
        {selectedPreset && <Btn variant="ghost" size="sm" onClick={deletePreset} disabled={!canEditLayout}>Delete preset</Btn>}
      </div>

      {isCollections && (
        <div style={{ marginBottom: 20 }}>{widgets["collectionsPtp"]}</div>
      )}

      <div className="stat-grid">
        <StatTile k={{ label: "Total calls", value: overview.totalCalls.toLocaleString(), delta: range.toUpperCase(), spark: [4,5,6,5,7,8,9,11,10,12,13,14,15,16,17,18,19,21,23,24] }} />
        <StatTile k={{ label: "Avg duration", value: fmtDuration(overview.avgDuration), delta: `${overview.completionRate}% completion`, spark: [22,20,21,19,18,18,17,16,15,16,15,14,15,14,13,13,12,13,12,11] }} />
        <StatTile k={{ label: "Structured coverage", value: `${overview.structuredRate}%`, delta: `${structuredBreakdown.length} fields`, spark: [55,58,57,60,62,61,63,65,64,66,67,66,67,68,67,67,68,68,67,67] }} />
        {(filterIndustry === "collections" || (filtered.length > 0 && filtered.filter(l => l.industry === "collections").length / filtered.length > 0.5)) ? (
          <StatTile k={{ label: "PTP to Collect", value: fmtRM(collectionsStats.totalBalance), delta: `${collectionsStats.ptpCount} customers committed`, spark: [60,62,64,63,65,68,67,70,71,72,73,72,74,75,76,77,77,78,78,78] }} />
        ) : (
          <StatTile k={{ label: "Revenue", value: fmtRM(overview.totalRevenue), delta: isAdminOrgEditor ? (editingOrganizationLabel || "Choose organization") : (filterClient || filterIndustry || "All customers"), spark: [60,62,64,63,65,68,67,70,71,72,73,72,74,75,76,77,77,78,78,78] }} />
        )}
      </div>

      <div className="row gap-2 mt-2" style={{ flexWrap: "wrap", marginBottom: 16 }}>
        <Badge tone="accent">Drag widgets to reorder</Badge>
        <Badge tone="default">{filtered.length.toLocaleString()} calls in view</Badge>
        {isAdminOrgEditor && editingOrganizationLabel && <Badge tone="info">Editing: {editingOrganizationLabel}</Badge>}
        {filterIndustry && <Badge tone="info">Industry: {filterIndustry}</Badge>}
        {filterAssistant && <Badge tone="info">Assistant: {filterAssistant}</Badge>}
        {showWorkspaceClientFilter && filterClient && <Badge tone="info">Client: {filterClient}</Badge>}
      </div>

      <div
        ref={gridRef}
        style={{
          display: "grid",
          gridTemplateColumns: `repeat(${gridColumnCount}, minmax(0, 1fr))`,
          gap: ANALYTICS_GRID_GAP,
          alignItems: "start",
        }}
      >
        {widgetOrder.filter(id => !(isCollections && id === "collectionsPtp")).map(id => <React.Fragment key={id}>{widgets[id]}</React.Fragment>)}
      </div>

      {tab !== "overview" && (
        <div className="g-2 mt-4">
          <Card title={tab === "performance" ? "Performance notes" : "Customer reporting notes"}>
            <div className="col gap-3">
              {tab === "performance"
                ? <>
                    <div className="text-12">Use the volume, outcome, and assistant widgets together to spot handoff issues or underperforming assistants.</div>
                    <div className="text-12">The structured coverage card helps show whether each workflow is producing the data needed for downstream reporting.</div>
                  </>
                : <>
                    <div className="text-12">Filter by client or industry to turn this page into a customer-specific reporting view.</div>
                    <div className="text-12">Each customer can now have a tailored structured-output report without changing the raw log model.</div>
                  </>}
            </div>
          </Card>
          <Card title="Top structured fields by customer">
            <div className="col gap-3">
              {clientBreakdown.map(([clientLabel]) => {
                const clientLogs = filtered.filter(log => (log.clientName || log.customerName || "Unassigned") === clientLabel);
                const fieldEntries = Object.entries(clientLogs.reduce((acc, log) => {
                  Object.keys(log.structuredOutput || {}).forEach(key => {
                    if (!allowedStructuredFieldSet.has(key)) return;
                    acc[key] = (acc[key] || 0) + 1;
                  });
                  return acc;
                }, {})).sort((a, b) => b[1] - a[1]).slice(0, 3);
                return (
                  <div key={clientLabel} style={{ paddingBottom: 8, borderBottom: "1px solid var(--c-line)" }}>
                    <div className="strong text-12 mb-1">{clientLabel}</div>
                    <div className="row gap-2" style={{ flexWrap: "wrap" }}>
                      {fieldEntries.length ? fieldEntries.map(([field, count]) => <span key={field} className="chip">{field} · {count}</span>) : <span className="muted text-11">No structured fields yet</span>}
                    </div>
                  </div>
                );
              })}
            </div>
          </Card>
        </div>
      )}
    </div>
  );
};

// ---------- Billing ----------
const BillingView = ({ mode, clients = [] }) => {
  const isClient = mode === "client";
  const session = React.useMemo(() => getSession(), []);
  const isAdmin = session?.role === "admin";

  const today = new Date();
  // Default to a wide 90-day window, not "this month" — a client with low call
  // volume can easily have zero activity in the current month, which made the
  // whole billing page look broken/empty for no real reason. Widening the
  // default gives a much better chance of showing real data on first load;
  // the date pickers are still there to narrow down to an exact billing period.
  const ninetyDaysAgo = new Date(today.getTime() - 90 * 24 * 60 * 60 * 1000);
  const [selectedClientId, setSelectedClientId] = React.useState("");
  const [from, setFrom] = React.useState(ninetyDaysAgo.toISOString().slice(0, 10));
  const [to, setTo] = React.useState(today.toISOString().slice(0, 10));
  const [invoice, setInvoice] = React.useState(null);
  const [loading, setLoading] = React.useState(false);
  const [err, setErr] = React.useState("");
  const [downloading, setDownloading] = React.useState(false);
  const [wallet, setWallet] = React.useState(null);
  const [ledger, setLedger] = React.useState(null);

  React.useEffect(() => {
    if (isAdmin && !selectedClientId && clients.length > 0) {
      setSelectedClientId(clients[0].id);
    }
  }, [isAdmin, clients, selectedClientId]);

  const activeClientId = isAdmin ? selectedClientId : session?.clientId;

  React.useEffect(() => {
    if (!activeClientId) { setInvoice(null); return; }
    let cancelled = false;
    setLoading(true);
    setErr("");
    const params = new URLSearchParams({
      from: new Date(from).toISOString(),
      to: new Date(`${to}T23:59:59`).toISOString(),
    });
    if (isAdmin) params.set("clientId", activeClientId);
    voxApi(`/invoices/summary?${params.toString()}`)
      .then((res) => { if (!cancelled) setInvoice(res.invoice); })
      .catch((e) => { if (!cancelled) { setErr(e.message); setInvoice(null); } })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [activeClientId, from, to, isAdmin]);

  React.useEffect(() => {
    if (!activeClientId) { setWallet(null); return; }
    let cancelled = false;
    const params = isAdmin ? `?clientId=${encodeURIComponent(activeClientId)}` : "";
    voxApi(`/wallet/${activeClientId}${params}`)
      .then((res) => { if (!cancelled) setWallet(res.wallet); })
      .catch(() => { if (!cancelled) setWallet(null); });
    return () => { cancelled = true; };
  }, [activeClientId, isAdmin]);

  React.useEffect(() => {
    if (!isAdmin) { setLedger(null); return; }
    let cancelled = false;
    const params = new URLSearchParams({
      from: new Date(from).toISOString(),
      to: new Date(`${to}T23:59:59`).toISOString(),
    });
    voxApi(`/wallet/admin/ledger?${params.toString()}`)
      .then((res) => { if (!cancelled) setLedger(res.clients || []); })
      .catch(() => { if (!cancelled) setLedger(null); });
    return () => { cancelled = true; };
  }, [isAdmin, from, to]);

  async function download(format) {
    if (!activeClientId) return;
    setDownloading(true);
    try {
      const token = session?.token || "";
      const params = new URLSearchParams({
        from: new Date(from).toISOString(),
        to: new Date(`${to}T23:59:59`).toISOString(),
        format,
      });
      if (isAdmin) params.set("clientId", activeClientId);
      const res = await fetch(`/invoices/download?${params.toString()}`, {
        headers: token ? { Authorization: `Bearer ${token}` } : {},
      });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        throw new Error(d.error || "Download failed");
      }
      const blob = await res.blob();
      const disposition = res.headers.get("Content-Disposition") || "";
      const match = disposition.match(/filename="([^"]+)"/);
      const url = URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url;
      link.download = match ? match[1] : `invoice.${format}`;
      link.click();
      window.setTimeout(() => URL.revokeObjectURL(url), 1000);
    } catch (e) {
      alert("Download failed: " + e.message);
    } finally {
      setDownloading(false);
    }
  }

  const dailyTotals = React.useMemo(() => {
    if (!invoice) return [];
    const map = new Map();
    for (const c of invoice.calls) {
      if (!c.date) continue;
      const day = new Date(c.date).toISOString().slice(0, 10);
      map.set(day, (map.get(day) || 0) + Number(c.cost || 0));
    }
    return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
  }, [invoice]);

  return (
    <div>
      <div className="ph">
        <div>
          <h1 className="ph-title">Billing</h1>
          <div className="ph-subtitle">{isClient ? "Review your calls, SMS/WhatsApp cost, and download invoices." : "Real per-client cost — actual rate configured per client, not a mock."}</div>
        </div>
        <div className="ph-actions">
          <Btn variant="ghost" icon="download" size="sm" onClick={() => download("pdf")} disabled={downloading || !invoice}>Invoice (PDF)</Btn>
          <Btn variant="primary" icon="download" size="sm" onClick={() => download("xlsx")} disabled={downloading || !invoice}>Telco bill (Excel)</Btn>
        </div>
      </div>

      <div className="row gap-3 mb-4" style={{ flexWrap: "wrap", alignItems: "flex-end" }}>
        {isAdmin && (
          <Field label="Client">
            <Select value={selectedClientId} onChange={e => setSelectedClientId(e.target.value)}>
              <option value="">— select —</option>
              {clients.map(c => <option key={c.id} value={c.id}>{c.company || c.name}</option>)}
            </Select>
          </Field>
        )}
        <Field label="From"><Input type="date" value={from} onChange={e => setFrom(e.target.value)} /></Field>
        <Field label="To"><Input type="date" value={to} onChange={e => setTo(e.target.value)} /></Field>
      </div>

      {err && <div className="text-12 mb-3" style={{ color: "var(--err)" }}>{err}</div>}
      {loading && <div className="muted text-12 mb-3">Loading…</div>}

      {invoice && invoice.calls.length === 0 && invoice.events.length === 0 && (
        <div className="mb-4" style={{ padding: "12px 16px", borderRadius: 8, background: "var(--warn-bg, #fef3c7)", border: "1px solid var(--warn, #d97706)", color: "var(--warn-text, #92400e)" }}>
          No calls, SMS, or WhatsApp activity between {new Date(from).toLocaleDateString("en-MY")} and {new Date(to).toLocaleDateString("en-MY")} for this client. The invoice/telco bill download will be an empty file — widen the date range above if you expected data here.
        </div>
      )}

      {invoice && (
        <>
          <div className="g-3 mb-4">
            <div className="stat">
              <div className="stat-label">Voice calls this period</div>
              <div className="stat-value">RM {invoice.callCostTotal.toFixed(2)}</div>
              <div className="muted text-11 mt-2">{invoice.callMinutesTotal.toFixed(1)} min across {invoice.calls.length} calls</div>
            </div>
            <div className="stat">
              <div className="stat-label">SMS &amp; WhatsApp this period</div>
              <div className="stat-value">RM {invoice.eventCostTotal.toFixed(2)}</div>
              <div className="muted text-11 mt-2">{invoice.events.reduce((s, e) => s + e.count, 0)} events sent</div>
            </div>
            <div className="stat" style={{ background: "linear-gradient(180deg, rgba(141, 139, 255, 0.08), rgba(141, 139, 255, 0.01))", border: "1px solid var(--a-line)" }}>
              <div className="stat-label">Total this period</div>
              <div className="stat-value">RM {invoice.grandTotal.toFixed(2)}</div>
              <div className="muted text-11 mt-2">{new Date(from).toLocaleDateString("en-MY")} – {new Date(to).toLocaleDateString("en-MY")}</div>
            </div>
          </div>

          {wallet?.walletEnforced && (
            <div className="g-3 mb-4">
              <div className="stat" style={{ border: `1px solid ${wallet.balanceMyr > 0 ? "var(--c-line)" : "var(--err, #dc2626)"}` }}>
                <div className="stat-label">Wallet balance</div>
                <div className="stat-value" style={{ color: wallet.balanceMyr > 0 ? undefined : "var(--err, #dc2626)" }}>RM {Number(wallet.balanceMyr).toFixed(2)}</div>
                <div className="muted text-11 mt-2">
                  {wallet.balanceMyr > 0 ? "Prepaid enforcement is on for this client" : "Balance is empty — outbound calls/SMS/WhatsApp are blocked until topped up"}
                </div>
              </div>
            </div>
          )}

          <Card title="Cost per day" subtitle="Billed call cost (RM)">
            <div className="chart-wrap">
              {dailyTotals.length > 0
                ? <LineChart data={dailyTotals.map(([, v]) => v)} labels={dailyTotals.map(([d]) => d.slice(5))} />
                : <div className="muted text-12">No calls in this period.</div>}
            </div>
          </Card>

          <div className="g-2 mt-4">
            <Card title="Calls" subtitle={`${invoice.calls.length} in period`}>
              <table className="tbl">
                <thead><tr><th>Date</th><th>Contact</th><th className="right">Min</th><th className="right">Cost</th></tr></thead>
                <tbody>
                  {invoice.calls.slice(0, 20).map(c => (
                    <tr key={c.id}>
                      <td className="text-12">{c.date ? new Date(c.date).toLocaleString("en-MY") : "—"}</td>
                      <td className="text-12">{c.contact}</td>
                      <td className="mono right text-12">{(c.durationSec / 60).toFixed(2)}</td>
                      <td className="mono right text-12 strong">RM {c.cost.toFixed(2)}</td>
                    </tr>
                  ))}
                  {invoice.calls.length === 0 && (
                    <tr><td colSpan={4} className="muted text-12">No calls in this period.</td></tr>
                  )}
                </tbody>
              </table>
              {invoice.calls.length > 20 && (
                <div className="muted text-11 mt-2">+{invoice.calls.length - 20} more — download the full invoice for the complete list.</div>
              )}
            </Card>

            <Card title="SMS & WhatsApp" subtitle="By event type">
              <table className="tbl">
                <thead><tr><th>Type</th><th className="right">Count</th><th className="right">Cost</th></tr></thead>
                <tbody>
                  {invoice.events.map(e => (
                    <tr key={e.type}>
                      <td className="strong text-12">{e.label}</td>
                      <td className="mono right text-12">{e.count}</td>
                      <td className="mono right text-12 strong">RM {e.cost.toFixed(2)}</td>
                    </tr>
                  ))}
                  {invoice.events.length === 0 && (
                    <tr><td colSpan={3} className="muted text-12">No SMS/WhatsApp events in this period.</td></tr>
                  )}
                </tbody>
              </table>
            </Card>
          </div>

          {wallet && wallet.ledger.length > 0 && (
            <Card title="Top-up history" subtitle="Most recent first" style={{ marginTop: 16 }}>
              <table className="tbl">
                <thead><tr><th>Date</th><th>Type</th><th>Note</th><th className="right">Amount</th><th className="right">Balance after</th></tr></thead>
                <tbody>
                  {wallet.ledger.slice(0, 30).map(e => (
                    <tr key={e.id}>
                      <td className="text-12">{new Date(e.occurredAt).toLocaleString("en-MY")}</td>
                      <td className="text-12"><Badge tone={e.type === "topup" ? "ok" : "neutral"}>{e.type}</Badge></td>
                      <td className="muted text-12">{e.note || "—"}</td>
                      <td className="mono right text-12 strong" style={{ color: e.amountMyr >= 0 ? "var(--ok)" : "var(--err, #dc2626)" }}>{e.amountMyr >= 0 ? "+" : ""}RM {Number(e.amountMyr).toFixed(2)}</td>
                      <td className="mono right text-12">RM {Number(e.balanceAfter).toFixed(2)}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </Card>
          )}
        </>
      )}

      {isAdmin && Array.isArray(ledger) && (
        <Card title="Client wallets" subtitle="Topped up vs. billed vs. gross profit, same period as above" style={{ marginTop: 16 }}>
          <table className="tbl">
            <thead>
              <tr>
                <th>Client</th><th>Prepaid</th>
                <th className="right">Balance</th>
                <th className="right">Topped up</th>
                <th className="right">Billed</th>
                <th className="right">Net</th>
                <th className="right">Gross profit</th>
              </tr>
            </thead>
            <tbody>
              {ledger.map(row => (
                <tr key={row.clientId}>
                  <td className="text-12 strong">{row.clientName}</td>
                  <td className="text-12"><Badge tone={row.walletEnforced ? "ok" : "neutral"}>{row.walletEnforced ? "Enforced" : "Off"}</Badge></td>
                  <td className="mono right text-12" style={{ color: row.balanceMyr > 0 ? undefined : "var(--err, #dc2626)" }}>RM {row.balanceMyr.toFixed(2)}</td>
                  <td className="mono right text-12">RM {row.toppedUpMyr.toFixed(2)}</td>
                  <td className="mono right text-12">RM {row.billedMyr.toFixed(2)}</td>
                  <td className="mono right text-12" style={{ color: row.netMyr >= 0 ? "var(--ok)" : "var(--err, #dc2626)" }}>RM {row.netMyr.toFixed(2)}</td>
                  <td className="mono right text-12 strong" style={{ color: row.grossProfitMyr >= 0 ? "var(--ok)" : "var(--err, #dc2626)" }}>RM {row.grossProfitMyr.toFixed(2)}</td>
                </tr>
              ))}
              {ledger.length === 0 && (
                <tr><td colSpan={7} className="muted text-12">No clients yet.</td></tr>
              )}
            </tbody>
          </table>
        </Card>
      )}

      {!invoice && !loading && !err && (
        <div className="muted text-12">{isAdmin ? "Select a client to see billing." : "No billing data yet."}</div>
      )}
    </div>
  );
};

Object.assign(window, { DashboardView, AnalyticsView, BillingView });
