/* OfficeTrack — Support Chat — main surface.

   Identify by phone (cold entry), then a natural free-text conversation with
   the assistant: it troubleshoots the customer's internet problem and, if it
   can't fix it remotely, books a technician visit. No canned chips, no
   decision-tree — just chat. The customer can send a photo of the modem; the
   model reads it. The only tappable UI is the final booking confirmation card.

   The browser renders what the server returns; all logic lives server-side. */

const { useState, useEffect, useRef } = React;

const BRAND_PRESETS = {
  navy:     { primary: '#15296B', headerBg: '#15296B', name: 'El Cuatro TV' },
  cobalt:   { primary: '#1A4FE8', headerBg: '#1A4FE8', name: 'El Cuatro TV' },
  lavender: { primary: '#7468C6', headerBg: '#9F95E3', name: 'El Cuatro TV' },
};

async function api(path, body) {
  try {
    const r = await fetch(path, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body || {}),
    });
    return await r.json();
  } catch (e) {
    return { ok: false, error: 'network' };
  }
}

// Downscale before upload (max 1280px, JPEG ~0.8). Canvas re-encode strips EXIF.
function downscaleImage(file, maxEdge = 1280, quality = 0.8) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const url = URL.createObjectURL(file);
    img.onload = () => {
      URL.revokeObjectURL(url);
      let { width, height } = img;
      const scale = Math.min(1, maxEdge / Math.max(width, height));
      width = Math.round(width * scale);
      height = Math.round(height * scale);
      const canvas = document.createElement('canvas');
      canvas.width = width; canvas.height = height;
      canvas.getContext('2d').drawImage(img, 0, 0, width, height);
      resolve(canvas.toDataURL('image/jpeg', quality));
    };
    img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('img')); };
    img.src = url;
  });
}

function ChatSurface({ lang, setLang, tweaks }) {
  const t = window.I18N[lang] || window.I18N.es;
  const brand = BRAND_PRESETS[(tweaks && tweaks.brandPreset)] || BRAND_PRESETS.navy;

  // El Cuatro: the customer arrives via a WhatsApp link bound to ONE task.
  // No identification gate, no OTP — we resolve the task from the link
  // parameter (or the demo fallback) and open the chat directly.
  const [authState, setAuthState] = useState('connecting');
  const sessionTokenRef = useRef(null);

  const [messages, setMessages] = useState([]);
  const [composer, setComposer] = useState('');
  const [pendingImage, setPendingImage] = useState(null);
  const [busy, setBusy] = useState(false);
  const [geo, setGeo] = useState(null);            // {lat,lng,address} confirmed on map
  const [showMap, setShowMap] = useState(false);
  const [listening, setListening] = useState(false);
  const [taskSummary, setTaskSummary] = useState(null);  // appointment card data
  const [locConfirmed, setLocConfirmed] = useState(false);
  const [showNote, setShowNote] = useState(false);
  const [noteSent, setNoteSent] = useState(false);
  const [booked, setBooked] = useState(false);   // visit confirmed → lock slots

  const convRef = useRef(null);
  const scrollRef = useRef(null);
  const recRef = useRef(null);
  const holdingRef = useRef(false);    // true while the mic button is held down
  const voiceFinalRef = useRef('');    // committed transcript across segments
  const voiceSupported = typeof window !== 'undefined'
    && (window.SpeechRecognition || window.webkitSpeechRecognition);

  const push = (msg) => setMessages(m => [...m, msg]);
  const dropTyping = () => setMessages(m => m.filter(x => x.kind !== 'typing'));
  const ai = (text) => push({ kind: 'ai', text });

  // On mount: resolve the task from the link and open the chat directly.
  useEffect(() => { startSession(); }, []);

  // best-effort audit flush on tab close
  useEffect(() => {
    function onUnload() {
      try {
        const conv = convRef.current, tok = sessionTokenRef.current;
        if (!conv || !tok) return;
        const payload = JSON.stringify({
          conversationId: conv, sessionToken: tok, reason: 'unload' });
        if (navigator.sendBeacon) {
          navigator.sendBeacon('/api/session/close',
            new Blob([payload], { type: 'application/json' }));
        }
      } catch (e) {}
    }
    window.addEventListener('beforeunload', onUnload);
    return () => window.removeEventListener('beforeunload', onUnload);
  }, []);

  useEffect(() => {
    const el = scrollRef.current;
    if (!el) return;
    requestAnimationFrame(() =>
      el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }));
  }, [messages]);

  /* ─── entry: resolve the task from the link, open the chat directly ─── */
  async function startSession() {
    setAuthState('connecting');
    let task = null;
    try { task = new URLSearchParams(window.location.search).get('task'); } catch (e) {}
    const d = await api('/api/session/start', task ? { task } : {});
    if (!d || d.ok === false || !d.sessionToken) {
      ai(t.netError);
      return;
    }
    sessionTokenRef.current = d.sessionToken;
    try { window.sessionStorage.setItem('otSessionToken', d.sessionToken); } catch (e) {}
    setAuthState('verified');
    if (d.task) {
      setTaskSummary(d.task);
      push({ kind: 'appointment', task: d.task });
    }
    // Already-booked guard: reopening a link for a task already booked through
    // the chat shows the booked card (with calendar / location / note actions)
    // and locks the slots flow — no re-booking. Otherwise, greet + offer slots.
    if (d.alreadyBooked) {
      setBooked(true);
      // Rehydrate the per-task "done" flags so the note/location actions reflect
      // what was already done (and the note stays capped at one).
      if (d.alreadyBooked.noteSent) setNoteSent(true);
      if (d.alreadyBooked.locationConfirmed) setLocConfirmed(true);
      ai(t.alreadyBookedIntro);
      push({ kind: 'success', booked: d.alreadyBooked });
      ai(t.etaInfo);
      return;
    }
    kickoffGreeting();
  }

  // Customer left a note for the technician → append it to the task notes.
  async function submitNote(text) {
    setShowNote(false);
    const d = await api('/api/note', {
      sessionToken: sessionTokenRef.current, text });
    if (d && d.ok) {
      setNoteSent(true);
      push({ kind: 'audit-note', text: t.noteSent });
    } else if (d && d.error === 'note_already_sent') {
      setNoteSent(true);
      ai(t.noteAlready);
    } else {
      ai(t.aiError);
    }
  }

  // Customer confirmed/adjusted the visit location on the map → write it back
  // to the task (non-blocking) and mark the appointment card as confirmed.
  function confirmLocation(g) {
    setGeo(g);
    setLocConfirmed(true);
    api('/api/location', { sessionToken: sessionTokenRef.current,
      lat: g.lat, lng: g.lng, address: g.address || '' });
  }

  /* ─── helpers: wire date + .ics calendar ─── */
  function parseWire(s) {
    if (!s || s.length < 12) return null;
    return new Date(+s.slice(0, 4), +s.slice(4, 6) - 1, +s.slice(6, 8),
                    +s.slice(8, 10), +s.slice(10, 12));
  }
  function fmtWindow(b) {
    const f = parseWire(b.fromDate), tt = parseWire(b.toDate);
    if (!f) return `${b.fromDate} → ${b.toDate}`;
    const day = f.toLocaleDateString(lang, { weekday: 'long', day: '2-digit', month: 'short' });
    const hh = (x) => x ? x.toLocaleTimeString(lang, { hour: '2-digit', minute: '2-digit' }) : '';
    return tt ? `${day} · ${hh(f)}–${hh(tt)}` : `${day} · ${hh(f)}`;
  }
  function addToCalendar(b) {
    const dt = (s) => s.slice(0, 8) + 'T' + s.slice(8, 12) + '00';
    const stamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
    const esc = (s) => String(s || '').replace(/([,;\\])/g, '\\$1').replace(/\n/g, '\\n');
    const ics = [
      'BEGIN:VCALENDAR', 'VERSION:2.0', 'CALSCALE:GREGORIAN',
      'PRODID:-//El Cuatro TV//Booking//ES',
      'BEGIN:VEVENT',
      'UID:' + b.fromDate + '-' + (convRef.current || 'elcuatro') + '@elcuatrotv',
      'DTSTAMP:' + stamp,
      'DTSTART:' + dt(b.fromDate), 'DTEND:' + dt(b.toDate),
      'SUMMARY:' + esc(t.calendarSummary || 'Visita técnica El Cuatro TV'),
      'LOCATION:' + esc((taskSummary && taskSummary.address) || ''),
      'END:VEVENT', 'END:VCALENDAR',
    ].join('\r\n');
    const url = URL.createObjectURL(new Blob([ics], { type: 'text/calendar;charset=utf-8' }));
    const a = document.createElement('a');
    a.href = url; a.download = 'visita-el-cuatro.ics';
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  // Hidden first turn so the assistant greets by name and offers slots.
  function kickoffGreeting() {
    handleUserText('Hola', undefined, undefined, { hidden: true });
  }

  /* ─── main pipeline ─── */
  async function handleUserText(text, displayText, image, opts) {
    if (busy) return;
    const hidden = opts && opts.hidden;
    const v = String(text || '').trim();
    if (!v && !image) return;

    setBusy(true);
    if (hidden) {
      setMessages(m => [...m, { kind: 'typing' }]);
    } else {
      const userMsg = image
        ? { kind: 'image', src: image, caption: displayText || (v || null) }
        : { kind: 'user', text: displayText || v };
      setMessages(m => [...m, userMsg, { kind: 'typing' }]);
    }

    const d = await api('/api/turn', {
      userPrompt: v,
      image: image || undefined,
      geo: geo || undefined,
      conversationId: convRef.current,
      lang,
      sessionToken: sessionTokenRef.current,
    });
    dropTyping();

    if (d && d.error === 'network') {
      ai(t.netError);
      setBusy(false);
      return;
    }
    if (d && d.error === 'unauthorized') {
      try { window.sessionStorage.removeItem('otSessionToken'); } catch (e) {}
      sessionTokenRef.current = null;
      setBusy(false);
      startSession();   // session expired → re-resolve the link
      return;
    }
    if (!d || d.ok === false) {
      ai(d && d.detail ? `${t.aiError} (${d.detail})` : t.aiError);
      setBusy(false);
      return;
    }

    if (d.conversationId) convRef.current = d.conversationId;
    const clean = window.cleanAiText ? window.cleanAiText(d.message) : (d.message || '');
    const hasSlots = Array.isArray(d.slots) && d.slots.length;
    if (d.booked) { setBooked(true); push({ kind: 'success', booked: d.booked }); ai(t.etaInfo); }
    else if (clean) ai(clean);
    else if (!d.pendingAction && !hasSlots) ai(t.aiClarify);

    if (hasSlots) {
      // Localize the chip labels to the UI language (the server formats them
      // in the C locale → English day names). We have fromDate/toDate.
      const hh = (x) => x ? x.toLocaleTimeString(lang, { hour: '2-digit', minute: '2-digit' }) : '';
      const locSlots = d.slots.map(s => {
        const f = parseWire(s.fromDate), tt = parseWire(s.toDate);
        return {
          ...s,
          label: f ? f.toLocaleDateString(lang, { weekday: 'short', day: '2-digit', month: 'short' }) : s.label,
          time: (f && tt) ? `${hh(f)}–${hh(tt)}` : (s.time || ''),
        };
      });
      push({ kind: 'slots', slots: locSlots });
    }
    if (d.pendingAction && d.pendingAction.summary) {
      // Localize the summary when we have the raw slot dates (server formats
      // day names in English); otherwise use the server's text.
      const pa = d.pendingAction;
      const summary = pa.fromDate
        ? (t.bookSummaryPrefix || '') + fmtWindow({ fromDate: pa.fromDate, toDate: pa.toDate })
        : pa.summary;
      push({ kind: 'pending-action', name: pa.name, summary });
    }
    if (d.auditLogged) push({ kind: 'audit-note', text: t.auditLogged });
    setBusy(false);
  }

  // Deterministic booking: tapping a slot arms the confirmation card directly
  // on the server (no LLM in the booking decision → reliable).
  async function pickSlot(s) {
    if (busy || !s.fromDate || !s.toDate) return;
    const label = [s.label, s.time].filter(Boolean).join(', ');
    setBusy(true);
    setMessages(m => [...m, { kind: 'user', text: label }, { kind: 'typing' }]);
    const d = await api('/api/book/propose', {
      sessionToken: sessionTokenRef.current,
      conversationId: convRef.current,
      fromDate: s.fromDate, toDate: s.toDate,
    });
    dropTyping();
    if (d && d.ok && d.pendingAction) {
      // Localized summary (server's strftime is English); we have the dates.
      const summary = (t.bookSummaryPrefix || '')
        + fmtWindow({ fromDate: s.fromDate, toDate: s.toDate });
      push({ kind: 'pending-action', name: d.pendingAction.name, summary });
    } else {
      ai(t.aiError);
    }
    setBusy(false);
  }

  function handlePendingDecision(decision) {
    const isConfirm = decision === 'confirm';
    handleUserText(
      isConfirm ? '__CONFIRM_PENDING__' : '__CANCEL_PENDING__',
      isConfirm ? t.confirmActionLabel : t.cancelActionLabel);
  }

  /* ─── composer: text + photo ─── */
  async function handlePickImage(file) {
    if (!file) return;
    try { setPendingImage(await downscaleImage(file)); }
    catch (e) { ai(t.aiError); }
  }

  function handleSend() {
    const v = composer.trim();
    const img = pendingImage;
    if (!v && !img) return;
    setComposer('');
    setPendingImage(null);
    handleUserText(v, undefined, img || undefined);
  }

  /* ─── voice: Web Speech API → composer ─── */
  // Push-to-talk voice (like a WhatsApp voice note): HOLD the mic to listen,
  // RELEASE to stop. The transcript is left in the input for the user to
  // review/edit — it is NOT auto-sent. Robust against the Web Speech engine
  // ending a segment mid-hold (we just restart it while still held).
  function beginVoiceSegment() {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) return;
    const rec = new SR();
    rec.lang = { es: 'es-AR', en: 'en-US', pt: 'pt-BR', he: 'he-IL' }[lang] || 'es-AR';
    rec.interimResults = true;
    rec.continuous = true;
    let segFinal = '';
    rec.onresult = (e) => {
      let interim = ''; segFinal = '';
      for (let i = 0; i < e.results.length; i++) {
        const r = e.results[i];
        if (r.isFinal) segFinal += r[0].transcript;
        else interim += r[0].transcript;
      }
      setComposer((voiceFinalRef.current + segFinal + interim).replace(/\s+/g, ' ').trimStart());
    };
    rec.onerror = () => {};
    rec.onend = () => {
      // Commit this segment's final text.
      voiceFinalRef.current = (voiceFinalRef.current + segFinal + ' ');
      if (holdingRef.current) {
        beginVoiceSegment();        // still held → keep listening seamlessly
      } else {
        setListening(false);
        setComposer(voiceFinalRef.current.replace(/\s+/g, ' ').trim());  // leave for review
      }
    };
    recRef.current = rec;
    try { rec.start(); } catch (e) {}
  }
  function startVoice() {
    if (holdingRef.current) return;
    if (!(window.SpeechRecognition || window.webkitSpeechRecognition)) return;
    holdingRef.current = true;
    voiceFinalRef.current = '';
    setComposer('');
    setListening(true);
    beginVoiceSegment();
  }
  function stopVoice() {
    if (!holdingRef.current) return;
    holdingRef.current = false;     // onend won't restart → text stays in input
    try { recRef.current && recRef.current.stop(); } catch (e) {}
  }

  function reset() {
    try {
      if (convRef.current && sessionTokenRef.current) {
        api('/api/session/close', {
          conversationId: convRef.current,
          sessionToken: sessionTokenRef.current, reason: 'reset' });
      }
    } catch (e) {}
    setBusy(false);
    convRef.current = null;
    setComposer('');
    setPendingImage(null);
    setGeo(null); setShowMap(false);
    setTaskSummary(null); setLocConfirmed(false);
    setShowNote(false); setNoteSent(false); setBooked(false);
    try { recRef.current && recRef.current.stop(); } catch (e) {}
    setListening(false);
    try { window.sessionStorage.removeItem('otSessionToken'); } catch (e) {}
    sessionTokenRef.current = null;
    setMessages([]);
    startSession();   // re-open the chat from the link
  }

  /* ─── render ─── */
  function renderMessage(m, i) {
    const live = i === messages.length - 1 && !busy;
    // Slot chips and the confirm card stay interactive based on state (busy /
    // already booked), NOT on message position — so a note/location badge
    // pushed afterwards doesn't grey them out.
    const canBook = !busy && !booked;
    if (m.kind === 'user')
      return <ChatMsg key={i} from="me" brandColor={brand.primary}>{m.text}</ChatMsg>;
    if (m.kind === 'ai')
      return <ChatMsg key={i} from="ai" brandColor={brand.primary}><RichText text={m.text}/></ChatMsg>;
    if (m.kind === 'image')
      return (
        <ChatMsg key={i} from="me" brandColor={brand.primary}>
          <img src={m.src} alt="" style={{ maxWidth: '200px', borderRadius: 10, display: 'block' }}/>
          {m.caption && <div style={{ marginTop: 4, fontSize: 13 }}>{m.caption}</div>}
        </ChatMsg>
      );
    if (m.kind === 'typing')
      return <TypingDots key={i} brandColor={brand.primary}/>;
    if (m.kind === 'audit-note')
      return (
        <div key={i} style={{ alignSelf: 'center', margin: '4px 0',
          padding: '4px 10px', borderRadius: 999,
          background: 'rgba(21,41,107,0.10)', color: brand.primary,
          fontSize: 12, fontWeight: 500 }}>{m.text}</div>
      );
    if (m.kind === 'slots')
      return (
        <div key={i} className="nc-card-wrap">
          <SlotChips slots={m.slots} disabled={!canBook}
                     onPick={(s) => canBook && pickSlot(s)}/>
        </div>
      );
    if (m.kind === 'buttons') // only used by the multi-account picker
      return (
        <div key={i} className="nc-card-wrap">
          <SuggestionChips
            items={m.buttons.map((b, k) => ({ id: 'b' + k, label: b }))}
            brandColor={brand.primary}
            onPick={(it) => live && handleUserText(it.label)}/>
        </div>
      );
    if (m.kind === 'appointment')
      return (
        <div key={i} className="nc-card-wrap">
          <AppointmentCard t={t} task={m.task} brandColor={brand.primary}/>
        </div>
      );
    if (m.kind === 'success')
      return (
        <div key={i} className="nc-card-wrap">
          <SuccessCard t={t} windowLabel={fmtWindow(m.booked)}
            address={taskSummary && taskSummary.address}
            brandColor={brand.primary}
            onAddToCalendar={() => addToCalendar(m.booked)}
            hasLocation={!!(taskSummary && taskSummary.x && taskSummary.y)}
            onLocation={() => setShowMap(true)} locConfirmed={locConfirmed}
            onNote={() => setShowNote(true)} noteSent={noteSent}/>
        </div>
      );
    if (m.kind === 'pending-action') {
      const isCancel = m.name === 'cancelAppointment';
      const accent = isCancel ? '#C03A2B' : brand.primary;
      return (
        <div key={i} className="nc-pending-card" style={{
          margin: '8px 0 6px', padding: '14px 16px', borderRadius: 14,
          border: `2px solid ${accent}`, background: 'rgba(255,255,255,0.9)',
          boxShadow: '0 2px 8px rgba(0,0,0,0.06)', alignSelf: 'stretch' }}>
          <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 0.4,
            textTransform: 'uppercase', color: accent, marginBottom: 6 }}>
            {isCancel ? t.confirmCancelTitle : t.confirmRescheduleTitle}
          </div>
          <div style={{ fontSize: 15, color: '#1F224A', lineHeight: 1.4,
            fontWeight: 500, marginBottom: 12 }}>{m.summary}</div>
          {/* Location confirm lives ONLY on the appointment card above —
              no duplicate here. */}
          <div style={{ display: 'flex', gap: 10 }}>
            <button disabled={!canBook} onClick={() => canBook && handlePendingDecision('confirm')}
              style={{ flex: 1, padding: '12px 14px', borderRadius: 10, border: 'none',
                cursor: canBook ? 'pointer' : 'default', background: accent, color: '#fff',
                fontSize: 14, fontWeight: 600, opacity: canBook ? 1 : 0.5 }}>
              {t.confirmActionLabel}
            </button>
            <button disabled={!canBook} onClick={() => canBook && handlePendingDecision('cancel')}
              style={{ flex: 1, padding: '12px 14px', borderRadius: 10,
                border: '1.5px solid #C9CBD9', cursor: canBook ? 'pointer' : 'default',
                background: '#fff', color: '#4A4D6E', fontSize: 14, fontWeight: 500,
                opacity: canBook ? 1 : 0.5 }}>
              {t.cancelActionLabel}
            </button>
          </div>
        </div>
      );
    }
    return null;
  }

  return (
    <div className="nc-screen" dir={t.dir} data-lang={lang}>
      {/* El Cuatro is a single Spanish-speaking tenant: language is locked to
          ES (no toggle) and the Prapii-era "start over" reset is removed. The
          full i18n (EN/PT/HE) stays in the code for other tenants. */}
      <ChatHeader t={t} lang={lang}
                  brandColor={brand.headerBg} brandName={brand.name}/>
      <div className="nc-chat-scroll" ref={scrollRef}>
        <div className="nc-chat-inner">
          {messages.map(renderMessage)}
        </div>
        <div className="nc-powered">{t.poweredBy}</div>
      </div>

      <Composer t={t} value={composer} onChange={setComposer}
                onSend={handleSend} brandColor={brand.primary}
                disabled={busy || authState !== 'verified' || booked}
                allowImage={false}
                pendingImage={pendingImage}
                onPickImage={handlePickImage}
                onClearImage={() => setPendingImage(null)}
                voiceSupported={!!voiceSupported}
                listening={listening}
                onVoiceStart={startVoice}
                onVoiceStop={stopVoice}/>

      <LocationSheet t={t} open={showMap} brandColor={brand.primary}
                     initialCoords={taskSummary && taskSummary.y && taskSummary.x
                       ? { lat: parseFloat(taskSummary.y), lng: parseFloat(taskSummary.x) }
                       : null}
                     onConfirm={(g) => { confirmLocation(g); setShowMap(false); }}
                     onCancel={() => setShowMap(false)}/>

      <NoteSheet t={t} open={showNote} brandColor={brand.primary}
                 onSend={submitNote} onCancel={() => setShowNote(false)}/>
    </div>
  );
}

function composerT(t) {
  return t;
}

Object.assign(window, { ChatSurface, BRAND_PRESETS });
