// Helvia Inbox PWA
const { useState, useEffect, useCallback, useRef, useMemo, createContext, useContext } = React;

const CONFIG = {
  API_URL: (() => {
    const p = new URLSearchParams(location.search);
    if (p.get('api')) return p.get('api').replace(/\/$/, '');
    if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
      return 'http://127.0.0.1:8787';
    }
    return 'https://api.vocal.ch';
  })(),
};

const STORAGE_TOKEN = 'helvia_live_token';
const STORAGE_USER  = 'helvia_live_user';

// ==================== Swipe-back gesture ====================
// Détecte un swipe horizontal de gauche -> droite démarrant près du bord gauche
// de l'écran et appelle onBack() à la fin du geste. Pour vues type "détail".
function useSwipeBack(onBack, options = {}) {
  const {
    edgeWidth = 28,    // zone de déclenchement depuis le bord gauche (px)
    minDistance = 70,  // distance minimale à parcourir (px)
    maxVertical = 60,  // tolérance verticale (px) pendant le swipe
    maxDuration = 600, // durée max du geste (ms)
  } = options;

  useEffect(() => {
    if (!onBack) return;
    let startX = 0, startY = 0, startTime = 0, tracking = false;

    const onStart = (e) => {
      const t = e.touches && e.touches[0];
      if (!t) return;
      if (t.clientX > edgeWidth) return;
      startX = t.clientX;
      startY = t.clientY;
      startTime = Date.now();
      tracking = true;
    };
    const onMove = (e) => {
      if (!tracking) return;
      const t = e.touches && e.touches[0];
      if (!t) return;
      if (Math.abs(t.clientY - startY) > maxVertical) tracking = false;
    };
    const onEnd = (e) => {
      if (!tracking) return;
      tracking = false;
      const t = e.changedTouches && e.changedTouches[0];
      if (!t) return;
      const dx = t.clientX - startX;
      const dy = Math.abs(t.clientY - startY);
      const dt = Date.now() - startTime;
      if (dx >= minDistance && dy <= maxVertical && dt <= maxDuration) {
        onBack();
      }
    };

    document.addEventListener('touchstart', onStart, { passive: true });
    document.addEventListener('touchmove', onMove, { passive: true });
    document.addEventListener('touchend', onEnd, { passive: true });
    document.addEventListener('touchcancel', onEnd, { passive: true });
    return () => {
      document.removeEventListener('touchstart', onStart);
      document.removeEventListener('touchmove', onMove);
      document.removeEventListener('touchend', onEnd);
      document.removeEventListener('touchcancel', onEnd);
    };
  }, [onBack, edgeWidth, minDistance, maxVertical, maxDuration]);
}

// ==================== API ====================
const api = {
  token: null,
  async req(path, opts = {}) {
    const headers = { 'Content-Type': 'application/json', ...opts.headers };
    if (this.token) headers.Authorization = `Bearer ${this.token}`;
    const resp = await fetch(`${CONFIG.API_URL}${path}`, { ...opts, headers });
    if (resp.status === 401 && this.token) {
      // Session perdue — purge.
      localStorage.removeItem(STORAGE_TOKEN);
      localStorage.removeItem(STORAGE_USER);
      api.token = null;
      window.dispatchEvent(new CustomEvent('helvia-live-logout'));
    }
    try { return await resp.json(); } catch { return { error: 'invalid response' }; }
  },
  get(p)         { return api.req(p); },
  post(p, b)     { return api.req(p, { method: 'POST', body: JSON.stringify(b || {}) }); },
};

// ==================== VOICE (Twilio Voice SDK) ====================
// Le device Twilio se connecte avec un token recupere via /voice/token (API Helvia).
// Si l'endpoint n'existe pas encore, on bascule en mode demo (UI sans WebRTC).
const VoiceCtx = createContext(null);
const useVoice = () => useContext(VoiceCtx) || {};

const VOICE_STATUS_LABEL = {
  offline:     'Hors ligne',
  connecting:  'Connexion…',
  ready:       'Prêt',
  ringing:     'Appel entrant',
  'in-call':   'En appel',
  error:       'Erreur',
  demo:        'Mode démo',
};

function VoiceProvider({ user, notify, children }) {
  const [status, setStatus] = useState('offline');
  const [incoming, setIncoming] = useState(null);
  const [active, setActive] = useState(null);
  const [muted, setMuted] = useState(false);
  const [duration, setDuration] = useState(0);
  const [identity, setIdentity] = useState(null);
  const [demoMode, setDemoMode] = useState(false);
  const deviceRef = useRef(null);
  const timerRef = useRef(null);
  const startRef = useRef(null);

  const cleanup = useCallback(() => {
    if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    setActive(null); setIncoming(null); setMuted(false); setDuration(0);
    startRef.current = null;
    setStatus(deviceRef.current ? 'ready' : (demoMode ? 'demo' : 'offline'));
  }, [demoMode]);

  const startTimer = useCallback(() => {
    startRef.current = Date.now();
    setDuration(0);
    timerRef.current = setInterval(() => {
      setDuration(Math.floor((Date.now() - startRef.current) / 1000));
    }, 1000);
  }, []);

  const init = useCallback(async () => {
    if (!user) return;
    if (!window.Twilio) { setStatus('error'); return; }
    setStatus('connecting');
    try {
      const data = await api.get('/voice/token');
      if (!data || data.error || !data.token) {
        // Endpoint pas encore branché côté API → on bascule en mode démo
        // (l'UI reste fonctionnelle, les boutons d'appel afficheront un toast).
        setDemoMode(true);
        setStatus('demo');
        setIdentity(user.email || 'demo');
        return;
      }
      setIdentity(data.identity || user.email);
      if (deviceRef.current) { deviceRef.current.destroy(); deviceRef.current = null; }
      const device = new window.Twilio.Device(data.token, {
        logLevel: 1,
        codecPreferences: ['opus', 'pcmu'],
        closeProtection: 'Un appel est en cours. Voulez-vous quitter ?',
        edge: 'dublin',
      });
      device.on('registered',  () => setStatus('ready'));
      device.on('unregistered', () => setStatus('offline'));
      device.on('error', (err) => {
        setStatus('error');
        const msg = err?.message || String(err);
        notify && notify(`Erreur voix : ${msg}`, 'error');
      });
      device.on('tokenWillExpire', async () => {
        try {
          const r = await api.get('/voice/token');
          if (r?.token) device.updateToken(r.token);
        } catch (_) {}
      });
      device.on('incoming', (call) => {
        const from = call.parameters?.From || 'Inconnu';
        const callSid = call.parameters?.CallSid || '';
        setStatus('ringing');
        setIncoming({ call, from, callSid, time: new Date() });
        call.on('cancel',     () => cleanup());
        call.on('disconnect', () => cleanup());
        call.on('reject',     () => cleanup());
        call.on('error',      () => cleanup());
      });
      await device.register();
      deviceRef.current = device;
    } catch (e) {
      setDemoMode(true);
      setStatus('demo');
    }
  }, [user, notify, cleanup]);

  // Auto-init dès qu'un utilisateur est connecté.
  useEffect(() => {
    if (user) init();
    return () => {
      if (deviceRef.current) { try { deviceRef.current.destroy(); } catch(_){} deviceRef.current = null; }
      if (timerRef.current) clearInterval(timerRef.current);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [user]);

  const accept = useCallback(() => {
    if (!incoming?.call) return;
    incoming.call.accept();
    setActive(incoming);
    setIncoming(null);
    setStatus('in-call');
    startTimer();
  }, [incoming, startTimer]);

  const reject = useCallback(() => {
    if (incoming?.call) try { incoming.call.reject(); } catch(_){}
    cleanup();
  }, [incoming, cleanup]);

  const hangup = useCallback(() => {
    if (active?.call) try { active.call.disconnect(); } catch(_){}
    cleanup();
  }, [active, cleanup]);

  const toggleMute = useCallback(() => {
    if (!active?.call) return;
    const next = !muted;
    try { active.call.mute(next); setMuted(next); } catch(_){}
  }, [active, muted]);

  const sendDigit = useCallback((d) => {
    if (active?.call) try { active.call.sendDigits(String(d)); } catch(_){}
  }, [active]);

  const makeCall = useCallback(async (to) => {
    if (!to) return { error: 'Numéro requis' };
    // Normalisation E.164 légère
    let dst = String(to).trim();
    if (dst.startsWith('00')) dst = '+' + dst.slice(2);
    if (!dst.startsWith('+')) dst = '+' + dst.replace(/[^\d]/g, '');
    if (demoMode || !deviceRef.current) {
      notify && notify('Voix non configurée — appel simulé', 'info');
      return { error: 'voice_not_configured' };
    }
    try {
      const call = await deviceRef.current.connect({ params: { To: dst } });
      const wrapped = { call, from: dst, to: dst, time: new Date(), outgoing: true };
      setActive(wrapped);
      setStatus('in-call');
      startTimer();
      call.on('disconnect', () => cleanup());
      call.on('cancel',     () => cleanup());
      call.on('error',      () => cleanup());
      return { ok: true };
    } catch (e) {
      return { error: e.message || 'Échec appel' };
    }
  }, [demoMode, startTimer, cleanup, notify]);

  const value = useMemo(() => ({
    status, incoming, active, muted, duration, identity, demoMode,
    init, accept, reject, hangup, toggleMute, sendDigit, makeCall,
  }), [status, incoming, active, muted, duration, identity, demoMode,
       init, accept, reject, hangup, toggleMute, sendDigit, makeCall]);

  return <VoiceCtx.Provider value={value}>{children}</VoiceCtx.Provider>;
}

function IncomingCallOverlay() {
  const { incoming, accept, reject } = useVoice();
  if (!incoming) return null;
  return (
    <div className="call-overlay">
      <div className="call-overlay-bg" />
      <div className="call-overlay-content">
        <div className="call-overlay-pulse" />
        <div className="call-overlay-avatar">{initials(incoming.from || '?')}</div>
        <div className="call-overlay-label">Appel entrant</div>
        <div className="call-overlay-num">{incoming.from}</div>
        <div className="call-overlay-actions">
          <button className="call-btn call-btn-reject" onClick={reject} aria-label="Refuser">
            <Icons.Phone />
          </button>
          <button className="call-btn call-btn-accept" onClick={accept} aria-label="Répondre">
            <Icons.Phone />
          </button>
        </div>
      </div>
    </div>
  );
}

function ActiveCallBar() {
  const { active, duration, muted, hangup, toggleMute } = useVoice();
  if (!active) return null;
  const mm = String(Math.floor(duration / 60)).padStart(2, '0');
  const ss = String(duration % 60).padStart(2, '0');
  return (
    <div className="active-call-bar safe-top">
      <div className="acb-dot" />
      <div className="acb-meta">
        <div className="acb-num">{active.outgoing ? '→ ' : '← '}{active.from || active.to}</div>
        <div className="acb-time">{mm}:{ss}</div>
      </div>
      <button className={`acb-btn ${muted ? 'on' : ''}`} onClick={toggleMute} aria-label="Couper micro">
        {muted ? '🔇' : '🎙️'}
      </button>
      <button className="acb-btn acb-hangup" onClick={hangup} aria-label="Raccrocher">
        <Icons.Phone />
      </button>
    </div>
  );
}

// ==================== ICONS ====================
const Icons = {
  Helvia: ({ size = 22 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none">
      <rect x="10" y="3" width="4" height="18" fill="currentColor"/>
      <rect x="3" y="10" width="18" height="4" fill="currentColor"/>
    </svg>
  ),
  WA: ({ size = 22 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
      <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.435 9.884-9.884 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" />
    </svg>
  ),
  Back: () => <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M19 12H5M12 19l-7-7 7-7" /></svg>,
  Search: () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" /></svg>,
  More: () => <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2" /><circle cx="12" cy="12" r="2" /><circle cx="12" cy="19" r="2" /></svg>,
  Refresh: () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="23 4 23 10 17 10" /><polyline points="1 20 1 14 7 14" /><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15" /></svg>,
  Send: () => <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>,
  Template: () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M8 14h8M8 17h5"/></svg>,
  Check: () => <svg width="14" height="14" viewBox="0 0 18 18" fill="currentColor"><path d="M14.27 4.31L7.49 11.09 5.62 9.23l-.71.71 2.58 2.58 7.49-7.49z"/></svg>,
  DoubleCheck: () => <svg width="16" height="16" viewBox="0 0 18 18" fill="currentColor"><path d="M11.27 4.31L4.49 11.09 2.62 9.23l-.71.71 2.58 2.58 7.49-7.49z"/><path d="M16.27 4.31L9.49 11.09l-1-1-.71.71 1.71 1.71 7.49-7.49z"/></svg>,
  Logout: () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9" /></svg>,
  Phone: () => <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M6.62 10.79a15.05 15.05 0 0 0 6.59 6.59l2.2-2.2a1 1 0 0 1 1.02-.24c1.12.37 2.33.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.45.57 3.57a1 1 0 0 1-.25 1.02l-2.2 2.2z"/></svg>,
  Bot: () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="6" width="18" height="14" rx="3" /><circle cx="9" cy="13" r="1.2" /><circle cx="15" cy="13" r="1.2" /><path d="M12 2v4M8 18h8" /></svg>,
  Chat: () => <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z" /></svg>,
  Archive: () => <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="21 8 21 21 3 21 3 8" /><rect x="1" y="3" width="22" height="5" rx="1" /><line x1="10" y1="12" x2="14" y2="12" /></svg>,
  Plus: () => <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M12 5v14M5 12h14" /></svg>,
  Edit: () => <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>,
  Dialpad: () => <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="6" cy="6" r="1.2"/><circle cx="12" cy="6" r="1.2"/><circle cx="18" cy="6" r="1.2"/><circle cx="6" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="18" cy="12" r="1.2"/><circle cx="6" cy="18" r="1.2"/><circle cx="12" cy="18" r="1.2"/><circle cx="18" cy="18" r="1.2"/></svg>,
  UserPlus: () => <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M16 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="8.5" cy="7" r="4"/><path d="M20 8v6M23 11h-6"/></svg>,
  Users: () => <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" /></svg>,
  Globe: () => <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><path d="M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" /></svg>,
  WA: () => <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M17.5 14.4c-.3-.2-1.8-.9-2.1-1-.3-.1-.5-.2-.7.2-.2.3-.8 1-1 1.2-.2.2-.4.2-.7.1-.3-.2-1.3-.5-2.4-1.5-.9-.8-1.5-1.8-1.7-2.1-.2-.3 0-.5.1-.7.1-.1.3-.4.5-.5.1-.2.2-.3.3-.5.1-.2.1-.4 0-.5-.1-.2-.7-1.7-1-2.3-.3-.6-.5-.5-.7-.5h-.6c-.2 0-.5.1-.8.4-.3.3-1.1 1.1-1.1 2.6 0 1.5 1.1 3 1.3 3.2.2.2 2.2 3.4 5.4 4.8.8.3 1.4.5 1.8.6.8.2 1.5.2 2 .1.6-.1 1.8-.7 2-1.4.2-.7.2-1.3.2-1.4-.1-.1-.3-.2-.6-.3zM12 2C6.5 2 2 6.5 2 12c0 1.7.4 3.3 1.2 4.7L2 22l5.4-1.4c1.4.7 2.9 1.1 4.6 1.1 5.5 0 10-4.5 10-10S17.5 2 12 2z"/></svg>,
  Star: ({ filled }) => <svg width="18" height="18" viewBox="0 0 24 24" fill={filled ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" /></svg>,
  PhoneIn: () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path fill="currentColor" stroke="none" d="M6.62 10.79a15.05 15.05 0 0 0 6.59 6.59l2.2-2.2a1 1 0 0 1 1.02-.24c1.12.37 2.33.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.45.57 3.57a1 1 0 0 1-.25 1.02l-2.2 2.2z"/><path d="M19 3l-7 7"/><path d="M12 4v6h6"/></svg>,
  PhoneOut: () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path fill="currentColor" stroke="none" d="M6.62 10.79a15.05 15.05 0 0 0 6.59 6.59l2.2-2.2a1 1 0 0 1 1.02-.24c1.12.37 2.33.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.45.57 3.57a1 1 0 0 1-.25 1.02l-2.2 2.2z"/><path d="M12 10l7-7"/><path d="M13 3h6v6"/></svg>,
  PhoneMissed: () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path fill="currentColor" stroke="none" d="M6.62 10.79a15.05 15.05 0 0 0 6.59 6.59l2.2-2.2a1 1 0 0 1 1.02-.24c1.12.37 2.33.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.45.57 3.57a1 1 0 0 1-.25 1.02l-2.2 2.2z"/><path d="M13 3l7 7M20 3l-7 7"/></svg>,
  Voicemail: () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="6" cy="12" r="4" /><circle cx="18" cy="12" r="4" /><line x1="10" y1="12" x2="14" y2="12" /></svg>,
  Settings: () => <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09a1.65 1.65 0 00-1-1.51 1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09a1.65 1.65 0 001.51-1 1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06a1.65 1.65 0 001.82.33h0a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51h0a1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82v0a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" /></svg>,
  ArchiveBox: ({ size = 36 }) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><polyline points="21 8 21 21 3 21 3 8" /><rect x="1" y="3" width="22" height="5" rx="1" /><line x1="10" y1="12" x2="14" y2="12" /></svg>,
  Devices: () => <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="4" width="14" height="11" rx="2"/><rect x="14" y="9" width="8" height="11" rx="1.5"/><line x1="5" y1="18" x2="13" y2="18"/></svg>,
  QrCode: () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><path d="M14 14h3v3h-3zM18 18h3v3h-3zM14 19h3"/></svg>,
  Trash: () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>,
};

// ==================== SKELETON LOADER ====================
// Placeholder animé pour les écrans en cours de chargement. Le shimmer
// est défini en CSS (.sk, .sk-shimmer). Le composant Skeleton dessine
// une ou plusieurs lignes type "conversation" / "appel" / "stat".
function Skeleton({ variant = 'row', count = 6 }) {
  const items = Array.from({ length: count });
  if (variant === 'row') {
    return (
      <div className="sk-list" aria-busy="true" aria-label="Chargement…">
        {items.map((_, i) => (
          <div key={i} className="sk-row">
            <div className="sk sk-avatar" />
            <div className="sk-row-body">
              <div className="sk-row-line1">
                <div className="sk sk-line" style={{ width: `${45 + (i * 7) % 35}%` }} />
                <div className="sk sk-line sk-small" style={{ width: 38 }} />
              </div>
              <div className="sk sk-line sk-faint" style={{ width: `${55 + (i * 11) % 30}%`, marginTop: 8 }} />
            </div>
          </div>
        ))}
      </div>
    );
  }
  if (variant === 'call') {
    return (
      <div className="sk-list" aria-busy="true" aria-label="Chargement…">
        {items.map((_, i) => (
          <div key={i} className="sk-row">
            <div className="sk sk-icon" />
            <div className="sk-row-body">
              <div className="sk sk-line" style={{ width: `${40 + (i * 9) % 30}%` }} />
              <div className="sk sk-line sk-faint" style={{ width: `${30 + (i * 5) % 25}%`, marginTop: 6 }} />
            </div>
            <div className="sk sk-line sk-small" style={{ width: 32 }} />
          </div>
        ))}
      </div>
    );
  }
  if (variant === 'detail') {
    return (
      <div className="sk-detail" aria-busy="true" aria-label="Chargement…">
        <div className="sk-detail-head">
          <div className="sk sk-avatar sk-avatar-lg" />
          <div className="sk sk-line" style={{ width: 160, marginTop: 14 }} />
          <div className="sk sk-line sk-faint" style={{ width: 110, marginTop: 8 }} />
        </div>
        <div className="sk-stats">
          {Array.from({ length: 4 }).map((_, i) => (
            <div key={i} className="sk-stat">
              <div className="sk sk-line sk-faint sk-small" style={{ width: '60%' }} />
              <div className="sk sk-line" style={{ width: '40%', height: 18, marginTop: 8 }} />
            </div>
          ))}
        </div>
        <Skeleton variant="row" count={3} />
      </div>
    );
  }
  if (variant === 'devices') {
    return (
      <div className="sk-list" aria-busy="true" aria-label="Chargement…">
        {items.slice(0, 3).map((_, i) => (
          <div key={i} className="sk-row">
            <div className="sk sk-icon" />
            <div className="sk-row-body">
              <div className="sk sk-line" style={{ width: '50%' }} />
              <div className="sk sk-line sk-faint" style={{ width: '70%', marginTop: 6 }} />
            </div>
          </div>
        ))}
      </div>
    );
  }
  return null;
}

// ==================== ARCHIVES (local) ====================
const STORAGE_ARCHIVED = 'helvia_live_archived';
const loadArchived = () => {
  try { return new Set(JSON.parse(localStorage.getItem(STORAGE_ARCHIVED) || '[]')); }
  catch { return new Set(); }
};
const saveArchived = (set) => {
  localStorage.setItem(STORAGE_ARCHIVED, JSON.stringify([...set]));
};

// ==================== UTILS ====================
function fmtTime(d) {
  if (!d) return '';
  const dt = new Date(d.includes('T') ? d : d.replace(' ', 'T') + 'Z');
  const now = new Date();
  const sameDay = dt.toDateString() === now.toDateString();
  if (sameDay) return dt.toLocaleTimeString('fr-CH', { hour: '2-digit', minute: '2-digit' });
  const diffDays = Math.floor((now - dt) / (24 * 3600 * 1000));
  if (diffDays < 7) return dt.toLocaleDateString('fr-CH', { weekday: 'short' });
  return dt.toLocaleDateString('fr-CH', { day: '2-digit', month: '2-digit' });
}
function fmtDay(d) {
  if (!d) return '';
  const dt = new Date(d.includes('T') ? d : d.replace(' ', 'T') + 'Z');
  const today = new Date();
  const y = new Date(); y.setDate(y.getDate() - 1);
  if (dt.toDateString() === today.toDateString()) return "Aujourd'hui";
  if (dt.toDateString() === y.toDateString()) return 'Hier';
  return dt.toLocaleDateString('fr-CH', { weekday: 'long', day: 'numeric', month: 'long' });
}
function initials(name) {
  if (!name) return '?';
  const parts = name.trim().split(/\s+/);
  if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
  return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
function avatarVariant(s) {
  if (!s) return '';
  let h = 0;
  for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
  return ['', 'v1', 'v2', 'v3', 'v4'][Math.abs(h) % 5];
}
function fmtPhone(n) {
  if (!n) return '';
  const s = String(n).replace(/[^\d+]/g, '');
  // Suisse : +41 XX XXX XX XX
  if (s.startsWith('+41') && s.length === 12) {
    return `+41 ${s.slice(3, 5)} ${s.slice(5, 8)} ${s.slice(8, 10)} ${s.slice(10, 12)}`;
  }
  // France : +33 X XX XX XX XX
  if (s.startsWith('+33') && s.length === 12) {
    return `+33 ${s.slice(3, 4)} ${s.slice(4, 6)} ${s.slice(6, 8)} ${s.slice(8, 10)} ${s.slice(10, 12)}`;
  }
  // Générique international : +XX(X) puis groupes de 2-3
  if (s.startsWith('+') && s.length > 7) {
    const cc = s.slice(0, 3);
    const rest = s.slice(3);
    const groups = [];
    let i = 0;
    if (rest.length % 2 === 1) { groups.push(rest.slice(0, 3)); i = 3; }
    for (; i < rest.length; i += 2) groups.push(rest.slice(i, i + 2));
    return `${cc} ${groups.join(' ')}`.trim();
  }
  return s;
}
function fmtDuration(sec) {
  if (!sec) return '—';
  const m = Math.floor(sec / 60); const s = sec % 60;
  return m ? `${m}m${s ? String(s).padStart(2, '0') + 's' : ''}` : `${s}s`;
}
function parseTs(d) {
  if (!d) return 0;
  return new Date(d.includes('T') ? d : d.replace(' ', 'T') + 'Z').getTime() || 0;
}

const CHANNEL_META = {
  whatsapp: { label: 'WhatsApp', css: 'ch-wa' },
  widget: { label: 'Widget web', css: 'ch-widget' },
  messenger: { label: 'Messenger', css: 'ch-msg' },
};
const CHANNEL_FILTERS = [
  { id: 'all', label: 'Tous', icon: 'Chat' },
  { id: 'whatsapp', label: 'WhatsApp', icon: 'WA' },
  { id: 'widget', label: 'Widget', icon: 'Globe' },
];

function normalizeWaConv(c) {
  return {
    uid: `wa:${c.id}`,
    channel: 'whatsapp',
    id: c.id,
    contact_name: c.contact_name || c.contact_number,
    contact_number: c.contact_number,
    last_message_body: c.last_message_body,
    last_message_at: c.last_message_at,
    unread_count: c.unread_count || 0,
    bot_enabled: c.bot_enabled,
    line_phone: c.line_phone,
    _raw: c,
  };
}
function normalizeWidgetConv(c, widget) {
  let preview = 'Visiteur web';
  try { if (c.origin) preview = new URL(c.origin).hostname; } catch {}
  return {
    uid: `widget:${widget.id}:${c.id}`,
    channel: 'widget',
    id: c.id,
    widgetId: widget.id,
    widgetName: widget.name,
    contact_name: preview,
    contact_number: (c.visitor_id || 'visiteur').slice(0, 12),
    last_message_body: `${c.messages_count || 0} message${(c.messages_count || 0) > 1 ? 's' : ''}`,
    last_message_at: c.last_message_at,
    unread_count: 0,
    bot_enabled: 1,
    line_phone: widget.phone_number,
    _raw: c,
  };
}
async function fetchUnifiedConversations(lineId) {
  const waQ = lineId ? `?line_id=${lineId}` : '';
  const [waR, widgetsR] = await Promise.all([
    api.get(`/my/whatsapp/conversations${waQ}`),
    api.get('/my/widgets'),
  ]);
  const wa = (waR?.conversations || []).map(normalizeWaConv);
  const widgets = widgetsR?.widgets || [];
  const widgetLists = await Promise.all(
    widgets.map(w => api.get(`/my/widgets/${w.id}/conversations`).then(r => ({ w, convs: r?.conversations || [] })).catch(() => ({ w, convs: [] })))
  );
  const widgetConvs = widgetLists.flatMap(({ w, convs }) => convs.map(c => normalizeWidgetConv(c, w)));
  return [...wa, ...widgetConvs].sort((a, b) => parseTs(b.last_message_at) - parseTs(a.last_message_at));
}

const STORAGE_FAVORITES = 'helvia_live_favorites';
const loadFavorites = () => {
  try { return new Set(JSON.parse(localStorage.getItem(STORAGE_FAVORITES) || '[]')); }
  catch { return new Set(); }
};
const saveFavorites = (set) => localStorage.setItem(STORAGE_FAVORITES, JSON.stringify([...set]));

const CALL_FILTERS = [
  { id: 'all', label: 'Tous', icon: 'Phone' },
  { id: 'inbound', label: 'Reçus', icon: 'PhoneIn' },
  { id: 'outbound', label: 'Émis', icon: 'PhoneOut' },
  { id: 'missed', label: 'Manqués', icon: 'PhoneMissed' },
];
const CALL_STATUS_LABEL = {
  completed: 'Terminé', ringing: 'Sonne', 'in-progress': 'En cours',
  busy: 'Occupé', 'no-answer': 'Sans réponse', failed: 'Échoué',
  canceled: 'Annulé', missed: 'Manqué',
};
function callPeer(call) {
  return call.direction === 'inbound' ? (call.from_number || '—') : (call.to_number || '—');
}
function isMissedCall(call) {
  return ['missed', 'no-answer', 'busy', 'canceled'].includes(call.status);
}

// ==================== TOAST ====================
const AppCtx = createContext(null);
const useApp = () => useContext(AppCtx);

function Toast({ msg, type, onDone }) {
  useEffect(() => {
    const delay = type === 'error' ? 6000 : 3500;
    const t = setTimeout(onDone, delay);
    return () => clearTimeout(t);
  }, [onDone, type]);
  return <div className={`toast ${type || ''}`} onClick={onDone}>{msg}</div>;
}

// ==================== TOP BAR ====================
function TopBarWithLineSelector({ subtitle, right }) {
  const { lines, lineId, setLineId, embed } = useApp();
  const [open, setOpen] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    if (!open) return;
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    document.addEventListener('touchstart', h);
    return () => {
      document.removeEventListener('mousedown', h);
      document.removeEventListener('touchstart', h);
    };
  }, [open]);

  const current = lines.find(l => l.id === lineId) || lines[0] || null;

  // Auto-sélection : si pas de ligne choisie mais des lignes existent, prend la 1ère.
  // En embed, ne pas reselectionner si la ligne forcee n'est pas encore dans `lines`.
  useEffect(() => {
    if (embed) return;
    if (!lineId && lines.length > 0) setLineId(lines[0].id);
  }, [lines, lineId, embed]);

  // En embed, on masque completement la topbar (pas de selecteur, pas de titre).
  if (embed) return null;

  const fmtLineSub = (l) => {
    if (!l) return '—';
    return l.label || l.description || l.line_label || 'Ligne WhatsApp';
  };

  return (
    <div className="topbar gradient safe-top" ref={ref}>
      <div className="safe-extend" />
      <button className="topbar-line-btn" onClick={() => setOpen(o => !o)} disabled={lines.length <= 1}>
        <div className="topbar-line-icon"><Icons.Phone /></div>
        <div className="topbar-line-text">
          <div className="topbar-line-num">{current ? (current.phone_number || '—') : 'Aucune ligne'}</div>
          <div className="topbar-line-sub">
            {current ? fmtLineSub(current) : (subtitle || '')}
            {subtitle && current ? ` · ${subtitle}` : ''}
          </div>
        </div>
        {lines.length > 1 && (
          <svg className={`topbar-line-chevron ${open ? 'open' : ''}`} width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
            <polyline points="6 9 12 15 18 9" />
          </svg>
        )}
      </button>
      {right && <div className="topbar-actions">{right}</div>}

      {open && lines.length > 1 && (
        <div className="line-dropdown">
          {lines.map(l => (
            <button key={l.id}
              className={`line-dropdown-item ${l.id === lineId ? 'active' : ''}`}
              onClick={() => { setLineId(l.id); setOpen(false); }}>
              <div className="line-dropdown-icon"><Icons.WA size={16} /></div>
              <div className="line-dropdown-text">
                <div className="line-dropdown-num">{l.phone_number || '—'}</div>
                <div className="line-dropdown-sub">{fmtLineSub(l)}</div>
              </div>
              {l.id === lineId && <div className="line-dropdown-check"><Icons.Check /></div>}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

// ==================== AUTH ====================
function AuthScreen({ onLogin }) {
  const [step, setStep]     = useState('email');
  const [email, setEmail]   = useState('');
  const [code, setCode]     = useState('');
  const [loading, setLoad]  = useState(false);
  const [error, setError]   = useState('');

  // Magic link ?email=&code=
  useEffect(() => {
    const p = new URLSearchParams(location.search);
    const e = p.get('email'); const c = p.get('code');
    if (e && c) {
      setEmail(e); setLoad(true);
      fetch(`${CONFIG.API_URL}/auth/verify-code`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: e.toLowerCase(), code: c }),
      }).then(r => r.json()).then(data => {
        if (data.success && data.token) {
          history.replaceState({}, '', location.pathname);
          onLogin(data.token, data.user || { email: e });
        } else { setStep('code'); setError(data.error || 'Lien expiré'); }
      }).catch(() => setError('Erreur réseau')).finally(() => setLoad(false));
    } else if (e) { setEmail(e); setStep('code'); }
  }, []);

  const requestCode = async (e) => {
    e?.preventDefault();
    if (!email.includes('@')) { setError('Email invalide'); return; }
    setLoad(true); setError('');
    try {
      const r = await fetch(`${CONFIG.API_URL}/auth/request-code`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: email.toLowerCase().trim() }),
      }).then(x => x.json());
      if (r.success) setStep('code');
      else setError(r.error || 'Erreur envoi');
    } catch { setError('Erreur réseau'); }
    finally { setLoad(false); }
  };

  const verify = async (e) => {
    e?.preventDefault();
    if (code.length < 4) return;
    setLoad(true); setError('');
    try {
      const r = await fetch(`${CONFIG.API_URL}/auth/verify-code`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: email.toLowerCase(), code }),
      }).then(x => x.json());
      if (r.success && r.token) onLogin(r.token, r.user || { email });
      else setError(r.error || 'Code invalide');
    } catch { setError('Erreur réseau'); }
    finally { setLoad(false); }
  };

  return (
    <div className="auth-splash">
      <div className="auth-splash-bg" />
      <div className="auth-splash-inner">
        <div className="auth-splash-logo">
          <div className="auth-splash-cross">
            <img src="/assets/img/helvia-logotype-favicon.svg" alt="Helvia" />
          </div>
        </div>
        <div className="auth-splash-version">
          Helvia Call · v{document.querySelector('meta[name="helvia:call:version"]')?.content || '?'}
        </div>
        <p className="auth-splash-tagline">
          {step === 'email'
            ? 'Conversations, appels & contacts en direct'
            : <>Entrez le code reçu à <strong>{email}</strong></>}
        </p>

        {step === 'email' ? (
          <form onSubmit={requestCode} className="auth-splash-form">
            <input type="email" value={email} onChange={e => setEmail(e.target.value)}
              placeholder="vous@exemple.com" className="auth-splash-input" autoFocus autoComplete="email" inputMode="email" />
            {error && <div className="auth-splash-error">{error}</div>}
            <button type="submit" disabled={loading || !email.includes('@')} className="auth-splash-btn">
              {loading ? <span className="auth-splash-spinner" /> : null}
              {loading ? 'Envoi…' : 'Recevoir le code'}
            </button>
          </form>
        ) : (
          <form onSubmit={verify} className="auth-splash-form">
            <input type="text" value={code} onChange={e => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
              placeholder="• • • • • •" className="auth-splash-input auth-splash-code" maxLength="6" autoFocus inputMode="numeric" />
            {error && <div className="auth-splash-error">{error}</div>}
            <button type="submit" disabled={loading || code.length < 4} className="auth-splash-btn">
              {loading ? <span className="auth-splash-spinner" /> : null}
              {loading ? 'Vérification…' : 'Se connecter'}
            </button>
            <button type="button" onClick={() => { setStep('email'); setCode(''); setError(''); }} className="auth-splash-link">
              Changer d'email
            </button>
          </form>
        )}

        <div className="auth-splash-foot">
          <img src="/assets/img/helvia-logotype-white.svg" alt="Helvia" className="auth-splash-wordmark-img" />
        </div>
      </div>
    </div>
  );
}

// ==================== CONV LIST ITEM ====================
function ChannelBadge({ channel }) {
  const meta = CHANNEL_META[channel] || { label: channel, css: 'ch-msg' };
  return <span className={`conv-channel ${meta.css}`}>{meta.label}</span>;
}

function ConvList({ items, onOpen, onArchive, archiveLabel }) {
  const [menuFor, setMenuFor] = useState(null);
  const longPressTimer = useRef(null);

  const startPress = (uid) => {
    longPressTimer.current = setTimeout(() => setMenuFor(uid), 500);
  };
  const cancelPress = () => {
    if (longPressTimer.current) { clearTimeout(longPressTimer.current); longPressTimer.current = null; }
  };

  return (
    <div className="inbox-list">
      {items.map(c => {
        const name = c.contact_name || c.contact_number;
        const variant = avatarVariant(c.contact_number || c.contact_name);
        const unread = c.unread_count > 0;
        return (
          <div key={c.uid} className="conv-row-wrap">
            <button className="conv-row"
              onClick={() => onOpen(c)}
              onContextMenu={(e) => { e.preventDefault(); setMenuFor(c.uid); }}
              onTouchStart={() => startPress(c.uid)}
              onTouchEnd={cancelPress}
              onTouchMove={cancelPress}
              onMouseDown={() => startPress(c.uid)}
              onMouseUp={cancelPress}
              onMouseLeave={cancelPress}>
              <div className={`conv-avatar ${variant}`}>{initials(name)}</div>
              <div className="conv-main">
                <div className="conv-top">
                  <div className="conv-name">{name}</div>
                  <div className={`conv-time ${unread ? 'unread' : ''}`}>{fmtTime(c.last_message_at)}</div>
                </div>
                <div className="conv-bottom">
                  <div className="conv-preview">
                    <ChannelBadge channel={c.channel} />
                    {c.bot_enabled === 1 && c.channel === 'whatsapp' && <span className="conv-bot">BOT</span>}
                    {c.last_message_body || (c.contact_number || '—')}
                  </div>
                  {unread > 0 && <span className="unread-badge">{c.unread_count}</span>}
                </div>
              </div>
            </button>
          </div>
        );
      })}
      {menuFor && (
        <>
          <div className="drawer-overlay" onClick={() => setMenuFor(null)} />
          <div className="drawer safe-bottom">
            <div className="drawer-handle" />
            <button className="drawer-row" onClick={() => { onArchive(menuFor); setMenuFor(null); }}>
              <div className="drawer-row-icon"><Icons.Archive /></div>
              <div className="drawer-row-text">
                <div className="drawer-row-label">{archiveLabel}</div>
              </div>
            </button>
            <button className="drawer-row" onClick={() => setMenuFor(null)}>
              <div className="drawer-row-icon"><Icons.More /></div>
              <div className="drawer-row-text">
                <div className="drawer-row-label">Annuler</div>
              </div>
            </button>
          </div>
        </>
      )}
    </div>
  );
}

// ==================== INBOX LIST ====================
function InboxView({ onOpen }) {
  const { notify, lineId } = useApp();
  const [convs, setConvs] = useState([]);
  const [loading, setLoading] = useState(true);
  const [search, setSearch] = useState('');
  const [searchFocused, setSearchFocused] = useState(false);
  const [refreshing, setRefresh] = useState(false);
  const [archived, setArchivedSet] = useState(() => loadArchived());

  // Quand la recherche est active (focus ou texte saisi), on masque la topbar
  // sélecteur de ligne et on remonte la search-bar en haut.
  const searchActive = searchFocused || search.trim().length > 0;

  const toggleArchive = (uid) => {
    const next = new Set(archived);
    if (next.has(uid)) next.delete(uid); else next.add(uid);
    setArchivedSet(next);
    saveArchived(next);
    notify(next.has(uid) ? 'Conversation archivée' : 'Conversation désarchivée', 'success');
  };

  const load = useCallback(async (showSpin = true) => {
    if (showSpin) setLoading(true); else setRefresh(true);
    try {
      const list = await fetchUnifiedConversations(lineId);
      setConvs(list);
    } catch { notify('Erreur réseau', 'error'); }
    finally { setLoading(false); setRefresh(false); }
  }, [lineId, notify]);

  useEffect(() => { load(); }, [load]);
  useEffect(() => {
    const t = setInterval(() => load(false), 20000);
    return () => clearInterval(t);
  }, [load]);

  const filtered = useMemo(() => {
    let list = convs.filter(c => !archived.has(c.uid));
    if (search.trim()) {
      const s = search.toLowerCase();
      list = list.filter(c =>
        (c.contact_name || '').toLowerCase().includes(s) ||
        (c.contact_number || '').includes(s) ||
        (c.last_message_body || '').toLowerCase().includes(s) ||
        (CHANNEL_META[c.channel]?.label || '').toLowerCase().includes(s)
      );
    }
    return list;
  }, [convs, search, archived]);

  const archivedCount = useMemo(() => convs.filter(c => archived.has(c.uid)).length, [convs, archived]);
  const unreadTotal = useMemo(() => filtered.reduce((s, c) => s + (c.unread_count || 0), 0), [filtered]);

  return (
    <div className="shell">
      {!searchActive && (
        <TopBarWithLineSelector
          subtitle={unreadTotal > 0 ? `${unreadTotal} non lu${unreadTotal > 1 ? 's' : ''}` : 'Tous canaux'}
          right={(
            <button className="topbar-icon-btn" onClick={() => load(false)} disabled={refreshing}>
              <span className={refreshing ? 'spin' : ''} style={{ display: 'inline-flex' }}><Icons.Refresh /></span>
            </button>
          )} />
      )}

      <div className="shell-main">
        <div className={`search-bar${searchActive ? ' search-bar-active safe-top' : ''}`}>
          {searchActive && (
            <button
              type="button"
              className="search-cancel-btn"
              onMouseDown={(e) => e.preventDefault()}
              onClick={() => { setSearch(''); setSearchFocused(false); }}
              aria-label="Fermer la recherche"
            >
              <Icons.Back />
            </button>
          )}
          <input
            className="search-input"
            type="search"
            placeholder="Rechercher une conversation…"
            value={search}
            onChange={e => setSearch(e.target.value)}
            onFocus={() => setSearchFocused(true)}
            onBlur={() => { if (!search.trim()) setSearchFocused(false); }}
          />
        </div>
        {loading ? (
          <Skeleton variant="row" count={7} />
        ) : filtered.length === 0 ? (
          <div className="empty-state">
            <div className="empty-icon"><Icons.Chat /></div>
            <div className="empty-title">
              {search ? 'Aucun résultat' : 'Aucune conversation'}
            </div>
            <div className="empty-desc">
              WhatsApp, widgets web et autres canaux apparaissent ici dans une seule boîte de réception.
            </div>
          </div>
        ) : (
          <ConvList items={filtered} onOpen={onOpen}
            onArchive={toggleArchive} archiveLabel="Archiver" />
        )}
        {archivedCount > 0 && filtered.length > 0 && (
          <div className="inbox-foot">
            <Icons.ArchiveBox size={16} /> {archivedCount} archivée{archivedCount > 1 ? 's' : ''}
          </div>
        )}
      </div>
    </div>
  );
}

// ==================== APPELS ====================
function CallsView({ onOpenCall }) {
  const { notify, lineId } = useApp();
  const [calls, setCalls] = useState([]);
  const [loading, setLoading] = useState(true);
  const [filter, setFilter] = useState('all');
  const [search, setSearch] = useState('');

  const load = useCallback(async () => {
    setLoading(true);
    try {
      let url = '/my/calls?page=1&limit=50';
      if (lineId) url += `&line_id=${lineId}`;
      if (filter === 'inbound') url += '&direction=inbound';
      else if (filter === 'outbound') url += '&direction=outbound';
      else if (filter === 'missed') url += '&status=missed';
      const r = await api.get(url);
      if (r?.error) notify(r.error, 'error');
      else setCalls(r.calls || []);
    } catch { notify('Erreur réseau', 'error'); }
    finally { setLoading(false); }
  }, [lineId, filter, notify]);

  useEffect(() => { load(); }, [load]);

  const filtered = useMemo(() => {
    if (!search.trim()) return calls;
    const s = search.toLowerCase();
    return calls.filter(c =>
      (c.from_number || '').includes(s) ||
      (c.to_number || '').includes(s) ||
      (c.line_phone || '').includes(s)
    );
  }, [calls, search]);

  return (
    <div className="shell">
      <TopBarWithLineSelector subtitle="Historique" />
      <div className="shell-main scroll-y">
        <div className="search-bar">
          <input className="search-input" type="search" placeholder="Rechercher un numéro…"
            value={search} onChange={e => setSearch(e.target.value)} />
        </div>
        <div className="seg-wrap">
          <div className="seg-control">
            {CALL_FILTERS.map(f => {
              const Ico = Icons[f.icon];
              return (
                <button key={f.id} className={`seg-btn ${filter === f.id ? 'on' : ''}`}
                  onClick={() => setFilter(f.id)}>
                  {Ico && <Ico />}<span>{f.label}</span>
                </button>
              );
            })}
          </div>
        </div>
        {loading ? (
          <Skeleton variant="call" count={7} />
        ) : filtered.length === 0 ? (
          <div className="empty-state">
            <div className="empty-icon"><Icons.Phone /></div>
            <div className="empty-title">Aucun appel</div>
            <div className="empty-desc">Entrants, sortants, manqués et messages vocaux s'affichent ici.</div>
          </div>
        ) : (
          <div className="calls-list">
            {filtered.map(c => {
              const peer = callPeer(c);
              const missed = isMissedCall(c);
              const hasVm = !!c.recording_url;
              const dirKey = missed ? 'missed' : c.direction;
              const dirLabel = missed ? 'Manqué' : c.direction === 'inbound' ? 'Reçu' : 'Émis';
              return (
                <button key={c.id || c.call_sid} className={`call-row dir-${dirKey}`} onClick={() => onOpenCall && onOpenCall(c)}>
                  <div className={`call-row-icon ${dirKey}`}>
                    {hasVm ? <Icons.Voicemail /> : missed ? <Icons.PhoneMissed /> : c.direction === 'inbound' ? <Icons.PhoneIn /> : <Icons.PhoneOut />}
                  </div>
                  <div className="call-row-main">
                    <div className="call-row-name">
                      <span className={`call-row-name-text ${missed ? 'missed' : ''}`}>{fmtPhone(peer)}</span>
                    </div>
                    <div className="call-row-meta">
                      <span className={`call-row-dir tag-${dirKey}`}>{dirLabel}</span>
                      {c.duration ? <span className="call-row-meta-sep">{fmtDuration(c.duration)}</span> : null}
                      {c.line_phone ? <span className="call-row-meta-sep">via {fmtPhone(c.line_phone)}</span> : null}
                    </div>
                  </div>
                  <div className="call-row-side">
                    <div className="call-row-time">{fmtTime(c.created_at)}</div>
                    {hasVm && <span className="call-row-vm-dot" aria-label="Message vocal" />}
                  </div>
                </button>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}

// ==================== CALL DETAIL ====================
function AudioPlayer({ src }) {
  const audioRef = useRef(null);
  const [playing, setPlaying] = useState(false);
  const [cur, setCur] = useState(0);
  const [dur, setDur] = useState(0);
  const [rate, setRate] = useState(1);

  useEffect(() => {
    const a = audioRef.current; if (!a) return;
    const onTime = () => setCur(a.currentTime || 0);
    const onMeta = () => setDur(a.duration || 0);
    const onEnd = () => { setPlaying(false); setCur(0); a.currentTime = 0; };
    a.addEventListener('timeupdate', onTime);
    a.addEventListener('loadedmetadata', onMeta);
    a.addEventListener('ended', onEnd);
    return () => {
      a.removeEventListener('timeupdate', onTime);
      a.removeEventListener('loadedmetadata', onMeta);
      a.removeEventListener('ended', onEnd);
    };
  }, []);

  const toggle = () => {
    const a = audioRef.current; if (!a) return;
    if (a.paused) { a.play(); setPlaying(true); } else { a.pause(); setPlaying(false); }
  };
  const seek = (e) => {
    const a = audioRef.current; if (!a || !dur) return;
    const rect = e.currentTarget.getBoundingClientRect();
    const x = (e.clientX || (e.touches && e.touches[0]?.clientX) || 0) - rect.left;
    const ratio = Math.max(0, Math.min(1, x / rect.width));
    a.currentTime = ratio * dur;
    setCur(a.currentTime);
  };
  const skip = (delta) => {
    const a = audioRef.current; if (!a) return;
    a.currentTime = Math.max(0, Math.min((a.duration || 0), a.currentTime + delta));
  };
  const setSpeed = (r) => {
    const a = audioRef.current; if (!a) return;
    a.playbackRate = r; setRate(r);
  };
  const dl = () => { const link = document.createElement('a'); link.href = src; link.download = 'message-vocal.mp3'; link.target = '_blank'; link.click(); };

  const pct = dur ? (cur / dur) * 100 : 0;

  return (
    <div className="audio-player">
      <audio ref={audioRef} src={src} preload="metadata" />
      <div className="ap-top">
        <button className="ap-play" onClick={toggle} aria-label={playing ? 'Pause' : 'Lecture'}>
          {playing ? (
            <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>
          ) : (
            <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
          )}
        </button>
        <div className="ap-info">
          <div className="ap-times">
            <span>{fmtDuration(Math.floor(cur))}</span>
            <span>{fmtDuration(Math.floor(dur))}</span>
          </div>
          <div className="ap-progress" onClick={seek}>
            <div className="ap-progress-bar" style={{ width: pct + '%' }} />
            <div className="ap-progress-thumb" style={{ left: pct + '%' }} />
          </div>
        </div>
      </div>
      <div className="ap-actions">
        <button className="ap-action" onClick={() => skip(-10)} aria-label="Reculer 10s">
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg>
          <span>10s</span>
        </button>
        <div className="ap-speeds">
          {[1, 1.25, 1.5, 2].map(r => (
            <button key={r} className={`ap-speed ${rate === r ? 'on' : ''}`} onClick={() => setSpeed(r)}>{r}×</button>
          ))}
        </div>
        <button className="ap-action" onClick={() => skip(10)} aria-label="Avancer 10s">
          <span>10s</span>
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg>
        </button>
        <button className="ap-action ap-dl" onClick={dl} aria-label="Télécharger">
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
        </button>
      </div>
    </div>
  );
}

function CallDetailView({ call, onBack }) {
  useSwipeBack(onBack);
  const peer = callPeer(call);
  const missed = isMissedCall(call);
  const hasVm = !!call.recording_url;
  const dt = parseTs(call.created_at);
  const dateStr = dt ? dt.toLocaleString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '';
  const dirLabel = missed ? 'Appel manqué' : call.direction === 'inbound' ? 'Appel entrant' : 'Appel sortant';
  const IconBig = hasVm ? Icons.Voicemail : missed ? Icons.PhoneMissed : call.direction === 'inbound' ? Icons.PhoneIn : Icons.PhoneOut;

  return (
    <div className="shell slide-in">
      <div className="topbar gradient safe-top">
        <div className="safe-extend" />
        <button className="topbar-back" onClick={onBack}><Icons.Back /></button>
        <div className="topbar-title">
          <div className="topbar-name">Détail de l'appel</div>
          <div className="topbar-sub">{dirLabel}</div>
        </div>
        <div className="topbar-actions">
          <a className="topbar-icon-btn" href={`tel:${peer}`} aria-label="Rappeler"><Icons.Phone /></a>
        </div>
      </div>
      <div className="shell-main scroll-y">
        <div className="call-detail">
          <div className={`call-detail-hero ${missed ? 'missed' : call.direction}`}>
            <div className="call-detail-iconbig"><IconBig /></div>
            <div className="call-detail-peer">{fmtPhone(peer)}</div>
            <div className="call-detail-dir">{dirLabel}</div>
            <div className="call-detail-date">{dateStr}</div>
          </div>

          {hasVm && (
            <div className="call-detail-section">
              <div className="call-detail-label">Message vocal</div>
              <AudioPlayer src={call.recording_url} />
            </div>
          )}

          <div className="call-detail-section">
            <div className="call-detail-label">Informations</div>
            <div className="call-detail-rows">
              <div className="call-detail-row"><span>Statut</span><strong>{CALL_STATUS_LABEL[call.status] || call.status || '—'}</strong></div>
              {call.duration != null && <div className="call-detail-row"><span>Durée</span><strong>{fmtDuration(call.duration)}</strong></div>}
              {call.line_phone && <div className="call-detail-row"><span>Ligne</span><strong>{fmtPhone(call.line_phone)}</strong></div>}
              {call.from_number && <div className="call-detail-row"><span>De</span><strong>{fmtPhone(call.from_number)}</strong></div>}
              {call.to_number && <div className="call-detail-row"><span>Vers</span><strong>{fmtPhone(call.to_number)}</strong></div>}
              {call.call_sid && <div className="call-detail-row"><span>SID</span><strong className="mono">{call.call_sid}</strong></div>}
            </div>
          </div>

          {call.transcript && (
            <div className="call-detail-section">
              <div className="call-detail-label">Transcription</div>
              <div className="call-detail-transcript">{call.transcript}</div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ==================== CONTACTS ====================
function ContactsView({ onOpenConv, onOpenContact }) {
  const { notify, lineId } = useApp();
  const [contacts, setContacts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [search, setSearch] = useState('');
  const [favorites, setFavorites] = useState(() => loadFavorites());

  useEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true);
      try {
        const [convs, callsR] = await Promise.all([
          fetchUnifiedConversations(lineId),
          api.get(`/my/calls?page=1&limit=100${lineId ? `&line_id=${lineId}` : ''}`),
        ]);
        const map = new Map();
        const upsert = (key, data) => {
          const prev = map.get(key);
          if (!prev || parseTs(data.last_at) >= parseTs(prev.last_at)) map.set(key, { key, ...prev, ...data });
          else map.set(key, { key, ...prev, ...data, last_at: prev.last_at, preview: prev.preview });
        };
        convs.forEach(c => {
          const key = c.channel === 'widget' ? `widget:${c.contact_number}` : `tel:${c.contact_number}`;
          upsert(key, {
            name: c.contact_name,
            phone: c.contact_number,
            channel: c.channel,
            preview: c.last_message_body,
            last_at: c.last_message_at,
            conv: c,
            type: 'chat',
          });
        });
        (callsR?.calls || []).forEach(call => {
          const peer = callPeer(call);
          if (!peer) return;
          upsert(`tel:${peer}`, {
            name: fmtPhone(peer),
            phone: peer,
            channel: 'phone',
            preview: `Appel ${call.direction === 'inbound' ? 'entrant' : 'sortant'}`,
            last_at: call.created_at,
            type: 'call',
          });
        });
        if (!cancelled) setContacts([...map.values()].sort((a, b) => parseTs(b.last_at) - parseTs(a.last_at)));
      } catch { if (!cancelled) notify('Erreur chargement contacts', 'error'); }
      finally { if (!cancelled) setLoading(false); }
    })();
    return () => { cancelled = true; };
  }, [lineId, notify]);

  const toggleFavorite = (key) => {
    const next = new Set(favorites);
    if (next.has(key)) next.delete(key); else next.add(key);
    setFavorites(next);
    saveFavorites(next);
  };

  const filtered = useMemo(() => {
    let list = contacts;
    if (search.trim()) {
      const s = search.toLowerCase();
      list = list.filter(c =>
        (c.name || '').toLowerCase().includes(s) ||
        (c.phone || '').includes(s)
      );
    }
    return list;
  }, [contacts, search]);

  const favs = filtered.filter(c => favorites.has(c.key));
  const others = filtered.filter(c => !favorites.has(c.key));

  const renderRow = (c) => (
    <button key={c.key} className="contact-row"
      onClick={() => onOpenContact ? onOpenContact(c) : (c.conv && onOpenConv(c.conv))}>
      <div className={`conv-avatar ${avatarVariant(c.phone || c.name)}`} style={{ width: 44, height: 44, fontSize: '0.9rem' }}>
        {initials(c.name || c.phone)}
      </div>
      <div className="contact-row-main">
        <div className="contact-row-name">{c.name || fmtPhone(c.phone)}</div>
        <div className="contact-row-sub">{c.preview || fmtPhone(c.phone)}</div>
      </div>
      <button className={`contact-fav-btn${favorites.has(c.key) ? ' on' : ''}`}
        onClick={(e) => { e.stopPropagation(); toggleFavorite(c.key); }}
        aria-label={favorites.has(c.key) ? 'Retirer des favoris' : 'Ajouter aux favoris'}>
        <Icons.Star filled={favorites.has(c.key)} />
      </button>
    </button>
  );

  return (
    <div className="shell">
      <TopBarWithLineSelector subtitle="Clients & prospects" />
      <div className="shell-main scroll-y">
        <div className="search-bar">
          <input className="search-input" type="search" placeholder="Rechercher un contact…"
            value={search} onChange={e => setSearch(e.target.value)} autoFocus={false} />
        </div>
        {loading ? (
          <Skeleton variant="row" count={6} />
        ) : filtered.length === 0 ? (
          <div className="empty-state">
            <div className="empty-icon"><Icons.Users /></div>
            <div className="empty-title">Aucun contact</div>
            <div className="empty-desc">Vos contacts sont agrégés depuis les conversations et les appels.</div>
          </div>
        ) : (
          <div className="contacts-list">
            {favs.length > 0 && (
              <>
                <div className="section-label">Favoris</div>
                {favs.map(renderRow)}
              </>
            )}
            <div className="section-label">{favs.length ? 'Tous les contacts' : 'Contacts'}</div>
            {others.map(renderRow)}
          </div>
        )}
      </div>
    </div>
  );
}

// ==================== DETAIL CONTACT ====================
function ContactDetailView({ contact, onBack, onOpenConv, onOpenCall }) {
  useSwipeBack(onBack);
  const { notify, lineId } = useApp();
  const [loading, setLoading] = useState(true);
  const [convs, setConvs] = useState([]);
  const [calls, setCalls] = useState([]);

  const phone = contact?.phone || '';
  const channelKey = contact?.channel === 'widget' ? `widget:${phone}` : `tel:${phone}`;

  useEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true);
      try {
        const [allConvs, callsR] = await Promise.all([
          fetchUnifiedConversations(lineId),
          api.get(`/my/calls?page=1&limit=100${lineId ? `&line_id=${lineId}` : ''}`),
        ]);
        if (cancelled) return;
        const matchedConvs = (allConvs || []).filter(c => {
          const k = c.channel === 'widget' ? `widget:${c.contact_number}` : `tel:${c.contact_number}`;
          return k === channelKey;
        });
        const matchedCalls = (callsR?.calls || []).filter(call => {
          const peer = callPeer(call);
          return peer && phone && peer === phone;
        });
        setConvs(matchedConvs);
        setCalls(matchedCalls);
      } catch {
        if (!cancelled) notify('Erreur chargement contact', 'error');
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [channelKey, phone, lineId, notify]);

  const stats = useMemo(() => {
    const inbound = calls.filter(c => c.direction === 'inbound').length;
    const outbound = calls.filter(c => c.direction === 'outbound').length;
    const missed = calls.filter(c => c.status === 'no-answer' || c.status === 'missed' || c.status === 'failed').length;
    const totalDuration = calls.reduce((s, c) => s + (c.duration || 0), 0);
    const messages = convs.reduce((s, c) => s + (c.message_count || 0), 0);
    return { inbound, outbound, missed, totalDuration, messages };
  }, [calls, convs]);

  const lastActivity = useMemo(() => {
    const all = [
      ...convs.map(c => parseTs(c.last_message_at)),
      ...calls.map(c => parseTs(c.created_at)),
    ];
    if (!all.length) return null;
    return Math.max(...all);
  }, [convs, calls]);

  return (
    <div className="shell">
      <div className="topbar gradient safe-top">
        <div className="safe-extend" />
        <button className="topbar-back" onClick={onBack}><Icons.Back /></button>
        <div className="topbar-title">
          <div className="topbar-title-main">{contact?.name || fmtPhone(phone)}</div>
          <div className="topbar-title-sub">{contact?.name ? fmtPhone(phone) : 'Contact'}</div>
        </div>
      </div>
      <div className="shell-main scroll-y">
        {loading ? (
          <Skeleton variant="detail" />
        ) : (
        <>
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '1.25rem 1rem .5rem' }}>
          <div className={`conv-avatar ${avatarVariant(phone || contact?.name)}`} style={{ width: 88, height: 88, fontSize: '1.8rem' }}>
            {initials(contact?.name || phone)}
          </div>
          <div style={{ marginTop: '.75rem', fontWeight: 700, fontSize: '1.15rem', textAlign: 'center' }}>
            {contact?.name || fmtPhone(phone)}
          </div>
          {contact?.name && (
            <div style={{ color: '#64748b', fontSize: '.9rem', marginTop: '.15rem' }}>{fmtPhone(phone)}</div>
          )}
          {lastActivity && (
            <div style={{ color: '#94a3b8', fontSize: '.78rem', marginTop: '.25rem' }}>
              Dernière activité {fmtTime(new Date(lastActivity).toISOString())}
            </div>
          )}
        </div>

        <div className="settings-group" style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '.5rem', padding: '0 1rem' }}>
          <div className="account-stat-row" style={{ flexDirection: 'column', alignItems: 'flex-start', padding: '.75rem' }}>
            <span style={{ fontSize: '.75rem', color: '#64748b' }}>Conversations</span>
            <strong style={{ fontSize: '1.15rem' }}>{convs.length}</strong>
          </div>
          <div className="account-stat-row" style={{ flexDirection: 'column', alignItems: 'flex-start', padding: '.75rem' }}>
            <span style={{ fontSize: '.75rem', color: '#64748b' }}>Messages</span>
            <strong style={{ fontSize: '1.15rem' }}>{stats.messages}</strong>
          </div>
          <div className="account-stat-row" style={{ flexDirection: 'column', alignItems: 'flex-start', padding: '.75rem' }}>
            <span style={{ fontSize: '.75rem', color: '#64748b' }}>Appels</span>
            <strong style={{ fontSize: '1.15rem' }}>{calls.length}</strong>
          </div>
          <div className="account-stat-row" style={{ flexDirection: 'column', alignItems: 'flex-start', padding: '.75rem' }}>
            <span style={{ fontSize: '.75rem', color: '#64748b' }}>Durée totale</span>
            <strong style={{ fontSize: '1.15rem' }}>{fmtDuration(stats.totalDuration)}</strong>
          </div>
        </div>

        {(stats.inbound || stats.outbound || stats.missed) ? (
          <div className="settings-group">
            <div className="account-stat-row"><span>Entrants</span><strong>{stats.inbound}</strong></div>
            <div className="account-stat-row"><span>Sortants</span><strong>{stats.outbound}</strong></div>
            <div className="account-stat-row"><span>Manqués</span><strong>{stats.missed}</strong></div>
          </div>
        ) : null}

        {!loading && convs.length > 0 && (
          <>
            <div className="section-label" style={{ padding: '.75rem 1rem .25rem', color: '#64748b', fontSize: '.78rem', textTransform: 'uppercase', fontWeight: 600 }}>
              Conversations
            </div>
            <div className="settings-group">
              {convs.map(c => (
                <button key={c.uid} className="drawer-row" onClick={() => onOpenConv && onOpenConv(c)}>
                  <div className="drawer-row-icon"><Icons.Chat /></div>
                  <div className="drawer-row-text" style={{ flex: 1 }}>
                    <div className="drawer-row-label">{CHANNEL_META[c.channel]?.label || c.channel}</div>
                    <div className="drawer-row-desc" style={{ fontSize: '.8rem', color: '#64748b' }}>
                      {c.last_message_body ? String(c.last_message_body).slice(0, 80) : 'Ouvrir la conversation'}
                    </div>
                  </div>
                  {c.unread_count > 0 && (
                    <div className="unread-badge" style={{ background: '#10B981', color: '#fff', borderRadius: 12, padding: '.1rem .5rem', fontSize: '.75rem', fontWeight: 600 }}>
                      {c.unread_count}
                    </div>
                  )}
                </button>
              ))}
            </div>
          </>
        )}

        {!loading && calls.length > 0 && (
          <>
            <div className="section-label" style={{ padding: '.75rem 1rem .25rem', color: '#64748b', fontSize: '.78rem', textTransform: 'uppercase', fontWeight: 600 }}>
              Historique d'appels
            </div>
            <div className="settings-group">
              {calls.slice(0, 30).map(call => {
                const isMissed = call.status === 'no-answer' || call.status === 'missed' || call.status === 'failed';
                const IconCmp = isMissed ? Icons.PhoneMissed : (call.direction === 'inbound' ? Icons.PhoneIn : Icons.PhoneOut);
                return (
                  <button key={call.id || call.call_sid} className="drawer-row" onClick={() => onOpenCall && onOpenCall(call)}>
                    <div className="drawer-row-icon" style={{ color: isMissed ? '#ef4444' : '#10B981' }}><IconCmp /></div>
                    <div className="drawer-row-text" style={{ flex: 1 }}>
                      <div className="drawer-row-label">
                        {call.direction === 'inbound' ? 'Entrant' : 'Sortant'}
                        {isMissed ? ' · manqué' : ''}
                      </div>
                      <div className="drawer-row-desc" style={{ fontSize: '.8rem', color: '#64748b' }}>
                        {fmtTime(call.created_at)}
                        {call.duration ? ` · ${fmtDuration(call.duration)}` : ''}
                      </div>
                    </div>
                  </button>
                );
              })}
            </div>
          </>
        )}

        {!loading && convs.length === 0 && calls.length === 0 && (
          <div className="empty-state">
            <div className="empty-title">Aucune activité</div>
            <div className="empty-desc">Aucune conversation ni appel avec ce contact.</div>
          </div>
        )}
        </>
        )}
      </div>
    </div>
  );
}

// ==================== COMPTE ====================
function AccountView() {
  const { user, logout, lines } = useApp();
  const waLines = lines.filter(l => l.whatsapp_enabled !== 0).length;
  const [subView, setSubView] = useState(null); // null | 'devices' | 'scanner'

  if (subView === 'devices') {
    return <DevicesView onBack={() => setSubView(null)} />;
  }
  if (subView === 'scanner') {
    // Direct scanner → on revient sur le tab Compte. La liste des appareils
    // est accessible via le menu (bouton secondaire en bas du scanner).
    return <ScannerView onBack={() => setSubView(null)} onShowList={() => setSubView('devices')} />;
  }

  const accountName = user?.name || (user?.email ? user.email.split('@')[0] : 'Mon compte');
  return (
    <div className="shell">
      <div className="topbar gradient safe-top">
        <div className="safe-extend" />
        <button className="topbar-line-btn" disabled>
          <div className="topbar-line-icon"><Icons.Phone /></div>
          <div className="topbar-line-text">
            <div className="topbar-line-num">{accountName}</div>
            <div className="topbar-line-sub">{user?.email || ''} · Compte</div>
          </div>
        </button>
      </div>
      <div className="shell-main scroll-y">
        <div className="settings-group">
          <button className="drawer-row" onClick={() => setSubView('scanner')}>
            <div className="drawer-row-icon"><Icons.Devices /></div>
            <div className="drawer-row-text">
              <div className="drawer-row-label">Lier un appareil</div>
              <div className="drawer-row-desc">Scanner le QR de inbox.helvia.app</div>
            </div>
          </button>
          <button className="drawer-row" onClick={() => setSubView('devices')}>
            <div className="drawer-row-icon"><Icons.Devices /></div>
            <div className="drawer-row-text">
              <div className="drawer-row-label">Appareils liés</div>
              <div className="drawer-row-desc">Voir et révoquer les sessions inbox</div>
            </div>
          </button>
          <button className="drawer-row danger" onClick={logout}>
            <div className="drawer-row-icon"><Icons.Logout /></div>
            <div className="drawer-row-text"><div className="drawer-row-label">Déconnexion</div></div>
          </button>
        </div>
      </div>
    </div>
  );
}

// ==================== APPAREILS LIÉS ====================
function DevicesView({ onBack }) {
  useSwipeBack(onBack);
  const { notify } = useApp();
  const [devices, setDevices] = useState([]);
  const [loading, setLoading] = useState(true);

  const load = useCallback(async () => {
    setLoading(true);
    try {
      const r = await api.get('/my/devices');
      setDevices(r?.devices || []);
    } catch {
      setDevices([]);
    }
    setLoading(false);
  }, []);

  useEffect(() => { load(); }, [load]);

  const revoke = async (id) => {
    if (!confirm('Déconnecter cet appareil ?')) return;
    const r = await api.req(`/my/devices/${id}`, { method: 'DELETE' });
    if (r?.ok) {
      notify('Appareil déconnecté', 'success');
      load();
    } else {
      notify(r?.error || 'Erreur', 'error');
    }
  };

  function relTime(s) {
    if (!s) return '';
    const d = new Date(s.includes('T') ? s : s.replace(' ', 'T') + 'Z');
    const diff = (Date.now() - d.getTime()) / 1000;
    if (diff < 60) return "à l'instant";
    if (diff < 3600) return `il y a ${Math.floor(diff / 60)} min`;
    if (diff < 86400) return `il y a ${Math.floor(diff / 3600)} h`;
    const days = Math.floor(diff / 86400);
    if (days < 7) return `il y a ${days} j`;
    return d.toLocaleDateString('fr-CH', { day: '2-digit', month: '2-digit', year: 'numeric' });
  }

  return (
    <div className="shell">
      <div className="topbar gradient safe-top">
        <div className="safe-extend" />
        <button className="topbar-back" onClick={onBack}><Icons.Back /></button>
        <div className="topbar-title">
          <div className="topbar-title-main">Appareils</div>
          <div className="topbar-title-sub">Inbox web liée</div>
        </div>
      </div>
      <div className="shell-main scroll-y">
        <div className="settings-group">
          <div className="section-label" style={{ padding: '0.5rem 1rem', color: '#64748b', fontSize: '.85rem', textTransform: 'uppercase', fontWeight: 600 }}>
            {loading ? 'Chargement…' : (devices.length ? `${devices.length} appareil${devices.length > 1 ? 's' : ''} lié${devices.length > 1 ? 's' : ''}` : 'Aucun appareil')}
          </div>
          {loading && <Skeleton variant="devices" />}
          {!loading && devices.length === 0 && (
            <div style={{ padding: '1.5rem', color: '#64748b', textAlign: 'center', lineHeight: 1.5 }}>
              Aucun appareil lié.<br/>
              Depuis <b>Compte → Lier un appareil</b>, scannez le QR<br/>
              affiché sur <b>inbox.helvia.app</b>.
            </div>
          )}
          {devices.map(d => (
            <div key={d.id} className="drawer-row" style={{ alignItems: 'flex-start' }}>
              <div className="drawer-row-icon"><Icons.Devices /></div>
              <div className="drawer-row-text" style={{ flex: 1 }}>
                <div className="drawer-row-label">{d.device_label || 'Navigateur'}</div>
                <div className="drawer-row-desc" style={{ fontSize: '.8rem', color: '#64748b' }}>
                  {d.host} · Connecté {relTime(d.linked_at)}
                  {d.last_seen_at && <> · vu {relTime(d.last_seen_at)}</>}
                </div>
              </div>
              <button
                onClick={() => revoke(d.id)}
                style={{ background: 'transparent', border: 'none', color: '#ef4444', cursor: 'pointer', padding: '.5rem' }}
                title="Déconnecter"
              >
                <Icons.Trash />
              </button>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ==================== SCANNER QR ====================
function ScannerView({ onBack, onShowList }) {
  useSwipeBack(onBack);
  const { notify } = useApp();
  const videoRef = useRef(null);
  const canvasRef = useRef(null);
  const streamRef = useRef(null);
  const intervalRef = useRef(null);
  const detectorRef = useRef(null);
  const jsQRRef = useRef(null);
  const handlingRef = useRef(false);
  const [error, setError] = useState(null);
  const [manualMode, setManualMode] = useState(false);
  const [manualInput, setManualInput] = useState('');
  const [busy, setBusy] = useState(false);

  function extractCode(text) {
    if (!text) return null;
    const m = String(text).match(/\/pair\/([a-f0-9]{20,})/i);
    return m ? m[1] : null;
  }

  const authorize = useCallback(async (code) => {
    if (!code || handlingRef.current) return;
    handlingRef.current = true;
    setBusy(true);
    try {
      const r = await api.post('/my/devices/authorize', { pairing_code: code });
      if (r?.ok) {
        notify(`Appareil lié : ${r.device?.device_label || 'OK'}`, 'success');
        // Stop camera and go back
        if (intervalRef.current) clearInterval(intervalRef.current);
        if (streamRef.current) streamRef.current.getTracks().forEach(t => t.stop());
        setTimeout(() => onBack(), 600);
      } else {
        // Si l'API a renvoyé "Non authentifié" l'intercepteur api.req a déjà
        // purgé le token et dispatché un logout : on rend le message explicite.
        const isAuth = /authentif|auth/i.test(r?.error || '');
        const msg = isAuth
          ? 'Connectez-vous d\'abord à votre compte Helvia'
          : (r?.error || 'Code invalide ou expiré');
        notify(msg, 'error');
        // On laisse retenter après une courte pause pour ne pas re-firer en boucle.
        setTimeout(() => { handlingRef.current = false; }, 1500);
      }
    } catch (e) {
      notify('Erreur réseau', 'error');
      setTimeout(() => { handlingRef.current = false; }, 1500);
    }
    setBusy(false);
  }, [notify, onBack]);

  // Initialize camera + decoder
  useEffect(() => {
    let cancelled = false;

    async function loadJsQR() {
      if (window.jsQR) return window.jsQR;
      return new Promise((resolve, reject) => {
        const s = document.createElement('script');
        s.src = 'https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js';
        s.onload = () => resolve(window.jsQR);
        s.onerror = () => reject(new Error('jsQR load failed'));
        document.head.appendChild(s);
      });
    }

    async function start() {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({
          video: { facingMode: { ideal: 'environment' } },
          audio: false,
        });
        if (cancelled) { stream.getTracks().forEach(t => t.stop()); return; }
        streamRef.current = stream;
        if (videoRef.current) {
          videoRef.current.srcObject = stream;
          await videoRef.current.play().catch(() => {});
        }

        // Try BarcodeDetector
        if ('BarcodeDetector' in window) {
          try {
            const formats = await window.BarcodeDetector.getSupportedFormats();
            if (formats.includes('qr_code')) {
              detectorRef.current = new window.BarcodeDetector({ formats: ['qr_code'] });
            }
          } catch {}
        }
        if (!detectorRef.current) {
          try { jsQRRef.current = await loadJsQR(); } catch { /* fallback may fail */ }
        }

        intervalRef.current = setInterval(scanFrame, 200);
      } catch (e) {
        setError(e?.message || 'Caméra refusée');
        setManualMode(true);
      }
    }

    async function scanFrame() {
      const video = videoRef.current;
      if (!video || video.readyState < 2 || handlingRef.current) return;
      try {
        if (detectorRef.current) {
          const codes = await detectorRef.current.detect(video);
          if (codes && codes[0]?.rawValue) {
            const code = extractCode(codes[0].rawValue);
            if (code) authorize(code);
            else notify('QR invalide', 'error');
          }
          return;
        }
        if (jsQRRef.current && canvasRef.current) {
          const cv = canvasRef.current;
          const w = video.videoWidth, h = video.videoHeight;
          if (!w || !h) return;
          cv.width = w; cv.height = h;
          const ctx = cv.getContext('2d', { willReadFrequently: true });
          ctx.drawImage(video, 0, 0, w, h);
          const img = ctx.getImageData(0, 0, w, h);
          const res = jsQRRef.current(img.data, w, h, { inversionAttempts: 'dontInvert' });
          if (res?.data) {
            const code = extractCode(res.data);
            if (code) authorize(code);
            else notify('QR invalide', 'error');
          }
        }
      } catch {}
    }

    start();
    return () => {
      cancelled = true;
      if (intervalRef.current) clearInterval(intervalRef.current);
      if (streamRef.current) streamRef.current.getTracks().forEach(t => t.stop());
    };
  }, [authorize, notify]);

  const submitManual = () => {
    const code = extractCode(manualInput) || (manualInput.trim().length >= 20 ? manualInput.trim() : null);
    if (!code) { notify('URL ou code invalide', 'error'); return; }
    authorize(code);
  };

  return (
    <div className="shell" style={{ background: '#000' }}>
      <div className="topbar gradient safe-top">
        <div className="safe-extend" />
        <button className="topbar-back" onClick={onBack}><Icons.Back /></button>
        <div className="topbar-title">
          <div className="topbar-title-main">Lier un appareil</div>
          <div className="topbar-title-sub">Scannez le code sur inbox.helvia.app</div>
        </div>
      </div>
      <div style={{ position: 'relative', flex: 1, overflow: 'hidden', background: '#000' }}>
        {!manualMode && (
          <>
            <video ref={videoRef} playsInline muted style={{ width: '100%', height: '100%', objectFit: 'cover', background: '#000' }} />
            <canvas ref={canvasRef} style={{ display: 'none' }} />
            <div style={{
              position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
              pointerEvents: 'none',
            }}>
              <div style={{
                width: 'min(70vw, 280px)', aspectRatio: '1 / 1',
                border: '3px solid rgba(255,255,255,.85)',
                borderRadius: 18,
                boxShadow: '0 0 0 9999px rgba(0,0,0,.45)',
              }} />
            </div>
            <div style={{
              position: 'absolute', bottom: 24, left: 0, right: 0, textAlign: 'center',
              color: '#fff', padding: '0 1rem', textShadow: '0 1px 4px rgba(0,0,0,.6)',
            }}>
              <div style={{ fontSize: '.95rem', marginBottom: '.75rem' }}>
                Pointez vers le QR affiché sur <b>inbox.helvia.app</b>
              </div>
              <div style={{ display: 'flex', gap: '.5rem', justifyContent: 'center', flexWrap: 'wrap' }}>
                <button onClick={() => setManualMode(true)} style={{
                  background: 'rgba(255,255,255,.18)', border: '1px solid rgba(255,255,255,.4)',
                  color: '#fff', padding: '.5rem 1rem', borderRadius: 20, fontSize: '.85rem', cursor: 'pointer',
                }}>Saisir le code manuellement</button>
                {onShowList && (
                  <button onClick={onShowList} style={{
                    background: 'rgba(255,255,255,.18)', border: '1px solid rgba(255,255,255,.4)',
                    color: '#fff', padding: '.5rem 1rem', borderRadius: 20, fontSize: '.85rem', cursor: 'pointer',
                  }}>Appareils déjà liés</button>
                )}
              </div>
            </div>
          </>
        )}
        {manualMode && (
          <div style={{ padding: '2rem 1.25rem', color: '#fff', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
            {error && <div style={{ background: 'rgba(239,68,68,.2)', padding: '.75rem', borderRadius: 8, fontSize: '.85rem' }}>{error}</div>}
            <div style={{ fontSize: '.95rem' }}>Collez l'URL du QR (ou le code) affichée sur inbox.helvia.app :</div>
            <input
              value={manualInput}
              onChange={e => setManualInput(e.target.value)}
              placeholder="https://inbox.helvia.app/pair/..."
              style={{
                width: '100%', padding: '.85rem 1rem', borderRadius: 10, border: 'none',
                fontSize: '.95rem', fontFamily: 'monospace',
              }}
              autoFocus
            />
            <button onClick={submitManual} disabled={busy} style={{
              padding: '.85rem', background: '#10B981', color: '#fff', border: 'none',
              borderRadius: 10, fontWeight: 600, fontSize: '1rem', cursor: 'pointer',
            }}>{busy ? 'Liaison…' : 'Lier cet appareil'}</button>
            {!error && (
              <button onClick={() => setManualMode(false)} style={{
                background: 'transparent', border: 'none', color: 'rgba(255,255,255,.7)', cursor: 'pointer',
              }}>← Retour au scanner</button>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

// ==================== NOUVEAU (actions) ====================
function NewActionSheet({ onClose, onMessage, onCalls }) {
  return (
    <div className="tpl-sheet-backdrop" onClick={onClose}>
      <div className="action-sheet" onClick={e => e.stopPropagation()}>
        <div className="action-sheet-head">
          <div className="tpl-sheet-title">Nouveau</div>
          <button className="tpl-sheet-close" onClick={onClose}>✕</button>
        </div>
        <button className="action-row" onClick={() => { onClose(); onMessage(); }}>
          <div className="action-row-icon"><Icons.Chat /></div>
          <div className="action-row-text">
            <div className="action-row-label">Nouveau message</div>
            <div className="action-row-desc">WhatsApp — choisir un contact existant</div>
          </div>
        </button>
        <button className="action-row" onClick={() => { onClose(); onCalls(); }}>
          <div className="action-row-icon"><Icons.Phone /></div>
          <div className="action-row-text">
            <div className="action-row-label">Voir les appels</div>
            <div className="action-row-desc">Historique entrants, sortants, manqués</div>
          </div>
        </button>
        <button className="action-row" onClick={() => window.open('https://my.helvia.app', '_blank')}>
          <div className="action-row-icon"><Icons.Globe /></div>
          <div className="action-row-text">
            <div className="action-row-label">Ouvrir my.helvia.app</div>
            <div className="action-row-desc">Templates, widgets, facturation</div>
          </div>
        </button>
      </div>
    </div>
  );
}

// ==================== DIALER (pavé tactile) ====================
function DialerView({ onClose }) {
  useSwipeBack(onClose);
  const { notify, lines, lineId } = useApp();
  const { makeCall, status, active, sendDigit } = useVoice();
  const [num, setNum] = useState('');
  const [calling, setCalling] = useState(false);
  const keys = [
    ['1', ''], ['2', 'ABC'], ['3', 'DEF'],
    ['4', 'GHI'], ['5', 'JKL'], ['6', 'MNO'],
    ['7', 'PQRS'], ['8', 'TUV'], ['9', 'WXYZ'],
    ['*', ''], ['0', '+'], ['#', ''],
  ];

  const push = (k) => {
    if (active) { sendDigit(k); return; } // DTMF pendant un appel actif
    setNum(n => (n + k).slice(0, 20));
  };
  const back = () => setNum(n => n.slice(0, -1));

  // Long-press 0 -> "+"
  const longPressRef = useRef(null);
  const handleKeyDown = (k) => {
    if (k === '0') {
      longPressRef.current = setTimeout(() => {
        if (!active) setNum(n => (n + '+').slice(0, 20));
      }, 500);
    }
  };
  const handleKeyUp = (k) => {
    if (longPressRef.current) { clearTimeout(longPressRef.current); longPressRef.current = null; }
  };

  const call = async () => {
    if (!num.trim() || calling) return;
    setCalling(true);
    const r = await makeCall(num);
    setCalling(false);
    if (r?.ok) { notify(`Appel vers ${num}`, 'success'); setNum(''); onClose(); }
    else if (r?.error === 'voice_not_configured') {
      // Mode démo : fallback tel: natif (utile sur iOS)
      window.location.href = `tel:${num}`;
    }
    else if (r?.error) notify(r.error, 'error');
  };

  const activeLine = (lines || []).find(l => l.id === lineId) || (lines || [])[0];

  return (
    <div className="shell">
      <div className="topbar gradient safe-top">
        <div className="safe-extend" />
        <button className="topbar-back" onClick={onClose}><Icons.Back /></button>
        <div className="topbar-title">
          <div className="topbar-name">Composer un numéro</div>
          <div className="topbar-sub">
            {activeLine ? `Depuis ${activeLine.phone_number}` : 'Pavé tactile'}
            {' · '}
            <span className={`voice-status voice-${status}`}>{VOICE_STATUS_LABEL[status] || status}</span>
          </div>
        </div>
      </div>
      <div className="shell-main dialer-wrap">
        <div className="dialer-display">
          <div className="dialer-num">{num || <span className="dialer-placeholder">Entrez un numéro</span>}</div>
          {num && !active && <button className="dialer-back" onClick={back} aria-label="Effacer">⌫</button>}
        </div>
        <div className="dialer-grid">
          {keys.map(([k, letters]) => (
            <button key={k}
              className="dialer-key"
              onMouseDown={() => handleKeyDown(k)}
              onMouseUp={() => handleKeyUp(k)}
              onMouseLeave={() => handleKeyUp(k)}
              onTouchStart={() => handleKeyDown(k)}
              onTouchEnd={() => handleKeyUp(k)}
              onClick={() => push(k)}>
              <span className="dialer-digit">{k}</span>
              {letters && <span className="dialer-letters">{letters}</span>}
            </button>
          ))}
        </div>
        <div className="dialer-actions">
          <button className="dialer-call" onClick={call} disabled={!num.trim() || calling}>
            {calling ? (
              <span className="loading-spinner" style={{ width: 22, height: 22, borderTopColor: '#fff' }} />
            ) : (
              <>
                <Icons.Phone /> Appeler
              </>
            )}
          </button>
        </div>
      </div>
    </div>
  );
}

// ==================== NEW CONTACT ====================
function NewContactView({ onClose, onSaved }) {
  useSwipeBack(onClose);
  const { notify } = useApp();
  const [name, setName] = useState('');
  const [phone, setPhone] = useState('');
  const save = () => {
    const n = name.trim(); const p = phone.trim();
    if (!n || !p) { notify('Nom et numéro requis', 'error'); return; }
    try {
      const raw = localStorage.getItem('helvia_live_contacts') || '[]';
      const list = JSON.parse(raw);
      list.unshift({ id: 'local-' + Date.now(), name: n, phone: p, created_at: new Date().toISOString() });
      localStorage.setItem('helvia_live_contacts', JSON.stringify(list));
      notify('Contact ajouté', 'success');
      onSaved && onSaved();
      onClose();
    } catch (e) { notify('Erreur', 'error'); }
  };
  return (
    <div className="shell">
      <div className="topbar gradient safe-top">
        <div className="safe-extend" />
        <button className="topbar-back" onClick={onClose}><Icons.Back /></button>
        <div className="topbar-title">
          <div className="topbar-name">Nouveau contact</div>
          <div className="topbar-sub">Ajout local</div>
        </div>
      </div>
      <div className="shell-main" style={{ padding: '1rem' }}>
        <div className="form-field">
          <label className="form-label">Nom</label>
          <input className="form-input" value={name} onChange={e => setName(e.target.value)} placeholder="Jean Dupont" autoFocus />
        </div>
        <div className="form-field">
          <label className="form-label">Numéro</label>
          <input className="form-input" type="tel" value={phone} onChange={e => setPhone(e.target.value)} placeholder="+41 79 123 45 67" />
        </div>
        <button className="btn-brand btn-block" onClick={save}>Enregistrer</button>
      </div>
    </div>
  );
}

// ==================== NEW MESSAGE (selector + dialer) ====================
function NewMessageView({ onClose, onOpenConv }) {
  useSwipeBack(onClose);
  const { notify, lineId } = useApp();
  const [convs, setConvs] = useState([]);
  const [loading, setLoading] = useState(true);
  const [search, setSearch] = useState('');

  useEffect(() => {
    fetchUnifiedConversations(lineId).then(list => {
      setConvs(list.filter(c => c.channel === 'whatsapp'));
    }).finally(() => setLoading(false));
  }, [lineId]);

  const filtered = useMemo(() => {
    if (!search.trim()) return convs;
    const s = search.toLowerCase();
    return convs.filter(c =>
      (c.contact_name || '').toLowerCase().includes(s) ||
      (c.contact_number || '').includes(s)
    );
  }, [convs, search]);

  return (
    <div className="shell">
      <div className="topbar gradient safe-top">
        <div className="safe-extend" />
        <button className="topbar-back" onClick={onClose}><Icons.Back /></button>
        <div className="topbar-title">
          <div className="topbar-name">Nouveau message</div>
          <div className="topbar-sub">Choisir un contact</div>
        </div>
      </div>
      <div className="shell-main">
        <div className="search-bar">
          <input className="search-input" type="search" placeholder="Rechercher un contact…"
            value={search} onChange={e => setSearch(e.target.value)} autoFocus />
        </div>
        {loading ? (
          <div className="empty-state"><div className="loading-spinner" style={{ width: 28, height: 28 }} /></div>
        ) : filtered.length === 0 ? (
          <div className="empty-state">
            <div className="empty-icon"><Icons.Chat /></div>
            <div className="empty-title">Aucun contact</div>
            <div className="empty-desc">L'envoi vers un nouveau numéro nécessite un template WhatsApp approuvé. Utilisez my.helvia.app pour gérer vos templates.</div>
          </div>
        ) : (
          <ConvList items={filtered} onOpen={(c) => { onClose(); onOpenConv(c); }} onArchive={() => {}} archiveLabel="" />
        )}
      </div>
    </div>
  );
}

// ==================== BOTTOM NAV (Helvia Call : Appels + Compte) ====================
function BottomNav({ tab, onTab, onNew }) {
  return (
    <div className="bottom-nav bottom-nav-2 safe-bottom">
      <button className={`nav-tab ${tab === 'calls' ? 'active' : ''}`} onClick={() => onTab('calls')}>
        <Icons.Phone />
        <span>Appels</span>
      </button>
      <div className="nav-fab-wrap">
        <button key={tab} className={`nav-fab fab-calls`} onClick={onNew} aria-label="Composer un numéro">
          <Icons.Dialpad />
        </button>
      </div>
      <button className={`nav-tab ${tab === 'account' ? 'active' : ''}`} onClick={() => onTab('account')}>
        <Icons.Settings />
        <span>Compte</span>
      </button>
    </div>
  );
}

// ==================== WIDGET THREAD ====================
function WidgetThreadView({ conv, onBack }) {
  const { notify } = useApp();
  const [msgs, setMsgs] = useState([]);
  const [loading, setLoading] = useState(true);
  const scrollRef = useRef(null);

  useEffect(() => {
    setLoading(true);
    api.get(`/my/widgets/${conv.widgetId}/conversations/${conv.id}/messages`)
      .then(r => {
        if (r?.error) notify(r.error, 'error');
        else setMsgs(r.messages || []);
      })
      .catch(() => notify('Erreur réseau', 'error'))
      .finally(() => setLoading(false));
  }, [conv.widgetId, conv.id, notify]);

  useEffect(() => {
    requestAnimationFrame(() => {
      if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
    });
  }, [msgs, loading]);

  const name = conv.contact_name || conv.widgetName || 'Widget web';

  return (
    <div className="thread-screen">
      <div className="topbar safe-top">
        <div className="safe-extend" />
        <button className="topbar-back" onClick={onBack}><Icons.Back /></button>
        <div className="topbar-avatar">{initials(name)}</div>
        <div className="topbar-title">
          <div className="topbar-name">{name}</div>
          <div className="topbar-sub">{conv.widgetName || 'Widget'} · lecture seule</div>
        </div>
      </div>
      <div className="thread-bg" ref={scrollRef}>
        {loading ? (
          <div className="empty-state"><div className="loading-spinner" style={{ width: 28, height: 28 }} /></div>
        ) : msgs.length === 0 ? (
          <div className="empty-state"><div className="empty-desc">Aucun message.</div></div>
        ) : msgs.map(m => {
          const out = m.role === 'assistant';
          return (
            <div key={m.id} className={`msg-row ${out ? 'out' : 'in'}`}>
              <div className="msg-bubble">
                <div className="msg-text">{m.content}</div>
                <span className="msg-foot">{fmtTime(m.created_at)}{out && <span className="msg-by-bot"> bot</span>}</span>
              </div>
            </div>
          );
        })}
      </div>
      <div className="thread-readonly-banner">
        Conversation widget — répondez depuis my.helvia.app si la prise en main humaine est activée.
      </div>
    </div>
  );
}

// ==================== THREAD ====================
function ThreadView({ conv, onBack }) {
  useSwipeBack(onBack);
  if (conv.channel === 'widget') return <WidgetThreadView conv={conv} onBack={onBack} />;
  const { notify } = useApp();
  const [msgs, setMsgs]       = useState([]);
  const [loading, setLoading] = useState(true);
  const [draft, setDraft]     = useState('');
  const [sending, setSending] = useState(false);
  const [botEnabled, setBot]  = useState(conv.bot_enabled === 1);
  const [data, setData]       = useState(conv);
  const [windowOpen, setWindow] = useState(true);
  const [templates, setTemplates] = useState([]);
  const [tplPickerOpen, setTplPicker] = useState(false);
  const [tplSending, setTplSending] = useState(false);
  const scrollRef = useRef(null);

  const load = useCallback(async () => {
    setLoading(true);
    try {
      const r = await api.get(`/my/whatsapp/conversations/${conv.id}/messages`);
      if (r?.error) notify(r.error, 'error');
      else {
        setMsgs(r.messages || []);
        if (r.conversation) {
          setData(r.conversation);
          setBot(r.conversation.bot_enabled === 1);
          // Fenêtre 24h : si dernier message inbound > 24h on le notifie.
          if (r.conversation.last_inbound_at) {
            const last = new Date(r.conversation.last_inbound_at.replace(' ', 'T') + 'Z');
            setWindow((Date.now() - last.getTime()) < 24 * 3600 * 1000);
          }
        }
      }
    } catch { notify('Erreur réseau', 'error'); }
    finally { setLoading(false); }
  }, [conv.id, notify]);

  useEffect(() => { load(); }, [load]);

  useEffect(() => {
    const t = setInterval(load, 15000);
    return () => clearInterval(t);
  }, [load]);

  // Charge les templates approuvés + l'état réel de la fenêtre 24h.
  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await api.get(`/my/whatsapp/conversations/${conv.id}/templates`);
        if (cancelled || r?.error) return;
        setTemplates(r.templates || []);
        if (typeof r.window_open === 'boolean') setWindow(r.window_open);
      } catch {}
    })();
    return () => { cancelled = true; };
  }, [conv.id]);

  const sendTemplate = async (tpl, variables) => {
    if (tplSending) return;
    setTplSending(true);
    try {
      const r = await api.post(`/my/whatsapp/conversations/${conv.id}/reply`,
        { template_id: tpl.id, variables, take_over: botEnabled ? 1 : 0 });
      if (r?.error) {
        const detail = r?.detail?.message || r?.detail?.error_message;
        const code = r?.detail?.code;
        notify(detail ? `${r.error}${code ? ` (${code})` : ''}: ${detail}` : r.error, 'error');
      } else {
        setTplPicker(false);
        setBot(false);
        notify('Template envoyé', 'success');
        load();
      }
    } catch (e) { notify('Erreur envoi template: ' + (e?.message || 'réseau'), 'error'); }
    finally { setTplSending(false); }
  };

  useEffect(() => {
    requestAnimationFrame(() => {
      if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
    });
  }, [msgs, loading]);

  const send = async () => {
    const text = draft.trim();
    if (!text || sending) return;
    setSending(true);
    try {
      const r = await api.post(`/my/whatsapp/conversations/${conv.id}/reply`,
        { body: text, take_over: botEnabled ? 1 : 0 });
      if (r?.error) {
        const detail = r?.detail?.message || r?.detail?.error_message;
        const code = r?.detail?.code;
        notify(detail ? `${r.error}${code ? ` (${code})` : ''}: ${detail}` : r.error, 'error');
        console.warn('[WA reply] error', r);
      } else {
        setDraft('');
        setBot(false);
        load();
      }
    } catch (e) { notify('Erreur envoi: ' + (e?.message || 'réseau'), 'error'); }
    finally { setSending(false); }
  };

  const toggleBot = async () => {
    const next = !botEnabled;
    setBot(next);
    try {
      const r = await api.post(`/my/whatsapp/conversations/${conv.id}/bot`, { enabled: next ? 1 : 0 });
      if (r?.error) { setBot(!next); notify(r.error, 'error'); }
      else notify(next ? 'Bot activé pour ce contact' : 'Bot désactivé — vous prenez la main', 'success');
    } catch { setBot(!next); notify('Erreur', 'error'); }
  };

  const name = data.contact_name || data.contact_number;
  const variant = avatarVariant(data.contact_number || name);

  // Group msgs by day
  const grouped = useMemo(() => {
    const out = [];
    let lastDay = null;
    msgs.forEach(m => {
      const day = (m.created_at || '').slice(0, 10);
      if (day !== lastDay) {
        out.push({ kind: 'day', day, key: 'd' + day + Math.random() });
        lastDay = day;
      }
      out.push({ kind: 'msg', m, key: m.id });
    });
    return out;
  }, [msgs]);

  return (
    <div className="thread-screen">
      <div className="topbar safe-top">
        <div className="safe-extend" />
        <button className="topbar-back" onClick={onBack}><Icons.Back /></button>
        <div className={`topbar-avatar ${variant}`} style={{ width: 36, height: 36, fontSize: '0.85rem' }}>
          {initials(name)}
        </div>
        <div className="topbar-title">
          <div className="topbar-name">{name}</div>
          <div className="topbar-sub">
            {data.contact_number}
            {data.line_phone ? ` · via ${data.line_phone}` : ''}
          </div>
        </div>
        <div className="topbar-actions">
          <button className={`topbar-icon-btn${botEnabled ? ' active-bot' : ''}`} onClick={toggleBot}
            title={botEnabled ? 'Bot actif (cliquer pour reprendre)' : 'Bot inactif (cliquer pour activer)'}>
            <Icons.Bot />
          </button>
        </div>
      </div>

      {!windowOpen && (
        <div className="thread-banner">
          <span>Fenêtre 24h dépassée — utilisez un template approuvé.</span>
          <button className="thread-banner-btn" onClick={() => setTplPicker(true)}>
            Choisir un template ({templates.length})
          </button>
        </div>
      )}

      <div className="thread-bg" ref={scrollRef}>
        {loading && (
          <div className="empty-state">
            <div className="loading-spinner" style={{ width: 28, height: 28 }} />
          </div>
        )}
        {!loading && grouped.length === 0 && (
          <div className="empty-state">
            <div className="empty-desc">Aucun message encore.</div>
          </div>
        )}
        {grouped.map(item => {
          if (item.kind === 'day') {
            return <div key={item.key} className="day-sep"><span>{fmtDay(item.day)}</span></div>;
          }
          const m = item.m;
          const out = m.direction === 'outbound';
          let media = null;
          if (m.media_urls) {
            try {
              const urls = JSON.parse(m.media_urls);
              media = urls.map((u, i) => (
                <a key={i} className="msg-media-link" href={u} target="_blank" rel="noreferrer">📎 Pièce jointe {i + 1}</a>
              ));
            } catch {}
          }
          const isRead = m.status === 'read';
          const isDelivered = m.status === 'delivered' || isRead;
          return (
            <div key={item.key} className={`msg-row ${out ? 'out' : 'in'}`}>
              <div className="msg-bubble">
                <div className="msg-text">{m.body || (m.num_media > 0 ? '📎 Média' : '')}</div>
                {media}
                <span className="msg-foot">
                  {m.sent_by === 'bot' && <span className="msg-by-bot">bot</span>}
                  {fmtTime(m.created_at)}
                  {out && (
                    <span className={`msg-status ${isRead ? 'read' : ''}`}>
                      {isDelivered ? <Icons.DoubleCheck /> : <Icons.Check />}
                    </span>
                  )}
                </span>
              </div>
            </div>
          );
        })}
      </div>

      <div className="compose-wrap">
        <button className="compose-tpl" onClick={() => setTplPicker(true)}
          title={`Templates approuvés (${templates.length})`}>
          <Icons.Template />
        </button>
        <div className="compose-input-wrap">
          <textarea className="compose-input" rows={1}
            placeholder={windowOpen ? 'Message' : 'Fenêtre fermée — utilisez un template'}
            value={draft}
            onChange={e => setDraft(e.target.value)}
            onKeyDown={e => {
              if (e.key === 'Enter' && !e.shiftKey && !e.metaKey) {
                e.preventDefault();
                send();
              }
            }} />
        </div>
        <button className="compose-send" disabled={sending || !draft.trim()} onClick={send}>
          <Icons.Send />
        </button>
      </div>

      {tplPickerOpen && (
        <TemplatePicker
          templates={templates}
          onClose={() => setTplPicker(false)}
          onSend={sendTemplate}
          sending={tplSending}
        />
      )}
    </div>
  );
}

// ==================== TEMPLATE PICKER ====================
function TemplatePicker({ templates, onClose, onSend, sending }) {
  const [selected, setSelected] = useState(null);
  const [vars, setVars] = useState({});

  // Variables détectées dans le body : {{1}}, {{2}}, ...
  const varKeys = useMemo(() => {
    if (!selected?.body) return [];
    const set = new Set();
    selected.body.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, k) => { set.add(k); return ''; });
    return Array.from(set).sort((a, b) => Number(a) - Number(b));
  }, [selected]);

  const preview = useMemo(() => {
    if (!selected?.body) return '';
    return selected.body.replace(/\{\{\s*(\w+)\s*\}\}/g, (m, k) => vars[k] || m);
  }, [selected, vars]);

  const canSend = !!selected && varKeys.every(k => (vars[k] || '').trim().length > 0);

  return (
    <div className="tpl-sheet-backdrop" onClick={onClose}>
      <div className="tpl-sheet" onClick={e => e.stopPropagation()}>
        <div className="tpl-sheet-head">
          <div className="tpl-sheet-title">{selected ? 'Préparer le template' : 'Choisir un template'}</div>
          <button className="tpl-sheet-close" onClick={onClose}>✕</button>
        </div>

        {!selected && (
          <div className="tpl-list">
            {templates.length === 0 && (
              <div className="tpl-empty">
                Aucun template approuvé. Créez et faites approuver un template depuis my.helvia.app.
              </div>
            )}
            {templates.map(t => (
              <button key={t.id} className="tpl-row" onClick={() => { setSelected(t); setVars({}); }}>
                <div className="tpl-row-head">
                  <span className="tpl-row-name">{t.friendly_name}</span>
                  <span className="tpl-row-tag">{t.category || 'utility'} · {t.language || 'fr'}</span>
                </div>
                <div className="tpl-row-body">{t.body}</div>
              </button>
            ))}
          </div>
        )}

        {selected && (
          <div className="tpl-form">
            <div className="tpl-form-head">
              <button className="tpl-back" onClick={() => setSelected(null)}>‹ Retour</button>
              <span className="tpl-row-tag">{selected.category || 'utility'} · {selected.language || 'fr'}</span>
            </div>
            <div className="tpl-name">{selected.friendly_name}</div>

            {varKeys.length > 0 && (
              <div className="tpl-vars">
                {varKeys.map(k => (
                  <label key={k} className="tpl-var">
                    <span>Variable {`{{${k}}}`}</span>
                    <input
                      value={vars[k] || ''}
                      onChange={e => setVars(v => ({ ...v, [k]: e.target.value }))}
                      placeholder={`Valeur pour {{${k}}}`}
                    />
                  </label>
                ))}
              </div>
            )}

            <div className="tpl-preview-label">Aperçu</div>
            <div className="tpl-preview">{preview}</div>

            <button
              className="tpl-send"
              disabled={!canSend || sending}
              onClick={() => onSend(selected, vars)}>
              {sending ? 'Envoi…' : 'Envoyer le template'}
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// ==================== ROOT ====================
function AppProvider({ children, value }) {
  return <AppCtx.Provider value={value}>{children}</AppCtx.Provider>;
}

const STORAGE_LINE = 'helvia_live_line';

function App() {
  const [user, setUser]   = useState(null);
  const [booting, setBoot]= useState(true);
  const [toast, setToast] = useState(null);
  const [tab, setTab] = useState('calls');
  const [dialerOpen, setDialerOpen] = useState(false);
  const [selectedCall, setSelectedCall] = useState(null);
  const handleFab = () => { setDialerOpen(true); };
  const [lines, setLines] = useState([]);
  // Mode embed (iframe depuis dash.helvia.app) : ?embed=1&line_id=X verrouille
  // la ligne sur celle passée en URL et masque le sélecteur.
  const embedParams = useMemo(() => {
    const p = new URLSearchParams(location.search);
    const embed  = p.get('embed') === '1';
    const lid    = parseInt(p.get('line_id') || '0');
    return { embed, forcedLineId: embed && lid ? lid : null };
  }, []);
  const [lineId, setLineIdState] = useState(() => {
    if (embedParams.forcedLineId) return embedParams.forcedLineId;
    const v = localStorage.getItem(STORAGE_LINE);
    return v ? parseInt(v) : null;
  });
  const setLineId = useCallback((id) => {
    // En embed, on refuse les changements (la ligne est imposée par l'URL).
    if (embedParams.embed && embedParams.forcedLineId) return;
    setLineIdState(id);
    if (id) localStorage.setItem(STORAGE_LINE, String(id));
    else localStorage.removeItem(STORAGE_LINE);
  }, [embedParams.embed, embedParams.forcedLineId]);

  const notify = useCallback((msg, type) => setToast({ msg, type }), []);

  const handleLogin = useCallback((token, u) => {
    api.token = token;
    localStorage.setItem(STORAGE_TOKEN, token);
    localStorage.setItem(STORAGE_USER, JSON.stringify(u));
    setUser(u);
  }, []);

  const logout = useCallback(() => {
    api.token = null;
    localStorage.removeItem(STORAGE_TOKEN);
    localStorage.removeItem(STORAGE_USER);
    setUser(null); setSelectedCall(null);
    notify('Déconnecté', 'success');
  }, [notify]);

  useEffect(() => {
    const t = localStorage.getItem(STORAGE_TOKEN);
    const u = localStorage.getItem(STORAGE_USER);
    if (t && u) {
      api.token = t;
      try { setUser(JSON.parse(u)); } catch {}
      api.get('/auth/me').then(r => {
        if (r?.user) setUser(prev => ({ ...prev, ...r.user }));
        else if (r?.error) { api.token = null; localStorage.removeItem(STORAGE_TOKEN); localStorage.removeItem(STORAGE_USER); setUser(null); }
      }).catch(() => {});
    }
    setBoot(false);
    const onLogout = () => { setUser(null); setSelectedCall(null); };
    window.addEventListener('helvia-live-logout', onLogout);
    return () => window.removeEventListener('helvia-live-logout', onLogout);
  }, []);

  useEffect(() => {
    if (!user) { setLines([]); return; }
    api.get('/my/lines').then(r => setLines(r?.lines || [])).catch(() => {});
  }, [user]);

  if (booting) {
    return <div className="app-loading"><div className="loading-spinner" /></div>;
  }

  const showBottomNav = user && !selectedCall && !embedParams.embed;

  return (
    <AppProvider value={{ user, notify, logout, lines, lineId, setLineId, embed: embedParams.embed }}>
      <VoiceProvider user={user} notify={notify}>
        <div className={`app-root ${showBottomNav ? 'has-bottom-nav' : ''}`}>
          {!user ? (
            <AuthScreen onLogin={handleLogin} />
          ) : dialerOpen ? (
            <DialerView onClose={() => setDialerOpen(false)} />
          ) : tab === 'calls' ? (
            selectedCall ? <CallDetailView call={selectedCall} onBack={() => setSelectedCall(null)} />
                         : <CallsView onOpenCall={setSelectedCall} />
          ) : tab === 'account' ? (
            <AccountView />
          ) : null}
          {showBottomNav && (
            <BottomNav tab={tab} onTab={setTab} onNew={handleFab} />
          )}
          <ActiveCallBar />
          <IncomingCallOverlay />
        </div>
        {toast && <Toast msg={toast.msg} type={toast.type} onDone={() => setToast(null)} />}
      </VoiceProvider>
    </AppProvider>
  );
}

ReactDOM.createRoot(document.getElementById('app')).render(<App />);
