// Dictée — machine d'état complète, entièrement mockée.
// idle → patient → type → recording (pause/reprise) → processing → review → envoi à l'agent
//
// Le compte rendu n'est pas « validé » : il part dans le chat comme un message
// adressé à l'assistant, qui l'analysera et proposera les actions à faire.
//
// Zones sûres : le châssis iOS dessine la barre d'état (zIndex 10) et la Dynamic
// Island (zIndex 50) PAR-DESSUS le contenu. Tout écran doit donc dégager 62px en
// haut et 34px en bas (home indicator) — même convention que les autres écrans.

const DT_SAFE_TOP = 62;
const DT_SAFE_BOTTOM = 34;

const DT_VT = window.VT || {};
const DT_INK = DT_VT.ink || '#0a0a0a';
const DT_SUB = DT_VT.sub || '#6b7280';
const DT_BG = DT_VT.paper || '#f6f5f1';
const DT_SURF = DT_VT.surface || '#ffffff';
const DT_HAIR = DT_VT.hair || 'rgba(10,10,10,0.06)';
const DT_HAIR2 = DT_VT.hair2 || 'rgba(10,10,10,0.10)';
const DT_MED = DT_VT.medBlue || '#3d6b8f';
const DT_MED_TINT = DT_VT.medBlueTint || 'rgba(61,107,143,0.10)';
const DT_DANGER = DT_VT.danger || '#b91c1c';

// Coupe une ligne trop longue au lieu de pousser les éléments voisins hors écran.
const DT_TRUNCATE = { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' };

// Teinte de l'état enregistrement — bleu clinique, accordé au micro.
// Elle DOIT être composée par-dessus le fond opaque : une simple rgba() à 4% en
// `background` laisse voir l'écran du dessous au lieu de le couvrir, et
// l'overlay paraît alors posé sur la page.
const DT_REC_BG = `linear-gradient(rgba(61,107,143,0.045), rgba(61,107,143,0.045)), ${DT_BG}`;

const DICT_TYPES = [
  { id: 'CRO', label: 'CRO', sub: 'Compte rendu opératoire' },
  { id: 'CRC', label: 'CRC', sub: 'Compte rendu de consultation' },
  { id: 'CRH', label: 'CRH', sub: "Compte rendu d'hospitalisation" },
  { id: 'NOTE', label: 'Note', sub: 'Note libre' },
  { id: 'LETTRE', label: 'Lettre', sub: 'Lettre au médecin traitant' },
];

// La phrase de rappel s'accorde avec le type : « ce compte rendu … envoyé », mais
// « cette lettre … envoyée ». On stocke donc la proposition entière, pas le seul nom.
const DICT_TYPE_SENTENCE = {
  CRO: 'Ce compte rendu opératoire sera envoyé',
  CRC: 'Ce compte rendu de consultation sera envoyé',
  CRH: "Ce compte rendu d'hospitalisation sera envoyé",
  NOTE: 'Cette note libre sera envoyée',
  LETTRE: 'Cette lettre au médecin traitant sera envoyée',
};

// ═══════════════════════════════════════════════════════════
// Boîte de réception de l'agent
// ═══════════════════════════════════════════════════════════
// L'écran de chat est démonté quand on ne le regarde pas : un envoi depuis le
// dashboard n'a donc aucun destinataire au moment où il part. On met en file
// d'attente, et le chat vide la file à son montage.
const AgentInbox = {
  queue: [],
  listeners: new Set(),

  subscribe(fn) {
    this.listeners.add(fn);
    if (this.queue.length) {
      const pending = this.queue;
      this.queue = [];
      pending.forEach((m) => fn(m));
    }
    return () => this.listeners.delete(fn);
  },

  send(payload) {
    if (this.listeners.size === 0) {
      this.queue.push(payload);
      return;
    }
    this.listeners.forEach((fn) => fn(payload));
  },
};

// Les transcriptions pré-écrites ont été retirées : elles remplissaient
// l'écran de relecture d'un compte rendu clinique complet que personne
// n'avait dicté, et qui partait ensuite dans l'assistant. La transcription
// vient du fournisseur, via le serveur, ou la dictée échoue.

// ── En-tête commun ──
function DictHeader({ title, sub, onClose, onBack }) {
  return (
    <div style={{
      padding: '14px 20px 12px', display: 'flex', alignItems: 'flex-start', gap: 12,
      flexShrink: 0,
    }}>
      {onBack && (
        <button onClick={onBack} className="press" aria-label="Retour" style={{
          background: 'none', border: 'none', cursor: 'pointer', padding: 0,
          marginTop: 2, flexShrink: 0,
        }}>
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={DT_INK}
            strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
            <path d="M15 6l-6 6 6 6" />
          </svg>
        </button>
      )}
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{
          fontSize: 18, fontWeight: 700, color: DT_INK, letterSpacing: '-0.3px', ...DT_TRUNCATE,
        }}>{title}</div>
        {sub && (
          <div style={{
            fontSize: 12, fontWeight: 500, color: DT_SUB, marginTop: 2, ...DT_TRUNCATE,
          }}>{sub}</div>
        )}
      </div>
      {onClose && (
        <button onClick={onClose} className="press" aria-label="Fermer" style={{
          background: 'none', border: 'none', cursor: 'pointer', padding: 0,
          marginTop: 2, flexShrink: 0,
        }}>
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={DT_SUB}
            strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
            <path d="M18 6L6 18M6 6l12 12" />
          </svg>
        </button>
      )}
    </div>
  );
}

// ── Waveform discrète ──
// Amplitude contenue, cadence lente, bleu clinique : elle signale l'activité
// sans capter l'attention. Une waveform agitée fait « composant technique ».
function DictWaveform({ active }) {
  const BARS = 24;
  const [bars, setBars] = React.useState(() => new Array(BARS).fill(4));

  React.useEffect(() => {
    if (!active) {
      setBars(new Array(BARS).fill(4));
      return;
    }
    let t = 0;
    const id = setInterval(() => {
      t += 1;
      setBars(new Array(BARS).fill(0).map((_, i) => {
        const wave = Math.sin(t / 4 + i * 0.5) * 8;
        return Math.max(4, Math.min(26, 13 + wave + (Math.random() * 7 - 3.5)));
      }));
    }, 140);
    return () => clearInterval(id);
  }, [active]);

  return (
    <div style={{
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      gap: 3, height: 28, width: '100%',
    }}>
      {bars.map((h, i) => (
        <div key={i} style={{
          flex: 1, minWidth: 0, maxWidth: 3,
          height: h,
          borderRadius: 999,
          background: DT_MED,
          opacity: active ? 0.3 + (h / 26) * 0.4 : 0.18,
          transition: 'height .14s ease-out, opacity .14s ease-out',
        }} />
      ))}
    </div>
  );
}

// ── Pastille d'état (type de CR, patient associé ou non) ──
function DictChip({ children, tone = 'neutral' }) {
  const tones = {
    accent: { bg: DT_MED_TINT, fg: DT_MED },
    neutral: { bg: 'rgba(10,10,10,0.05)', fg: DT_SUB },
  };
  const c = tones[tone] || tones.neutral;
  return (
    <span className="mono" style={{
      fontSize: 10.5, fontWeight: 600, letterSpacing: '0.3px',
      padding: '5px 10px', borderRadius: 999,
      background: c.bg, color: c.fg,
      whiteSpace: 'nowrap', maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis',
    }}>{children}</span>
  );
}

// ── Étape 1 : choix du patient ──
function DictPatientPicker({ onPick, onClose, title, sub, runtime = null }) {
  const [q, setQ] = React.useState('');
  // `PATIENTS_FULL` contient encore les patients de la maquette tant que
  // l'onglet Patients n'a pas été ouvert : choisir l'un d'eux aurait envoyé au
  // serveur une clé qui n'existe pas. La dictée interroge donc sa propre
  // source, celle que l'API expose pour ce choix.
  const [roster, setRoster] = React.useState(null);

  React.useEffect(() => {
    if (!runtime?.patients) return undefined;
    let cancelled = false;
    runtime.patients()
      .then((items) => {
        if (cancelled) return;
        setRoster(items.map((item) => ({
          id: item.id,
          last: item.family_name.toLocaleUpperCase('fr-FR'),
          first: item.given_name,
          ipp: item.medical_record_number,
          age: item.age,
        })));
      })
      .catch((failure) => {
        if (!cancelled) { showToast(failure?.message || 'Patients indisponibles', 'error'); setRoster([]); }
      });
    return () => { cancelled = true; };
  }, [runtime]);

  const source = roster || (runtime ? [] : (typeof PATIENTS_FULL !== 'undefined' ? PATIENTS_FULL : []));
  const list = source.filter((p) => {
    const s = q.trim().toLowerCase();
    if (!s) return true;
    return `${p.last} ${p.first} ${p.ipp}`.toLowerCase().includes(s);
  });

  return (
    <>
      <DictHeader
        title={title || 'Nouvelle dictée'}
        sub={sub || 'Étape 1 sur 2 — choisir le patient'}
        onClose={onClose} />
      <div style={{ padding: '0 20px 12px', flexShrink: 0 }}>
        <input
          value={q}
          onChange={(e) => setQ(e.target.value)}
          placeholder="Rechercher par nom, prénom ou IPP"
          style={{
            width: '100%', height: DT_VT.hMd || 40, borderRadius: DT_VT.rMd || 12,
            border: `1px solid ${DT_HAIR2}`, background: DT_SURF,
            padding: '0 14px', fontFamily: 'inherit', fontSize: 14, color: DT_INK,
            outline: 'none',
          }} />
      </div>
      {/* minHeight: 0 est indispensable : sans lui, un enfant flex refuse de
          rétrécir sous sa hauteur de contenu et déborde au lieu de défiler. */}
      <div className="vasco-scroll" style={{
        flex: 1, minHeight: 0, overflowY: 'auto', padding: '0 20px 12px',
      }}>
        {list.length === 0 && (
          <div style={{ textAlign: 'center', padding: '32px 0', fontSize: 13, color: DT_SUB }}>
            Aucun patient ne correspond à « {q} »
          </div>
        )}
        {list.map((p) => (
          <button key={p.id} onClick={() => onPick(p)} className="press" style={{
            width: '100%', display: 'flex', alignItems: 'center', gap: 12,
            background: DT_SURF, border: `1px solid ${DT_HAIR}`,
            borderRadius: DT_VT.rLg || 16, padding: '12px 14px', marginBottom: 8,
            cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left',
          }}>
            <div style={{
              width: 36, height: 36, borderRadius: '50%', background: DT_MED_TINT,
              display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
              fontSize: 12, fontWeight: 700, color: DT_MED,
            }}>{p.first[0]}{p.last[0]}</div>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 14, fontWeight: 600, color: DT_INK, ...DT_TRUNCATE }}>
                {p.last} <span style={{ fontWeight: 500 }}>{p.first}</span>
              </div>
              <div className="mono" style={{
                fontSize: 11, fontWeight: 500, color: DT_SUB, marginTop: 2, ...DT_TRUNCATE,
              }}>IPP {p.ipp} · {p.age} ans</div>
            </div>
          </button>
        ))}
      </div>
    </>
  );
}

// ── Étape 2 : type de dictée ──
function DictTypePicker({ patient, onPick, onBack, onClose }) {
  return (
    <>
      <DictHeader
        title={`${patient.last} ${patient.first}`}
        sub="Étape 2 sur 2 — type de dictée"
        onClose={onClose} onBack={onBack} />
      <div className="vasco-scroll" style={{
        flex: 1, minHeight: 0, overflowY: 'auto', padding: '4px 20px 12px',
      }}>
        {DICT_TYPES.map((t) => (
          <button key={t.id} onClick={() => onPick(t.id)} className="press" style={{
            width: '100%', display: 'flex', alignItems: 'center', gap: 12,
            background: DT_SURF, border: `1px solid ${DT_HAIR}`,
            borderRadius: DT_VT.rLg || 16, padding: '16px 14px', marginBottom: 10,
            cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left',
          }}>
            <span className="mono" style={{
              fontSize: 13, fontWeight: 700, color: DT_MED,
              background: DT_MED_TINT, borderRadius: 999, padding: '6px 12px', flexShrink: 0,
            }}>{t.label}</span>
            <span style={{
              flex: 1, minWidth: 0, fontSize: 14, fontWeight: 500, color: DT_INK, ...DT_TRUNCATE,
            }}>{t.sub}</span>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={DT_SUB}
              strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"
              style={{ flexShrink: 0 }}>
              <path d="M9 6l6 6-6 6" />
            </svg>
          </button>
        ))}
      </div>
    </>
  );
}

// ── Étape 3 : enregistrement ──
// Une carte dédiée, centrée, qui installe un vrai « mode dictée » : contexte en
// haut (type + patient), micro bleu au centre, chrono, waveform discrète.
// Les actions sont sorties de la carte pour garder son intérieur calme.
function DictRecorder({ patient, type, onStop, onCancel, onChangeType, live = false }) {
  const [elapsed, setElapsed] = React.useState(0);
  const [paused, setPaused] = React.useState(false);
  // Micro effectivement ouvert. Tant qu'il ne l'est pas, l'écran ne prétend pas
  // enregistrer et « Terminer » dit pourquoi il n'a rien à rendre.
  const [ready, setReady] = React.useState(false);
  // Le chrono tournait sans que rien ne soit capté : « Arrêter » rendait la
  // main sans audio. Le micro est ouvert pour de bon quand l'écran est relié
  // au serveur.
  const captureRef = React.useRef(null);

  React.useEffect(() => {
    if (!live) return undefined;
    let cancelled = false;
    const open = async () => {
      try {
        if (!navigator.mediaDevices?.getUserMedia) {
          throw new Error('Ce navigateur ne permet pas l’enregistrement.');
        }
        const AudioContextClass = window.AudioContext || window.webkitAudioContext;
        if (!AudioContextClass) throw new Error('L’enregistrement n’est pas disponible.');
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        if (cancelled) { stream.getTracks().forEach((track) => track.stop()); return; }
        const context = new AudioContextClass();
        const source = context.createMediaStreamSource(stream);
        const processor = context.createScriptProcessor(4096, 1, 1);
        const silentOutput = context.createGain();
        const chunks = [];
        silentOutput.gain.value = 0;
        processor.onaudioprocess = (event) => {
          if (captureRef.current?.paused) return;
          chunks.push(new Float32Array(event.inputBuffer.getChannelData(0)));
        };
        source.connect(processor);
        processor.connect(silentOutput);
        silentOutput.connect(context.destination);
        captureRef.current = {
          context, source, processor, silentOutput, stream, chunks,
          sampleRate: context.sampleRate, startedAt: performance.now(), paused: false,
        };
        setReady(true);
      } catch (error) {
        showToast(error?.message || 'Le micro n’a pas pu démarrer', 'error');
        onCancel();
      }
    };
    void open();
    return () => {
      cancelled = true;
      const capture = captureRef.current;
      captureRef.current = null;
      if (!capture) return;
      capture.processor.onaudioprocess = null;
      capture.source.disconnect();
      capture.processor.disconnect();
      capture.silentOutput.disconnect();
      capture.stream.getTracks().forEach((track) => track.stop());
      capture.context.close().catch(() => {});
    };
  }, [live, onCancel]);

  React.useEffect(() => {
    if (captureRef.current) captureRef.current.paused = paused;
  }, [paused]);

  const finish = () => {
    const capture = captureRef.current;
    // Terminer sans micro ouvert fermait la dictée sans un mot : ni son, ni
    // transcription, ni explication. L'autorisation peut n'avoir jamais été
    // donnée — le navigateur attend une réponse que personne n'a cliquée.
    if (live && !capture) {
      showToast('Le micro n’a pas démarré : rien n’a été enregistré', 'error');
      onCancel();
      return;
    }
    if (!live || !capture) { onStop(elapsed, null); return; }
    captureRef.current = null;
    capture.processor.onaudioprocess = null;
    capture.source.disconnect();
    capture.processor.disconnect();
    capture.silentOutput.disconnect();
    capture.stream.getTracks().forEach((track) => track.stop());
    capture.context.close().catch(() => {});
    const durationMs = Math.max(1, performance.now() - capture.startedAt);
    const audio = window.encodePcmWav(capture.chunks, capture.sampleRate);
    if (audio.size <= 44) {
      showToast('Aucun son n’a été enregistré', 'error');
      onCancel();
      return;
    }
    onStop(elapsed, { blob: audio, durationMs });
  };
  const [typeOpen, setTypeOpen] = React.useState(false);
  // Le menu s'ouvre vers le haut (l'espace y est vide, et vers le bas il
  // masquerait le micro). Sur un écran court il n'y a pas la place : on mesure
  // après ouverture et on rebascule vers le bas plutôt que de le laisser rogné.
  const [menuUp, setMenuUp] = React.useState(true);
  const menuRef = React.useRef(null);

  React.useLayoutEffect(() => {
    if (!typeOpen) { setMenuUp(true); return; }
    const el = menuRef.current;
    if (!el) return;
    const flow = el.closest('[data-dict-flow]');
    const limit = flow ? flow.getBoundingClientRect().top : 0;
    if (el.getBoundingClientRect().top < limit + 4) setMenuUp(false);
  }, [typeOpen]);

  // Le chrono ne part qu'une fois le micro réellement ouvert. Tant que
  // l'autorisation n'est pas donnée, il n'y a rien à compter : afficher
  // « Enregistrement 00:06 » pendant que le navigateur attend une réponse
  // annonce une captation qui n'a pas lieu.
  React.useEffect(() => {
    if (paused || (live && !ready)) return undefined;
    const id = setInterval(() => setElapsed((e) => e + 1), 1000);
    return () => clearInterval(id);
  }, [paused, live, ready]);

  const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
  const ss = String(elapsed % 60).padStart(2, '0');
  const typeLabel = (DICT_TYPES.find((t) => t.id === type) || {}).label || 'Note';

  const ghostBtn = {
    flex: 1, minWidth: 0, height: 46, borderRadius: DT_VT.rMd || 12,
    background: 'none', border: `1px solid ${DT_HAIR2}`,
    fontFamily: 'inherit', fontSize: 13, fontWeight: 600, color: DT_INK,
    cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
  };

  return (
    <>
      <div style={{
        flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column',
        alignItems: 'center', justifyContent: 'center', padding: '0 22px',
      }}>
        <div style={{
          width: '100%', maxWidth: 330,
          background: DT_SURF, borderRadius: DT_VT.rXl2 || 22,
          border: `1px solid ${DT_HAIR}`,
          boxShadow: DT_VT.shadowCard || '0 2px 8px rgba(10,10,10,0.06)',
          padding: '26px 22px 24px',
          display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 24,
          position: 'relative',
        }}>
          {/* Contexte : quel type, quel patient. Le type reste modifiable sans
              interrompre l'enregistrement — le chrono continue de tourner. */}
          <div style={{
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            gap: 6, flexWrap: 'wrap', maxWidth: '100%', position: 'relative',
          }}>
            <button onClick={() => setTypeOpen((o) => !o)} className="press" aria-label="Changer le type"
              style={{
                background: 'none', border: 'none', padding: 0, cursor: 'pointer',
                fontFamily: 'inherit', display: 'flex', alignItems: 'center',
              }}>
              <span className="mono" style={{
                fontSize: 10.5, fontWeight: 600, letterSpacing: '0.3px',
                padding: '5px 8px 5px 10px', borderRadius: 999,
                background: DT_MED_TINT, color: DT_MED,
                display: 'flex', alignItems: 'center', gap: 4, whiteSpace: 'nowrap',
              }}>
                {typeLabel}
                <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke={DT_MED}
                  strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M6 9l6 6 6-6" />
                </svg>
              </span>
            </button>
            <DictChip>{patient ? `${patient.last} ${patient.first}` : 'Aucun patient associé'}</DictChip>

            {typeOpen && (
              <>
                {/* Ferme au clic à côté, sans masquer la carte. */}
                <div onClick={() => setTypeOpen(false)} style={{
                  position: 'fixed', inset: 0, zIndex: 4,
                }} />
                <div ref={menuRef} style={{
                  position: 'absolute', zIndex: 5, left: 0,
                  ...(menuUp ? { bottom: 'calc(100% + 8px)' } : { top: 'calc(100% + 8px)' }),
                  background: DT_SURF, borderRadius: DT_VT.rMd || 12,
                  border: `1px solid ${DT_HAIR2}`,
                  boxShadow: DT_VT.shadowLg || '0 12px 32px rgba(10,10,10,0.12)',
                  padding: 4, minWidth: 190, overflow: 'hidden',
                  animation: 'dictStepIn .16s ease-out',
                }}>
                  {DICT_TYPES.map((t) => {
                    const on = t.id === type;
                    return (
                      <button key={t.id} className="press"
                        onClick={() => { onChangeType && onChangeType(t.id); setTypeOpen(false); }}
                        style={{
                          width: '100%', display: 'flex', alignItems: 'center', gap: 9,
                          background: on ? DT_MED_TINT : 'none', border: 'none',
                          borderRadius: 8, padding: '9px 10px', cursor: 'pointer',
                          fontFamily: 'inherit', textAlign: 'left',
                        }}>
                        <span style={{
                          width: 7, height: 7, borderRadius: '50%', flexShrink: 0,
                          background: on ? DT_MED : 'rgba(10,10,10,0.18)',
                        }} />
                        {/* Colonne fixe pour que les intitulés du menu restent alignés.
                            Elle tient le plus long des cinq, « Lettre ». */}
                        <span className="mono" style={{
                          fontSize: 11, fontWeight: 700, color: on ? DT_MED : DT_INK,
                          flexShrink: 0, width: 44,
                        }}>{t.label}</span>
                        <span style={{
                          fontSize: 11.5, fontWeight: 500, color: DT_SUB,
                          flex: 1, minWidth: 0, ...DT_TRUNCATE,
                        }}>{t.sub}</span>
                      </button>
                    );
                  })}
                </div>
              </>
            )}
          </div>

          {/* Micro bleu — halo léger, jamais clignotant */}
          <div style={{
            position: 'relative', width: 88, height: 88, flexShrink: 0,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            {!paused && (
              <>
                <span style={{
                  position: 'absolute', inset: 0, borderRadius: '50%',
                  border: `1px solid ${DT_MED}`,
                  animation: 'dictHalo 2.8s cubic-bezier(.4,0,.2,1) infinite',
                }} />
                <span style={{
                  position: 'absolute', inset: 0, borderRadius: '50%',
                  border: `1px solid ${DT_MED}`,
                  animation: 'dictHalo 2.8s cubic-bezier(.4,0,.2,1) 1.4s infinite',
                }} />
              </>
            )}
            <div style={{
              width: 68, height: 68, borderRadius: '50%',
              background: paused ? DT_MED_TINT : DT_MED,
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              transition: 'background .25s ease',
            }}>
              <svg width="30" height="30" viewBox="0 0 24 24" fill="none"
                stroke={paused ? DT_MED : '#fff'} strokeWidth="1.7"
                strokeLinecap="round" strokeLinejoin="round">
                <rect x="9" y="3" width="6" height="12" rx="3" />
                <path d="M5 11a7 7 0 0 0 14 0" />
                <path d="M12 18v3" />
              </svg>
            </div>
          </div>

          {/* Chrono */}
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
            <span className="mono" style={{
              fontSize: 34, fontWeight: 600, color: DT_INK,
              letterSpacing: '1px', fontVariantNumeric: 'tabular-nums', lineHeight: 1,
            }}>{mm}:{ss}</span>
            <span className="mono" style={{
              fontSize: 10, fontWeight: 600, color: DT_SUB,
              letterSpacing: '0.6px', textTransform: 'uppercase',
            }}>{live && !ready ? 'Micro en attente' : (paused ? 'En pause' : 'Enregistrement')}</span>
          </div>

          <DictWaveform active={!paused} />
        </div>
      </div>

      {/* Actions — hors de la carte, alignées sur les autres écrans */}
      <div style={{
        display: 'flex', gap: 8, padding: '0 22px 8px', flexShrink: 0,
        width: '100%', maxWidth: 374, margin: '0 auto',
      }}>
        <button onClick={onCancel} className="press" style={{ ...ghostBtn, color: DT_SUB }}>
          Annuler
        </button>
        <button onClick={() => setPaused((p) => !p)} className="press" style={ghostBtn}
          aria-label={paused ? 'Reprendre' : 'Pause'}>
          {paused ? (
            <svg width="14" height="14" viewBox="0 0 24 24" fill={DT_INK} stroke="none">
              <path d="M8 5l11 7-11 7z" />
            </svg>
          ) : (
            <svg width="13" height="13" viewBox="0 0 24 24" fill={DT_INK} stroke="none">
              <rect x="6" y="4" width="4" height="16" rx="1" />
              <rect x="14" y="4" width="4" height="16" rx="1" />
            </svg>
          )}
          {paused ? 'Reprendre' : 'Pause'}
        </button>
        <button onClick={finish} className="press" aria-label="Terminer" style={{
          flex: 1, minWidth: 0, height: 46, borderRadius: DT_VT.rMd || 12,
          background: DT_INK, border: 'none',
          fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: '#fff',
          cursor: 'pointer',
        }}>Terminer</button>
      </div>
    </>
  );
}

// ── Étape 4 : transcription ──
function DictProcessing({ phase = null }) {
  return (
    <div style={{
      flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column',
      alignItems: 'center', justifyContent: 'center', gap: 18, padding: '0 32px',
    }}>
      <div style={{
        width: 44, height: 44, borderRadius: '50%', flexShrink: 0,
        border: `2.5px solid ${DT_HAIR2}`, borderTopColor: DT_MED,
        animation: 'dictSpin .8s linear infinite',
      }} />
      <div style={{ fontSize: 15, fontWeight: 600, color: DT_INK, textAlign: 'center' }}>
        {phase === 'uploading' ? 'Envoi de l’enregistrement…' : 'Transcription en cours…'}
      </div>
      <div style={{ fontSize: 12, fontWeight: 500, color: DT_SUB, textAlign: 'center' }}>
        {phase === 'uploading' ? 'Le fichier part vers le serveur' : 'Le fournisseur traite l’audio'}
      </div>
    </div>
  );
}

// ── Étape 5 : relecture avant envoi à l'assistant ──
function DictReview({ patient, type, text, onChange, onSend, onRetake, onDiscard, onAssociate, busy = false }) {
  const typeLabel = (DICT_TYPES.find((t) => t.id === type) || {}).label || 'Note';
  return (
    <>
      <DictHeader
        title={patient ? `${patient.last} ${patient.first}` : 'Dictée sans patient'}
        sub={`${typeLabel} — relire avant envoi à l'assistant`} />

      {/* Statut d'association — explicite tant qu'aucun dossier n'est rattaché */}
      <div style={{
        display: 'flex', alignItems: 'center', gap: 8,
        padding: '0 20px 10px', flexShrink: 0,
      }}>
        <DictChip tone={patient ? 'accent' : 'neutral'}>
          {patient ? `${patient.last} ${patient.first}` : 'Aucun patient associé'}
        </DictChip>
        <button onClick={onAssociate} className="press" style={{
          background: 'none', border: 'none', cursor: 'pointer', padding: 0,
          fontFamily: 'inherit', fontSize: 12, fontWeight: 600, color: DT_MED,
          whiteSpace: 'nowrap',
        }}>
          {patient ? 'Changer' : 'Associer à un patient'}
        </button>
      </div>

      <div style={{ flex: 1, minHeight: 0, padding: '0 20px 10px', display: 'flex' }}>
        <textarea
          value={text}
          onChange={(e) => onChange(e.target.value)}
          className="vasco-scroll"
          style={{
            width: '100%', flex: 1, minHeight: 0, resize: 'none',
            background: DT_SURF, border: `1px solid ${DT_HAIR2}`,
            borderRadius: DT_VT.rLg || 16, padding: '14px 16px',
            fontFamily: "'Source Serif 4', Georgia, serif", fontSize: 14, lineHeight: 1.55,
            color: DT_INK, outline: 'none',
          }} />
      </div>

      {/* Dit explicitement où part le texte : ce n'est pas un simple archivage. */}
      <div style={{
        display: 'flex', alignItems: 'flex-start', gap: 8,
        padding: '0 20px 12px', flexShrink: 0,
      }}>
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={DT_MED}
          strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"
          style={{ flexShrink: 0, marginTop: 1 }}>
          <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
        </svg>
        <span style={{ fontSize: 11.5, fontWeight: 500, color: DT_SUB, lineHeight: 1.35 }}>
          {patient
            ? `${DICT_TYPE_SENTENCE[type] || 'Ce compte rendu sera envoyé'} à l'assistant, qui l'analysera et proposera les actions à faire.`
            : "Sans patient associé, l'assistant ouvrira une nouvelle conversation. Vous pourrez y rattacher un dossier existant, en créer un, ou traiter plus tard."}
        </span>
      </div>

      {/* Deux rangées : le libellé principal est long, une seule rangée déborderait. */}
      <div style={{ padding: '0 20px 12px', flexShrink: 0 }}>
        <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
          <button onClick={onDiscard} className="press" style={{
            flex: 1, height: 44, borderRadius: DT_VT.rMd || 12,
            background: 'none', border: `1px solid ${DT_HAIR2}`,
            fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: DT_DANGER,
            cursor: 'pointer',
          }}>Rejeter</button>
          <button onClick={onRetake} className="press" style={{
            flex: 1, height: 44, borderRadius: DT_VT.rMd || 12,
            background: 'none', border: `1px solid ${DT_HAIR2}`,
            fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: DT_INK,
            cursor: 'pointer',
          }}>Reprendre</button>
        </div>
        <button onClick={onSend} disabled={busy} className="press" style={{
          width: '100%', height: DT_VT.hLg || 48, borderRadius: DT_VT.rLg || 16,
          background: DT_INK, border: 'none',
          fontFamily: 'inherit', fontSize: 14, fontWeight: 700, color: '#fff',
          cursor: busy ? 'default' : 'pointer', opacity: busy ? 0.6 : 1,
          display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
        }}>
          {busy ? 'Envoi…' : 'Envoyer à l\'assistant'}
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff"
            strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
            <line x1="22" y1="2" x2="11" y2="13" />
            <polygon points="22 2 15 22 11 13 2 9 22 2" />
          </svg>
        </button>
      </div>
    </>
  );
}

// ── Le flux complet ──
function DictationFlow({ onClose, onSent, mode = 'full', runtime = null }) {
  const quick = mode === 'quick';
  // Dictée rapide : on entre directement en enregistrement, sans patient ni
  // choix de type. Le type sera « Note » par défaut, modifiable après coup.
  const [step, setStep] = React.useState(quick ? 'recording' : 'patient');
  const [patient, setPatient] = React.useState(null);
  const [type, setType] = React.useState(quick ? 'NOTE' : null);
  const [duration, setDuration] = React.useState(0);
  const [text, setText] = React.useState('');
  // La dictée du serveur, une fois transcrite : c'est elle qui porte la version
  // attendue par les corrections et par la remise à l'assistant.
  const [dictation, setDictation] = React.useState(null);
  const [phase, setPhase] = React.useState(null);
  const [busy, setBusy] = React.useState(false);

  // La maquette attendait deux secondes puis affichait un texte pris dans
  // `MOCK_TRANSCRIPTS`. Le praticien relisait un compte rendu que personne
  // n'avait dicté, et pouvait l'envoyer à l'assistant. Sans serveur, la
  // dictée s'arrête maintenant au lieu d'en inventer un.
  const stop = async (elapsed, capture) => {
    setDuration(elapsed);
    if (!runtime) {
      showToast('La dictée nécessite une connexion au serveur', 'error');
      onClose();
      return;
    }
    if (!capture) { onClose(); return; }
    setStep('processing');
    setPhase('uploading');
    try {
      const result = await runtime.start({
        patientId: patient?.id || null,
        documentType: type || 'NOTE',
        blob: capture.blob,
        durationMs: capture.durationMs,
      }, (progress) => setPhase(progress.stage));
      setDictation(result);
      setText(result.transcript?.text || '');
      setStep('review');
    } catch (failure) {
      showToast(failure?.message || 'La transcription a échoué', 'error');
      onClose();
    } finally {
      setPhase(null);
    }
  };

  // Envoyer, c'est remettre la dictée à l'assistant côté serveur : une
  // conversation réelle est créée, avec le texte relu.
  const sendToAgent = async () => {
    if (!runtime || !dictation) { onClose(); return; }
    setBusy(true);
    try {
      let current = dictation;
      const edited = text.trim();
      if (edited && edited !== (current.transcript?.text || '').trim()) {
        current = await runtime.editTranscript(current, edited);
      }
      const handoff = await runtime.handoff(current);
      // L'écran bascule sur la conversation qui porte la dictée : la
      // destination vaut mieux qu'une annonce sur l'écran qu'on quitte.
      onClose();
      onSent && onSent(handoff.conversation);
    } catch (failure) {
      showToast(failure?.message || 'Envoi impossible', 'error');
    } finally {
      setBusy(false);
    }
  };

  const discard = () => {
    if (runtime && dictation) runtime.cancel(dictation).catch(() => {});
    onClose();
  };

  return (
    <div data-dict-flow="1" style={{
      position: 'absolute', inset: 0, zIndex: 300,
      // Fond toujours opaque : l'overlay remplace l'écran, il ne se pose pas dessus.
      background: step === 'recording' ? DT_REC_BG : DT_BG,
      display: 'flex', flexDirection: 'column',
      paddingTop: DT_SAFE_TOP,
      paddingBottom: DT_SAFE_BOTTOM,
      overflow: 'hidden',
      transition: 'background .3s ease',
      animation: 'dictFlowIn .26s cubic-bezier(.16,1,.3,1)',
      fontFamily: "'Inter Tight', -apple-system, system-ui, sans-serif",
    }}>
      {/* key={step} remonte le sous-arbre à chaque étape, ce qui rejoue
          l'animation d'entrée : la nouvelle vue prend la place de l'ancienne
          au lieu d'apparaître d'un coup. */}
      <div key={step} style={{
        flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column',
        animation: 'dictStepIn .24s cubic-bezier(.16,1,.3,1)',
      }}>
        {step === 'patient' && (
          <DictPatientPicker
            runtime={runtime}
            onPick={(p) => { setPatient(p); setStep('type'); }}
            onClose={onClose} />
        )}
        {step === 'type' && (
          <DictTypePicker
            patient={patient}
            onPick={(t) => { setType(t); setStep('recording'); }}
            onBack={() => setStep('patient')}
            onClose={onClose} />
        )}
        {step === 'recording' && (
          <DictRecorder
            patient={patient} type={type} onStop={stop} live={Boolean(runtime)}
            onChangeType={setType}
            onCancel={onClose} />
        )}
        {step === 'processing' && <DictProcessing phase={phase} />}
        {step === 'review' && (
          <DictReview
            patient={patient} type={type} text={text} onChange={setText}
            onSend={() => void sendToAgent()} busy={busy}
            onRetake={() => { setDictation(null); setStep('recording'); }}
            onAssociate={() => setStep('associate')}
            onDiscard={discard} />
        )}
        {step === 'associate' && (
          <DictPatientPicker
            title="Associer à un patient"
            sub="Choisir le dossier à rattacher"
            runtime={runtime}
            onPick={(p) => { setPatient(p); setStep('review'); }}
            onClose={() => setStep('review')} />
        )}
      </div>

      <style>{`
        @keyframes dictFlowIn {
          from { opacity: 0; transform: translateY(10px); }
          to   { opacity: 1; transform: translateY(0); }
        }
        @keyframes dictStepIn {
          from { opacity: 0; transform: translateY(6px); }
          to   { opacity: 1; transform: translateY(0); }
        }
        @keyframes dictHalo {
          0%   { transform: scale(.78); opacity: .5; }
          70%  { transform: scale(1);   opacity: 0; }
          100% { transform: scale(1);   opacity: 0; }
        }
        @keyframes dictDot {
          0%, 100% { opacity: 1; transform: scale(1); }
          50%      { opacity: .35; transform: scale(.8); }
        }
        @keyframes dictSpin { to { transform: rotate(360deg); } }
      `}</style>
    </div>
  );
}

window.DictationFlow = DictationFlow;
window.AgentInbox = AgentInbox;
