/* global React, Icon, initials, fmtDate, fmtTime, fmtDuration, fmtPhone, relTime, serviceLabel, serviceChip, api, toast, useApi */

/* ---------- Recording player (real audio + scrubbing) ----------
   Loads /api/calls/<id>/recording (the agent track) and renders a real
   HTML5 <audio> element with play/pause and click-to-seek on the
   progress bar.

   Pushes (currentTime, isPlaying) up to the parent via onTimeUpdate /
   onPlayingChange so the surrounding transcript can highlight which
   turns have already played. The state is owned here, the parent just
   observes — keeps the player self-contained while letting the
   transcript stay in sync. */
const RecordingPlayer = ({ callId, durSec, onTimeUpdate, onPlayingChange, audioRef: extRef }) => {
  // Parent can pass its own ref to control playback (e.g. for click-to-seek
  // on transcript turns); fall back to a local ref if not.
  const localRef = React.useRef(null);
  const audioRef = extRef || localRef;
  const [playing, setPlaying] = React.useState(false);
  const [pos, setPos] = React.useState(0);
  const [dur, setDur] = React.useState(durSec || 0);
  const [error, setError] = React.useState(null);

  // Reset when the underlying call changes (drawer closed and reopened
  // on a different row).
  React.useEffect(() => {
    setPlaying(false);
    setPos(0);
    setError(null);
    onPlayingChange?.(false);
    onTimeUpdate?.(0);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [callId]);

  const onLoaded = () => {
    if (audioRef.current && isFinite(audioRef.current.duration)) {
      setDur(audioRef.current.duration);
    }
  };
  const onTime = () => {
    if (!audioRef.current) return;
    const t = audioRef.current.currentTime;
    setPos(t);
    onTimeUpdate?.(t);
  };
  const onEnded = () => { setPlaying(false); onPlayingChange?.(false); };
  const onErr = () => setError("Recording unavailable");

  const togglePlay = () => {
    const a = audioRef.current;
    if (!a) return;
    if (a.paused) {
      a.play().then(() => { setPlaying(true); onPlayingChange?.(true); })
              .catch(() => setError("Playback blocked"));
    } else {
      a.pause();
      setPlaying(false);
      onPlayingChange?.(false);
    }
  };
  const seekToPct = (e) => {
    const a = audioRef.current;
    if (!a || !a.duration) return;
    const rect = e.currentTarget.getBoundingClientRect();
    const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
    a.currentTime = pct * a.duration;
  };

  const pct = Math.min(100, (pos / Math.max(dur, 1)) * 100);
  // `track=mixed` returns a server-side mix of caller + agent so Bill
  // hears both sides at once. The bridge generates the mix on first
  // request and caches it. Real bug 2026-05-02: this previously asked
  // for ?track=agent and the bridge silently ignored the param, so Bill
  // only ever heard the agent's side - making the transcript step-by-
  // step view feel "off" because half the conversation was silent gaps.
  const src = `/api/calls/${encodeURIComponent(callId)}/recording?track=mixed`;

  return (
    <div className="recording-player">
      <audio
        ref={audioRef}
        src={src}
        preload="metadata"
        onLoadedMetadata={onLoaded}
        onTimeUpdate={onTime}
        onEnded={onEnded}
        onError={onErr}
      />
      <button className="play-btn" onClick={togglePlay} aria-label={playing ? "Pause" : "Play"}>
        <Icon name={playing ? "pause" : "play"} size={16} />
      </button>
      <div
        className="waveform"
        style={{"--pct": `${pct}%`, cursor: "pointer"}}
        onClick={seekToPct}
        title="Click to seek"
      ></div>
      <div className="timing">{fmtDuration(pos)} / {fmtDuration(dur)}</div>
      {error && <div style={{fontSize: 11, color: "var(--clay)", marginLeft: 8}}>{error}</div>}
    </div>
  );
};

/* ====================================================================
   Calls tab — REAL DATA
   ==================================================================== */
const CallsView = ({ onOpenCustomer, openCallId }) => {
  const [q, setQ] = React.useState("");
  const [openId, setOpenId] = React.useState(openCallId || null);
  const { data: calls, loading } = useApi(() => api.calls(q || null, 50), [q]);

  // Fetch call detail + transcript when a call is opened
  const { data: callDetail } = useApi(
    () => openId ? api.call(openId) : Promise.resolve(null),
    [openId]
  );

  React.useEffect(() => { if (openCallId) setOpenId(openCallId); }, [openCallId]);

  // Live transcript append. The bridge broadcasts {type:"turn", call_id,
  // role, text, ts} via /api/live every time a turn is committed. While
  // the drawer is open on a call, append matching turns in real time so
  // the operator doesn't have to refresh to see them roll in.
  const [liveTurns, setLiveTurns] = React.useState([]);
  React.useEffect(() => {
    setLiveTurns([]);   // reset whenever the drawer opens a different call
    if (!openId) return;
    let es = null;
    let reconnectTimer = null;
    const open = () => {
      es = new EventSource('/api/live');
      es.onmessage = (ev) => {
        let m;
        try { m = JSON.parse(ev.data); } catch { return; }
        if (m.type === 'turn' && m.call_id === openId) {
          setLiveTurns(prev => {
            // De-dup against earlier live appends — same (ts, role, text)
            // means same turn (the initial fetch may also include early
            // turns; the merge below handles that).
            const key = `${m.ts}|${m.role}|${m.text}`;
            if (prev.some(t => `${t.ts}|${t.role}|${t.text}` === key)) return prev;
            return [...prev, { ts: m.ts, role: m.role, text: m.text }];
          });
        }
      };
      es.onerror = () => {
        if (es) { es.close(); es = null; }
        reconnectTimer = setTimeout(open, 3000);
      };
    };
    open();
    return () => {
      if (es) es.close();
      if (reconnectTimer) clearTimeout(reconnectTimer);
    };
  }, [openId]);

  // Audio playback state lifted from RecordingPlayer so the transcript
  // can highlight which turns have been "played past." Reset whenever
  // a different call is opened.
  const [audioTime, setAudioTime] = React.useState(0);
  const [audioPlaying, setAudioPlaying] = React.useState(false);
  // Audio element ref also lifted here so transcript clicks can seek.
  const audioRef = React.useRef(null);
  React.useEffect(() => { setAudioTime(0); setAudioPlaying(false); }, [openId]);

  // Click on a transcript turn → jump audio to that turn's offset and
  // start playing. Wrapped here so the JSX click handler stays trivial.
  const seekToTurn = (offsetSec) => {
    const a = audioRef.current;
    if (!a) return;
    a.currentTime = Math.max(0, offsetSec);
    if (a.paused) {
      a.play().then(() => setAudioPlaying(true)).catch(() => {/* user-gesture-required edge cases */});
    }
  };

  // Strip typographic dashes from text BEFORE display so historical
  // transcripts (already in calls.db with em-dashes from old templates)
  // render cleanly. New calls won't have them now that the YAML is fixed
  // and the bake script normalizes; this layer covers the backlog.
  const cleanTurnText = (s) => (s || "").replace(/\s*[—–―]\s*/g, ", ").replace(/\s+,/g, ",");

  const callList = calls || [];
  const opened = callDetail;
  const openedCall = opened?.call;
  const openedTurns = opened?.turns || [];

  // Compute per-turn audio offset (seconds from call start). Once the
  // user engages with audio (currentTime > 0 OR currently playing),
  // future turns render gray; before that, all turns show in their
  // normal colors.
  const callStartedMs = openedCall ? new Date(openedCall.started_at).getTime() : 0;
  const audioEngaged = audioPlaying || audioTime > 0.01;

  // Merge the initial fetched transcript with any turns that arrived
  // live (via the SSE subscription above). Dedup by (ts, role, text)
  // so the same turn doesn't show up twice if it was both in the
  // initial fetch and re-broadcast.
  const mergedTurns = React.useMemo(() => {
    const out = [...openedTurns];
    const seen = new Set(out.map(t => `${t.ts}|${t.role}|${t.text}`));
    for (const t of liveTurns) {
      const key = `${t.ts}|${t.role}|${t.text}`;
      if (!seen.has(key)) { out.push(t); seen.add(key); }
    }
    out.sort((a, b) => new Date(a.ts) - new Date(b.ts));
    return out;
  }, [openedTurns, liveTurns]);

  return (
    <div>
      <div className="page-head">
        <div>
          <h1>Calls</h1>
          <div className="subtitle">All inbound calls handled by the voice agent. Click any call for transcript and intake.</div>
        </div>
      </div>

      <div className="filters-bar">
        <div className="search-inline">
          <Icon name="search" size={16} />
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search caller, phone, address…" />
        </div>
        <span className="results-count">{loading ? '…' : `${callList.length} calls`}</span>
      </div>

      <div className="roster calls">
        <div className="roster-head">
          <div>Time</div><div>Caller</div><div>Service</div><div>Address</div><div style={{textAlign:"right"}}>Length</div><div></div>
        </div>
        {callList.map(k => (
          <div key={k.call_id} className="roster-row" onClick={() => setOpenId(k.call_id)}>
            <div className="call-time">
              {fmtTime(k.started_at)}
              <small>{relTime(k.started_at)}</small>
            </div>
            <div className="call-caller">
              <div className={`avatar sm ${k.name ? "" : "clay"}`}>{initials(k.name || 'U')}</div>
              <div style={{minWidth:0}}>
                <div className="call-name">{k.name || 'Unknown'}</div>
                <div className="call-phone">{fmtPhone(k.caller_phone)}</div>
              </div>
            </div>
            <div className="call-service">
              <span className={`chip ${serviceChip(k.service)}`} style={{alignSelf:"flex-start"}}>{serviceLabel(k.service)}</span>
            </div>
            <div className="call-address">{k.address || '—'}</div>
            <div className="call-dur">{fmtDuration(k.duration_s)}</div>
            <div className="call-chev"><Icon name="chev" size={16} /></div>
          </div>
        ))}
        {callList.length === 0 && !loading && <div className="empty-state card">No calls found.</div>}
      </div>

      {openedCall && (
        <Drawer
          title={openedCall.name || 'Unknown caller'}
          subtitle={`${fmtPhone(openedCall.caller_phone)} · ${fmtTime(openedCall.started_at)} · ${fmtDuration(openedCall.duration_s)}`}
          onClose={() => setOpenId(null)}
          actions={[
            { label: "View customer", primary: true, onClick: () => { setOpenId(null); toast("Customer lookup — coming soon"); } },
          ]}
        >
          {/* Intake fields from call */}
          <div className="intake-grid">
            <div className="intake-field"><div className="if-label">Name</div><div className={`if-value ${openedCall.name ? "" : "missing"}`}>{openedCall.name || "—"}</div></div>
            <div className="intake-field"><div className="if-label">Service</div><div className={`if-value ${openedCall.service ? "" : "missing"}`}>{serviceLabel(openedCall.service)}</div></div>
            <div className="intake-field"><div className="if-label">Pool size</div><div className={`if-value ${openedCall.pool_size ? "" : "missing"}`}>{openedCall.pool_size || "—"}</div></div>
            <div className="intake-field"><div className="if-label">Address</div><div className={`if-value ${openedCall.address ? "" : "missing"}`}>{openedCall.address || "—"}</div></div>
            <div className="intake-field"><div className="if-label">SMS sent</div><div className={`if-value ${openedCall.sms_sent_at ? "" : "missing"}`}>{openedCall.sms_sent_at ? fmtTime(openedCall.sms_sent_at) : "—"}</div></div>
          </div>

          {/* Recording sits right above the transcript so the listener
              can scrub audio while reading the matching turn. */}
          <RecordingPlayer
            callId={openedCall.call_id}
            durSec={openedCall.duration_s || 0}
            onTimeUpdate={setAudioTime}
            onPlayingChange={setAudioPlaying}
            audioRef={audioRef}
          />

          {/* Real transcript — turns gray out for "future" content once
              audio is engaged, lighting up to their role color as the
              playhead crosses each turn's offset. Click a turn to jump
              the audio to that turn's spot. */}
          <div className="eyebrow" style={{marginTop: 16, marginBottom: 8}}>Transcript ({mergedTurns.length} turns)</div>
          <div className="transcript-log">
            {mergedTurns.map((t, i) => {
              const offsetSec = (new Date(t.ts).getTime() - callStartedMs) / 1000;
              const future = audioEngaged && offsetSec > audioTime + 0.25; // small forgiveness window
              return (
                <div
                  key={i}
                  className={`tr ${t.role === 'user' ? 'user' : 'agent'} ${future ? 'pending' : ''} clickable`}
                  onClick={() => seekToTurn(offsetSec)}
                  title={`Jump to ${fmtDuration(offsetSec)}`}
                >
                  <div className="tr-role">{t.role === 'user' ? 'Caller' : 'Agent'}</div>
                  <div className="tr-text">{cleanTurnText(t.text)}</div>
                </div>
              );
            })}
            {mergedTurns.length === 0 && <div style={{color: 'var(--ink-3)', fontSize: 13, padding: '8px 0'}}>No transcript recorded for this call.</div>}
          </div>
        </Drawer>
      )}
    </div>
  );
};

/* ====================================================================
   Customers tab — REAL DATA
   ==================================================================== */
const CustomersView = ({ onOpenCustomer }) => {
  const [q, setQ] = React.useState("");
  const [weekly, setWeekly] = React.useState(false);
  const { data: customers, loading } = useApi(() => api.customers(q || null, weekly), [q, weekly]);

  const custList = customers || [];

  return (
    <div>
      <div className="page-head">
        <div>
          <h1>Customers</h1>
          <div className="subtitle">{loading ? '…' : `${custList.length} accounts.`} Weekly-service clients are flagged at the top.</div>
        </div>
        <button className="btn primary" onClick={() => toast("Add customer — coming soon")}><Icon name="plus" size={16} /> Add customer</button>
      </div>

      <div className="filters-bar">
        <div className="search-inline">
          <Icon name="search" size={16} />
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search name, phone, address…" />
        </div>
        <button className={`filter-toggle ${weekly ? "active" : ""}`} onClick={() => setWeekly(w => !w)}>
          Weekly only
        </button>
        <span className="results-count">{loading ? '…' : `${custList.length} customers`}</span>
      </div>

      <div className="customer-grid">
        {custList.map(c => {
          const isWeekly = c.last_service?.toLowerCase() === 'weekly service';
          return (
            <div key={c.phone} className={`customer-card ${isWeekly ? "weekly" : ""}`} onClick={() => onOpenCustomer(c)}>
              <div className="cc-head">
                <div className={`avatar lg ${isWeekly ? "" : "clay"}`}>{initials(c.name)}</div>
                <div style={{minWidth:0}}>
                  <div className="cc-name">{c.name || 'Unknown'}</div>
                  <div className="cc-phone">{fmtPhone(c.phone)}</div>
                </div>
              </div>
              <div className="cc-address">{c.address || '—'}</div>
              <div style={{fontSize:12.5, color:"var(--ink-3)"}}>{c.pool_size || '—'}</div>
              <div className="cc-tags">
                {isWeekly && <span className="chip pool">Weekly</span>}
                <span className={`chip ${serviceChip(c.last_service)}`}>{serviceLabel(c.last_service)}</span>
              </div>
              <div className="cc-foot">
                <span>Last seen · <b>{c.last_seen ? fmtDate(c.last_seen) : "Never"}</b></span>
                <span>First seen · <b>{c.first_seen ? fmtDate(c.first_seen) : "—"}</b></span>
              </div>
            </div>
          );
        })}
        {custList.length === 0 && !loading && <div className="empty-state card">No customers found.</div>}
      </div>
    </div>
  );
};

/* ====================================================================
   Drawer (shared)
   ==================================================================== */
const Drawer = ({ title, subtitle, onClose, actions = [], children }) => {
  React.useEffect(() => {
    const onEsc = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onEsc);
    return () => document.removeEventListener("keydown", onEsc);
  }, [onClose]);
  return (
    <>
      <div className="drawer-backdrop open" onClick={onClose}></div>
      <div className="drawer open">
        <div className="drawer-head">
          {/* Leading "Back" button — primary affordance on mobile where
              the drawer fills the entire viewport and the backdrop isn't
              tappable. On desktop it doubles up with the trailing X but
              that's fine; both work and either gets you out. */}
          <button className="drawer-back-btn" onClick={onClose} aria-label="Back">
            <Icon name="chev-left" size={20} />
            <span className="drawer-back-text">Back</span>
          </button>
          <div className="dh-title">
            <h2>{title}</h2>
            {subtitle && <div className="dh-sub">{subtitle}</div>}
          </div>
          <button className="close-btn" onClick={onClose} aria-label="Close"><Icon name="close" size={16} /></button>
        </div>
        <div className="drawer-body">{children}</div>
        {actions.length > 0 && (
          <div className="drawer-foot">
            {actions.map((a,i) => (
              <button key={i} className={`btn ${a.primary ? "primary" : "secondary"}`} onClick={a.onClick}>{a.label}</button>
            ))}
          </div>
        )}
      </div>
    </>
  );
};

window.CallsView = CallsView;
window.CustomersView = CustomersView;
window.Drawer = Drawer;
