// ---------- Telephony, SIP, Asterisk, Logs, Blast ----------

// Converts sequences of Malay digit words back to numerals for transcript readability.
// e.g. "satu tiga kosong kosong lapan lapan..." → "1300889945"
// Converts spoken digit words to numerals for transcript readability.
// Malay: 3+ consecutive words (satu tiga kosong...) → numerals
// English: 4+ consecutive words (zero eight two three...) → numerals (stricter to avoid normal-speech false positives)
function humanizeTranscript(text) {
  if (!text) return text;
  const MS = { kosong:"0",satu:"1",dua:"2",tiga:"3",empat:"4",lima:"5",enam:"6",tujuh:"7",lapan:"8",sembilan:"9" };
  const EN = { zero:"0",one:"1",two:"2",three:"3",four:"4",five:"5",six:"6",seven:"7",eight:"8",nine:"9" };
  const msW = "kosong|satu|dua|tiga|empat|lima|enam|tujuh|lapan|sembilan";
  const enW = "zero|one|two|three|four|five|six|seven|eight|nine";
  text = text.replace(
    new RegExp(`\\b((?:${msW})(?:\\s+(?:${msW})){2,})\\b`, "gi"),
    m => m.toLowerCase().trim().split(/\s+/).map(v => MS[v] ?? v).join("")
  );
  text = text.replace(
    new RegExp(`\\b((?:${enW})(?:\\s+(?:${enW})){3,})\\b`, "gi"),
    m => m.toLowerCase().trim().split(/\s+/).map(v => EN[v] ?? v).join("")
  );
  return text;
}

const TelephonyView = ({ callLogs = [], assistants = [] }) => {
  const [phoneNum, setPhoneNum] = React.useState("");
  const [selAssistant, setSelAssistant] = React.useState("");
  const [calling, setCalling] = React.useState(false);
  const [callResult, setCallResult] = React.useState(null);

  async function placeCall() {
    if (!phoneNum) return;
    setCalling(true); setCallResult(null);
    try {
      const aName = assistants.find(a => a.id === selAssistant)?.name || assistants[0]?.name || "Assistant";
      const aId = selAssistant || assistants[0]?.id || "";
      await voxApi("/outbound/call", {
        method: "POST",
        body: JSON.stringify({ to: phoneNum, assistantId: aId, assistantName: aName }),
      });
      setCallResult({ ok: true, msg: `Call placed to ${phoneNum}` });
    } catch (e) {
      setCallResult({ ok: false, msg: e.message });
    } finally { setCalling(false); }
  }

  return (
    <div>
      <div className="ph">
        <div>
          <h1 className="ph-title">Telephony</h1>
          <div className="ph-subtitle">Live channels, originating calls, transfers, and routing.</div>
        </div>
      </div>

      <div className="stat-grid">
        <StatTile k={{ label: "Calls total", value: callLogs.length.toLocaleString(), delta: "+12", spark: [1,2,1,2,2,3,2,3,4,3,2,3,3,2,3,4,3,3,3,3] }} />
        <StatTile k={{ label: "Calls / hr", value: "—", delta: "+22", spark: [80,92,108,124,118,142,158,138,162,184,182,156,168,184,178,162,158,142,168,184] }} />
        <StatTile k={{ label: "Trunk utilization", value: "62%", delta: "+8%", spark: [40,42,48,52,48,54,58,56,60,62,60,58,62,64,60,58,60,62,62,62] }} />
        <StatTile k={{ label: "Failed (24h)", value: "—", delta: "-4", spark: [22,18,16,18,14,12,14,12,10,12,10,8,10,12,10,8,10,12,10,12] }} />
      </div>

      <div className="g-2 mt-2">
        <Card title="Originate call" subtitle="Place a live outbound call via the SIP trunk">
          <Field label="Destination number">
            <Input placeholder="+60 12-345-6789" value={phoneNum} onChange={e => setPhoneNum(e.target.value)} />
          </Field>
          <Field label="Assistant">
            <Select value={selAssistant} onChange={e => setSelAssistant(e.target.value)}>
              <option value="">— Select assistant —</option>
              {assistants.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
            </Select>
          </Field>
          <Btn variant="primary" icon="phone" onClick={placeCall} disabled={calling || !phoneNum}>
            {calling ? "Placing call…" : "Place call"}
          </Btn>
          {callResult && (
            <div className="mt-3" style={{ padding: "10px 12px", borderRadius: 8, background: callResult.ok ? "var(--ok-soft)" : "var(--err-soft)", color: callResult.ok ? "var(--ok)" : "var(--err)", fontSize: 12 }}>
              {callResult.msg}
            </div>
          )}
        </Card>

        <Card title="Live call monitor" subtitle="Real-time channel status">
          <div className="call-viz" style={{ height: 60, margin: "8px 0" }}>
            {Array.from({ length: 24 }).map((_, i) => (
              <i key={i} style={{ height: `${20 + Math.sin(i*0.6)*40 + 30}%`, animationDelay: `${i * 0.08}s` }} />
            ))}
          </div>
          <div className="center text-12 muted mt-2">Waiting for active call…</div>
        </Card>
      </div>
    </div>
  );
};

// ---------- SIP setup ----------
function unwrapConfigPayload(payload) {
  if (payload && typeof payload === "object" && payload.data && typeof payload.data === "object") {
    return payload.data;
  }
  return payload;
}

function normalizeSipViewConfig(raw) {
  const config = unwrapConfigPayload(raw) || {};
  return {
    provider: config.provider || "asterisk",
    trunkName: config.trunkName || config.trunk || "alienvoip",
    displayName: config.displayName || "AlienVoIP",
    host: config.host || "",
    port: config.port || 5060,
    transport: config.transport || "udp",
    username: config.username || "",
    password: config.password || "",
    callerId: config.callerId || "",
    context: config.context || "outbound-blast",
    allowInbound: config.allowInbound === true,
    allowOutbound: config.allowOutbound !== false,
    optionsPing: config.optionsPing === true,
    useRegistration: config.useRegistration !== false,
  };
}

const BLANK_TRUNK = { name: "", host: "", port: 5060, transport: "udp", username: "", password: "", callerId: "", context: "outbound-blast", allowInbound: false, allowOutbound: true, optionsPing: false, useRegistration: true, notes: "" };

const SipTrunkForm = ({ trunk, setTrunk, onSave, onCancel }) => (
  <div className="modal-back" onClick={onCancel}>
    <div className="modal" style={{ maxWidth: 520 }} onClick={e => e.stopPropagation()}>
      <div className="modal-head">
        <div className="card-title">{trunk.id ? "Edit trunk" : "Add SIP trunk"}</div>
        <Btn variant="ghost" size="sm" icon="x" onClick={onCancel} />
      </div>
      <div className="modal-body">
        <Field label="Display name"><Input value={trunk.name || ""} onChange={e => setTrunk(d => ({ ...d, name: e.target.value }))} placeholder="e.g. YTL Voice 2" /></Field>
        <div className="fld-row">
          <Field label="Host"><Input value={trunk.host || ""} onChange={e => setTrunk(d => ({ ...d, host: e.target.value }))} placeholder="sip.provider.com" /></Field>
          <Field label="Port"><Input value={trunk.port || 5060} onChange={e => setTrunk(d => ({ ...d, port: e.target.value }))} /></Field>
        </div>
        <div className="fld-row">
          <Field label="Transport">
            <Select value={trunk.transport || "udp"} onChange={e => setTrunk(d => ({ ...d, transport: e.target.value }))}>
              <option value="udp">UDP</option><option value="tcp">TCP</option><option value="tls">TLS</option>
            </Select>
          </Field>
          <Field label="Caller ID"><Input value={trunk.callerId || ""} onChange={e => setTrunk(d => ({ ...d, callerId: e.target.value }))} placeholder="+60312345678" /></Field>
        </div>
        <div className="fld-row">
          <Field label="Username"><Input value={trunk.username || ""} onChange={e => setTrunk(d => ({ ...d, username: e.target.value }))} /></Field>
          <Field label="Password"><Input type="password" value={trunk.password || ""} onChange={e => setTrunk(d => ({ ...d, password: e.target.value }))} placeholder={trunk.id ? "Leave blank to keep current" : ""} /></Field>
        </div>
        <Field label="Notes (optional)"><Input value={trunk.notes || ""} onChange={e => setTrunk(d => ({ ...d, notes: e.target.value }))} placeholder="e.g. Assigned to YTL / Celcom clients" /></Field>
        <Field label="Dialplan context"><Input value={trunk.context || ""} onChange={e => setTrunk(d => ({ ...d, context: e.target.value }))} placeholder="inbound-voxpro" /></Field>
        <div className="hr" />
        <div className="col gap-3">
          {[
            { key: "allowInbound", label: "Allow inbound calls", desc: "Accept calls dialed TO the trunk's number." },
            { key: "allowOutbound", label: "Allow outbound calls", desc: "Originate via this trunk for blasts and assistants." },
            { key: "optionsPing", label: "OPTIONS keepalive", desc: "Send SIP OPTIONS every 30s to keep NAT pinhole open." },
          ].map(r => (
            <div key={r.key} className="row gap-3" style={{ justifyContent: "space-between" }}>
              <div>
                <div className="text-12 strong">{r.label}</div>
                <div className="muted text-11 mt-1">{r.desc}</div>
              </div>
              <Toggle checked={!!trunk[r.key]} onChange={v => setTrunk(d => ({ ...d, [r.key]: v }))} />
            </div>
          ))}
        </div>
      </div>
      <div className="modal-foot">
        <Btn variant="ghost" onClick={onCancel}>Cancel</Btn>
        <Btn variant="primary" disabled={!trunk.name || !trunk.host} onClick={() => onSave(trunk)}>Save trunk</Btn>
      </div>
    </div>
  </div>
);

const SipView = () => {
  const [cfg, setCfg] = React.useState(null);
  const [saving, setSaving] = React.useState(false);
  const [msg, setMsg] = React.useState(null);
  const [trunks, setTrunks] = React.useState([]);
  const [showAdd, setShowAdd] = React.useState(false);
  const [editTrunk, setEditTrunk] = React.useState(null);
  const [newTrunk, setNewTrunk] = React.useState({ ...BLANK_TRUNK });

  React.useEffect(() => {
    voxApiOptional("/sip/config", null).then(d => {
      const next = normalizeSipViewConfig(d);
      if (next?.trunkName || next?.host) setCfg(next);
    });
    voxApiOptional("/sip/trunks", { trunks: [] }).then(d => setTrunks(d.trunks || []));
  }, []);

  const s = cfg || {
    provider: "asterisk", trunkName: "alienvoip", displayName: "AlienVoIP",
    host: "sip3.alienvoip.com", port: 5060, transport: "udp",
    username: "646006407", password: "", callerId: "646006407",
    context: "outbound-blast", allowInbound: false, allowOutbound: true, optionsPing: true, useRegistration: true,
  };

  function handleChange(field, val) { setCfg(c => ({ ...(c || s), [field]: val })); }

  async function save() {
    setSaving(true); setMsg(null);
    try {
      const saved = await voxApi("/sip/config", { method: "POST", body: JSON.stringify(s) });
      setCfg(normalizeSipViewConfig(saved));
      setMsg(saved.asteriskSyncError
        ? { ok: false, text: `Trunk saved, but Asterisk sync failed: ${saved.asteriskSyncError}` }
        : { ok: true, text: "Default trunk saved and synced to Asterisk." });
    } catch (e) { setMsg({ ok: false, text: e.message }); }
    finally { setSaving(false); }
  }

  async function saveTrunk(trunk) {
    try {
      const r = await voxApi("/sip/trunks", { method: "POST", body: JSON.stringify(trunk) });
      const saved = r.trunk;
      setTrunks(t => {
        const idx = t.findIndex(x => x.id === saved.id);
        return idx >= 0 ? t.map(x => x.id === saved.id ? saved : x) : [...t, saved];
      });
      setShowAdd(false); setEditTrunk(null);
      setNewTrunk({ ...BLANK_TRUNK });
      setMsg(saved.asteriskSyncError
        ? { ok: false, text: `Trunk saved, but Asterisk sync failed: ${saved.asteriskSyncError}` }
        : { ok: true, text: "Trunk saved and synced to Asterisk." });
    } catch (e) { alert("Failed to save trunk: " + e.message); }
  }

  async function removeTrunk(id) {
    if (!confirm("Delete this SIP trunk? Any clients assigned to it will fall back to the default trunk.")) return;
    try {
      await voxApi(`/sip/trunks/${id}`, { method: "DELETE" });
      setTrunks(t => t.filter(x => x.id !== id));
      setMsg({ ok: true, text: "Trunk deleted and removed from Asterisk." });
    } catch (e) { alert("Failed to delete: " + e.message); }
  }

  return (
    <div>
      <div className="ph">
        <div>
          <h1 className="ph-title">SIP Numbers</h1>
          <div className="ph-subtitle">Manage SIP trunks and assign them to specific clients.</div>
        </div>
        <div className="ph-actions">
          <Btn variant="primary" size="sm" icon="plus" onClick={() => { setNewTrunk({ ...BLANK_TRUNK }); setShowAdd(true); }}>Add trunk</Btn>
        </div>
      </div>

      {msg && <div className="mb-4" style={{ padding: "10px 14px", borderRadius: 8, background: msg.ok ? "var(--ok-soft)" : "var(--err-soft)", color: msg.ok ? "var(--ok)" : "var(--err)", fontSize: 12 }}>{msg.text}</div>}

      {showAdd && <SipTrunkForm trunk={newTrunk} setTrunk={setNewTrunk} onSave={saveTrunk} onCancel={() => setShowAdd(false)} />}
      {editTrunk && <SipTrunkForm trunk={editTrunk} setTrunk={setEditTrunk} onSave={saveTrunk} onCancel={() => setEditTrunk(null)} />}

      {/* Additional trunks table */}
      <Card pad="none" style={{ marginBottom: 16 }}>
        <div style={{ padding: "14px 20px 10px", borderBottom: "1px solid var(--c-line-2)" }}>
          <div className="strong text-13">Additional trunks</div>
          <div className="muted text-11 mt-1">Assign these to specific clients in the client editor.</div>
        </div>
        <div className="tbl-wrap">
          <table className="tbl">
            <thead>
              <tr><th>Name</th><th>Host</th><th>Caller ID</th><th>Notes</th><th></th></tr>
            </thead>
            <tbody>
              {trunks.map(t => (
                <tr key={t.id}>
                  <td><div className="strong">{t.name}</div><div className="mono text-11 muted">{t.username || "—"}</div></td>
                  <td><span className="mono text-12">{t.host}:{t.port || 5060}</span></td>
                  <td><span className="mono text-12">{t.callerId || "—"}</span></td>
                  <td><span className="muted text-12">{t.notes || "—"}</span></td>
                  <td className="right">
                    <div className="row gap-1 justify-end">
                      <Btn variant="ghost" size="sm" icon="edit-2" onClick={() => setEditTrunk({ ...t })} />
                      <Btn variant="ghost" size="sm" icon="trash-2" onClick={() => removeTrunk(t.id)} />
                    </div>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
          {trunks.length === 0 && (
            <div className="empty-state">
              <div className="empty-state-title">No additional trunks</div>
              <div>The default trunk below is used for all calls. Add more to assign specific numbers to clients.</div>
            </div>
          )}
        </div>
      </Card>

      {/* Default trunk config */}
      <div className="g-2-1">
        <Card title="Default trunk" action={<Badge tone="ok" dot>Active</Badge>}>
          <div className="fld-row">
            <Field label="Provider">
              <Select value={s.provider || "asterisk"} onChange={e => handleChange("provider", e.target.value)}>
                <option value="asterisk">Asterisk (PJSIP)</option>
                <option value="livekit">LiveKit SIP</option>
              </Select>
            </Field>
            <Field label="Trunk name"><Input value={s.trunkName || ""} onChange={e => handleChange("trunkName", e.target.value)} /></Field>
          </div>
          <div className="fld-row">
            <Field label="Host"><Input value={s.host || ""} onChange={e => handleChange("host", e.target.value)} /></Field>
            <Field label="Port"><Input value={s.port || 5060} onChange={e => handleChange("port", e.target.value)} /></Field>
          </div>
          <div className="fld-row">
            <Field label="Transport">
              <Select value={s.transport || "udp"} onChange={e => handleChange("transport", e.target.value)}>
                <option value="udp">UDP</option><option value="tcp">TCP</option><option value="tls">TLS</option>
              </Select>
            </Field>
            <Field label="Caller ID"><Input value={s.callerId || ""} onChange={e => handleChange("callerId", e.target.value)} /></Field>
          </div>
          <div className="fld-row">
            <Field label="Username"><Input value={s.username || ""} onChange={e => handleChange("username", e.target.value)} /></Field>
            <Field label="Password"><Input type="password" value={s.password || ""} onChange={e => handleChange("password", e.target.value)} /></Field>
          </div>
          <Field label="Dialplan context"><Input value={s.context || ""} onChange={e => handleChange("context", e.target.value)} /></Field>
          <div className="hr" />
          <div className="col gap-3">
            {[
              { key: "allowInbound", label: "Allow inbound calls", desc: "Accept calls dialed TO the trunk's number." },
              { key: "allowOutbound", label: "Allow outbound calls", desc: "Originate via this trunk for blasts and assistants." },
              { key: "optionsPing", label: "OPTIONS keepalive", desc: "Send SIP OPTIONS every 30s to keep NAT pinhole open." },
            ].map(r => (
              <div key={r.key} className="row gap-3" style={{ justifyContent: "space-between" }}>
                <div>
                  <div className="text-12 strong">{r.label}</div>
                  <div className="muted text-11 mt-1">{r.desc}</div>
                </div>
                <Toggle checked={!!s[r.key]} onChange={v => handleChange(r.key, v)} />
              </div>
            ))}
          </div>
          <div className="hr" />
          <div className="row gap-2">
            <Btn variant="primary" size="sm" icon="check" onClick={save}>{saving ? "Saving…" : "Save default trunk"}</Btn>
          </div>
        </Card>

        <div style={{ fontSize: 12, color: "var(--c-muted)", background: "var(--c-surface)", border: "1px solid var(--c-line-2)", borderRadius: 10, padding: 16 }}>
          <div className="strong text-12 mb-2" style={{ color: "var(--c-fg)" }}>How it works</div>
          <div className="col gap-2">
            <div>1. The <strong>default trunk</strong> is used for all outbound calls unless a client has a specific trunk assigned.</div>
            <div>2. Add an <strong>additional trunk</strong> above for a new SIP number or provider.</div>
            <div>3. In the <strong>Client editor</strong>, pick the trunk you want that client to use for blasting.</div>
            <div>4. Calls for that client will route through their assigned trunk automatically.</div>
          </div>
        </div>
      </div>
    </div>
  );
};

// ---------- Asterisk control ----------
const AsteriskView = ({ assistants = [] }) => {
  const [cfg, setCfg] = React.useState(null);
  const [status, setStatus] = React.useState(null);
  const [destNum, setDestNum] = React.useState("");
  const [selAsst, setSelAsst] = React.useState("");
  const [origResult, setOrigResult] = React.useState(null);

  React.useEffect(() => {
    voxApiOptional("/asterisk/config", null).then(d => { if (d?.host) setCfg(d); });
    voxApiOptional("/asterisk/status", null).then(d => { if (d) setStatus(d); });
  }, []);

  const a = cfg || { host: "127.0.0.1", port: 5038, username: "voxpro", context: "outbound-blast", callerId: "646006407", timeout: 10000 };

  function handleChange(k, v) { setCfg(c => ({ ...(c || a), [k]: v })); }

  async function save() {
    try { await voxApi("/asterisk/config", { method: "POST", body: JSON.stringify(a) }); } catch {}
  }

  async function originate() {
    if (!destNum) return;
    try {
      const aName = assistants.find(x => x.id === selAsst)?.name || assistants[0]?.name || "Assistant";
      const r = await voxApi("/asterisk/originate", {
        method: "POST",
        body: JSON.stringify({ destination: destNum, assistantId: selAsst, assistantName: aName }),
      });
      setOrigResult({ ok: true, msg: "Originate sent: " + (r.actionId || "ok") });
    } catch (e) { setOrigResult({ ok: false, msg: e.message }); }
  }

  return (
    <div>
      <div className="ph">
        <div>
          <h1 className="ph-title">Asterisk control</h1>
          <div className="ph-subtitle">AMI connectivity, runtime inspection, manual originate, channel control.</div>
        </div>
        <div className="ph-actions">
          <Btn variant="ghost" size="sm" icon="settings" onClick={save}>Save config</Btn>
          <Btn variant="primary" size="sm" icon="phone" onClick={originate}>Originate</Btn>
        </div>
      </div>

      <div className="g-2-1">
        <div className="col gap-3" style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          <Card title="AMI connection" action={<Badge tone={cfg ? "ok" : "warn"} dot>{cfg ? "Connected" : "Not configured"}</Badge>}>
            <div className="fld-row">
              <Field label="AMI host"><Input value={a.host || ""} onChange={e => handleChange("host", e.target.value)} /></Field>
              <Field label="Port"><Input value={a.port || 5038} onChange={e => handleChange("port", e.target.value)} /></Field>
            </div>
            <div className="fld-row">
              <Field label="Username"><Input value={a.username || ""} onChange={e => handleChange("username", e.target.value)} /></Field>
              <Field label="Secret"><Input type="password" value={a.secret || ""} onChange={e => handleChange("secret", e.target.value)} /></Field>
            </div>
          </Card>

          <Card title="Originate test call">
            <Field label="Destination number">
              <Input placeholder="+60 12-345-6789" value={destNum} onChange={e => setDestNum(e.target.value)} />
            </Field>
            <Field label="Assistant">
              <Select value={selAsst} onChange={e => setSelAsst(e.target.value)}>
                <option value="">— Select —</option>
                {assistants.map(x => <option key={x.id} value={x.id}>{x.name}</option>)}
              </Select>
            </Field>
            <Btn variant="primary" icon="phone" onClick={originate}>Place originate</Btn>
            {origResult && (
              <div className="mt-3" style={{ padding: "10px 12px", borderRadius: 8, background: origResult.ok ? "var(--ok-soft)" : "var(--err-soft)", color: origResult.ok ? "var(--ok)" : "var(--err)", fontSize: 12 }}>
                {origResult.msg}
              </div>
            )}
          </Card>
        </div>

        <Card title="Runtime" subtitle="Live snapshot of Asterisk channels"
          action={<Btn variant="ghost" size="sm" onClick={() => voxApiOptional("/asterisk/status", null).then(d => { if (d) setStatus(d); })}>Refresh</Btn>}>
          <div className="g-3" style={{ gap: 10, marginBottom: 16 }}>
            {[
              { l: "Uptime", v: status?.uptime || "—" },
              { l: "Channels", v: status?.activeChannels != null ? `${status.activeChannels} active` : "—" },
              { l: "Version", v: status?.version || "20.x" },
              { l: "Status", v: status ? "Online" : "Offline" },
            ].map(s => (
              <div key={s.l} className="stat" style={{ padding: 12 }}>
                <div className="stat-label">{s.l}</div>
                <div className="mono" style={{ fontSize: 16, marginTop: 4 }}>{s.v}</div>
              </div>
            ))}
          </div>
          {(!status?.channels?.length) && (
            <div className="empty-state">No active channels. {!cfg && "Configure AMI first."}</div>
          )}
        </Card>
      </div>
    </div>
  );
};

// ---------- Recording player ----------
function getSessionToken() {
  return getSession()?.token || "";
}

function buildRecordingUrl(logId, { download = false } = {}) {
  const url = new URL(`/logs/${logId}/recording`, window.location.origin);
  const token = getSessionToken();
  if (download) {
    url.searchParams.set("download", "1");
  }
  if (token) {
    url.searchParams.set("token", token);
  }
  return url.toString();
}

const RecordingPlayer = ({ logId }) => {
  const [url, setUrl] = React.useState(null);
  const [loading, setLoading] = React.useState(false);
  const [err, setErr] = React.useState("");

  React.useEffect(() => () => { if (url) URL.revokeObjectURL(url); }, [url]);

  async function fetchRecordingBlob() {
    const token = getSessionToken();
    const res = await fetch(`/logs/${logId}/recording`, {
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    });
    if (!res.ok) {
      const d = await res.json().catch(() => ({}));
      throw new Error(d.error || "Failed to load");
    }
    return res.blob();
  }

  async function load() {
    setLoading(true); setErr("");
    try {
      const blob = await fetchRecordingBlob();
      setUrl(u => { if (u) URL.revokeObjectURL(u); return URL.createObjectURL(blob); });
    } catch (e) {
      setErr(e.message);
    } finally { setLoading(false); }
  }

  async function download() {
    setLoading(true); setErr("");
    try {
      const blob = await fetchRecordingBlob();
      const downloadUrl = URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = downloadUrl;
      link.download = `${logId}.wav`;
      link.click();
      window.setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000);
    } catch (e) {
      setErr(e.message);
    } finally { setLoading(false); }
  }

  if (url) {
    return (
      <div>
        <audio controls src={url} style={{ width: "100%", marginTop: 4 }} />
        <div className="row gap-2 mt-2">
          <Btn variant="ghost" size="sm" icon="download" onClick={download} disabled={loading}>
            Download recording
          </Btn>
        </div>
        <div className="muted text-11 mt-1">Mono mix — caller + AI dialogue</div>
      </div>
    );
  }

  return (
    <div>
      <Btn variant="ghost" size="sm" icon="play" onClick={load} disabled={loading}>
        {loading ? "Loading…" : "Play recording"}
      </Btn>
      <Btn variant="ghost" size="sm" icon="download" onClick={download} disabled={loading}>
        Download recording
      </Btn>
      {err && <div className="text-11 mt-1" style={{ color: "var(--err)" }}>{err}</div>}
    </div>
  );
};

// ---------- Session detail side panel ----------
// Rendered as an inline flex child — width scales with viewport via clamp()
const SessionDetail = ({ log, mode = "dev", onClose }) => {
  if (!log) return null;
  const isClientMode = mode === "client";
  const direction = inferCallDirection(log);
  const sourceLabel = summarizeSource(log);
  const [tab, setTab] = React.useState("overview");

  return (
    <div style={{
      width: "clamp(300px, 28vw, 540px)",
      flexShrink: 0,
      borderLeft: "1px solid var(--c-line-2)",
      background: "var(--c-surface)",
      display: "flex",
      flexDirection: "column",
      position: "sticky",
      top: 0,
      maxHeight: "calc(100vh - 52px)",
      overflowY: "auto",
    }}>
      <div style={{ padding: "14px 16px", borderBottom: "1px solid var(--c-line-2)", display: "flex", alignItems: "center", justifyContent: "space-between", position: "sticky", top: 0, background: "var(--c-surface)", zIndex: 1 }}>
        <div>
          <div className="card-title">Session detail</div>
          <div className="muted text-11 mono mt-1" style={{ wordBreak: "break-all" }}>{log.id?.slice(0, 32) || "—"}</div>
        </div>
        <button onClick={onClose} style={{ background: "none", border: "none", color: "var(--c-text-3)", cursor: "pointer", fontSize: 18, lineHeight: 1, padding: 4 }}>✕</button>
      </div>

      <div style={{ padding: "6px 16px 0", position: "sticky", top: 53, background: "var(--c-surface)", zIndex: 1, borderBottom: "1px solid var(--c-line-2)" }}>
        <Tabs
          tabs={[
            { id: "overview", label: "Overview" },
            { id: "cost", label: "Cost breakdown" },
          ]}
          active={tab}
          onSelect={setTab}
        />
      </div>

      <div style={{ padding: "14px 16px", flex: 1 }}>
        {tab === "overview" && (
        <>
        {/* ── Core meta ── */}
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 12 }}>
          <div style={{ gridColumn: "1 / -1" }}>
            <div className="text-11 muted">Contact</div>
            <div className="strong text-13 mt-1">{log.customerName || log.contactName || log.contact || "Unknown"}</div>
            <div className="mono text-11 muted">{log.contact || "—"}</div>
          </div>
          <div>
            <div className="text-11 muted">Outcome</div>
            <div className="mt-1"><Badge tone={outcomeTone(log.endedReason || log.outcome)} dot>{log.endedReason || log.outcome || "—"}</Badge></div>
          </div>
          <div>
            <div className="text-11 muted">Direction</div>
            <div className="mt-1"><Badge tone={callDirectionTone(direction)} dot>{labelCallDirection(direction)}</Badge></div>
          </div>
          <div>
            <div className="text-11 muted">Duration</div>
            <div className="mono text-12 mt-1">{log.durationSeconds != null ? fmtDuration(log.durationSeconds) : (log.duration || "—")}</div>
          </div>
          <div>
            <div className="text-11 muted">{isClientMode ? "Your cost" : "Cost"}</div>
            <div className="mono text-12 mt-1" style={{ color: "var(--a)" }}>{log.sellingMyr != null ? fmtRM(log.sellingMyr) : "—"}</div>
          </div>
          <div>
            <div className="text-11 muted">Assistant</div>
            <div className="text-12 mt-1">{log.assistantName || "—"}</div>
          </div>
          <div>
            <div className="text-11 muted">Source</div>
            <div className="text-12 mt-1">{sourceLabel}</div>
          </div>
          {log.useCase && (
            <div>
              <div className="text-11 muted">Use case</div>
              <div className="mono text-12 mt-1">{log.useCase}</div>
            </div>
          )}
          {(log.clientName || log.organizationName) && (
            <div>
              <div className="text-11 muted">{isClientMode ? "Workspace" : "Client"}</div>
              <div className="text-12 mt-1">{log.clientName || log.organizationName}</div>
            </div>
          )}
          {!isClientMode && (log.initiatedByUserName || log.initiatedByUserEmail) && (
            <div style={{ gridColumn: "1 / -1" }}>
              <div className="text-11 muted">Triggered by</div>
              <div className="text-12 mt-1">{log.initiatedByUserName || log.initiatedByUserEmail}</div>
            </div>
          )}
          {log.campaignName && (
            <div style={{ gridColumn: "1 / -1" }}>
              <div className="text-11 muted">Campaign</div>
              <div className="text-12 mt-1">{log.campaignName}</div>
            </div>
          )}
          {log.campaignType && (
            <div>
              <div className="text-11 muted">Type</div>
              <div className="mt-1"><Badge tone="neutral">{log.campaignType}</Badge></div>
            </div>
          )}
          {log.language && (
            <div>
              <div className="text-11 muted">Language</div>
              <div className="mono text-12 mt-1">{log.language}</div>
            </div>
          )}
        </div>

        {/* ── Structured outputs — dynamic per campaign type ── */}
        {(() => {
          // Merge top-level known fields + structuredOutput into one flat object
          const so = {
            ...(log.structuredOutput || {}),
            ...(log.balance != null ? { balance: log.balance } : {}),
            ...(log.expected_payment_date ? { expected_payment_date: log.expected_payment_date } : {}),
          };
          const entries = Object.entries(so).filter(([, v]) => v != null && String(v).trim() !== "");
          if (!entries.length) return null;

          const moneyKeys = new Set(["balance","amount","outstanding","outstanding_amount","payment_amount","debt","total_payment"]);
          const dateKeys  = new Set(["expected_payment_date","ptp_date","follow_up_date","appointment_date","next_contact_date"]);
          const goodKeys  = new Set(["resolved","interested","confirmed","opted_out","callback_requested"]);

          function formatSOValue(key, val) {
            const k = key.toLowerCase();
            if (moneyKeys.has(k) && !isNaN(Number(val))) return `RM ${Number(val).toFixed(2)}`;
            return String(val);
          }
          function soColor(key, val) {
            const k = key.toLowerCase();
            if (dateKeys.has(k)) return "var(--ok)";
            if (goodKeys.has(k)) return val === true || val === "true" || val === "yes" ? "var(--ok)" : "var(--err)";
            return "var(--c-text)";
          }
          function soLabel(key) {
            return key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
          }

          return (
            <>
              <div className="hr" />
              <div className="card-title mb-2">
                {log.campaignType ? `${soLabel(log.campaignType)} outputs` : "Structured outputs"}
              </div>
              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
                {entries.map(([key, val]) => (
                  <div key={key}>
                    <div className="text-11 muted">{soLabel(key)}</div>
                    <div className="mono text-12 mt-1" style={{ color: soColor(key, val) }}>
                      {formatSOValue(key, val)}
                    </div>
                  </div>
                ))}
              </div>
            </>
          );
        })()}

        {/* Recording */}
        {log.recordingPath && (
          <>
            <div className="hr" />
            <div className="card-title mb-2">Recording</div>
            <RecordingPlayer logId={log.id} />
          </>
        )}

        {/* Transcript */}
        {log.turns?.length > 0 && (
          <>
            <div className="hr" />
            <div className="card-title mb-2">Transcript</div>
            <div style={{ maxHeight: 320, overflowY: "auto", padding: "4px 2px" }}>
              {log.turns.map((t, i) => {
                const lines = [];
                if (String(t.user || "").trim()) {
                  lines.push({ who: "user", text: t.user, ts: t.createdAt });
                }
                if (String(t.assistant || "").trim()) {
                  lines.push({ who: "assistant", text: t.assistant, ts: t.createdAt });
                }
                return lines.map((line, j) => (
                  <div key={`${i}-${j}`} className={`bubble ${line.who}`} style={{ maxWidth: "100%" }}>
                    <div className="bubble-avatar">
                      {line.who === "user" ? (log.customerName || "U").slice(0, 2).toUpperCase() : "AI"}
                    </div>
                    <div>
                      <div className="bubble-body">{humanizeTranscript(line.text)}</div>
                      <div className="bubble-meta">{line.ts ? fmtDate(line.ts) : ""}</div>
                    </div>
                  </div>
                ));
              })}
            </div>
          </>
        )}
        </>
        )}

        {/* ── Cost breakdown tab ── */}
        {tab === "cost" && log.breakdown && (
          <>
            {isClientMode ? (
              log.pricingMode === "flat" ? (
                <table className="tbl">
                  <thead><tr><th>Line</th><th className="mono right">Rate</th><th className="mono right" style={{ color: "var(--a)" }}>Cost</th></tr></thead>
                  <tbody>
                    <tr>
                      <td className="text-12">Voice minutes</td>
                      <td className="mono right text-11">RM {Number(log.clientMarkupMyrPerMin || 0).toFixed(3)}/min &times; {(Number(log.durationSeconds || 0) / 60).toFixed(2)} min</td>
                      <td className="mono right text-11 strong" style={{ color: "var(--a)" }}>RM {Number(log.sellingMyr || 0).toFixed(4)}</td>
                    </tr>
                    <tr>
                      <td className="strong">Total</td>
                      <td></td>
                      <td className="mono right strong" style={{ color: "var(--a)" }}>RM {Number(log.sellingMyr || 0).toFixed(4)}</td>
                    </tr>
                  </tbody>
                </table>
              ) : (
                <>
                  {Array.isArray(log.breakdown.usageBreakdown) && log.breakdown.usageBreakdown.length > 0 && (
                    <table className="tbl" style={{ marginBottom: 8 }}>
                      <thead>
                        <tr>
                          <th>Service</th><th>Vendor</th>
                          <th className="mono right">Usage</th>
                          <th className="mono right" style={{ color: "var(--a)" }}>Cost</th>
                        </tr>
                      </thead>
                      <tbody>
                        {log.breakdown.usageBreakdown.map(row => (
                          <tr key={row.key}>
                            <td className="text-12">{row.label}</td>
                            <td className="muted text-11">{row.vendor}</td>
                            <td className="mono right text-11">{Number(row.usage || 0).toLocaleString()} {row.unit}</td>
                            <td className="mono right text-11" style={{ color: "var(--a)" }}>RM {Number(row.billableCost || 0).toFixed(4)}</td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  )}
                  <table className="tbl">
                    <tbody>
                      <tr>
                        <td className="strong">Total</td>
                        <td className="mono right strong" style={{ color: "var(--a)" }}>RM {Number(log.sellingMyr || 0).toFixed(4)}</td>
                      </tr>
                    </tbody>
                  </table>
                </>
              )
            ) : (
              <>
                {Array.isArray(log.breakdown.usageBreakdown) && log.breakdown.usageBreakdown.length > 0 && (
                  <table className="tbl" style={{ marginBottom: 8 }}>
                    <thead>
                      <tr>
                        <th>Service</th><th>Vendor</th>
                        <th className="mono right">Usage</th>
                        <th className="mono right">Internal</th>
                        <th className="mono right" style={{ color: "var(--a)" }}>Billed</th>
                      </tr>
                    </thead>
                    <tbody>
                      {log.breakdown.usageBreakdown.map(row => (
                        <tr key={row.key}>
                          <td className="text-12">{row.label}</td>
                          <td className="muted text-11">{row.vendor}</td>
                          <td className="mono right text-11">{Number(row.usage || 0).toLocaleString()} {row.unit}</td>
                          <td className="mono right text-11">RM {Number(row.internalCost || 0).toFixed(4)}</td>
                          <td className="mono right text-11" style={{ color: "var(--a)" }}>RM {Number(row.billableCost || 0).toFixed(4)}</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                )}
                <table className="tbl">
                  <tbody>
                    {log.breakdown.totalInternal != null && (
                      <tr>
                        <td className="muted">Total internal cost</td>
                        <td className="mono right">RM {Number(log.breakdown.totalInternal).toFixed(4)}</td>
                      </tr>
                    )}
                    {log.pricingMode === "flat" ? (
                      <tr>
                        <td className="muted">Client rate ({Number(log.clientMarkupMyrPerMin || 0).toFixed(3)} RM/min &times; {(Number(log.durationSeconds || 0) / 60).toFixed(2)} min)</td>
                        <td className="mono right">RM {Number(log.sellingMyr || 0).toFixed(4)}</td>
                      </tr>
                    ) : (
                      log.breakdown.totalBillable != null && (
                        <tr>
                          <td className="muted">Provider markup subtotal (no client rate configured)</td>
                          <td className="mono right">RM {Number(log.breakdown.totalBillable).toFixed(4)}</td>
                        </tr>
                      )
                    )}
                    <tr>
                      <td className="strong">Total billed</td>
                      <td className="mono right strong" style={{ color: "var(--a)" }}>RM {Number(log.sellingMyr || 0).toFixed(4)}</td>
                    </tr>
                    {log.breakdown.totalInternal != null && (
                      <tr>
                        <td className="muted text-11">Gross profit</td>
                        <td className="mono right text-11" style={{ color: (log.grossProfit ?? 0) >= 0 ? "var(--ok)" : "var(--err)" }}>
                          RM {Number(log.grossProfit ?? ((log.sellingMyr || 0) - Number(log.breakdown.totalInternal || 0))).toFixed(4)}
                        </td>
                      </tr>
                    )}
                  </tbody>
                </table>
              </>
            )}
          </>
        )}
      </div>
    </div>
  );
};

const CampaignContactDetail = ({ campaign, job, log, onClose }) => {
  if (!job) return null;

  const customerName = log?.customerName || job.customerName || "Unknown";
  const contactNumber = log?.contact || log?.to || job.to || "—";
  const outcome = log?.endedReason || job.endedReason || job.status || "—";
  const duration = log?.durationSeconds != null
    ? fmtDuration(log.durationSeconds)
    : (job.durationSeconds != null ? fmtDuration(job.durationSeconds) : "—");
  const cost = log?.sellingMyr != null
    ? fmtRM(log.sellingMyr)
    : (job.sellingMyr != null ? fmtRM(job.sellingMyr) : "—");
  const balance = log?.balance != null
    ? fmtRM(log.balance)
    : (job.balance != null ? fmtRM(job.balance) : "—");
  const expectedPayment = log?.expected_payment_date || job.expectedPaymentDate || "—";
  const transcriptTurns = log?.turns || [];

  return (
    <div style={{
      width: "clamp(300px, 28vw, 540px)",
      flexShrink: 0,
      borderLeft: "1px solid var(--c-line-2)",
      background: "var(--c-surface)",
      display: "flex",
      flexDirection: "column",
      position: "sticky",
      top: 0,
      maxHeight: "calc(100vh - 52px)",
      overflowY: "auto",
    }}>
      <div style={{ padding: "14px 16px", borderBottom: "1px solid var(--c-line-2)", display: "flex", alignItems: "center", justifyContent: "space-between", position: "sticky", top: 0, background: "var(--c-surface)", zIndex: 1 }}>
        <div>
          <div className="card-title">Customer detail</div>
          <div className="muted text-11 mono mt-1" style={{ wordBreak: "break-all" }}>{job.roomName || log?.id || "—"}</div>
        </div>
        <button onClick={onClose} style={{ background: "none", border: "none", color: "var(--c-text-3)", cursor: "pointer", fontSize: 18, lineHeight: 1, padding: 4 }}>×</button>
      </div>

      <div style={{ padding: "14px 16px", flex: 1 }}>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 12 }}>
          <div style={{ gridColumn: "1 / -1" }}>
            <div className="text-11 muted">Contact</div>
            <div className="strong text-13 mt-1">{customerName}</div>
            <div className="mono text-11 muted">{contactNumber}</div>
          </div>
          <div>
            <div className="text-11 muted">Campaign</div>
            <div className="text-12 mt-1">{campaign?.campaignName || log?.campaignName || "—"}</div>
          </div>
          <div>
            <div className="text-11 muted">Status</div>
            <div className="mt-1"><Badge tone={job.status === "completed" ? "accent" : (job.status === "live" ? "ok" : (job.status === "retrying" ? "warn" : "default"))} dot>{job.status || "—"}</Badge></div>
          </div>
          <div>
            <div className="text-11 muted">Outcome</div>
            <div className="mt-1"><Badge tone={outcomeTone(outcome)} dot>{outcome}</Badge></div>
          </div>
          <div>
            <div className="text-11 muted">Connected</div>
            <div className="text-12 mt-1">{log?.connected || job.connected || "—"}</div>
          </div>
          <div>
            <div className="text-11 muted">Duration</div>
            <div className="mono text-12 mt-1">{duration}</div>
          </div>
          <div>
            <div className="text-11 muted">Cost</div>
            <div className="mono text-12 mt-1" style={{ color: "var(--a)" }}>{cost}</div>
          </div>
          <div>
            <div className="text-11 muted">Balance</div>
            <div className="mono text-12 mt-1">{balance}</div>
          </div>
          <div>
            <div className="text-11 muted">Expected payment</div>
            <div className="mono text-12 mt-1">{expectedPayment}</div>
          </div>
          <div>
            <div className="text-11 muted">Attempts</div>
            <div className="mono text-12 mt-1">{job.attempts || 0} / {job.maxAttempts || 0}</div>
          </div>
          <div>
            <div className="text-11 muted">Next retry</div>
            <div className="mono text-12 mt-1">{job.nextRetryAt ? fmtDate(job.nextRetryAt) : "—"}</div>
          </div>
          {(log?.transferredTo || outcome === "transferred") && (
            <div style={{ gridColumn: "1 / -1" }}>
              <div className="text-11 muted">Transferred to</div>
              <div className="mono text-12 mt-1" style={{ color: "var(--info, #3b82f6)" }}>{log?.transferredTo || "—"}</div>
            </div>
          )}
        </div>

        {(log?.recordingPath || job.recordingPath) && (
          <>
            <div className="hr" />
            <div className="card-title mb-2">Recording</div>
            {log?.id ? <RecordingPlayer logId={log.id} /> : <div className="muted text-11">Recording is attached to the session log.</div>}
          </>
        )}

        {transcriptTurns.length > 0 && (
          <>
            <div className="hr" />
            <div className="card-title mb-2">Transcript</div>
            <div style={{ maxHeight: 320, overflowY: "auto", padding: "4px 2px" }}>
              {transcriptTurns.map((t, i) => {
                const lines = [];
                if (String(t.user || "").trim()) lines.push({ who: "user", text: t.user, ts: t.createdAt });
                if (String(t.assistant || "").trim()) lines.push({ who: "assistant", text: t.assistant, ts: t.createdAt });
                return lines.map((line, j) => {
                  const isSystemEvent = /^\[TRANSFER (INITIATED|FAILED)/.test(line.text);
                  if (isSystemEvent) {
                    const isFail = line.text.startsWith("[TRANSFER FAILED");
                    return (
                      <div key={`${i}-${j}`} style={{ textAlign: "center", margin: "8px 0" }}>
                        <span style={{
                          display: "inline-block",
                          fontSize: 11,
                          padding: "3px 10px",
                          borderRadius: 999,
                          background: isFail ? "var(--err-bg, #fee2e2)" : "var(--info-bg, #dbeafe)",
                          color: isFail ? "var(--err, #dc2626)" : "var(--info, #1d4ed8)",
                          fontFamily: "monospace",
                        }}>{line.text}</span>
                        <div className="muted text-11 mt-1">{line.ts ? fmtDate(line.ts) : ""}</div>
                      </div>
                    );
                  }
                  return (
                    <div key={`${i}-${j}`} className={`bubble ${line.who}`} style={{ maxWidth: "100%" }}>
                      <div className="bubble-avatar">
                        {line.who === "user" ? customerName.slice(0, 2).toUpperCase() : "AI"}
                      </div>
                      <div>
                        <div className="bubble-body">{humanizeTranscript(line.text)}</div>
                        <div className="bubble-meta">{line.ts ? fmtDate(line.ts) : ""}</div>
                      </div>
                    </div>
                  );
                });
              })}
            </div>
          </>
        )}

        {!log && (
          <>
            <div className="hr" />
            <div className="muted text-11">This customer row has not produced a full VoxPro session log yet, so transcript, recording, and exact billing details are not available yet.</div>
          </>
        )}
      </div>
    </div>
  );
};

// ---------- Call logs ----------
const OUTCOME_OPTIONS = [
  "customer-ended-call",
  "silence-timed-out",
  "voicemail",
  "customer-busy",
  "customer-did-not-answer",
  "call.in-progress.error-sip-outbound-call-failed-to-connect",
  "call.error-processing-failed",
];

function prettyStructuredFieldName(key = "") {
  return String(key || "")
    .replace(/_/g, " ")
    .replace(/\b\w/g, ch => ch.toUpperCase());
}

function formatStructuredValue(value, key = "") {
  if (value == null || value === "") return "—";
  if (typeof value === "boolean") return value ? "Yes" : "No";
  if (typeof value === "number") {
    if (/(amount|balance|price|revenue|cost)/i.test(key)) return `RM ${Number(value).toFixed(2)}`;
    return String(value);
  }
  return String(value);
}

function inferCallDirection(log = {}) {
  const source = String(log.source || "").trim().toLowerCase();
  const useCase = String(log.useCase || "").trim().toLowerCase();

  if (source.includes("blast") || source.includes("outbound") || source.includes("live-sip")) {
    return "outbound";
  }
  if (source.includes("inbound") || source.includes("pstn-in")) {
    return "inbound";
  }
  if (log.campaignId || log.campaignName) {
    return "outbound";
  }
  if (useCase === "zaraangkasa") {
    return "inbound";
  }
  if (String(log.contactId || "").startsWith("user_")) {
    return "web";
  }
  return "unknown";
}

function callDirectionTone(direction = "") {
  switch (direction) {
    case "inbound": return "ok";
    case "outbound": return "accent";
    case "web": return "info";
    default: return "default";
  }
}

function labelCallDirection(direction = "") {
  switch (direction) {
    case "inbound": return "Inbound";
    case "outbound": return "Outbound";
    case "web": return "Web";
    default: return "Unknown";
  }
}

function summarizeSource(log = {}) {
  const source = String(log.source || "").trim();
  if (!source) return "Unspecified";
  const normalized = source.toLowerCase();
  if (normalized.includes("blast")) return "Live SIP blast";
  if (normalized.includes("live-sip")) return "Live SIP";
  if (normalized.includes("inbound")) return "SIP inbound";
  if (normalized.includes("interact")) return "Browser interact";
  return source.replace(/[-_]+/g, " ");
}

const LogsView = ({ mode, callLogs = [], assistants = [], clients = [] }) => {
  const [selected, setSelected] = React.useState(null);
  const [selectedLog, setSelectedLog] = React.useState(null);
  const [search, setSearch] = React.useState("");
  const [filterAssistant, setFilterAssistant] = React.useState("");
  const [filterIndustry, setFilterIndustry] = React.useState("");
  const [filterClient, setFilterClient] = React.useState("");
  const [filterCampaign, setFilterCampaign] = React.useState("");
  const [filterCampaignType, setFilterCampaignType] = React.useState("");
  const [filterOutcome, setFilterOutcome] = React.useState("");
  const [filterDirection, setFilterDirection] = React.useState("");
  const [filterDateFrom, setFilterDateFrom] = React.useState("");
  const [filterDateTo, setFilterDateTo] = React.useState("");

  const log = selected ? selectedLog || callLogs.find(l => l.id === selected) || null : null;

  React.useEffect(() => {
    let cancelled = false;

    if (!selected) {
      setSelectedLog(null);
      return undefined;
    }

    // Only fall back to the list item if we don't already have this log loaded.
    // When callLogs refreshes (parent polling), keeping the existing selectedLog
    // prevents the RecordingPlayer from unmounting mid-play.
    setSelectedLog(prev => {
      if (prev?.id === selected) return prev;
      return callLogs.find(l => l.id === selected) || null;
    });

    (async () => {
      try {
        const fresh = await voxApi(`/logs/${selected}`);
        if (!cancelled && fresh?.id === selected) {
          setSelectedLog(fresh);
        }
      } catch (_) {
        if (!cancelled) {
          setSelectedLog(prev => prev?.id === selected ? prev : (callLogs.find(l => l.id === selected) || null));
        }
      }
    })();

    return () => {
      cancelled = true;
    };
  }, [selected, callLogs]);

  // Unique campaign names from logs
  const campaignNames = React.useMemo(() => {
    const names = new Set(callLogs.map(l => l.campaignName).filter(Boolean));
    return [...names].sort();
  }, [callLogs]);

  // Unique campaign types from logs
  const campaignTypes = React.useMemo(() => {
    const types = new Set(callLogs.map(l => l.campaignType).filter(Boolean));
    return [...types].sort();
  }, [callLogs]);

  // Unique assistant names from logs + assistants prop
  const assistantNames = React.useMemo(() => {
    const names = new Set([
      ...callLogs.map(l => l.assistantName).filter(Boolean),
      ...assistants.map(a => a.name).filter(Boolean),
    ]);
    return [...names].sort();
  }, [callLogs, assistants]);

  const clientNames = React.useMemo(() => {
    const names = new Set([
      ...callLogs.map(l => l.clientName || l.customerName).filter(Boolean),
      ...clients.map(c => c.company || c.name).filter(Boolean),
    ]);
    return [...names].sort();
  }, [callLogs, clients]);

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

  const filtered = React.useMemo(() => {
    return callLogs.filter(l => {
      if (filterAssistant && l.assistantName !== filterAssistant) return false;
      if (filterIndustry && l.industry !== filterIndustry) return false;
      if (filterClient) {
        const clientLabel = l.clientName || l.customerName || "";
        if (clientLabel !== filterClient) return false;
      }
      if (filterDirection && inferCallDirection(l) !== filterDirection) return false;
      if (filterCampaign && l.campaignName !== filterCampaign) return false;
      if (filterCampaignType && l.campaignType !== filterCampaignType) return false;
      if (filterOutcome && (l.endedReason || l.outcome) !== filterOutcome) return false;
      if (filterDateFrom) {
        const d = new Date(l.startedAt || 0);
        if (d < new Date(filterDateFrom)) return false;
      }
      if (filterDateTo) {
        const d = new Date(l.startedAt || 0);
        const to = new Date(filterDateTo);
        to.setHours(23, 59, 59, 999);
        if (d > to) return false;
      }
      if (search) {
        const q = search.toLowerCase();
        const haystack = [l.customerName, l.contact, l.assistantName, l.campaignName, l.endedReason, l.source, l.useCase]
          .filter(Boolean).join(" ").toLowerCase();
        if (!haystack.includes(q)) return false;
      }
      return true;
    });
  }, [callLogs, filterAssistant, filterIndustry, filterClient, filterDirection, filterCampaign, filterCampaignType, filterOutcome, filterDateFrom, filterDateTo, search]);

  const activeAssistant = React.useMemo(() => {
    if (filterAssistant) return assistants.find(a => a.name === filterAssistant) || null;
    if (log?.assistantId) return assistants.find(a => a.id === log.assistantId) || null;
    const ids = [...new Set(filtered.map(l => l.assistantId).filter(Boolean))];
    return ids.length === 1 ? assistants.find(a => a.id === ids[0]) || null : null;
  }, [assistants, filterAssistant, filtered, log]);

  const activeLogView = activeAssistant?.logView || log?.assistantLogView || null;
  const hiddenFieldSet = React.useMemo(() => new Set(activeLogView?.hiddenFields || []), [activeLogView]);
  const pinnedFields = React.useMemo(() => (activeLogView?.pinnedFields || []).filter(Boolean), [activeLogView]);

  const structuredColumns = React.useMemo(() => {
    const counts = new Map();
    filtered.forEach(l => {
      if (!l.structuredOutput || typeof l.structuredOutput !== "object") return;
      Object.keys(l.structuredOutput).forEach(key => {
        if (hiddenFieldSet.has(key)) return;
        if (["balance", "expected_payment_date"].includes(key) && (activeLogView?.displayMode || "dynamic") !== "focused") {
          // allow these fields to be managed like all others rather than hard-coding them
        }
        counts.set(key, (counts.get(key) || 0) + 1);
      });
    });
    const dynamicKeys = [...counts.entries()]
      .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
      .map(([key]) => key)
      .filter(key => !pinnedFields.includes(key));
    const ordered = [...pinnedFields, ...dynamicKeys];
    if ((activeLogView?.displayMode || "dynamic") === "focused") {
      return ordered.slice(0, Math.max(1, Math.min(4, pinnedFields.length || 3)));
    }
    return ordered.slice(0, 4);
  }, [filtered, hiddenFieldSet, pinnedFields, activeLogView]);

  const analytics = React.useMemo(() => {
    const total = filtered.length;
    const withStructured = filtered.filter(l => l.structuredOutput && Object.keys(l.structuredOutput).length > 0).length;
    const totalRevenue = filtered.reduce((sum, l) => sum + Number(l.sellingMyr || 0), 0);
    const totalMargin = filtered.reduce((sum, l) => sum + Number(l.grossProfit ?? l.breakdown?.grossProfit ?? 0), 0);
    const totalDuration = filtered.reduce((sum, l) => sum + Number(l.durationSeconds || 0), 0);
    const directionCounts = filtered.reduce((acc, l) => {
      const key = inferCallDirection(l);
      acc[key] = (acc[key] || 0) + 1;
      return acc;
    }, {});
    const inboundCount = directionCounts.inbound || 0;
    const outboundCount = directionCounts.outbound || 0;
    const recordings = filtered.filter(l => l.recordingPath).length;
    const withTranscript = filtered.filter(l => Array.isArray(l.turns) && l.turns.length > 0).length;
    const topOutcomes = Object.entries(filtered.reduce((acc, l) => {
      const key = l.endedReason || l.outcome || "unknown";
      acc[key] = (acc[key] || 0) + 1;
      return acc;
    }, {})).sort((a, b) => b[1] - a[1]);
    const topIndustries = Object.entries(filtered.reduce((acc, l) => {
      const key = l.industry || "Unassigned";
      acc[key] = (acc[key] || 0) + 1;
      return acc;
    }, {})).sort((a, b) => b[1] - a[1]);
    return {
      total,
      withStructured,
      totalRevenue,
      totalMargin,
      avgDuration: total ? Math.round(totalDuration / total) : 0,
      topOutcome: topOutcomes[0]?.[0] || "—",
      topIndustry: topIndustries[0]?.[0] || "—",
      inboundCount,
      outboundCount,
      recordings,
      withTranscript,
    };
  }, [filtered]);
  const showInitiatorUserColumn = mode === "client";
  const isClientMode = mode === "client";
  const showSourceColumn = !isClientMode;
  const showUseCaseColumn = !isClientMode;
  const showDirectionColumn = true;

  function clearFilters() {
    setSearch(""); setFilterAssistant(""); setFilterIndustry(""); setFilterClient(""); setFilterCampaign("");
    setFilterCampaignType(""); setFilterOutcome(""); setFilterDirection(""); setFilterDateFrom(""); setFilterDateTo("");
  }

  function exportCSV() {
    if (!filtered.length) return;
    const baseHeaders = ["id","startedAt","direction","source","useCase","industry","clientName","organizationName","contact","customerName","assistantName","campaignName","endedReason","durationSeconds","pricingMode","clientMarkupMyrPerMin","sellingMyr"];
    if (showInitiatorUserColumn) {
      baseHeaders.push("initiatedByUserName", "initiatedByUserEmail");
    }
    // Collect campaign row input keys (CSV columns like akaun_number)
    const crKeySet = new Set();
    filtered.forEach(l => {
      if (l.campaignRow && typeof l.campaignRow === "object") {
        Object.keys(l.campaignRow).forEach(k => {
          if (!baseHeaders.includes(k)) crKeySet.add(k);
        });
      }
    });
    // Collect extra structured output keys (AI-extracted fields like balance, expected_payment_date)
    const soKeySet = new Set();
    filtered.forEach(l => {
      if (l.structuredOutput && typeof l.structuredOutput === "object") {
        Object.keys(l.structuredOutput).forEach(k => {
          if (!baseHeaders.includes(k) && !crKeySet.has(k)) soKeySet.add(k);
        });
      }
    });
    const headers = [...baseHeaders, ...Array.from(crKeySet), ...Array.from(soKeySet), "transcript", "recording_url"];

    function buildTranscript(l) {
      if (Array.isArray(l.turns) && l.turns.length) {
        return l.turns.flatMap(t => [
          t.user ? `Customer: ${t.user}` : null,
          t.assistant ? `Agent: ${t.assistant}` : null,
        ]).filter(Boolean).join("\n");
      }
      return l.transcriptText || "";
    }

    // Proper RFC-4180 CSV escaping: wrap in quotes, escape internal quotes as ""
    function csvCell(value) {
      const s = String(value ?? "");
      if (s.includes('"') || s.includes(",") || s.includes("\n") || s.includes("\r")) {
        return '"' + s.replace(/"/g, '""') + '"';
      }
      return s;
    }

    function cellValue(l, k) {
      if (k === "direction") return labelCallDirection(inferCallDirection(l));
      if (k === "source") return summarizeSource(l);
      if (k === "transcript") return buildTranscript(l);
      if (k === "recording_url") return l.recordingPath ? `${window.location.origin}/logs/${l.id}/recording` : "";
      // top-level first, then campaignRow, then structuredOutput
      const topVal = l[k];
      if (topVal != null && topVal !== "") return topVal;
      if (l.campaignRow?.[k] != null) return l.campaignRow[k];
      return l.structuredOutput?.[k] ?? "";
    }

    const rows = filtered.map(l => headers.map(k => csvCell(cellValue(l, k))).join(","));
    const csv = [headers.join(","), ...rows].join("\n");
    const a = document.createElement("a");
    a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
    a.download = `call-logs-${Date.now()}.csv`;
    a.click();
  }

  const hasFilters = search || filterAssistant || filterIndustry || filterClient || filterDirection || filterCampaign || filterCampaignType || filterOutcome || filterDateFrom || filterDateTo;

  return (
    <div style={{ display: "flex", alignItems: "flex-start", gap: 0, minWidth: 0 }}>
      {/* ── Left: main content ── */}
      <div style={{ flex: 1, minWidth: 0, overflow: "hidden" }}>
      {/* Page header */}
      <div className="ph">
        <div>
          <h1 className="ph-title">Call logs</h1>
          <div className="ph-subtitle">
            {isClientMode
              ? "Inbound-friendly workspace for recordings, transcripts, and customer outcomes."
              : "Developer workspace for inbound and outbound call traces, routing context, and billing detail."}
            {filtered.length !== callLogs.length && (
              <span className="muted"> · showing {filtered.length.toLocaleString()} of {callLogs.length.toLocaleString()}</span>
            )}
          </div>
        </div>
        <div className="ph-actions">
          {hasFilters && <Btn variant="ghost" size="sm" onClick={clearFilters}>Clear filters</Btn>}
          <Btn variant="ghost" size="sm" icon="download" onClick={exportCSV}>Export CSV</Btn>
        </div>
      </div>

      {/* Filter bar */}
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 12, alignItems: "center" }}>
        <Input
          placeholder={isClientMode ? "Search caller, assistant, outcome…" : "Search caller, source, use case…"}
          value={search}
          onChange={e => setSearch(e.target.value)}
          style={{ width: 200 }}
        />
        <Select value={filterDirection} onChange={e => setFilterDirection(e.target.value)} style={{ width: 140 }}>
          <option value="">All directions</option>
          <option value="inbound">Inbound</option>
          <option value="outbound">Outbound</option>
          <option value="web">Web</option>
          <option value="unknown">Unknown</option>
        </Select>
        <Select value={filterAssistant} onChange={e => setFilterAssistant(e.target.value)} style={{ width: 160 }}>
          <option value="">All assistants</option>
          {assistantNames.map(n => <option key={n} value={n}>{n}</option>)}
        </Select>
        {industries.length > 0 && (
          <Select value={filterIndustry} onChange={e => setFilterIndustry(e.target.value)} style={{ width: 160 }}>
            <option value="">All industries</option>
            {industries.map(n => <option key={n} value={n}>{n}</option>)}
          </Select>
        )}
        {clientNames.length > 0 && (
          <Select value={filterClient} onChange={e => setFilterClient(e.target.value)} style={{ width: 180 }}>
            <option value="">{mode === "client" ? "Your company" : "All clients"}</option>
            {clientNames.map(n => <option key={n} value={n}>{n}</option>)}
          </Select>
        )}
        <Select value={filterCampaign} onChange={e => setFilterCampaign(e.target.value)} style={{ width: 160 }}>
          <option value="">All campaigns</option>
          {campaignNames.map(n => <option key={n} value={n}>{n}</option>)}
        </Select>
        {campaignTypes.length > 0 && (
          <Select value={filterCampaignType} onChange={e => setFilterCampaignType(e.target.value)} style={{ width: 150 }}>
            <option value="">All types</option>
            {campaignTypes.map(t => <option key={t} value={t}>{t}</option>)}
          </Select>
        )}
        <Select value={filterOutcome} onChange={e => setFilterOutcome(e.target.value)} style={{ width: 200 }}>
          <option value="">All outcomes</option>
          {OUTCOME_OPTIONS.map(o => <option key={o} value={o}>{o}</option>)}
        </Select>
        <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
          <span className="muted text-11">From</span>
          <input type="date" value={filterDateFrom} onChange={e => setFilterDateFrom(e.target.value)}
            style={{ background: "var(--c-surface-2)", border: "1px solid var(--c-line-2)", borderRadius: 6, color: "var(--c-text)", fontSize: 12, padding: "5px 8px" }} />
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
          <span className="muted text-11">To</span>
          <input type="date" value={filterDateTo} onChange={e => setFilterDateTo(e.target.value)}
            style={{ background: "var(--c-surface-2)", border: "1px solid var(--c-line-2)", borderRadius: 6, color: "var(--c-text)", fontSize: 12, padding: "5px 8px" }} />
        </div>
      </div>

      <div className="g-4" style={{ marginBottom: 12 }}>
        <StatTile k={{ label: isClientMode ? "Calls in view" : "Calls traced", value: analytics.total.toLocaleString(), delta: "", spark: [3,4,5,4,6,7,8,7,9,8,10,11,10,12,11,13,14,13,12,14] }} />
        <StatTile k={{ label: "Inbound", value: analytics.inboundCount.toLocaleString(), delta: `${analytics.outboundCount.toLocaleString()} outbound`, spark: [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,8,9,9,10] }} />
        <StatTile k={{ label: isClientMode ? "Recordings ready" : "Transcript coverage", value: isClientMode ? analytics.recordings.toLocaleString() : (analytics.total ? `${Math.round((analytics.withTranscript / analytics.total) * 100)}%` : "0%"), delta: isClientMode ? `${analytics.withTranscript.toLocaleString()} transcripts` : `${analytics.recordings.toLocaleString()} recordings`, spark: [1,2,1,2,2,3,2,3,4,3,4,4,5,4,5,5,6,5,6,6] }} />
        <StatTile k={{ label: isClientMode ? "Avg duration" : "Revenue", value: isClientMode ? fmtDuration(analytics.avgDuration) : fmtRM(analytics.totalRevenue), delta: isClientMode ? analytics.topOutcome : (analytics.topIndustry === "—" ? "" : analytics.topIndustry), spark: [2,3,3,4,5,5,6,7,7,8,8,9,10,10,11,11,12,13,13,14] }} />
      </div>

      {(activeAssistant || filterIndustry || filterClient || analytics.inboundCount || !isClientMode) && (
        <div className="row gap-2" style={{ flexWrap: "wrap", marginBottom: 12 }}>
          {activeAssistant && <Badge tone="accent">{activeAssistant.name} log view</Badge>}
          {filterIndustry && <Badge tone="default">Industry: {filterIndustry}</Badge>}
          {filterClient && <Badge tone="default">Client: {filterClient}</Badge>}
          {analytics.inboundCount > 0 && <Badge tone="ok">{analytics.inboundCount} inbound call{analytics.inboundCount === 1 ? "" : "s"}</Badge>}
          {analytics.topOutcome !== "—" && <Badge tone="info">Top outcome: {analytics.topOutcome}</Badge>}
          {!isClientMode && analytics.totalMargin !== 0 && <Badge tone={analytics.totalMargin >= 0 ? "ok" : "err"}>Margin: {fmtRM(analytics.totalMargin)}</Badge>}
        </div>
      )}

      {callLogs.length === 0 ? (
        <div className="empty-state">
          <div className="empty-state-title">No call logs yet</div>
          <div>Make your first outbound call to see logs here.</div>
        </div>
      ) : (
        <Card pad="none">
          <div className="tbl-wrap" style={{ overflowX: "auto" }}>
            <table className="tbl tbl-logs" style={{ minWidth: showInitiatorUserColumn ? 1700 : 1600 }}>
              <colgroup>
                <col style={{ width: 128 }} />{/* Time */}
                {showDirectionColumn && <col style={{ width: 96 }} />}{/* Direction */}
                <col style={{ width: 180 }} />{/* Contact */}
                <col style={{ width: 148 }} />{/* Assistant */}
                <col style={{ width: 160 }} />{/* Client */}
                {showSourceColumn && <col style={{ width: 144 }} />}{/* Source */}
                {showUseCaseColumn && <col style={{ width: 140 }} />}{/* Use case */}
                <col style={{ width: 132 }} />{/* Industry */}
                {showInitiatorUserColumn && <col style={{ width: 150 }} />}{/* User */}
                <col style={{ width: 160 }} />{/* Outcome */}
                {structuredColumns.map(key => <col key={key} style={{ width: activeLogView?.tableDensity === "compact" ? 132 : 156 }} />)}
                <col style={{ width: 112 }} /> {/* Duration */}
                <col style={{ width: 112 }} /> {/* Cost */}
                <col style={{ width: 44 }} /> {/* Recording dot */}
              </colgroup>
              <thead>
                <tr>
                  <th>Time</th>
                  {showDirectionColumn && <th>Direction</th>}
                  <th>{isClientMode ? "Caller" : "Contact"}</th>
                  <th>Assistant</th>
                  <th>Client</th>
                  {showSourceColumn && <th>Source</th>}
                  {showUseCaseColumn && <th>Use case</th>}
                  <th>Industry</th>
                  {showInitiatorUserColumn && <th>User</th>}
                  <th>Outcome</th>
                  {structuredColumns.map(key => <th key={key}>{prettyStructuredFieldName(key)}</th>)}
                  <th>Duration</th>
                  <th className="right">Cost</th>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                {filtered.map(l => {
                  const name = l.customerName || l.contactName || l.contact || "Unknown";
                  const rawNum = l.contactId && !/^user_/.test(l.contactId) ? l.contactId : (l.contact || l.to || "");
                  const num = rawNum || "—";
                  const asst = l.assistantName || l.assistant || "—";
                  const industry = l.industry || "—";
                  const clientLabel = l.clientName || l.customerName || "—";
                  const initiatorLabel = l.initiatedByUserName || l.initiatedByUserEmail || "—";
                  const outcome = l.endedReason || l.outcome || "completed";
                  const direction = inferCallDirection(l);
                  const directionLabel = labelCallDirection(direction);
                  const sourceLabel = summarizeSource(l);
                  const dur = l.durationSeconds != null ? fmtDuration(l.durationSeconds) : (l.duration || "—");
                  const cost = l.sellingMyr != null ? fmtRM(l.sellingMyr) : (l.cost || "—");
                  const isSelected = l.id === selected;
                  return (
                    <tr
                      key={l.id}
                      onClick={() => setSelected(isSelected ? null : l.id)}
                      style={{ cursor: "pointer", background: isSelected ? "rgba(141,139,255,0.07)" : "" }}
                    >
                      <td className="mono" style={{ fontSize: 11 }}>{l.startedAt ? fmtDate(l.startedAt) : (l.at || "—")}</td>
                      {showDirectionColumn && <td><Badge tone={callDirectionTone(direction)} dot>{directionLabel}</Badge></td>}
                      <td>
                        <div className="strong">{name}</div>
                        <div className="mono" style={{ fontSize: 10.5, color: "var(--c-text-4)" }}>{num}</div>
                      </td>
                      <td className="text-12" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{asst}</td>
                      <td className="text-12 muted" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={clientLabel}>{clientLabel}</td>
                      {showSourceColumn && <td className="text-12 muted" title={sourceLabel}>{sourceLabel}</td>}
                      {showUseCaseColumn && <td className="text-12 muted">{l.useCase || "—"}</td>}
                      <td className="text-12 muted" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{industry}</td>
                      {showInitiatorUserColumn && (
                        <td className="text-12 muted" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={initiatorLabel}>
                          {initiatorLabel}
                        </td>
                      )}
                      <td><Badge tone={outcomeTone(outcome)} dot>{outcome}</Badge></td>
                      {structuredColumns.map(key => {
                        const value = l[key] ?? l.structuredOutput?.[key];
                        return (
                          <td key={`${l.id}-${key}`} className="mono text-12" style={{ color: value != null && value !== "" ? "var(--c-text)" : "var(--c-text-4)" }}>
                            {formatStructuredValue(value, key)}
                          </td>
                        );
                      })}
                      <td className="mono text-12">{dur}</td>
                      <td className="mono right strong">{cost}</td>
                      <td style={{ width: 24 }}>
                        {l.recordingPath && <span title="Has recording" style={{ color: "var(--a)", fontSize: 12 }}>◉</span>}
                      </td>
                    </tr>
                  );
                })}
                {filtered.length === 0 && (
                  <tr><td colSpan={(showInitiatorUserColumn ? 11 : 10) + structuredColumns.length + (showSourceColumn ? 1 : 0) + (showUseCaseColumn ? 1 : 0)} className="empty-state">No results match the current filters</td></tr>
                )}
              </tbody>
            </table>
          </div>
        </Card>
      )}

      </div>{/* end left main content */}

      {/* ── Right: session detail — scales with viewport ── */}
      <SessionDetail log={log} mode={mode} onClose={() => setSelected(null)} />
    </div>
  );
};

// ---------- Blast Campaigns ----------
const CAMPAIGN_TYPES = [
  { value: "collections",       label: "Debt Collections" },
  { value: "customer_service",  label: "Customer Service" },
  { value: "lead_generation",   label: "Lead Generation" },
  { value: "appointment",       label: "Appointment Reminder" },
  { value: "survey",            label: "Survey / Feedback" },
  { value: "retention",         label: "Retention / Winback" },
  { value: "general",           label: "General" },
];

const RETRY_ATTEMPT_OPTIONS = [
  { value: 1, label: "1 - no retry" },
  { value: 2, label: "2 - retry once" },
  { value: 3, label: "3 - retry twice" },
  { value: 4, label: "4 - retry three times" },
  { value: 5, label: "5 - retry four times" },
];

const RETRY_INTERVAL_OPTIONS = [
  { value: 5, label: "5 minutes" },
  { value: 15, label: "15 minutes" },
  { value: 30, label: "30 minutes" },
  { value: 60, label: "1 hour" },
  { value: 180, label: "3 hours" },
  { value: 1440, label: "24 hours" },
];

const BLAST_STEPS = [
  { key: "campaign", label: "Campaign" },
  { key: "ai", label: "AI Setup" },
  { key: "contacts", label: "Contacts" },
  { key: "launch", label: "Launch" },
];

function getCsvTemplateColumns(assistant) {
  const required = ["customer_name", "number"];
  const cols = required.map(n => ({ name: n, type: "required", hint: "required" }));
  const seen = new Set(required);
  const promptVars = [];
  if (assistant.systemPrompt) {
    for (const m of String(assistant.systemPrompt).matchAll(/\{\{([^}]+)\}\}/g)) {
      const v = m[1].trim();
      if (!seen.has(v) && !v.startsWith("system.")) { seen.add(v); promptVars.push(v); }
    }
  }
  const explicit = Array.isArray(assistant.csvColumns) ? assistant.csvColumns : [];
  for (const v of explicit) {
    if (!seen.has(v)) { seen.add(v); cols.push({ name: v, type: "custom", hint: "custom" }); }
  }
  for (const v of promptVars) {
    cols.push({ name: v, type: "prompt", hint: "from prompt" });
  }
  return cols;
}

function downloadCsvTemplate(assistant) {
  const cols = getCsvTemplateColumns(assistant);
  const header = cols.map(c => c.name).join(",");
  const blob = new Blob([header + "\n"], { type: "text/csv" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `${(assistant.name || "assistant").replace(/[^a-z0-9]/gi, "_")}_template.csv`;
  document.body.appendChild(a); a.click(); document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

const BlastView = ({ assistants = [], session = null }) => {
  const [sipCfg, setSipCfg] = React.useState(null);
  const [availableTrunks, setAvailableTrunks] = React.useState([]);
  const [blastSipTrunkId, setBlastSipTrunkId] = React.useState("");
  const [csvFile, setCsvFile] = React.useState(null);
  const [csvHeaders, setCsvHeaders] = React.useState([]);
  const [csvRows, setCsvRows] = React.useState([]);
  const [destColumn, setDestColumn] = React.useState("");
  const [nameColumn, setNameColumn] = React.useState("");
  const [selAsst, setSelAsst] = React.useState("");
  const [campaignName, setCampaignName] = React.useState("");
  const [campaignType, setCampaignType] = React.useState("general");
  const [currentStep, setCurrentStep] = React.useState(0);
  const [concurrent, setConcurrent] = React.useState(3);
  const [maxAttempts, setMaxAttempts] = React.useState(3);
  const [retryIntervalMinutes, setRetryIntervalMinutes] = React.useState(15);
  const [blastCallerId, setBlastCallerId] = React.useState("");
  const [launching, setLaunching] = React.useState(false);
  const [launchResult, setLaunchResult] = React.useState(null);
  const [campaigns, setCampaigns] = React.useState([]);
  const [campaignsLoading, setCampaignsLoading] = React.useState(false);
  const [selectedCampaignId, setSelectedCampaignId] = React.useState("");
  const [blastScreen, setBlastScreen] = React.useState("launch");
  const [campaignDetail, setCampaignDetail] = React.useState(null);
  const [histPage, setHistPage] = React.useState(0);
  const [detailLoading, setDetailLoading] = React.useState(false);
  const [selectedCampaignJobKey, setSelectedCampaignJobKey] = React.useState("");
  const [selectedCampaignLog, setSelectedCampaignLog] = React.useState(null);
  const [reblasting, setReblasting] = React.useState(false);
  const [reblastResult, setReblastResult] = React.useState(null);
  const [stopping, setStopping] = React.useState(false);
  const [timerNow, setTimerNow] = React.useState(Date.now());

  React.useEffect(() => {
    const t = setInterval(() => setTimerNow(Date.now()), 1000);
    return () => clearInterval(t);
  }, []);

  const PHONE_HINTS = ["phone", "number", "mobile", "tel", "contact", "no", "nombor", "telefon"];
  const NAME_HINTS  = ["name", "nama", "customer", "client", "person", "full"];
  const EMPTY_STATS = React.useMemo(() => ({
    totalTargets: 0,
    called: 0,
    pending: 0,
    active: 0,
    completed: 0,
    failed: 0,
    requestedConcurrency: 0,
  }), []);

  const loadCampaigns = React.useCallback(async ({ silent = false } = {}) => {
    if (!silent) setCampaignsLoading(true);
    try {
      const response = await voxApi("/outbound/live-sip-blast/campaigns");
      const list = Array.isArray(response?.campaigns) ? response.campaigns : [];
      const ordered = [...list].sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0));
      setCampaigns(ordered);
      setSelectedCampaignId(current => {
        if (current && ordered.some(item => item.id === current)) return current;
        return ordered[0]?.id || "";
      });
    } catch (e) {
      if (!silent) setLaunchResult({ ok: false, msg: e.message || "Failed to load campaigns", errors: [] });
    } finally {
      if (!silent) setCampaignsLoading(false);
    }
  }, []);

  const loadCampaignDetail = React.useCallback(async (campaignId, { silent = false } = {}) => {
    if (!campaignId) {
      setCampaignDetail(null);
      return;
    }
    if (!silent) setDetailLoading(true);
    try {
      const response = await voxApi(`/outbound/live-sip-blast/campaigns/${campaignId}`);
      setCampaignDetail(response?.campaign || null);
    } catch (e) {
      if (!silent) setLaunchResult({ ok: false, msg: e.message || "Failed to load campaign detail", errors: [] });
    } finally {
      if (!silent) setDetailLoading(false);
    }
  }, []);

  async function onFileChange(file) {
    setCsvFile(file);
    setCsvHeaders([]); setCsvRows([]); setDestColumn(""); setLaunchResult(null);
    if (!file) return;
    const text = await file.text();
    const lines = text.trim().split(/\r?\n/);
    const headers = lines[0].split(",").map(h => h.trim().replace(/^"|"$/g, ""));
    const rows = lines.slice(1).filter(Boolean).map(line => {
      const cols = line.split(",").map(c => c.trim().replace(/^"|"$/g, ""));
      const row = {};
      headers.forEach((h, j) => { row[h] = cols[j] || ""; });
      return row;
    });
    setCsvHeaders(headers);
    setCsvRows(rows);
    const autoCol = headers.find(h => PHONE_HINTS.some(hint => h.toLowerCase().includes(hint))) || headers[0] || "";
    setDestColumn(autoCol);
    const autoName = headers.find(h => NAME_HINTS.some(hint => h.toLowerCase().includes(hint))) || "";
    setNameColumn(autoName);
  }

  async function launchCampaign() {
    if (!csvFile || !selAsst || !destColumn || !campaignName.trim()) return;
    setLaunching(true); setLaunchResult(null);
    try {
      const assistant = assistants.find(a => a.id === selAsst);
      if (!assistant) throw new Error("Selected assistant not found");
      const r = await voxApi("/outbound/live-sip-blast", {
        method: "POST",
        body: JSON.stringify({
          rows: csvRows,
          destinationColumn: destColumn,
          nameColumn: nameColumn || undefined,
          campaignName: campaignName.trim(),
          campaignType,
          from: blastCallerId || undefined,
          sipTrunkId: blastSipTrunkId || undefined,
          assistant: {
            id: assistant.id,
            name: assistant.name,
            firstMessage: assistant.firstMessage || "",
            systemPrompt: assistant.systemPrompt || "",
            language: assistant.language || "en",
            voiceProvider: assistant.voiceProvider || "openai",
            voice: assistant.voice || "",
          },
          concurrency: concurrent,
          maxAttempts,
          retryIntervalMinutes,
        }),
      });
      const blastInfo = r.blast || r;
      const errors = blastInfo.errors || [];
      const launchedCampaignId = r.campaign?.id || blastInfo.id || "";
      if (launchedCampaignId) {
        setSelectedCampaignId(launchedCampaignId);
        setBlastScreen("report");
        setSelectedCampaignJobKey("");
      }
      setLaunchResult({ ok: blastInfo.failed === 0, msg: `Campaign launched: ${blastInfo.queued ?? csvRows.length} calls queued, ${blastInfo.failed ?? 0} failed.`, errors });
      await loadCampaigns({ silent: true });
      if (launchedCampaignId) {
        await loadCampaignDetail(launchedCampaignId, { silent: true });
      }
    } catch (e) { setLaunchResult({ ok: false, msg: e.message, errors: [] }); }
    finally { setLaunching(false); }
  }

  React.useEffect(() => {
    voxApiOptional("/sip/config", null).then((data) => {
      const config = normalizeSipViewConfig(data);
      if (!config) return;
      setSipCfg(config);
      setBlastCallerId((current) => current || config.callerId || config.username || "");
    });
    voxApiOptional("/sip/trunks", { trunks: [] }).then(d => {
      const all = d.trunks || [];
      const assignedIds = Array.isArray(session?.sipTrunkIds) ? session.sipTrunkIds : [];
      const filtered = assignedIds.length ? all.filter(t => assignedIds.includes(t.id)) : all;
      setAvailableTrunks(filtered);
    });
  }, []);

  React.useEffect(() => {
    loadCampaigns();
    const timer = setInterval(() => { loadCampaigns({ silent: true }); }, 15000);
    return () => clearInterval(timer);
  }, [loadCampaigns]);

  React.useEffect(() => {
    if (!selectedCampaignId) {
      setCampaignDetail(null);
      return undefined;
    }
    loadCampaignDetail(selectedCampaignId);
    const timer = setInterval(() => { loadCampaignDetail(selectedCampaignId, { silent: true }); }, 8000);
    return () => clearInterval(timer);
  }, [selectedCampaignId, loadCampaignDetail]);

  const canLaunch = !launching && csvFile && selAsst && destColumn && campaignName.trim() && blastSipTrunkId;
  const selectedCampaignSummary = React.useMemo(
    () => campaigns.find(item => item.id === selectedCampaignId) || null,
    [campaigns, selectedCampaignId],
  );
  const selectedCampaign = campaignDetail?.id === selectedCampaignId
    ? campaignDetail
    : selectedCampaignSummary;
  const selectedStats = selectedCampaign?.stats || EMPTY_STATS;
  const selectedJobs = Array.isArray(selectedCampaign?.jobs) ? selectedCampaign.jobs : [];
  const activeAgents = Array.isArray(selectedCampaign?.activeAgents) ? selectedCampaign.activeAgents : [];
  const activeCount = activeAgents.length;
  const selectedAssistant = assistants.find(a => a.id === selAsst) || null;
  const sipCallerOptions = React.useMemo(() => {
    const values = [sipCfg?.callerId, sipCfg?.username]
      .filter(Boolean)
      .map((value) => String(value).trim())
      .filter(Boolean);
    return Array.from(new Set(values));
  }, [sipCfg]);
  const selectedRetryLabel = RETRY_ATTEMPT_OPTIONS.find(option => option.value === maxAttempts)?.label || `${maxAttempts}`;
  const selectedRetryIntervalLabel = RETRY_INTERVAL_OPTIONS.find(option => option.value === retryIntervalMinutes)?.label || `${retryIntervalMinutes} minutes`;
  const stepCanContinue = [
    Boolean(campaignName.trim()),
    Boolean(selAsst && concurrent > 0),
    Boolean(csvFile && destColumn),
    canLaunch,
  ];
  const reportSummary = selectedCampaign?.reportSummary || {
    total: selectedStats.totalTargets || 0,
    completed: selectedStats.completed || 0,
    pending: selectedStats.pending || 0,
    retrying: 0,
    maxRetriesReached: 0,
    voicemail: 0,
    failed: selectedStats.failed || 0,
  };

  // Reblast: contacts that didn't pick up OR completed but gave no PTP
  const reblastCandidates = React.useMemo(() => selectedJobs.filter(job => {
    if (job.status === "failed") return true;
    if (job.status === "max_retries_reached") return true;
    if (job.status === "completed") return !job.expectedPaymentDate;
    return false;
  }), [selectedJobs]);

  const noPtpCount = React.useMemo(() => selectedJobs.filter(j =>
    j.status === "completed" && !j.expectedPaymentDate
  ).length, [selectedJobs]);

  const noAnswerCount = React.useMemo(() => selectedJobs.filter(j =>
    (j.status === "max_retries_reached" || j.status === "failed")
  ).length, [selectedJobs]);

  // Blast completion timer
  const blastTimerInfo = React.useMemo(() => {
    if (!selectedCampaign?.createdAt) return null;
    const total = reportSummary.total || 0;
    const doneStatuses = new Set(["completed", "max_retries_reached", "failed"]);
    const done = selectedJobs.filter(j => doneStatuses.has(j.status)).length;
    const remaining = Math.max(0, total - done);
    const startMs = new Date(selectedCampaign.createdAt).getTime();
    const isComplete = remaining === 0 && done > 0 && done >= total;
    // When complete, freeze elapsed at campaign updatedAt rather than ticking timerNow
    const endMs = isComplete ? new Date(selectedCampaign.updatedAt || timerNow).getTime() : timerNow;
    const elapsedMs = Math.max(0, endMs - startMs);
    const elapsedSec = elapsedMs / 1000;
    const rate = done > 0 ? done / elapsedSec : null;
    const etaSec = rate && remaining > 0 ? Math.ceil(remaining / rate) : null;
    const pct = total > 0 ? Math.round((done / total) * 100) : 0;
    return { total, done, remaining, elapsedSec, etaSec, pct, isComplete };
  }, [selectedCampaign, selectedJobs, reportSummary.total, timerNow]);

  function fmtSecs(s) {
    if (s == null || !Number.isFinite(s) || s < 0) return "—";
    const h = Math.floor(s / 3600);
    const m = Math.floor((s % 3600) / 60);
    const sec = Math.floor(s % 60);
    if (h > 0) return `${h}h ${m}m`;
    if (m > 0) return `${m}m ${sec}s`;
    return `${sec}s`;
  }

  async function launchReblast() {
    if (!reblastCandidates.length || reblasting) return;
    const campAssistant = assistants.find(a => a.id === selectedCampaign?.assistantId);
    if (!campAssistant) { setReblastResult({ ok: false, msg: "Assistant not found — cannot reblast" }); return; }
    setReblasting(true); setReblastResult(null);
    try {
      const destCol = selectedCampaign.destinationColumn || "number";
      const nameCol = selectedCampaign.nameColumn || "name";
      const rows = reblastCandidates.map(job => ({
        [destCol]: job.to,
        [nameCol]: job.customerName || "",
        ...(job.balance != null ? { balance: job.balance } : {}),
      }));
      const r = await voxApi("/outbound/live-sip-blast", {
        method: "POST",
        body: JSON.stringify({
          rows,
          destinationColumn: destCol,
          nameColumn: nameCol,
          campaignName: `${selectedCampaign.campaignName || "Campaign"} – Reblast`,
          campaignType: selectedCampaign.campaignType || "general",
          from: selectedCampaign.from || undefined,
          assistant: {
            id: campAssistant.id, name: campAssistant.name,
            firstMessage: campAssistant.firstMessage || "",
            systemPrompt: campAssistant.systemPrompt || "",
            language: campAssistant.language || "en",
            voiceProvider: campAssistant.voiceProvider || "openai",
            voice: campAssistant.voice || "",
          },
          concurrency: selectedCampaign.requestedConcurrency || 3,
          maxAttempts: selectedCampaign.maxAttempts || 3,
          retryIntervalMinutes: selectedCampaign.retryIntervalMinutes || 15,
        }),
      });
      const newId = r.campaign?.id || r.blast?.id;
      setReblastResult({ ok: true, msg: `Reblast launched: ${rows.length} contacts queued` });
      if (newId) {
        await loadCampaigns({ silent: true });
        setSelectedCampaignId(newId);
        await loadCampaignDetail(newId, { silent: true });
      }
    } catch (e) { setReblastResult({ ok: false, msg: e.message || "Reblast failed" }); }
    finally { setReblasting(false); }
  }

  async function stopCampaign() {
    if (!selectedCampaignId || stopping) return;
    if (!confirm("Stop this blast campaign? Pending retries will be cancelled.")) return;
    setStopping(true);
    try {
      await voxApi(`/outbound/live-sip-blast/campaigns/${selectedCampaignId}/stop`, { method: "POST" });
      await loadCampaignDetail(selectedCampaignId, { silent: true });
    } catch (e) {
      alert(e.message || "Failed to stop campaign");
    } finally {
      setStopping(false);
    }
  }

  const getCampaignJobKey = React.useCallback((job) => `${job.index}-${job.to}-${job.logId || job.roomName || ""}`, []);
  const selectedCampaignJob = React.useMemo(
    () => selectedJobs.find(job => getCampaignJobKey(job) === selectedCampaignJobKey) || null,
    [selectedJobs, selectedCampaignJobKey, getCampaignJobKey],
  );

  React.useEffect(() => {
    let cancelled = false;
    if (!selectedCampaignJob) {
      setSelectedCampaignLog(null);
      return undefined;
    }
    if (!selectedCampaignJob.logId) {
      setSelectedCampaignLog(null);
      return undefined;
    }

    // Keep the existing log while re-fetching so the RecordingPlayer
    // isn't unmounted mid-play during the 8s campaign detail poll.
    setSelectedCampaignLog(prev => (prev?.id === selectedCampaignJob.logId ? prev : null));

    (async () => {
      try {
        const fresh = await voxApi(`/logs/${selectedCampaignJob.logId}`);
        if (!cancelled && fresh?.id === selectedCampaignJob.logId) {
          setSelectedCampaignLog(fresh);
        }
      } catch (_) {
        if (!cancelled) {
          setSelectedCampaignLog(prev => (prev?.id === selectedCampaignJob.logId ? prev : null));
        }
      }
    })();

    return () => {
      cancelled = true;
    };
  }, [selectedCampaignJob]);

  return (
    <div>
      <div className="ph">
        <div>
          <h1 className="ph-title">Blast campaigns</h1>
          <div className="ph-subtitle">
            {blastScreen === "report"
              ? "Campaign report, contact outcomes, and customer-level detail."
              : "Outbound dialing at scale with concurrency, throttling, and retries."}
          </div>
        </div>
      </div>

      {blastScreen === "launch" && (
      <div className="mb-4">
        <Card title="Create blast" subtitle="Campaign setup, AI setup, contacts, and launch in one flow.">
          <div className="blast-stepper">
            {BLAST_STEPS.map((step, index) => {
              const state = index < currentStep ? "done" : (index === currentStep ? "active" : "idle");
              return (
                <button
                  key={step.key}
                  type="button"
                  className={`blast-step ${state}`}
                  onClick={() => {
                    if (index <= currentStep || stepCanContinue.slice(0, index).every(Boolean)) {
                      setCurrentStep(index);
                    }
                  }}
                >
                  <span className="blast-step-dot">{index < currentStep ? "OK" : index + 1}</span>
                  <span className="blast-step-label">{step.label}</span>
                </button>
              );
            })}
          </div>

          <div className="hr" />

          {currentStep === 0 && (
            <div className="blast-pane">
              <div className="text-11 muted mb-3" style={{ letterSpacing: ".08em", textTransform: "uppercase" }}>Campaign details</div>
              <div className="fld-row">
                <Field label="Campaign name" hint="Required">
                  <Input
                    placeholder="e.g. June Debt Recovery Batch 1"
                    value={campaignName}
                    onChange={e => setCampaignName(e.target.value)}
                  />
                </Field>
                <Field label="Campaign type">
                  <Select value={campaignType} onChange={e => setCampaignType(e.target.value)}>
                    {CAMPAIGN_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
                  </Select>
                </Field>
              </div>
              <div className="fld-row">
                <Field label="SIP number / Caller ID" hint="Outbound number used for this blast">
                  {sipCallerOptions.length ? (
                    <Select value={blastCallerId} onChange={e => setBlastCallerId(e.target.value)}>
                      {sipCallerOptions.map((value) => <option key={value} value={value}>{value}</option>)}
                    </Select>
                  ) : (
                    <Input placeholder="e.g. 646006407" value={blastCallerId} onChange={e => setBlastCallerId(e.target.value)} />
                  )}
                </Field>
                <Field label="SIP trunk" hint="Which trunk to route this blast through">
                  <Select value={blastSipTrunkId} onChange={e => {
                    const id = e.target.value;
                    setBlastSipTrunkId(id);
                    const trunk = availableTrunks.find(t => t.id === id);
                    setBlastCallerId(trunk ? (trunk.callerId || trunk.username || "") : "");
                  }}>
                    <option value="" disabled>— Select SIP trunk —</option>
                    {availableTrunks.map(t => <option key={t.id} value={t.id}>{t.name} — {t.callerId || t.username || t.host}</option>)}
                  </Select>
                </Field>
              </div>
              <div className="blast-review-grid mt-3">
                <div className="blast-review-card">
                  <div className="text-11 muted">Launch mode</div>
                  <div className="strong mt-1">Campaign blast</div>
                  <div className="muted text-11 mt-1">Starts calling as soon as you hit launch.</div>
                </div>
                <div className="blast-review-card">
                  <div className="text-11 muted">Current setup</div>
                  <div className="strong mt-1">{campaignName.trim() || "Name this blast"}</div>
                  <div className="muted text-11 mt-1">{CAMPAIGN_TYPES.find(t => t.value === campaignType)?.label || "General"}</div>
                </div>
              </div>
            </div>
          )}

          {currentStep === 1 && (
            <div className="blast-pane">
              <div className="text-11 muted mb-3" style={{ letterSpacing: ".08em", textTransform: "uppercase" }}>AI setup</div>
              <div className="fld-row">
                <Field label="Assistant">
                  <Select value={selAsst} onChange={e => setSelAsst(e.target.value)}>
                    <option value="">-- Select assistant --</option>
                    {assistants.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
                  </Select>
                </Field>
                <Field label="Max concurrent calls">
                  <Input type="number" min="1" max="10" value={concurrent} onChange={e => setConcurrent(Number(e.target.value))} />
                </Field>
              </div>
              <div className="fld-row">
                <Field label="Max attempts per contact">
                  <Select value={String(maxAttempts)} onChange={e => setMaxAttempts(Number(e.target.value))}>
                    {RETRY_ATTEMPT_OPTIONS.map(option => <option key={option.value} value={option.value}>{option.label}</option>)}
                  </Select>
                </Field>
                <Field label="Retry interval">
                  <Select value={String(retryIntervalMinutes)} onChange={e => setRetryIntervalMinutes(Number(e.target.value))}>
                    {RETRY_INTERVAL_OPTIONS.map(option => <option key={option.value} value={option.value}>{option.label}</option>)}
                  </Select>
                </Field>
              </div>
              <div className="blast-review-grid mt-3">
                <div className="blast-review-card">
                  <div className="text-11 muted">Selected assistant</div>
                  <div className="strong mt-1">{selectedAssistant?.name || "No assistant selected"}</div>
                  <div className="muted text-11 mt-1">{selectedAssistant?.language || "--"} · {selectedAssistant?.voiceProvider || "--"}</div>
                </div>
                <div className="blast-review-card">
                  <div className="text-11 muted">Retry policy</div>
                  <div className="strong mt-1">{selectedRetryLabel}</div>
                  <div className="muted text-11 mt-1">Wait {selectedRetryIntervalLabel} between retry attempts.</div>
                </div>
                <div className="blast-review-card">
                  <div className="text-11 muted">Outbound caller ID</div>
                  <div className="strong mt-1">{blastCallerId || sipCfg?.callerId || sipCfg?.username || "--"}</div>
                  <div className="muted text-11 mt-1">This number will be attached to calls launched from this campaign.</div>
                </div>
              </div>

              {selectedAssistant && (
                <div style={{ marginTop: 16, padding: "14px 16px", background: "var(--c-bg2)", borderRadius: 10, border: "1px solid var(--c-line)" }}>
                  <div className="row gap-2" style={{ alignItems: "center", justifyContent: "space-between", flexWrap: "wrap" }}>
                    <div>
                      <div className="strong text-12">CSV columns for this assistant</div>
                      <div className="muted text-11 mt-1">Your campaign CSV must include these exact column headers.</div>
                    </div>
                    <Btn variant="ghost" size="sm" onClick={() => downloadCsvTemplate(selectedAssistant)}>
                      ↓ Download template
                    </Btn>
                  </div>
                  <div className="row gap-2 mt-2" style={{ flexWrap: "wrap" }}>
                    {getCsvTemplateColumns(selectedAssistant).map(col => (
                      <span key={col.name} style={{
                        padding: "4px 10px",
                        borderRadius: 20,
                        fontSize: 11,
                        fontWeight: 600,
                        background: col.type === "required" ? "var(--a-soft)" : col.type === "prompt" ? "rgba(34,197,94,.1)" : "var(--c-bg3)",
                        color: col.type === "required" ? "var(--a)" : col.type === "prompt" ? "var(--ok)" : "var(--c-text-3)",
                        border: `1px solid ${col.type === "required" ? "rgba(124,92,255,.2)" : col.type === "prompt" ? "rgba(34,197,94,.2)" : "var(--c-line)"}`,
                      }}>
                        {col.name}
                        <span style={{ opacity: 0.6, marginLeft: 5, fontSize: 10 }}>{col.hint}</span>
                      </span>
                    ))}
                  </div>
                </div>
              )}
            </div>
          )}

          {currentStep === 2 && (
            <div className="blast-pane">
              <div className="text-11 muted mb-3" style={{ letterSpacing: ".08em", textTransform: "uppercase" }}>Contacts</div>
              <Field label="CSV file">
                <Input type="file" accept=".csv" onChange={e => onFileChange(e.target.files?.[0] || null)} />
              </Field>
              {csvHeaders.length > 0 && (
                <div className="fld-row">
                  <Field label="Phone number column">
                    <Select value={destColumn} onChange={e => setDestColumn(e.target.value)}>
                      {csvHeaders.map(h => <option key={h} value={h}>{h}</option>)}
                    </Select>
                  </Field>
                  <Field label="Customer name column" hint="Optional">
                    <Select value={nameColumn} onChange={e => setNameColumn(e.target.value)}>
                      <option value="">-- None --</option>
                      {csvHeaders.map(h => <option key={h} value={h}>{h}</option>)}
                    </Select>
                  </Field>
                </div>
              )}
              <div className="blast-review-grid mt-3">
                <div className="blast-review-card">
                  <div className="text-11 muted">Upload status</div>
                  <div className="strong mt-1">{csvFile?.name || "No file chosen"}</div>
                  <div className="muted text-11 mt-1">{csvRows.length ? `${csvRows.length} contacts ready` : "Upload your contact list to continue."}</div>
                </div>
                <div className="blast-review-card">
                  <div className="text-11 muted">Mapped columns</div>
                  <div className="strong mt-1">{destColumn || "--"}</div>
                  <div className="muted text-11 mt-1">Name column: {nameColumn || "--"}</div>
                </div>
              </div>
            </div>
          )}

          {currentStep === 3 && (
            <div className="blast-pane">
              <div className="text-11 muted mb-3" style={{ letterSpacing: ".08em", textTransform: "uppercase" }}>Review and launch</div>
              <div className="blast-review-grid">
                <div className="blast-review-card">
                  <div className="text-11 muted">Campaign</div>
                  <div className="strong mt-1">{campaignName.trim() || "--"}</div>
                  <div className="muted text-11 mt-1">{CAMPAIGN_TYPES.find(t => t.value === campaignType)?.label || "General"}</div>
                </div>
                <div className="blast-review-card">
                  <div className="text-11 muted">Assistant</div>
                  <div className="strong mt-1">{selectedAssistant?.name || "--"}</div>
                  <div className="muted text-11 mt-1">Concurrency: {concurrent}</div>
                </div>
                <div className="blast-review-card">
                  <div className="text-11 muted">SIP number</div>
                  <div className="strong mt-1">{blastCallerId || sipCfg?.callerId || sipCfg?.username || "--"}</div>
                  <div className="muted text-11 mt-1">Used as the caller ID for this blast.</div>
                </div>
                <div className="blast-review-card">
                  <div className="text-11 muted">Retry settings</div>
                  <div className="strong mt-1">{selectedRetryLabel}</div>
                  <div className="muted text-11 mt-1">Interval: {selectedRetryIntervalLabel}</div>
                </div>
                <div className="blast-review-card">
                  <div className="text-11 muted">Contacts</div>
                  <div className="strong mt-1">{csvRows.length || 0}</div>
                  <div className="muted text-11 mt-1">{csvFile?.name || "No file chosen"}</div>
                </div>
              </div>
            </div>
          )}

          <div className="blast-actions">
            <Btn variant="ghost" onClick={() => setCurrentStep(step => Math.max(0, step - 1))} disabled={currentStep === 0}>
              Back
            </Btn>
            {currentStep < BLAST_STEPS.length - 1 ? (
              <Btn variant="primary" onClick={() => setCurrentStep(step => Math.min(BLAST_STEPS.length - 1, step + 1))} disabled={!stepCanContinue[currentStep]}>
                Next
              </Btn>
            ) : (
              <Btn variant="primary" icon="send" onClick={launchCampaign} disabled={!canLaunch}>
                {launching ? "Launching..." : "Launch blast"}
              </Btn>
            )}
          </div>

          {launchResult && (
            <div className="mt-3" style={{ borderRadius: 8, overflow: "hidden" }}>
              <div style={{ padding: "10px 12px", background: launchResult.ok ? "var(--ok-soft)" : "var(--err-soft)", color: launchResult.ok ? "var(--ok)" : "var(--err)", fontSize: 12 }}>
                {launchResult.msg}
              </div>
              {launchResult.errors?.length > 0 && (
                <div style={{ padding: "8px 12px", background: "var(--c-bg3)", borderTop: "1px solid var(--c-line)" }}>
                  {launchResult.errors.map((e, i) => (
                    <div key={i} style={{ fontSize: 11, color: "var(--err)", marginBottom: 4, lineHeight: 1.4 }}>
                      <span className="mono">{e.to}</span> - {e.error}
                    </div>
                  ))}
                </div>
              )}
            </div>
          )}
        </Card>

      </div>
      )}

      {blastScreen === "launch" && (() => {
        const HIST_PAGE_SIZE = 10;
        const histTotal = campaigns.length;
        const histPages = Math.ceil(histTotal / HIST_PAGE_SIZE);
        const histSlice = campaigns.slice(histPage * HIST_PAGE_SIZE, (histPage + 1) * HIST_PAGE_SIZE);
        return (
        <Card title="Blast history" subtitle="Jump between recent outbound campaigns">
          {campaigns.length ? (
            <>
              <div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
                {histSlice.map((campaign, idx) => {
                  const stats = campaign.stats || EMPTY_STATS;
                  const selected = campaign.id === selectedCampaignId;
                  const isLast = idx === histSlice.length - 1;
                  return (
                    <button
                      key={campaign.id}
                      type="button"
                      onClick={() => {
                        setSelectedCampaignId(campaign.id);
                        setBlastScreen("report");
                        setSelectedCampaignJobKey("");
                      }}
                      style={{
                        textAlign: "left",
                        border: "none",
                        borderBottom: isLast ? "none" : "1px solid var(--c-line)",
                        borderLeft: selected ? "3px solid var(--a)" : "3px solid transparent",
                        background: selected ? "rgba(124,92,255,.06)" : "transparent",
                        padding: "10px 12px",
                        color: "inherit",
                        cursor: "pointer",
                        display: "grid",
                        gridTemplateColumns: "1fr auto auto auto auto",
                        alignItems: "center",
                        gap: 16,
                      }}
                    >
                      <div style={{ minWidth: 0 }}>
                        <div className="strong text-13" style={{ lineHeight: 1.3, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{campaign.campaignName}</div>
                        <div className="muted text-11 mt-1">{fmtDate(campaign.createdAt)}</div>
                      </div>
                      <Badge tone={stats.failed ? "warn" : "ok"} dot>{campaign.campaignType || "general"}</Badge>
                      <div style={{ textAlign: "right", minWidth: 40 }}>
                        <div className="text-11 muted">Called</div>
                        <div className="mono text-12 strong">{stats.called}</div>
                      </div>
                      <div style={{ textAlign: "right", minWidth: 40 }}>
                        <div className="text-11 muted">Pending</div>
                        <div className="mono text-12 strong">{stats.pending}</div>
                      </div>
                      <div style={{ textAlign: "right", minWidth: 40 }}>
                        <div className="text-11 muted">Failed</div>
                        <div className="mono text-12 strong" style={{ color: stats.failed ? "var(--err)" : "inherit" }}>{stats.failed}</div>
                      </div>
                    </button>
                  );
                })}
              </div>
              {histPages > 1 && (
                <div className="row gap-2 mt-3" style={{ justifyContent: "space-between", alignItems: "center" }}>
                  <span className="muted text-12">{histPage * HIST_PAGE_SIZE + 1}–{Math.min((histPage + 1) * HIST_PAGE_SIZE, histTotal)} of {histTotal}</span>
                  <div className="row gap-2">
                    <button className="btn btn-sm" disabled={histPage === 0} onClick={() => setHistPage(p => p - 1)}>Back</button>
                    <button className="btn btn-sm" disabled={histPage >= histPages - 1} onClick={() => setHistPage(p => p + 1)}>Next</button>
                  </div>
                </div>
              )}
            </>
          ) : (
            <div className="empty-state">
              <div className="empty-state-title">No blast history yet</div>
              <div>Your completed and active campaigns will appear here automatically.</div>
            </div>
          )}
        </Card>
        );
      })()}

      {blastScreen === "report" && (
        selectedCampaign ? (
          <div style={{ display: "flex", alignItems: "flex-start", gap: 0, minWidth: 0 }}>
            <div style={{ flex: 1, minWidth: 0, overflow: "hidden" }}>
              <div className="row gap-2 mb-4" style={{ justifyContent: "space-between", alignItems: "center" }}>
                <div>
                  <Btn variant="ghost" onClick={() => { setBlastScreen("launch"); setSelectedCampaignJobKey(""); }}>
                    Back To Blast Builder
                  </Btn>
                  <div className="mt-3 strong" style={{ fontSize: 20 }}>{selectedCampaign.campaignName}</div>
                  <div className="muted text-11 mt-1">
                    {fmtDate(selectedCampaign.createdAt)} · {selectedCampaign.assistantName || "Assistant"} · {selectedCampaign.campaignType || "general"}
                  </div>
                </div>
                {detailLoading ? <Badge tone="info">Refreshing</Badge> : null}
              </div>

              {/* Live blast timer */}
              {blastTimerInfo && (
                <div className="card mt-2" style={{ padding: "12px 16px" }}>
                  <div className="row" style={{ justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 12 }}>
                    <div className="row gap-4" style={{ flexWrap: "wrap" }}>
                      {blastTimerInfo.isComplete ? (
                        <div className="row gap-2">
                          <Badge tone="ok" dot>Completed</Badge>
                          <span className="mono text-12">Finished in {fmtSecs(blastTimerInfo.elapsedSec)}</span>
                        </div>
                      ) : (
                        <>
                          <div>
                            <div className="text-10 muted">Elapsed</div>
                            <div className="mono strong">{fmtSecs(blastTimerInfo.elapsedSec)}</div>
                          </div>
                          <div>
                            <div className="text-10 muted">ETA</div>
                            <div className="mono strong" style={{ color: "var(--a)" }}>
                              {blastTimerInfo.etaSec != null ? `~${fmtSecs(blastTimerInfo.etaSec)}` : "Calculating…"}
                            </div>
                          </div>
                          <div>
                            <div className="text-10 muted">Progress</div>
                            <div className="mono strong">{blastTimerInfo.done} / {blastTimerInfo.total}</div>
                          </div>
                        </>
                      )}
                    </div>
                    <div className="row gap-3" style={{ alignItems: "center" }}>
                      <div style={{ minWidth: 120, maxWidth: 240 }}>
                        <div style={{ height: 6, background: "var(--c-surface-2)", borderRadius: 3, overflow: "hidden" }}>
                          <div style={{ width: `${blastTimerInfo.pct}%`, height: "100%", background: blastTimerInfo.isComplete ? "var(--ok)" : "var(--a)", borderRadius: 3, transition: "width .5s" }} />
                        </div>
                        <div className="row mt-1" style={{ justifyContent: "space-between", fontSize: 10, color: "var(--c-text-4)" }}>
                          <span>{blastTimerInfo.pct}% done</span>
                          <span>{blastTimerInfo.remaining} remaining</span>
                        </div>
                      </div>
                      {!blastTimerInfo.isComplete && !selectedCampaign?.stopped && (
                        <Btn variant="ghost" size="sm" icon="x" onClick={stopCampaign} disabled={stopping} style={{ color: "var(--err)", borderColor: "var(--err)" }}>
                          {stopping ? "Stopping…" : "Stop blast"}
                        </Btn>
                      )}
                      {selectedCampaign?.stopped && (
                        <Badge tone="err" dot>Stopped</Badge>
                      )}
                    </div>
                  </div>
                </div>
              )}

              <div className="stat-grid mt-2">
                <StatTile k={{ label: "Targets", value: String(selectedStats.totalTargets || 0), delta: selectedCampaign.destinationColumn || "", spark: [1,2,3,4,5,5,6,7,7,8,9,9,10,11,11,12,13,13,14,14] }} />
                <StatTile k={{ label: "Completed", value: String(reportSummary.completed || 0), delta: `${reportSummary.voicemail || 0} voicemail`, spark: [1,1,2,2,3,3,4,5,5,6,6,7,8,8,9,10,10,11,11,12] }} />
                <StatTile k={{ label: "Pending", value: String(reportSummary.pending || 0), delta: `${reportSummary.retrying || 0} retrying`, spark: [12,11,11,10,10,9,9,8,8,7,7,6,6,5,5,4,4,3,3,2] }} />
                <StatTile k={{ label: "Max retries", value: String(reportSummary.maxRetriesReached || 0), delta: `${reportSummary.failed || 0} failed`, spark: [2,2,2,2,3,3,3,4,4,4,4,4,5,5,5,5,6,6,6,6] }} />
              </div>

              <div className="mt-2">
                <Card
                  title="Campaign report"
                  subtitle={`${selectedJobs.length} contacts in this campaign`}
                  action={reblastCandidates.length > 0 ? (
                    <div className="row gap-2" style={{ alignItems: "center" }}>
                      <span className="text-11 muted">
                        {noAnswerCount > 0 && `${noAnswerCount} no answer`}
                        {noAnswerCount > 0 && noPtpCount > 0 && " · "}
                        {noPtpCount > 0 && `${noPtpCount} no PTP`}
                      </span>
                      <Btn variant="primary" size="sm" icon="send" onClick={launchReblast} disabled={reblasting}>
                        {reblasting ? "Launching…" : `Reblast ${reblastCandidates.length}`}
                      </Btn>
                    </div>
                  ) : null}
                >
                  {selectedJobs.length ? (
                    <div style={{ maxHeight: 620, overflow: "auto" }}>
                      <table className="tbl">
                        <thead>
                          <tr>
                            <th>#</th>
                            <th>Customer</th>
                            <th>Number</th>
                            <th>Status</th>
                            <th>Ended reason</th>
                            <th>Outcome</th>
                            <th>Connected</th>
                            <th>Balance</th>
                            <th>Expected payment</th>
                            <th>Duration</th>
                            <th>Cost</th>
                            <th>Attempts</th>
                            <th>Next retry</th>
                            <th>Audio</th>
                          </tr>
                        </thead>
                        <tbody>
                          {selectedJobs.map(job => {
                            const rowKey = getCampaignJobKey(job);
                            const isSelected = rowKey === selectedCampaignJobKey;
                            return (
                              <tr
                                key={`${selectedCampaign.id}-${rowKey}`}
                                onClick={() => setSelectedCampaignJobKey(isSelected ? "" : rowKey)}
                                style={{ cursor: "pointer", background: isSelected ? "rgba(141,139,255,0.07)" : "" }}
                              >
                                <td className="mono text-11">{job.index + 1}</td>
                                <td>{job.customerName || "--"}</td>
                                <td className="mono text-11">{job.to || "--"}</td>
                                <td>
                                  <Badge tone={job.status === "failed" ? "err" : (job.status === "live" ? "ok" : (job.status === "completed" ? "accent" : (job.status === "retrying" ? "warn" : (job.status === "max_retries_reached" ? "err" : "default"))))} dot>
                                    {job.status || "--"}
                                  </Badge>
                                </td>
                                <td><Badge tone={outcomeTone(job.endedReason || "default")} dot>{job.endedReason || "--"}</Badge></td>
                                <td>{job.callOutcome || "--"}</td>
                                <td>{job.connected || "--"}</td>
                                <td>{job.balance != null ? fmtRM(job.balance) : (job.logState === "awaiting_log" ? "Awaiting log" : "--")}</td>
                                <td>{job.expectedPaymentDate || (job.logState === "awaiting_log" ? "Awaiting log" : "--")}</td>
                                <td className="mono">{job.durationSeconds != null ? fmtDuration(job.durationSeconds) : (job.logState === "awaiting_log" ? "Awaiting log" : "--")}</td>
                                <td className="mono" style={{ color: "var(--a)" }}>{job.sellingMyr != null ? fmtRM(job.sellingMyr) : (job.logState === "awaiting_log" ? "Awaiting log" : "--")}</td>
                                <td className="mono">{job.attempts} / {job.maxAttempts}</td>
                                <td className="mono text-11">{job.nextRetryAt ? fmtDate(job.nextRetryAt) : "--"}</td>
                                <td>
                                  {job.logId && job.recordingPath ? (
                                    <div className="row gap-2">
                                      <a className="link" href={buildRecordingUrl(job.logId)} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()}>Play</a>
                                      <a className="link" href={buildRecordingUrl(job.logId, { download: true })} onClick={(e) => e.stopPropagation()}>Download</a>
                                    </div>
                                  ) : (
                                    <span className="muted text-11">{job.error || (job.logState === "awaiting_log" ? "No VoxPro call log yet" : "No audio")}</span>
                                  )}
                                </td>
                              </tr>
                            );
                          })}
                        </tbody>
                      </table>
                    </div>
                  ) : (
                    <div className="empty-state">
                      <div className="empty-state-title">No campaign rows yet</div>
                      <div>The campaign report will appear here after launch.</div>
                    </div>
                  )}
                </Card>
                {reblastResult && (
                  <div className="mt-2" style={{ padding: "10px 14px", borderRadius: 8, background: reblastResult.ok ? "var(--ok-soft)" : "var(--err-soft)", color: reblastResult.ok ? "var(--ok)" : "var(--err)", fontSize: 12 }}>
                    {reblastResult.msg}
                  </div>
                )}
              </div>
            </div>

            <CampaignContactDetail
              campaign={selectedCampaign}
              job={selectedCampaignJob}
              log={selectedCampaignLog}
              onClose={() => setSelectedCampaignJobKey("")}
            />
          </div>
        ) : selectedCampaignId ? (
          <div className="empty-state">
            <div className="empty-state-title">Loading campaign…</div>
            <div>Fetching campaign report, please wait.</div>
          </div>
        ) : (
          <div className="empty-state">
            <div className="empty-state-title">Campaign not selected</div>
            <div>Pick a campaign from Blast history to open its report screen.</div>
          </div>
        )
      )}
    </div>
  );
};

const PtpView = () => {
  const [contacts, setContacts] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [campaigns, setCampaigns] = React.useState([]);
  const [filterCampaign, setFilterCampaign] = React.useState("");
  const [paymentCsv, setPaymentCsv] = React.useState(null);
  const [paymentHeaders, setPaymentHeaders] = React.useState([]);
  const [paymentRows, setPaymentRows] = React.useState([]);
  const [paymentAkaunCol, setPaymentAkaunCol] = React.useState("");
  const [paymentNameCol, setPaymentNameCol] = React.useState("");
  const [paymentAmtCol, setPaymentAmtCol] = React.useState("");
  const [paymentDateCol, setPaymentDateCol] = React.useState("");
  const [overrides, setOverrides] = React.useState({});
  const [saving, setSaving] = React.useState(false);
  const [saveResult, setSaveResult] = React.useState(null);
  const [dragOver, setDragOver] = React.useState(false);

  const loadContacts = React.useCallback((campaign = filterCampaign) => {
    setLoading(true);
    const qp = campaign ? `?campaignId=${encodeURIComponent(campaign)}` : "";
    voxApi(`/ptp/contacts${qp}`)
      .then(d => {
        const list = d.contacts || [];
        setContacts(list);
        const cs = [...new Set(list.map(c => c.campaignName).filter(Boolean))];
        setCampaigns(prev => [...new Set([...prev, ...cs])]);
      })
      .catch(() => {})
      .finally(() => setLoading(false));
  }, [filterCampaign]);

  React.useEffect(() => { loadContacts(); }, [filterCampaign]);

  const mergedContacts = React.useMemo(() => contacts.map(c => ({
    ...c,
    ...(overrides[c.logId] || {}),
  })), [contacts, overrides]);

  const stats = React.useMemo(() => {
    const total = mergedContacts.length;
    const confirmed = mergedContacts.filter(c => c.status === "confirmed").length;
    const unconfirmed = total - confirmed;
    const collected = mergedContacts.filter(c => c.status === "confirmed")
      .reduce((s, c) => s + (Number(c.paidAmount) || Number(c.balance) || 0), 0);
    const outstanding = mergedContacts.filter(c => c.status !== "confirmed")
      .reduce((s, c) => s + (Number(c.balance) || 0), 0);
    return { total, confirmed, unconfirmed, collected, outstanding };
  }, [mergedContacts]);

  async function onPaymentFile(file) {
    if (!file) return;
    setPaymentCsv(file);
    const text = await file.text();
    const lines = text.trim().split(/\r?\n/);
    const headers = lines[0].split(",").map(h => h.trim().replace(/^"|"$/g, ""));
    const rows = lines.slice(1).filter(Boolean).map(line => {
      const cols = line.split(",").map(c => c.trim().replace(/^"|"$/g, ""));
      const row = {};
      headers.forEach((h, j) => { row[h] = cols[j] || ""; });
      return row;
    });
    setPaymentHeaders(headers);
    setPaymentRows(rows);
    const akaunHints = ["akaun", "account", "acc_no", "account_number"];
    const nameHints = ["name", "nama", "customer", "client"];
    const amtHints = ["amount", "jumlah", "amt", "paid", "bayar"];
    const dateHints = ["date", "tarikh", "payment_date", "paid_date"];
    const pick = hints => headers.find(h => hints.some(hint => h.toLowerCase().includes(hint))) || "";
    setPaymentAkaunCol(pick(akaunHints));
    setPaymentNameCol(pick(nameHints));
    setPaymentAmtCol(pick(amtHints));
    setPaymentDateCol(pick(dateHints));
  }

  function normPhone(p) { return String(p || "").replace(/[^0-9]/g, ""); }
  function normName(n) { return String(n || "").toLowerCase().trim().replace(/\s+/g, " "); }

  function runMatch() {
    if (!paymentRows.length) return;
    const newOverrides = { ...overrides };
    for (const contact of contacts) {
      const paidRow = paymentRows.find(row => {
        if (contact.akaunNumber && paymentAkaunCol && String(row[paymentAkaunCol] || "").trim() === String(contact.akaunNumber).trim()) return true;
        if (contact.phone) {
          const cp = normPhone(contact.phone);
          if (cp && paymentHeaders.some(h => normPhone(row[h]) === cp)) return true;
        }
        if (contact.customerName && paymentNameCol && normName(row[paymentNameCol]) === normName(contact.customerName)) return true;
        return false;
      });
      if (paidRow) {
        const matchedBy = (contact.akaunNumber && paymentAkaunCol && String(paidRow[paymentAkaunCol] || "").trim() === String(contact.akaunNumber).trim()) ? "akaun_number"
          : (paymentNameCol && normName(paidRow[paymentNameCol]) === normName(contact.customerName)) ? "name" : "phone";
        newOverrides[contact.logId] = {
          status: "confirmed",
          paidAmount: paymentAmtCol ? (Number(paidRow[paymentAmtCol]) || null) : null,
          paidDate: paymentDateCol ? (paidRow[paymentDateCol] || "") : "",
          matchedBy,
        };
      }
    }
    setOverrides(newOverrides);
    setSaveResult(null);
  }

  function toggleStatus(logId, currentStatus) {
    setOverrides(prev => ({
      ...prev,
      [logId]: { ...(prev[logId] || {}), status: currentStatus === "confirmed" ? "unconfirmed" : "confirmed", matchedBy: prev[logId]?.matchedBy || "manual" },
    }));
    setSaveResult(null);
  }

  async function saveVerifications() {
    setSaving(true); setSaveResult(null);
    try {
      const updates = Object.entries(overrides).map(([logId, v]) => ({ logId, ...v }));
      await voxApi("/ptp/verify", { method: "POST", body: JSON.stringify({ updates }) });
      setSaveResult({ ok: true, msg: `${updates.length} record${updates.length !== 1 ? "s" : ""} saved.` });
      setOverrides({});
      loadContacts();
    } catch (e) { setSaveResult({ ok: false, msg: e.message || "Save failed" }); }
    finally { setSaving(false); }
  }

  const pendingChanges = Object.keys(overrides).length;

  return (
    <div>
      <div className="ph">
        <div>
          <h1 className="ph-title">PTP Checker</h1>
          <div className="ph-subtitle">Cross-reference payment receipts with customers who promised to pay.</div>
        </div>
      </div>

      <Card title="How it works" subtitle="Match your payment file against PTP contacts in 3 steps.">
        <div className="row gap-3" style={{ flexWrap: "wrap" }}>
          {[
            { n: 1, title: "Run collections blast", desc: "Agent calls customers and captures promise-to-pay date and amount." },
            { n: 2, title: "Contacts appear here", desc: "Every call with an expected payment date is listed below automatically." },
            { n: 3, title: "Drop payment CSV", desc: "After payment day, drop your bank statement or payment report CSV." },
            { n: 4, title: "Auto-match by account", desc: "System matches by account number first, then phone, then customer name." },
            { n: 5, title: "Save & confirm", desc: "Review matches, toggle any manually, then save to mark who paid." },
          ].map(step => (
            <div key={step.n} style={{ flex: "1 1 160px", padding: "12px 14px", background: "var(--c-bg2)", borderRadius: 10, border: "1px solid var(--c-line)" }}>
              <div style={{ width: 24, height: 24, borderRadius: "50%", background: "var(--a-soft)", color: "var(--a)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 11, fontWeight: 700, marginBottom: 8 }}>{step.n}</div>
              <div className="strong text-12">{step.title}</div>
              <div className="muted text-11 mt-1" style={{ lineHeight: 1.5 }}>{step.desc}</div>
            </div>
          ))}
        </div>
      </Card>

      <Card
        title="PTP Contacts"
        subtitle="All calls where a customer gave a promise-to-pay date."
        action={pendingChanges > 0 ? (
          <Btn variant="primary" onClick={saveVerifications} disabled={saving}>
            {saving ? "Saving…" : `Save ${pendingChanges} change${pendingChanges !== 1 ? "s" : ""}`}
          </Btn>
        ) : null}
      >
        {/* Campaign filter */}
        <div className="row gap-2 mb-3" style={{ flexWrap: "wrap", alignItems: "center" }}>
          <Select value={filterCampaign} onChange={e => setFilterCampaign(e.target.value)} style={{ width: 240 }}>
            <option value="">All campaigns</option>
            {campaigns.map(c => <option key={c} value={c}>{c}</option>)}
          </Select>
          {filterCampaign && <Btn variant="ghost" size="sm" onClick={() => setFilterCampaign("")}>Clear</Btn>}
        </div>

        {/* Stats */}
        <div className="stat-grid mb-3">
          <StatTile k={{ label: "Total PTP", value: String(stats.total), delta: "contacts", spark: [1,2,3,4,5,5,6,7,8,9,9,10,10,11,11,12,12,13,13,14] }} />
          <StatTile k={{ label: "Confirmed Paid", value: String(stats.confirmed), delta: `${stats.total > 0 ? Math.round((stats.confirmed / stats.total) * 100) : 0}% confirmed`, spark: [0,0,1,1,2,2,3,4,4,5,5,6,6,7,7,8,8,9,9,10] }} />
          <StatTile k={{ label: "Collected", value: fmtRM(stats.collected), delta: `${stats.confirmed} paid`, spark: [0,1,1,2,3,3,4,5,5,6,6,7,7,8,8,9,9,10,10,11] }} />
          <StatTile k={{ label: "Outstanding", value: fmtRM(stats.outstanding), delta: `${stats.unconfirmed} unpaid`, spark: [14,13,13,12,11,11,10,9,9,8,8,7,7,6,6,5,5,4,4,3] }} />
        </div>

        {/* Payment CSV drop zone */}
        <div
          onDragOver={e => { e.preventDefault(); setDragOver(true); }}
          onDragLeave={() => setDragOver(false)}
          onDrop={e => { e.preventDefault(); setDragOver(false); const f = e.dataTransfer.files?.[0]; if (f) onPaymentFile(f); }}
          onClick={() => document.getElementById("ptp-csv-input").click()}
          style={{
            border: `2px dashed ${dragOver ? "var(--a)" : "var(--c-line)"}`,
            borderRadius: 10, padding: "22px 24px", textAlign: "center",
            marginBottom: 16, background: dragOver ? "var(--a-soft)" : "var(--c-bg2)",
            transition: "all .15s", cursor: "pointer",
          }}
        >
          <input id="ptp-csv-input" type="file" accept=".csv" style={{ display: "none" }} onChange={e => onPaymentFile(e.target.files?.[0] || null)} />
          {paymentCsv ? (
            <div>
              <div className="strong text-13">{paymentCsv.name}</div>
              <div className="muted text-11 mt-1">{paymentRows.length} payment records loaded</div>
            </div>
          ) : (
            <div>
              <div className="strong text-13">Drop payment CSV here</div>
              <div className="muted text-11 mt-1">Or click to browse — drop your bank statement or payment confirmation file.</div>
            </div>
          )}
        </div>

        {/* Column mapping */}
        {paymentHeaders.length > 0 && (
          <div style={{ marginBottom: 16, padding: "14px 16px", background: "var(--c-bg3)", borderRadius: 10, border: "1px solid var(--c-line)" }}>
            <div className="text-11 muted mb-2" style={{ letterSpacing: ".08em", textTransform: "uppercase" }}>Map payment CSV columns</div>
            <div className="fld-row">
              <Field label="Account number col" hint="Highest match priority">
                <Select value={paymentAkaunCol} onChange={e => setPaymentAkaunCol(e.target.value)}>
                  <option value="">-- None --</option>
                  {paymentHeaders.map(h => <option key={h} value={h}>{h}</option>)}
                </Select>
              </Field>
              <Field label="Customer name col">
                <Select value={paymentNameCol} onChange={e => setPaymentNameCol(e.target.value)}>
                  <option value="">-- None --</option>
                  {paymentHeaders.map(h => <option key={h} value={h}>{h}</option>)}
                </Select>
              </Field>
              <Field label="Amount paid col" hint="Optional">
                <Select value={paymentAmtCol} onChange={e => setPaymentAmtCol(e.target.value)}>
                  <option value="">-- None --</option>
                  {paymentHeaders.map(h => <option key={h} value={h}>{h}</option>)}
                </Select>
              </Field>
              <Field label="Payment date col" hint="Optional">
                <Select value={paymentDateCol} onChange={e => setPaymentDateCol(e.target.value)}>
                  <option value="">-- None --</option>
                  {paymentHeaders.map(h => <option key={h} value={h}>{h}</option>)}
                </Select>
              </Field>
            </div>
            <div className="mt-2">
              <Btn variant="primary" onClick={runMatch}>Auto-match {paymentRows.length} payments</Btn>
            </div>
          </div>
        )}

        {saveResult && (
          <div style={{ padding: "10px 12px", borderRadius: 8, marginBottom: 12, background: saveResult.ok ? "var(--ok-soft)" : "var(--err-soft)", color: saveResult.ok ? "var(--ok)" : "var(--err)", fontSize: 12 }}>
            {saveResult.msg}
          </div>
        )}

        {/* Contacts table */}
        {loading ? (
          <div className="muted text-12">Loading PTP contacts…</div>
        ) : mergedContacts.length === 0 ? (
          <div className="empty-state">
            <div className="empty-state-title">No PTP contacts yet</div>
            <div>Run a collections blast campaign. Contacts where the customer committed to a payment date will appear here automatically.</div>
          </div>
        ) : (
          <div style={{ overflowX: "auto" }}>
            <table className="log-table">
              <thead>
                <tr>
                  <th>Status</th>
                  <th>Customer</th>
                  <th>Phone</th>
                  <th>Akaun No.</th>
                  <th>Campaign</th>
                  <th>Expected Payment</th>
                  <th>Amount</th>
                  <th>Matched by</th>
                </tr>
              </thead>
              <tbody>
                {mergedContacts.map(c => {
                  const isPaid = c.status === "confirmed";
                  const hasOverride = Boolean(overrides[c.logId]);
                  return (
                    <tr key={c.logId}>
                      <td>
                        <button
                          type="button"
                          onClick={() => toggleStatus(c.logId, c.status)}
                          style={{
                            padding: "3px 10px", borderRadius: 20, border: "none", cursor: "pointer",
                            fontSize: 11, fontWeight: 600,
                            background: isPaid ? "var(--ok-soft)" : "var(--c-bg3)",
                            color: isPaid ? "var(--ok)" : "var(--c-text-3)",
                            outline: hasOverride ? "1px solid var(--a)" : "none",
                          }}
                        >
                          {isPaid ? "✓ Paid" : "Unpaid"}
                        </button>
                      </td>
                      <td className="strong text-12">{c.customerName || "—"}</td>
                      <td className="mono text-11">{c.phone || "—"}</td>
                      <td className="mono text-11">{c.akaunNumber || "—"}</td>
                      <td className="text-11 muted">{c.campaignName || "—"}</td>
                      <td className="mono text-11">{c.expectedPaymentDate || "—"}</td>
                      <td className="mono text-11">{c.paidAmount != null ? fmtRM(c.paidAmount) : (c.balance != null ? fmtRM(c.balance) : "—")}</td>
                      <td className="text-11 muted">{c.matchedBy || "—"}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </Card>
    </div>
  );
};

// ---------- Phone Numbers (Inbound DID → Assistant routing) ----------

const BLANK_INBOUND = { number: "", label: "", trunkId: null, assistantId: null, assistantName: "", enabled: true };

const PhoneNumberForm = ({ entry, setEntry, onSave, onCancel, assistants = [], trunks = [] }) => {
  const isEdit = Boolean(entry.id);
  return (
    <div className="modal-back" onClick={onCancel}>
      <div className="modal" style={{ maxWidth: 480 }} onClick={e => e.stopPropagation()}>
        <div className="modal-head">
          <div className="card-title">{isEdit ? "Edit phone number" : "Add inbound number"}</div>
          <Btn variant="ghost" size="sm" icon="x" onClick={onCancel} />
        </div>
        <div className="modal-body">
          <div className="fld-row">
            <Field label="Phone number" help="The DID / inbound number your SIP provider routes to you (e.g. +60312345678)">
              <Input value={entry.number || ""} onChange={e => setEntry(d => ({ ...d, number: e.target.value }))} placeholder="+60312345678" />
            </Field>
            <Field label="Label">
              <Input value={entry.label || ""} onChange={e => setEntry(d => ({ ...d, label: e.target.value }))} placeholder="e.g. Angkasa Main Line" />
            </Field>
          </div>
          <Field label="Assigned assistant" help="This agent will answer all inbound calls to this number">
            <Select value={entry.assistantId || ""} onChange={e => {
              const asst = assistants.find(a => a.id === e.target.value);
              setEntry(d => ({ ...d, assistantId: e.target.value || null, assistantName: asst?.name || "" }));
            }}>
              <option value="">— No assistant assigned —</option>
              {assistants.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
            </Select>
          </Field>
          <Field label="SIP trunk" help="Leave as default unless this number comes from a specific trunk">
            <Select value={entry.trunkId || ""} onChange={e => setEntry(d => ({ ...d, trunkId: e.target.value || null }))}>
              <option value="">Default trunk</option>
              {trunks.map(t => <option key={t.id} value={t.id}>{t.name} ({t.host})</option>)}
            </Select>
          </Field>
        </div>
        <div className="modal-foot">
          <Btn variant="ghost" onClick={onCancel}>Cancel</Btn>
          <Btn variant="primary" disabled={!entry.number} onClick={() => onSave(entry)}>Save</Btn>
        </div>
      </div>
    </div>
  );
};

const PhoneNumbersView = ({ assistants = [] }) => {
  const [numbers, setNumbers] = React.useState([]);
  const [trunks, setTrunks] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [showNew, setShowNew] = React.useState(false);
  const [newEntry, setNewEntry] = React.useState({ ...BLANK_INBOUND });
  const [editEntry, setEditEntry] = React.useState(null);
  const [deleting, setDeleting] = React.useState(null);

  const webhookBase = window.location.origin;

  async function reload() {
    const d = await voxApiOptional("/inbound/numbers", { numbers: [] });
    setNumbers(d.numbers || []);
  }

  React.useEffect(() => {
    Promise.all([
      voxApiOptional("/inbound/numbers", { numbers: [] }).then(d => setNumbers(d.numbers || [])),
      voxApiOptional("/sip/trunks", { trunks: [] }).then(d => setTrunks(d.trunks || [])),
    ]).finally(() => setLoading(false));
  }, []);

  async function saveEntry(entry) {
    try {
      const r = await voxApi("/inbound/numbers", { method: "POST", body: JSON.stringify(entry) });
      await reload();
      setShowNew(false);
      setEditEntry(null);
      setNewEntry({ ...BLANK_INBOUND });
    } catch (e) { alert("Save failed: " + e.message); }
  }

  async function toggleEnabled(entry) {
    try {
      await voxApi("/inbound/numbers", { method: "POST", body: JSON.stringify({ ...entry, enabled: !entry.enabled }) });
      await reload();
    } catch (e) { alert("Failed: " + e.message); }
  }

  async function removeEntry(id) {
    if (!confirm("Remove this inbound number?")) return;
    setDeleting(id);
    try {
      await voxApi(`/inbound/numbers/${id}`, { method: "DELETE" });
      setNumbers(n => n.filter(x => x.id !== id));
    } catch (e) { alert("Failed: " + e.message); }
    finally { setDeleting(null); }
  }

  function getAssistantName(id) {
    const a = assistants.find(x => x.id === id);
    return a?.name || id || "—";
  }

  function getTrunkName(id) {
    if (!id) return "Default trunk";
    const t = trunks.find(x => x.id === id);
    return t?.name || id;
  }

  return (
    <div>
      <div className="ph">
        <div>
          <h1 className="ph-title">Phone Numbers</h1>
          <div className="ph-subtitle">Assign AI agents to your inbound SIP numbers. Each DID routes to a specific assistant automatically.</div>
        </div>
        <div className="ph-actions">
          <Btn variant="primary" size="sm" icon="plus" onClick={() => { setNewEntry({ ...BLANK_INBOUND }); setShowNew(true); }}>Add number</Btn>
        </div>
      </div>

      {showNew && <PhoneNumberForm entry={newEntry} setEntry={setNewEntry} onSave={saveEntry} onCancel={() => setShowNew(false)} assistants={assistants} trunks={trunks} />}
      {editEntry && <PhoneNumberForm entry={editEntry} setEntry={setEditEntry} onSave={saveEntry} onCancel={() => setEditEntry(null)} assistants={assistants} trunks={trunks} />}

      <Card pad="none">
        {loading ? (
          <div className="empty-state"><div className="muted">Loading…</div></div>
        ) : (
          <div className="tbl-wrap">
            <table className="tbl">
              <thead>
                <tr><th>Number</th><th>Assistant</th><th>Trunk</th><th>Status</th><th></th></tr>
              </thead>
              <tbody>
                {numbers.map(n => (
                  <tr key={n.id}>
                    <td>
                      <div className="row gap-3">
                        <div style={{ width: 32, height: 32, borderRadius: 8, background: "var(--c-surface)", border: "1px solid var(--c-line-2)", display: "grid", placeItems: "center" }}>
                          <Icon name="hash" style={{ width: 14, height: 14 }} />
                        </div>
                        <div>
                          <div className="strong mono text-13">{n.number}</div>
                          <div className="muted text-11">{n.label || "No label"}</div>
                        </div>
                      </div>
                    </td>
                    <td>
                      {n.assistantId ? (
                        <div className="row gap-2">
                          <div style={{ width: 22, height: 22, borderRadius: 6, background: "var(--c-accent-soft)", display: "grid", placeItems: "center" }}>
                            <Icon name="bot" style={{ width: 12, height: 12, color: "var(--c-accent)" }} />
                          </div>
                          <span className="text-12">{getAssistantName(n.assistantId)}</span>
                        </div>
                      ) : (
                        <span className="muted text-12">No assistant</span>
                      )}
                    </td>
                    <td><span className="mono text-11 muted">{getTrunkName(n.trunkId)}</span></td>
                    <td>
                      <div className="row gap-2">
                        <Badge tone={n.enabled ? "ok" : "default"} dot>{n.enabled ? "Active" : "Disabled"}</Badge>
                      </div>
                    </td>
                    <td className="right">
                      <div className="row gap-1 justify-end">
                        <Toggle checked={!!n.enabled} onChange={() => toggleEnabled(n)} />
                        <Btn variant="ghost" size="sm" icon="edit-2" onClick={() => setEditEntry({ ...n })} />
                        <Btn variant="ghost" size="sm" icon="trash-2" disabled={deleting === n.id} onClick={() => removeEntry(n.id)} />
                      </div>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
            {numbers.length === 0 && (
              <div className="empty-state">
                <div className="empty-state-title">No phone numbers yet</div>
                <div>Add your inbound DIDs and assign an assistant to each one.</div>
              </div>
            )}
          </div>
        )}
      </Card>

      <div className="g-2 mt-4">
        <Card title="How inbound routing works" subtitle="Connect your SIP provider or Asterisk dialplan to VoxPro.">
          <div className="col gap-3" style={{ fontSize: 12 }}>
            <div><strong>Step 1:</strong> Add your DID above and assign an assistant.</div>
            <div><strong>Step 2:</strong> In your Asterisk dialplan or SIP provider's webhook settings, send a POST to the Call Webhook URL below when a call arrives on that DID.</div>
            <div><strong>Step 3:</strong> VoxPro starts the assigned assistant in a LiveKit room and returns the room name. Bridge your caller into that room.</div>
            <div style={{ marginTop: 4 }}>
              <div className="text-11 muted mb-1">Call Webhook URL</div>
              <div style={{ background: "var(--c-surface)", border: "1px solid var(--c-line-2)", borderRadius: 8, padding: "8px 12px", fontFamily: "var(--f-mono)", fontSize: 11, wordBreak: "break-all" }}>
                POST {webhookBase}/inbound/call
              </div>
            </div>
            <div style={{ marginTop: 4 }}>
              <div className="text-11 muted mb-1">Request body</div>
              <pre style={{ background: "var(--c-surface)", border: "1px solid var(--c-line-2)", borderRadius: 8, padding: "8px 12px", fontSize: 11, margin: 0 }}>{`{
  "to": "+60312345678",   // The dialed DID
  "from": "+601234567890", // Caller's number
  "callId": "abc123"       // Optional
}`}</pre>
            </div>
            <div style={{ marginTop: 4 }}>
              <div className="text-11 muted mb-1">Asterisk dialplan example</div>
              <pre style={{ background: "var(--c-surface)", border: "1px solid var(--c-line-2)", borderRadius: 8, padding: "8px 12px", fontSize: 11, margin: 0, whiteSpace: "pre-wrap" }}>{`[inbound-voxpro]
exten => _+6031XXXXXXXX,1,NoOp(Inbound to VoxPro)
 same => n,AGI(agi://voxpro-agi-bridge)
 same => n,Hangup()`}</pre>
            </div>
          </div>
        </Card>

        <Card title="Inbound call log" subtitle="Recent inbound calls handled by assigned assistants.">
          <div className="empty-state" style={{ minHeight: 120 }}>
            <div className="muted text-12">Inbound call history will appear here once calls are received via the webhook.</div>
          </div>
        </Card>
      </div>
    </div>
  );
};

Object.assign(window, { TelephonyView, SipView, AsteriskView, LogsView, BlastView, PtpView, PhoneNumbersView });
