// ---------- Angkasa Analytics ----------
// Dedicated dashboard for ANGKASA organization clients: why customers
// called, which tools fired, cost/minutes, and how calls ended. Scoped by
// matching assistant name/id against "angkasa" rather than a hardcoded
// client id or assistant name, so any assistant later assigned to an
// ANGKASA org lights this view up automatically.

const ANGKASA_REASON_DEFS = [
  { key: "potongan_gaji", toolId: "query_tool", color: "var(--a)" },
  { key: "aduan", toolId: "aduan_whatsapp_send", color: "var(--warn)" },
  { key: "pendaftaran", toolId: "send_whatsapp_newmember", color: "var(--ok)" },
  { key: "uncategorized", toolId: null, color: "var(--c-text-4)" },
];
// A call only reaches its "signature" tool (query_tool, etc.) if the caller
// stays on the line through the whole flow. Map every tool that belongs to a
// flow — including the NRIC/OTP verification steps that only potongan_gaji
// uses — so a call that hangs up mid-verification still classifies correctly.
const ANGKASA_REASON_BY_TOOL = {
  query_tool: "potongan_gaji",
  verify_otp: "potongan_gaji",
  send_otp_sms: "potongan_gaji",
  ytaa_identify: "potongan_gaji",
  aduan_whatsapp_send: "aduan",
  send_whatsapp_newmember: "pendaftaran",
};
// Reason/outcome/tool labels are looked up through t("angkasa.reason.<key>") etc.
// at render time (see reasonLabel/outcomeLabel/toolLabel helpers below) — the
// defs above only carry the stable key/color/toolId, never the display text,
// so switching language never has to touch classification logic or filter state.
function angkasaReasonLabel(t, key) {
  return t(`angkasa.reason.${key}`);
}

// Fallback for calls that end (hang-up, silence timeout) before any flow
// tool fires at all — infer intent from what the customer actually said
// rather than leaving an unambiguous call bucketed as Uncategorized.
const ANGKASA_REASON_KEYWORDS = {
  potongan_gaji: ["potongan gaji", "potongan", "gaji", "salary deduction", "deduction", "semak status"],
  aduan: ["aduan", "laporan", "melaporkan", "complain", "complaint", "report", "gangguan"],
  pendaftaran: ["pendaftaran", "daftar", "mendaftar", "register", "registration", "ahli baharu", "ahli baru"],
};
function angkasaInferReasonFromText(turns) {
  const text = (Array.isArray(turns) ? turns : [])
    .map(t => String(t.user || ""))
    .join(" ")
    .toLowerCase();
  if (!text.trim()) return null;
  for (const key of Object.keys(ANGKASA_REASON_KEYWORDS)) {
    if (ANGKASA_REASON_KEYWORDS[key].some(kw => text.includes(kw))) return key;
  }
  return null;
}

function angkasaToolLabel(t, lang, toolId) {
  const key = `angkasa.tool.${toolId}`;
  const known = (STRINGS[lang] && STRINGS[lang][key]) || STRINGS.en[key];
  return known ? t(key) : toolId;
}

const ANGKASA_ENDED_REASON_LABELS = {
  "customer-ended-call": "Customer Ended Call",
  "assistant-ended-call": "Assistant Ended Call",
  "transferred": "Transferred to Manager",
  "call-transferred": "Transferred to Manager",
  "voicemail": "Voicemail",
  "customer-busy": "Customer Busy",
  "customer-did-not-answer": "No Answer",
  "silence-timed-out": "Silence Timeout",
  "call.error-processing-failed": "Call Failed",
  "call.in-progress.error-sip-outbound-call-failed-to-connect": "Call Failed to Connect",
};
const ANGKASA_ENDED_REASON_COLOR = {
  "Customer Ended Call": "var(--a)",
  "Assistant Ended Call": "var(--ok)",
  "Transferred to Manager": "var(--info)",
  "Voicemail": "var(--warn)",
  "Call Failed": "var(--err)",
  "Call Failed to Connect": "var(--err)",
  "Customer Busy": "var(--warn)",
  "No Answer": "var(--c-text-4)",
  "Silence Timeout": "var(--c-text-4)",
};
const ANGKASA_ENDED_REASON_PALETTE = ["var(--a)", "var(--ok)", "var(--info)", "var(--warn)", "var(--err)", "var(--c-text-4)"];
// Ended-reason categories that represent the call leaving without a resolved
// outcome (tender D.6 — "conversation drop-off rate"), as opposed to
// customer-ended-call/assistant-ended-call/transferred which all represent
// some kind of completion.
const ANGKASA_DROP_OFF_REASONS = new Set(["silence-timed-out", "customer-busy", "customer-did-not-answer"]);

function angkasaFriendlyEndedReason(raw) {
  const key = String(raw || "").trim().toLowerCase();
  if (ANGKASA_ENDED_REASON_LABELS[key]) return ANGKASA_ENDED_REASON_LABELS[key];
  if (!key) return "Unknown";
  if (key.includes("fail") || key.includes("error")) return "Call Failed";
  if (key.includes("transfer")) return "Transferred to Manager";
  return key.replace(/[-_.]/g, " ").replace(/\b\w/g, c => c.toUpperCase());
}

// angkasaFriendlyEndedReason() above is also used as the stable filter value
// (filterOutcome state, dropdown option values) — translating it directly
// would break those comparisons. This maps its fixed English output to an
// i18n key instead, so display text can switch language while filter state
// stays keyed on the untranslated canonical string. Any outcome string that
// isn't one of the nine known buckets (a raw provider status that slipped
// through) is shown as-is in both languages rather than guessed at.
const ANGKASA_ENDED_REASON_I18N_KEY = {
  "Customer Ended Call": "customerEndedCall",
  "Assistant Ended Call": "assistantEndedCall",
  "Transferred to Manager": "transferred",
  "Voicemail": "voicemail",
  "Customer Busy": "customerBusy",
  "No Answer": "noAnswer",
  "Silence Timeout": "silenceTimeout",
  "Call Failed": "callFailed",
  "Call Failed to Connect": "callFailedConnect",
  "Unknown": "unknown",
};
function angkasaOutcomeLabel(t, friendlyLabel) {
  const i18nKey = ANGKASA_ENDED_REASON_I18N_KEY[friendlyLabel];
  return i18nKey ? t(`angkasa.outcome.${i18nKey}`) : friendlyLabel;
}

// 1-5 AI QA score, colored so a bare number doesn't read as "bad" by
// default — a customer-service rubric score of ~3 is typically middling,
// not alarming, so the red band only starts below that.
function qaScoreColor(score) {
  if (score == null) return undefined;
  if (score < 2.5) return "var(--err)";
  if (score < 3.5) return "var(--warn)";
  return "var(--ok)";
}

function angkasaExtractToolIds(text) {
  const ids = [];
  const re = /\[\[tool:([a-zA-Z0-9_-]+)\s+\{[\s\S]*?\}\]\]/g;
  let m;
  while ((m = re.exec(String(text || "")))) ids.push(m[1]);
  return ids;
}

const ANGKASA_RANGE_PRESETS = [
  { id: "today" },
  { id: "7d" },
  { id: "30d" },
  { id: "90d" },
  { id: "all" },
];

function AngkasaBarRow({ label, value, count, max, color = "var(--a)", onClick, active }) {
  const pct = max > 0 ? Math.max(2, Math.round((count / max) * 100)) : 0;
  return (
    <div
      onClick={onClick}
      style={onClick ? {
        cursor: "pointer",
        padding: "4px 6px",
        margin: "-4px -6px",
        borderRadius: 6,
        background: active ? "rgba(141,139,255,0.09)" : "transparent",
      } : undefined}
    >
      <div className="row" style={{ justifyContent: "space-between" }}>
        <span className="text-12" style={active ? { fontWeight: 600 } : undefined}>{label}</span>
        <span className="mono text-11">{value}</span>
      </div>
      <div style={{ height: 6, background: "var(--c-surface-2)", borderRadius: 3, marginTop: 4, overflow: "hidden" }}>
        <div style={{ width: `${pct}%`, height: "100%", background: color, borderRadius: 3 }} />
      </div>
    </div>
  );
}

function AngkasaChartToggle({ value, onChange, lang = "en" }) {
  const t = makeT(lang);
  return (
    <div className="row gap-1">
      <Btn variant={value === "donut" ? "primary" : "ghost"} size="sm" onClick={() => onChange("donut")} title={t("angkasa.chart.donutView")}>◔</Btn>
      <Btn variant={value === "bar" ? "primary" : "ghost"} size="sm" onClick={() => onChange("bar")} title={t("angkasa.chart.barView")}>▤</Btn>
    </div>
  );
}

const AngkasaAnalyticsView = ({ mode, callLogs = [], assistants = [], clients = [], lang = "en" }) => {
  const t = makeT(lang);
  const isClient = mode === "client";
  const [rangePreset, setRangePreset] = React.useState("30d");
  const [customFrom, setCustomFrom] = React.useState("");
  const [customTo, setCustomTo] = React.useState("");
  const [filterClientId, setFilterClientId] = React.useState("");
  const [searchQuery, setSearchQuery] = React.useState("");
  const [filterOutcome, setFilterOutcome] = React.useState("");
  const [filterReasonKey, setFilterReasonKey] = React.useState("");
  const [filterSentimentValue, setFilterSentimentValue] = React.useState("");
  const [selected, setSelected] = React.useState(null);
  // Only for dimensions that don't have a page-wide filter of their own
  // (tool, risk) — reason/outcome/sentiment used to have a separate
  // click-a-chart-segment "drill" state here too, which meant a reason could
  // be filtered two different ways at once. They now write directly to
  // filterReasonKey/filterOutcome/filterSentimentValue instead, so there's
  // exactly one filter model per dimension.
  const [drillFilter, setDrillFilter] = React.useState(null); // { type: "tool"|"risk", value, label }
  const [reasonChartType, setReasonChartType] = React.useState("donut");
  const [outcomeChartType, setOutcomeChartType] = React.useState("donut");

  const angkasaAssistantIds = React.useMemo(() => new Set(
    assistants
      .filter(a => /angkasa/i.test(a.name || "") || /angkasa/i.test(a.id || ""))
      .map(a => a.id)
  ), [assistants]);

  const angkasaLogsAll = React.useMemo(() => callLogs.filter(l =>
    (l.assistantId && angkasaAssistantIds.has(l.assistantId)) || /angkasa/i.test(l.assistantName || "")
  ), [callLogs, angkasaAssistantIds]);

  const angkasaClients = React.useMemo(() => {
    const clientIdsInLogs = new Set(angkasaLogsAll.map(l => l.clientId).filter(Boolean));
    return clients.filter(c => clientIdsInLogs.has(c.id) || angkasaAssistantIds.has(c.assistantId));
  }, [clients, angkasaLogsAll, angkasaAssistantIds]);

  // Classified independently of date/search/outcome filters (over the full
  // Angkasa log set) so "reason" can itself be used as a filter criterion
  // below without a circular dependency on filteredLogs.
  const reasonKeyByLogId = React.useMemo(() => {
    const map = {};
    angkasaLogsAll.forEach(log => {
      let sessionReasonKey = null;
      (Array.isArray(log.turns) ? log.turns : []).forEach(turn => {
        angkasaExtractToolIds(turn.assistant).forEach(id => {
          if (!sessionReasonKey && ANGKASA_REASON_BY_TOOL[id]) sessionReasonKey = ANGKASA_REASON_BY_TOOL[id];
        });
      });
      if (!sessionReasonKey) sessionReasonKey = angkasaInferReasonFromText(log.turns);
      map[log.id] = sessionReasonKey || "uncategorized";
    });
    return map;
  }, [angkasaLogsAll]);

  const { rangeStart, rangeEnd, rangeLabel } = React.useMemo(() => {
    const now = new Date();
    if (rangePreset === "custom" && customFrom) {
      const start = new Date(`${customFrom}T00:00:00`).getTime();
      const end = customTo ? new Date(`${customTo}T23:59:59`).getTime() : now.getTime();
      return { rangeStart: start, rangeEnd: end, rangeLabel: `${customFrom} – ${customTo || t("angkasa.range.now")}` };
    }
    if (rangePreset === "all") {
      return { rangeStart: 0, rangeEnd: now.getTime(), rangeLabel: t("angkasa.range.all") };
    }
    if (rangePreset === "today") {
      const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
      return { rangeStart: start, rangeEnd: now.getTime(), rangeLabel: t("angkasa.range.today") };
    }
    const days = { "7d": 7, "30d": 30, "90d": 90 }[rangePreset] || 30;
    return {
      rangeStart: now.getTime() - days * 86400000,
      rangeEnd: now.getTime(),
      rangeLabel: t("angkasa.range.lastNDays", { days }),
    };
  }, [rangePreset, customFrom, customTo, lang]);

  // Search/outcome/reason/sentiment filters all apply here — at the same
  // level as the date range and org filter — whether they were set by typing
  // in the search box, picking a dropdown, or clicking a chart segment above
  // (those now write to this same state, see toggleReason/toggleOutcome/
  // toggleSentiment below). drillFilter, further down, only remains for
  // "tool" and "risk", which don't have a page-wide filter of their own.
  const filteredLogs = React.useMemo(() => angkasaLogsAll.filter(l => {
    if (filterClientId && l.clientId !== filterClientId) return false;
    const t = new Date(l.startedAt || l.updatedAt || 0).getTime();
    if (t < rangeStart || t > rangeEnd) return false;
    if (filterOutcome && angkasaFriendlyEndedReason(l.endedReason) !== filterOutcome) return false;
    if (filterReasonKey && reasonKeyByLogId[l.id] !== filterReasonKey) return false;
    if (filterSentimentValue && (l.aiAnalysis?.sentiment || "unanalyzed") !== filterSentimentValue) return false;
    if (searchQuery.trim()) {
      const q = searchQuery.trim().toLowerCase();
      const haystack = `${l.customerName || ""} ${l.contactId || ""} ${l.contact || ""} ${l.to || ""}`.toLowerCase();
      if (!haystack.includes(q)) return false;
    }
    return true;
  }), [angkasaLogsAll, filterClientId, rangeStart, rangeEnd, filterOutcome, filterReasonKey, filterSentimentValue, reasonKeyByLogId, searchQuery]);

  const { reasonCounts, toolFireCounts, callToolIdsById } = React.useMemo(() => {
    const reasonCounts = { potongan_gaji: 0, aduan: 0, pendaftaran: 0, uncategorized: 0 };
    const toolFireCounts = {};
    const callToolIdsById = {};
    filteredLogs.forEach(log => {
      const toolIds = new Set();
      (Array.isArray(log.turns) ? log.turns : []).forEach(turn => {
        angkasaExtractToolIds(turn.assistant).forEach(id => {
          toolFireCounts[id] = (toolFireCounts[id] || 0) + 1;
          toolIds.add(id);
        });
      });
      const key = reasonKeyByLogId[log.id] || "uncategorized";
      reasonCounts[key] = (reasonCounts[key] || 0) + 1;
      callToolIdsById[log.id] = toolIds;
    });
    return { reasonCounts, toolFireCounts, callToolIdsById };
  }, [filteredLogs, reasonKeyByLogId]);

  const costSummary = React.useMemo(() => {
    const totalCost = filteredLogs.reduce((s, l) => s + (Number(l.sellingMyr) || 0), 0);
    const totalMinutes = filteredLogs.reduce((s, l) => s + (Number(l.durationSeconds) || 0), 0) / 60;
    const relevantClients = filterClientId ? clients.filter(c => c.id === filterClientId) : angkasaClients;
    const allocatedMinutes = relevantClients.reduce((s, c) => s + (Number(c.monthlyMinuteLimit) || 0), 0);
    return { totalCost, totalMinutes, allocatedMinutes, callCount: filteredLogs.length };
  }, [filteredLogs, clients, filterClientId, angkasaClients]);

  const endedReasonBreakdown = React.useMemo(() => {
    const map = {};
    filteredLogs.forEach(l => {
      const label = angkasaFriendlyEndedReason(l.endedReason);
      map[label] = (map[label] || 0) + 1;
    });
    return Object.entries(map).sort((a, b) => b[1] - a[1]);
  }, [filteredLogs]);

  // AI-scored sentiment/QA/risk per call (tender 7.5/7.7) — populated
  // asynchronously by callAnalysisWorker.js after each call ends, so a
  // freshly-ended call may briefly show as "unanalyzed" until its next sweep.
  const sentimentSummary = React.useMemo(() => {
    const counts = { positive: 0, neutral: 0, negative: 0, unanalyzed: 0 };
    let qaScoreSum = 0;
    let qaScoreCount = 0;
    let riskFlaggedCount = 0;
    filteredLogs.forEach(l => {
      if (l.aiAnalysis) {
        const s = l.aiAnalysis.sentiment;
        counts[s] = (counts[s] || 0) + (s in counts ? 1 : 0);
        if (!(s in counts)) counts.neutral += 1;
        if (Number.isFinite(l.aiAnalysis.qaScore)) {
          qaScoreSum += l.aiAnalysis.qaScore;
          qaScoreCount += 1;
        }
        if (l.aiAnalysis.riskFlagged) riskFlaggedCount += 1;
      } else {
        counts.unanalyzed += 1;
      }
    });
    return {
      counts,
      avgQaScore: qaScoreCount ? qaScoreSum / qaScoreCount : null,
      riskFlaggedCount,
      analyzedCount: filteredLogs.length - counts.unanalyzed,
    };
  }, [filteredLogs]);

  // AI Observability (tender D.6) — proxy metrics built from data already
  // computed elsewhere on this page (reasonKeyByLogId, endedReason,
  // aiAnalysis), not a separate evaluation model. "Drop-off" reuses the same
  // ended-reason categories already shown in the "How calls ended" chart;
  // "intent recognition success" reuses the same classification that drives
  // the "Reasons for calling" chart; "AI accuracy" reuses sentiment/risk
  // already scored per call (Appendix 7.5/7.7) rather than re-scoring calls.
  const aiObservability = React.useMemo(() => {
    const total = filteredLogs.length;
    if (!total) return { intentSuccessRate: null, dropOffRate: null, aiAccuracyRate: null, scoredCount: 0 };

    const intentSuccessCount = filteredLogs.filter(l => (reasonKeyByLogId[l.id] || "uncategorized") !== "uncategorized").length;
    const dropOffCount = filteredLogs.filter(l => ANGKASA_DROP_OFF_REASONS.has(String(l.endedReason || "").trim().toLowerCase())).length;
    const scored = filteredLogs.filter(l => l.aiAnalysis);
    const cleanCount = scored.filter(l => !l.aiAnalysis.riskFlagged && l.aiAnalysis.sentiment !== "negative").length;

    return {
      intentSuccessRate: Math.round((intentSuccessCount / total) * 100),
      dropOffRate: Math.round((dropOffCount / total) * 100),
      aiAccuracyRate: scored.length ? Math.round((cleanCount / scored.length) * 100) : null,
      scoredCount: scored.length,
    };
  }, [filteredLogs, reasonKeyByLogId]);

  // AI failure patterns (tender G.2) — keyword frequency scoped to calls
  // already scored poorly or flagged by callAnalysisWorker.js, same data
  // source as aiObservability/sentimentSummary above, just a different cut.
  const failurePatterns = React.useMemo(() => topFailureTopics(filteredLogs, 8), [filteredLogs]);
  const maxFailureTopic = Math.max(1, ...failurePatterns.topKeywords.map(([, count]) => count));

  // CSAT (tender G.3) — averages scores submitted via the public
  // /csat/:callId link (routes/csat.js); calls with no submission are
  // excluded from the average but counted toward the response rate.
  const csatData = React.useMemo(() => {
    const rated = filteredLogs.filter(l => Number.isFinite(l.csat?.score));
    const distribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
    rated.forEach(l => { distribution[l.csat.score] = (distribution[l.csat.score] || 0) + 1; });
    return {
      responseCount: rated.length,
      avgScore: rated.length ? rated.reduce((s, l) => s + l.csat.score, 0) / rated.length : null,
      responseRate: filteredLogs.length ? Math.round((rated.length / filteredLogs.length) * 100) : 0,
      distribution,
    };
  }, [filteredLogs]);
  const maxCsatBucket = Math.max(1, ...Object.values(csatData.distribution));

  // Reuses the same PII-safe topTopics() helper defined in dashboard.jsx
  // (both files share one global script scope) — scoped to filteredLogs so
  // it reflects only Angkasa calls under the current filters, not every
  // client's calls the way the general Analytics dashboard's widget does.
  const topTopicsBreakdown = React.useMemo(() => topTopics(filteredLogs, 8), [filteredLogs]);
  const maxTopic = Math.max(1, ...topTopicsBreakdown.map(([, count]) => count));

  const toolFireRows = React.useMemo(() => {
    const rows = Object.entries(toolFireCounts).sort((a, b) => b[1] - a[1]);
    const max = rows.reduce((m, [, v]) => Math.max(m, v), 0);
    return { rows, max };
  }, [toolFireCounts]);

  // Only "tool" and "risk" remain here — reason/outcome/sentiment moved to
  // the page-wide filteredLogs above (see comment there).
  const drillFilteredLogs = React.useMemo(() => {
    if (!drillFilter) return filteredLogs;
    if (drillFilter.type === "tool") return filteredLogs.filter(l => callToolIdsById[l.id]?.has(drillFilter.value));
    if (drillFilter.type === "risk") return filteredLogs.filter(l => l.aiAnalysis?.riskFlagged);
    return filteredLogs;
  }, [filteredLogs, drillFilter, callToolIdsById]);

  function toggleDrill(type, value, label) {
    setDrillFilter(prev => (prev && prev.type === type && prev.value === value) ? null : { type, value, label });
  }
  function toggleReason(key, label) {
    setFilterReasonKey(prev => prev === key ? "" : key);
  }
  function toggleOutcome(label) {
    setFilterOutcome(prev => prev === label ? "" : label);
  }
  function toggleSentiment(key) {
    setFilterSentimentValue(prev => prev === key ? "" : key);
  }
  function clearAllFilters() {
    setSearchQuery("");
    setFilterReasonKey("");
    setFilterOutcome("");
    setFilterSentimentValue("");
    setDrillFilter(null);
  }

  // One combined summary line for every active filter, regardless of which
  // control set it (chart click, dropdown, or search box) — a single source
  // of truth instead of tracking drillFilter and the dropdowns separately.
  const activeFilterLabels = [];
  if (filterReasonKey) activeFilterLabels.push(angkasaReasonLabel(t, filterReasonKey));
  if (filterOutcome) activeFilterLabels.push(angkasaOutcomeLabel(t, filterOutcome));
  if (filterSentimentValue) activeFilterLabels.push(filterSentimentValue === "unanalyzed" ? t("angkasa.sentiment.unanalyzed") : t(`angkasa.sentiment.${filterSentimentValue}`));
  if (searchQuery.trim()) activeFilterLabels.push(`"${searchQuery.trim()}"`);
  if (drillFilter) activeFilterLabels.push(drillFilter.label);
  const hasActiveFilter = activeFilterLabels.length > 0;

  const recentLogs = React.useMemo(
    () => hasActiveFilter ? drillFilteredLogs : drillFilteredLogs.slice(0, 8),
    [drillFilteredLogs, hasActiveFilter]
  );

  const selectedLogObj = React.useMemo(
    () => selected ? (angkasaLogsAll.find(l => l.id === selected) || null) : null,
    [selected, angkasaLogsAll]
  );

  const totalReasonCalls = filteredLogs.length || 1;

  return (
    <div style={{ display: "flex", alignItems: "flex-start", gap: 0, minWidth: 0 }}>
    <div className="angkasa-compact" style={{ flex: 1, minWidth: 0, overflow: "hidden" }}>
      <div className="ph">
        <div>
          <h1 className="ph-title">{t("nav.angkasa")}</h1>
          <div className="ph-subtitle">{t("app.summary.angkasa")}</div>
        </div>
      </div>

      <div className="row gap-1 mb-4" style={{ justifyContent: "flex-end" }}>
        {ANGKASA_RANGE_PRESETS.map(r => (
          <Btn key={r.id} variant={rangePreset === r.id ? "primary" : "ghost"} size="sm" onClick={() => setRangePreset(r.id)}>{t(`angkasa.range.${r.id}`)}</Btn>
        ))}
      </div>

      <div className="row gap-3 mb-4" style={{ flexWrap: "wrap", alignItems: "flex-end" }}>
        {!isClient && angkasaClients.length > 0 && (
          <Field label={t("angkasa.field.organization")}>
            <Select value={filterClientId} onChange={e => setFilterClientId(e.target.value)}>
              <option value="">{t("angkasa.field.allClients")}</option>
              {angkasaClients.map(c => (
                <option key={c.id} value={c.id}>{c.organizationName || c.company || c.name}</option>
              ))}
            </Select>
          </Field>
        )}
        <Field label={t("angkasa.field.customFrom")}>
          <Input type="date" value={customFrom} onChange={e => { setCustomFrom(e.target.value); setRangePreset("custom"); }} />
        </Field>
        <Field label={t("angkasa.field.customTo")}>
          <Input type="date" value={customTo} onChange={e => { setCustomTo(e.target.value); setRangePreset("custom"); }} />
        </Field>
      </div>

      {angkasaLogsAll.length === 0 && (
        <div className="card" style={{ padding: 28, textAlign: "center", color: "var(--c-text-4)" }}>
          {t("angkasa.empty.noCalls")}
        </div>
      )}

      {angkasaLogsAll.length > 0 && (
        <>
          <div className="g-4 mb-4">
            <div className="stat">
              <div className="stat-label">{t("angkasa.stat.totalCost")}</div>
              <div className="stat-value">{fmtRM(costSummary.totalCost)}</div>
              <div className="muted text-11 mt-2">{t("angkasa.stat.callsInRange", { count: costSummary.callCount, plural: costSummary.callCount !== 1 ? "s" : "", range: rangeLabel })}</div>
            </div>
            <div className="stat">
              <div className="stat-label">{t("angkasa.stat.minutesUsed")}</div>
              <div className="stat-value">{costSummary.totalMinutes.toFixed(1)}</div>
              <div className="muted text-11 mt-2">{t("angkasa.stat.minutesUsedMeta")}</div>
            </div>
            <div className="stat">
              <div className="stat-label">{t("angkasa.stat.monthlyAllocation")}</div>
              <div className="stat-value">{costSummary.allocatedMinutes > 0 ? costSummary.allocatedMinutes.toLocaleString() : t("angkasa.stat.noLimit")}</div>
              <div className="muted text-11 mt-2">{filterClientId ? t("angkasa.stat.selectedClient") : t("angkasa.stat.summedAcross")}</div>
            </div>
            <div className="stat">
              <div className="stat-label">{t("angkasa.stat.totalCalls")}</div>
              <div className="stat-value">{costSummary.callCount.toLocaleString()}</div>
              <div className="muted text-11 mt-2">{rangeLabel}</div>
            </div>
          </div>

          <div className="g-2 mb-4">
            <Card
              title={t("angkasa.card.reasons.title")}
              subtitle={t("angkasa.subtitle.callsRange", { count: filteredLogs.length, plural: filteredLogs.length !== 1 ? "s" : "", range: rangeLabel })}
              action={<AngkasaChartToggle value={reasonChartType} onChange={setReasonChartType} lang={lang} />}
            >
              {reasonChartType === "donut" && (
                <div className="row" style={{ justifyContent: "center", padding: "2px 0" }}>
                  <Donut size={112} label={filteredLogs.length.toLocaleString()} sub="calls"
                    segments={filteredLogs.length > 0
                      ? ANGKASA_REASON_DEFS.filter(r => reasonCounts[r.key] > 0).map(r => ({ value: reasonCounts[r.key], color: r.color }))
                      : [{ value: 1, color: "var(--c-line-2)" }]
                    } />
                </div>
              )}
              <div className="col gap-2 mt-3">
                {ANGKASA_REASON_DEFS.map(r => {
                  const isActive = filterReasonKey === r.key;
                  const label = angkasaReasonLabel(t, r.key);
                  return reasonCounts[r.key] > 0 && (
                    reasonChartType === "bar" ? (
                      <AngkasaBarRow
                        key={r.key}
                        label={label}
                        value={`${reasonCounts[r.key]} · ${Math.round(reasonCounts[r.key] / totalReasonCalls * 100)}%`}
                        count={reasonCounts[r.key]}
                        max={Math.max(...ANGKASA_REASON_DEFS.map(d => reasonCounts[d.key] || 0))}
                        color={r.color}
                        active={isActive}
                        onClick={() => toggleReason(r.key, label)}
                      />
                    ) : (
                      <div
                        key={r.key}
                        className="row"
                        onClick={() => toggleReason(r.key, label)}
                        style={{
                          justifyContent: "space-between", fontSize: 12, cursor: "pointer",
                          padding: "4px 6px", margin: "-4px -6px", borderRadius: 6,
                          background: isActive ? "rgba(141,139,255,0.09)" : "transparent",
                        }}
                      >
                        <span className="row gap-2 muted">
                          <span style={{ width: 8, height: 8, borderRadius: 2, background: r.color }} />
                          <span style={isActive ? { fontWeight: 600, color: "var(--c-text)" } : undefined}>{label}</span>
                        </span>
                        <span className="mono">{reasonCounts[r.key]} · {Math.round(reasonCounts[r.key] / totalReasonCalls * 100)}%</span>
                      </div>
                    )
                  );
                })}
                {filteredLogs.length === 0 && <div className="muted text-11">{t("angkasa.table.noCallsWindow")}</div>}
              </div>
            </Card>

            <Card
              title={t("angkasa.card.outcomes.title")}
              subtitle={t("angkasa.subtitle.callsRange", { count: filteredLogs.length, plural: filteredLogs.length !== 1 ? "s" : "", range: rangeLabel })}
              action={<AngkasaChartToggle value={outcomeChartType} onChange={setOutcomeChartType} lang={lang} />}
            >
              {outcomeChartType === "donut" && (
                <div className="row" style={{ justifyContent: "center", padding: "2px 0" }}>
                  <Donut size={112} label={filteredLogs.length.toLocaleString()} sub="calls"
                    segments={endedReasonBreakdown.length > 0
                      ? endedReasonBreakdown.map(([label, count], i) => ({
                          value: count,
                          color: ANGKASA_ENDED_REASON_COLOR[label] || ANGKASA_ENDED_REASON_PALETTE[i % ANGKASA_ENDED_REASON_PALETTE.length],
                        }))
                      : [{ value: 1, color: "var(--c-line-2)" }]
                    } />
                </div>
              )}
              <div className="col gap-2 mt-3">
                {endedReasonBreakdown.map(([label, count], i) => {
                  const color = ANGKASA_ENDED_REASON_COLOR[label] || ANGKASA_ENDED_REASON_PALETTE[i % ANGKASA_ENDED_REASON_PALETTE.length];
                  const isActive = filterOutcome === label;
                  const displayLabel = angkasaOutcomeLabel(t, label);
                  return outcomeChartType === "bar" ? (
                    <AngkasaBarRow
                      key={label}
                      label={displayLabel}
                      value={`${count} · ${Math.round(count / totalReasonCalls * 100)}%`}
                      count={count}
                      max={endedReasonBreakdown.reduce((m, [, c]) => Math.max(m, c), 0)}
                      color={color}
                      active={isActive}
                      onClick={() => toggleOutcome(label)}
                    />
                  ) : (
                    <div
                      key={label}
                      className="row"
                      onClick={() => toggleOutcome(label)}
                      style={{
                        justifyContent: "space-between", fontSize: 12, cursor: "pointer",
                        padding: "4px 6px", margin: "-4px -6px", borderRadius: 6,
                        background: isActive ? "rgba(141,139,255,0.09)" : "transparent",
                      }}
                    >
                      <span className="row gap-2 muted">
                        <span style={{ width: 8, height: 8, borderRadius: 2, background: color }} />
                        <span style={isActive ? { fontWeight: 600, color: "var(--c-text)" } : undefined}>{displayLabel}</span>
                      </span>
                      <span className="mono">{count} · {Math.round(count / totalReasonCalls * 100)}%</span>
                    </div>
                  );
                })}
                {endedReasonBreakdown.length === 0 && <div className="muted text-11">{t("angkasa.table.noCallsWindow")}</div>}
              </div>
            </Card>
          </div>

          <div className="g-2 mb-4">
            <Card
              title={t("angkasa.card.sentiment.title")}
              subtitle={t("angkasa.subtitle.analyzedSoFar", { analyzed: sentimentSummary.analyzedCount, total: filteredLogs.length, plural: filteredLogs.length !== 1 ? "s" : "", range: rangeLabel })}
            >
              <div className="row" style={{ justifyContent: "center", padding: "2px 0" }}>
                <Donut size={112} label={sentimentSummary.analyzedCount.toLocaleString()} sub="analyzed"
                  segments={sentimentSummary.analyzedCount > 0
                    ? [
                        { value: sentimentSummary.counts.positive, color: "var(--ok)" },
                        { value: sentimentSummary.counts.neutral, color: "var(--info)" },
                        { value: sentimentSummary.counts.negative, color: "var(--err)" },
                      ]
                    : [{ value: 1, color: "var(--c-line-2)" }]
                  } />
              </div>
              <div className="col gap-2 mt-3">
                {[
                  ["positive", t("angkasa.sentiment.positive"), sentimentSummary.counts.positive, "var(--ok)"],
                  ["neutral", t("angkasa.sentiment.neutral"), sentimentSummary.counts.neutral, "var(--info)"],
                  ["negative", t("angkasa.sentiment.negative"), sentimentSummary.counts.negative, "var(--err)"],
                ].filter(([, , count]) => count > 0).map(([key, label, count, color]) => {
                  const isActive = filterSentimentValue === key;
                  return (
                    <div
                      key={key}
                      className="row"
                      onClick={() => toggleSentiment(key)}
                      style={{
                        justifyContent: "space-between", fontSize: 12, cursor: "pointer",
                        padding: "4px 6px", margin: "-4px -6px", borderRadius: 6,
                        background: isActive ? "rgba(141,139,255,0.09)" : "transparent",
                      }}
                    >
                      <span className="row gap-2 muted">
                        <span style={{ width: 8, height: 8, borderRadius: 2, background: color }} />
                        <span style={isActive ? { fontWeight: 600, color: "var(--c-text)" } : undefined}>{label}</span>
                      </span>
                      <span className="mono">{count} · {Math.round(count / totalReasonCalls * 100)}%</span>
                    </div>
                  );
                })}
                {filteredLogs.length === 0 && <div className="muted text-11">{t("angkasa.table.noCallsWindow")}</div>}
                {sentimentSummary.counts.unanalyzed > 0 && (
                  // Deliberately NOT styled like a sentiment row (no colored dot,
                  // smaller/muted) — this is a processing-status, not a fourth
                  // sentiment value, and listing it identically to Positive/
                  // Neutral/Negative made the AI pipeline look broken/incomplete
                  // rather than "still catching up" (it runs async, ~5min behind).
                  <div
                    className="text-11 muted mt-1"
                    onClick={() => toggleSentiment("unanalyzed")}
                    style={{ cursor: "pointer", textDecoration: filterSentimentValue === "unanalyzed" ? "underline" : undefined }}
                  >
                    {t("angkasa.sentiment.stillProcessing", { count: sentimentSummary.counts.unanalyzed, plural: sentimentSummary.counts.unanalyzed !== 1 ? "s" : "" })}
                  </div>
                )}
              </div>
            </Card>

            <Card
              title={t("angkasa.card.qualityRisk.title")}
              subtitle={t("angkasa.card.qualityRisk.subtitle", { range: rangeLabel })}
            >
              <div className="g-2 mb-3">
                <div className="stat">
                  <div className="stat-label">{t("angkasa.qa.avgScore")}</div>
                  <div className="stat-value" style={{ color: qaScoreColor(sentimentSummary.avgQaScore) }}>{sentimentSummary.avgQaScore != null ? sentimentSummary.avgQaScore.toFixed(1) : "—"}</div>
                  <div className="muted text-11 mt-2">{sentimentSummary.analyzedCount > 0 ? t("angkasa.qa.acrossAnalyzed", { count: sentimentSummary.analyzedCount, plural: sentimentSummary.analyzedCount !== 1 ? "s" : "" }) : t("angkasa.qa.noneAnalyzed")}</div>
                </div>
                <div
                  className="stat"
                  onClick={() => sentimentSummary.riskFlaggedCount > 0 && toggleDrill("risk", "flagged", t("angkasa.qa.highRiskFlagged"))}
                  style={{
                    cursor: sentimentSummary.riskFlaggedCount > 0 ? "pointer" : undefined,
                    borderRadius: 8,
                    outline: drillFilter?.type === "risk" ? "2px solid var(--err)" : undefined,
                  }}
                >
                  <div className="stat-label">{t("angkasa.qa.highRiskFlagged")}</div>
                  <div className="stat-value" style={{ color: sentimentSummary.riskFlaggedCount > 0 ? "var(--err)" : undefined }}>{sentimentSummary.riskFlaggedCount}</div>
                  <div className="muted text-11 mt-2">{sentimentSummary.riskFlaggedCount > 0 ? t("angkasa.qa.riskClickDrill") : t("angkasa.qa.riskNoClick")}</div>
                </div>
              </div>
              <div className="muted text-11">{t("angkasa.qa.footnote")}</div>
            </Card>
          </div>

          <div className="mb-4">
            <Card
              title={t("angkasa.card.aiObservability.title")}
              subtitle={t("angkasa.card.aiObservability.subtitle", { range: rangeLabel })}
            >
              <div className="g-3 mb-3">
                <div className="stat">
                  <div className="stat-label">{t("angkasa.observability.aiAccuracy")}</div>
                  <div className="stat-value" style={{ color: qaScoreColor(aiObservability.aiAccuracyRate != null ? aiObservability.aiAccuracyRate / 20 : null) }}>
                    {aiObservability.aiAccuracyRate != null ? `${aiObservability.aiAccuracyRate}%` : "—"}
                  </div>
                  <div className="muted text-11 mt-2">{t("angkasa.observability.aiAccuracyNote")}</div>
                </div>
                <div className="stat">
                  <div className="stat-label">{t("angkasa.observability.intentSuccess")}</div>
                  <div className="stat-value">{aiObservability.intentSuccessRate != null ? `${aiObservability.intentSuccessRate}%` : "—"}</div>
                  <div className="muted text-11 mt-2">{t("angkasa.observability.intentSuccessNote")}</div>
                </div>
                <div className="stat">
                  <div className="stat-label">{t("angkasa.observability.dropOff")}</div>
                  <div className="stat-value" style={{ color: aiObservability.dropOffRate > 20 ? "var(--err)" : undefined }}>
                    {aiObservability.dropOffRate != null ? `${aiObservability.dropOffRate}%` : "—"}
                  </div>
                  <div className="muted text-11 mt-2">{t("angkasa.observability.dropOffNote")}</div>
                </div>
              </div>
              <div className="muted text-11">{t("angkasa.observability.footnote")}</div>
            </Card>
          </div>

          <div className="g-2 mb-4">
            <Card
              title={t("angkasa.card.failurePatterns.title")}
              subtitle={t("angkasa.card.failurePatterns.subtitle", { range: rangeLabel })}
            >
              <div className="stat mb-3">
                <div className="stat-label">{t("angkasa.card.failurePatterns.count")}</div>
                <div className="stat-value" style={{ color: failurePatterns.failureCallCount > 0 ? "var(--err)" : undefined }}>{failurePatterns.failureCallCount}</div>
              </div>
              <div className="col gap-3">
                {failurePatterns.topKeywords.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 / maxFailureTopic) * 100}%`, height: "100%", background: "var(--err)", borderRadius: 999 }} /></div>
                  </div>
                ))}
                {failurePatterns.topKeywords.length === 0 && <div className="muted text-11">{t("angkasa.card.failurePatterns.empty")}</div>}
              </div>
              <div className="muted text-11 mt-3">{t("angkasa.card.failurePatterns.footnote")}</div>
            </Card>

            <Card
              title={t("angkasa.card.csat.title")}
              subtitle={t("angkasa.card.csat.subtitle", { range: rangeLabel })}
            >
              <div className="g-2 mb-3">
                <div className="stat">
                  <div className="stat-label">{t("angkasa.csat.avgScore")}</div>
                  <div className="stat-value">{csatData.avgScore != null ? `${csatData.avgScore.toFixed(1)} / 5` : "—"}</div>
                </div>
                <div className="stat">
                  <div className="stat-label">{t("angkasa.csat.responseRate")}</div>
                  <div className="stat-value">{csatData.responseRate}%</div>
                  <div className="muted text-11 mt-2">{t("angkasa.csat.responses", { count: csatData.responseCount, total: filteredLogs.length, plural: filteredLogs.length !== 1 ? "s" : "" })}</div>
                </div>
              </div>
              {csatData.responseCount > 0 ? (
                <div className="col gap-2">
                  {[5, 4, 3, 2, 1].map(score => (
                    <div key={score}>
                      <div className="row" style={{ justifyContent: "space-between" }}><span className="text-12 mono">{score}★</span><span className="mono text-11">{csatData.distribution[score]}</span></div>
                      <div style={{ height: 5, background: "var(--c-surface-2)", borderRadius: 999, marginTop: 4 }}><div style={{ width: `${(csatData.distribution[score] / maxCsatBucket) * 100}%`, height: "100%", background: "var(--ok)", borderRadius: 999 }} /></div>
                    </div>
                  ))}
                </div>
              ) : (
                <div className="muted text-11">{t("angkasa.csat.empty")}</div>
              )}
            </Card>
          </div>

          <div className="g-2-1 mb-4">
            <Card title={t("angkasa.card.toolsFired.title")} subtitle={t("angkasa.card.toolsFired.subtitle", { range: rangeLabel })}>
              <div className="col gap-3">
                {toolFireRows.rows.length === 0 && <div className="muted text-11">{t("angkasa.card.toolsFired.empty")}</div>}
                {toolFireRows.rows.map(([toolId, count]) => (
                  <AngkasaBarRow
                    key={toolId}
                    label={angkasaToolLabel(t, lang, toolId)}
                    value={count}
                    count={count}
                    max={toolFireRows.max}
                    active={drillFilter?.type === "tool" && drillFilter.value === toolId}
                    onClick={() => toggleDrill("tool", toolId, angkasaToolLabel(t, lang, toolId))}
                  />
                ))}
              </div>
            </Card>

            <Card title={t("angkasa.card.callVolume.title")} subtitle={rangeLabel}>
              <div className="col gap-3" style={{ fontSize: 12 }}>
                <div className="row" style={{ justifyContent: "space-between" }}>
                  <span className="muted">{t("angkasa.card.callVolume.avgCost")}</span>
                  <span className="mono strong">{fmtRM(costSummary.callCount > 0 ? costSummary.totalCost / costSummary.callCount : 0)}</span>
                </div>
                <div className="row" style={{ justifyContent: "space-between" }}>
                  <span className="muted">{t("angkasa.card.callVolume.avgDuration")}</span>
                  <span className="mono strong">{costSummary.callCount > 0 ? fmtDuration(Math.round(costSummary.totalMinutes * 60 / costSummary.callCount)) : "—"}</span>
                </div>
                <div className="row" style={{ justifyContent: "space-between" }}>
                  <span className="muted">{t("angkasa.card.callVolume.clients")}</span>
                  <span className="mono strong">{angkasaClients.length}</span>
                </div>
              </div>
            </Card>
          </div>

          <Card
            title={t("angkasa.card.topTopics.title")}
            subtitle={t("angkasa.card.topTopics.subtitle", { range: rangeLabel })}
            className="mb-4"
          >
            <div className="col gap-3">
              {topTopicsBreakdown.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 / maxTopic) * 100}%`, height: "100%", background: "var(--a)", borderRadius: 999 }} /></div>
                </div>
              ))}
              {topTopicsBreakdown.length === 0 && <div className="muted text-11">{t("angkasa.card.topTopics.empty")}</div>}
            </div>
          </Card>

          <Card
            title={t("angkasa.card.recentCalls.title")}
            subtitle={hasActiveFilter
              ? t("angkasa.card.recentCalls.subtitleFiltered", { filters: activeFilterLabels.join(", "), count: recentLogs.length, plural: recentLogs.length !== 1 ? "s" : "" })
              : t("angkasa.card.recentCalls.subtitle", { count: recentLogs.length, plural: recentLogs.length !== 1 ? "s" : "" })}
            action={hasActiveFilter && <Btn variant="ghost" size="sm" onClick={clearAllFilters}>{t("angkasa.clearFilters")}</Btn>}
          >
            <div className="row gap-2 mb-3" style={{ flexWrap: "wrap", alignItems: "center" }}>
              <Input
                placeholder={t("angkasa.search.placeholder")}
                value={searchQuery}
                onChange={e => setSearchQuery(e.target.value)}
                style={{ width: 220 }}
              />
              <Select value={filterReasonKey} onChange={e => setFilterReasonKey(e.target.value)} style={{ width: 200 }}>
                <option value="">{t("angkasa.filter.allReasons")}</option>
                {ANGKASA_REASON_DEFS.map(r => <option key={r.key} value={r.key}>{angkasaReasonLabel(t, r.key)}</option>)}
              </Select>
              <Select value={filterOutcome} onChange={e => setFilterOutcome(e.target.value)} style={{ width: 160 }}>
                <option value="">{t("angkasa.filter.allOutcomes")}</option>
                {Object.values(ANGKASA_ENDED_REASON_LABELS).filter((v, i, arr) => arr.indexOf(v) === i).map(label => (
                  <option key={label} value={label}>{angkasaOutcomeLabel(t, label)}</option>
                ))}
              </Select>
              <Select value={filterSentimentValue} onChange={e => setFilterSentimentValue(e.target.value)} style={{ width: 150 }}>
                <option value="">{t("angkasa.filter.allSentiment")}</option>
                <option value="positive">{t("angkasa.sentiment.positive")}</option>
                <option value="neutral">{t("angkasa.sentiment.neutral")}</option>
                <option value="negative">{t("angkasa.sentiment.negative")}</option>
                <option value="unanalyzed">{t("angkasa.sentiment.unanalyzed")}</option>
              </Select>
              <span className="muted text-11" style={{ marginLeft: "auto" }}>
                {t("angkasa.filter.oneModel")}
              </span>
            </div>
            <div className="tbl-wrap" style={{ margin: "0 -18px -18px" }}>
              <table className="tbl">
                <thead>
                  <tr><th>{t("angkasa.table.time")}</th><th>{t("angkasa.table.contact")}</th><th>{t("angkasa.table.reason")}</th><th>{t("angkasa.table.outcome")}</th><th className="right">{t("angkasa.table.duration")}</th><th className="right">{t("angkasa.table.cost")}</th></tr>
                </thead>
                <tbody>
                  {recentLogs.length === 0 && (
                    <tr><td colSpan={6} style={{ textAlign: "center", color: "var(--c-text-4)", padding: 20 }}>{t("angkasa.table.noCalls")}</td></tr>
                  )}
                  {recentLogs.map(l => {
                    const contactName = l.customerName || l.contactName || l.contact || t("angkasa.table.unknown");
                    const contactNumber = (l.contactId && !/^(user_|assistant-test-)/.test(l.contactId) ? l.contactId : (l.contact || l.to || "")) || "";
                    const reasonKey = reasonKeyByLogId[l.id] || "uncategorized";
                    const outcome = angkasaFriendlyEndedReason(l.endedReason);
                    const isSelected = l.id === selected;
                    return (
                      <tr
                        key={l.id}
                        onClick={() => setSelected(prev => prev === l.id ? null : l.id)}
                        style={{ cursor: "pointer", background: isSelected ? "rgba(141,139,255,0.07)" : "" }}
                      >
                        <td className="mono" style={{ fontSize: 11 }}>{fmtDate(l.startedAt)}</td>
                        <td>
                          <div className="strong">{contactName}</div>
                          {contactNumber && <div className="mono text-11 muted">{contactNumber}</div>}
                        </td>
                        <td><Badge tone="accent">{angkasaReasonLabel(t, reasonKey)}</Badge></td>
                        <td><Badge tone={outcomeTone(l.endedReason)} dot>{angkasaOutcomeLabel(t, outcome)}</Badge></td>
                        <td className="mono right">{fmtDuration(l.durationSeconds)}</td>
                        <td className="mono right strong">{fmtRM(l.sellingMyr)}</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </Card>
        </>
      )}
    </div>
    <SessionDetail log={selectedLogObj} mode={mode} onClose={() => setSelected(null)} />
    </div>
  );
};
