// Coque Web MVP. Les composants et leurs styles proviennent directement de la V5 figée.

function VascoConnectionOverlay({ label }) {
  return (
    <div aria-live="polite" style={{
      position: 'absolute', inset: 0, zIndex: 20,
      background: 'rgba(246,245,241,.84)', backdropFilter: 'blur(3px)',
      display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12,
      color: '#0a0a0a', fontFamily: "'Inter Tight', -apple-system, system-ui, sans-serif",
    }}>
      <span style={{
        width: 28, height: 28, borderRadius: '50%',
        border: '2px solid rgba(61,107,143,.2)', borderTopColor: '#3d6b8f',
        animation: 'vascoMvpSpin .7s linear infinite',
      }} />
      <span style={{ fontSize: 13, fontWeight: 650 }}>{label}</span>
      <style>{`@keyframes vascoMvpSpin { to { transform: rotate(360deg); } }`}</style>
    </div>
  );
}

// Les écrans secondaires de la V5 attendent `BottomNav`, défini dans la
// coque historique qui n'est volontairement pas montée par ce MVP connecté.
// On réutilise donc la TabBar visuellement identique du dashboard afin de
// conserver la navigation sur Patients, Agenda, Assistant et Compte.
function BottomNav(props) {
  return <TabBar {...props} />;
}

// Onglet retenu d'un rechargement à l'autre, en local seulement. Les écrans qui
// dépendent d'un état non rejouable — un dossier patient ouvert, un assistant de
// création en cours — n'en font pas partie : les rouvrir sans leur contexte
// donnerait un écran vide.
const DEV_ROUTE_KEY = 'vasco_dev_route';
const DEV_RESTORABLE_ROUTES = ['dashboard', 'patients', 'agenda', 'aichat', 'account'];
const devLocal = ['localhost', '127.0.0.1', '[::1]'].includes(window.location.hostname);

function initialRoute() {
  if (!devLocal) return 'dashboard';
  let stored = null;
  try { stored = window.sessionStorage.getItem(DEV_ROUTE_KEY); } catch { return 'dashboard'; }
  return DEV_RESTORABLE_ROUTES.includes(stored) ? stored : 'dashboard';
}

function rememberRoute(route) {
  if (!devLocal || !DEV_RESTORABLE_ROUTES.includes(route)) return;
  try { window.sessionStorage.setItem(DEV_ROUTE_KEY, route); } catch { /* stockage refusé */ }
}

// `loadMvpData` ne charge que le tableau de bord. Afficher l'écran retenu des
// patients, de l'agenda ou du compte sans charger ses données montrait les
// fixtures de la maquette — trente patients inventés dans une session qui en
// compte huit. On charge donc l'écran restauré avant de le montrer, que l'on
// arrive par une reprise de session ou par une connexion fraîche : les deux
// chemins partent du même onglet retenu et méritent la même vérité.
// Rend `false` quand le chargement a échoué, pour retomber sur le tableau de
// bord : mieux vaut un écran de moins qu'un écran qui ment.
async function loadRouteData(route) {
  const loaders = {
    patients: window.VascoApi.loadPatients,
    agenda: window.VascoApi.loadAgenda,
    account: window.VascoApi.loadAccount,
  };
  const loader = loaders[route];
  if (!loader) return true;
  try {
    await loader();
    return true;
  } catch {
    return false;
  }
}

// Le QR d'un dossier encode un lien vers cette application, l'IPP porté par le
// fragment — la partie de l'URL qu'aucun navigateur n'envoie à un serveur. On
// le lit une seule fois, au chargement du script, et on efface le fragment dans
// la foulée : un rechargement ne doit pas rejouer le geste, et l'IPP n'a pas à
// rester dans la barre d'adresse ni dans l'historique.
function takeRecordIppFromHash() {
  let raw = null;
  try {
    const match = /^#dossier=(.+)$/.exec(window.location.hash || '');
    if (!match) return null;
    raw = match[1];
    window.history.replaceState(null, '', window.location.pathname + window.location.search);
  } catch { return null; }
  try {
    return decodeURIComponent(raw).trim() || null;
  } catch {
    // Fragment malformé : on garde ce qui a été lu plutôt que d'abandonner en
    // silence. Un IPP qui ne correspond à rien se dit à l'écran.
    return raw.trim() || null;
  }
}

const SCANNED_RECORD_IPP = takeRecordIppFromHash();

function AppMobileMvp() {
  const accent = '#3d6b8f';
  const currentName = 'Bleu médical';

  const [authed, setAuthed] = React.useState(false);
  const [onboarded, setOnboarded] = React.useState(false);
  const [busy, setBusy] = React.useState(null);
  const [loginKey, setLoginKey] = React.useState(0);
  const [errorView, setErrorView] = React.useState(() => window.vascoErrorFromUrl());
  const [accountStats, setAccountStats] = React.useState(false);
  const [route, setRoute] = React.useState(initialRoute);
  // Écran vers lequel refermer l'assistant de création de patient, quand il a été
  // ouvert au milieu d'un geste commencé ailleurs. `null` : retour au répertoire.
  const [wizardReturn, setWizardReturn] = React.useState(null);
  // Enregistrement de compte rendu interrompu par une création de patient :
  // `pending` retient la carte, `resume` la rend à l'Assistant avec le dossier créé.
  const [pendingReportSave, setPendingReportSave] = React.useState(null);
  const [resumeReportSave, setResumeReportSave] = React.useState(null);
  const [patientId, setPatientId] = React.useState(null);
  // Onglet d'ouverture du dossier : « Nouvelle note » depuis la pile des
  // dossiers doit tomber sur la saisie, pas sur le résumé.
  const [patientTab, setPatientTab] = React.useState('resume');
  // Patient désigné pour une création de rendez-vous engagée depuis son dossier.
  const [newRdvFor, setNewRdvFor] = React.useState(null);
  const [prevRoute, setPrevRoute] = React.useState(null);
  const [slideAnim, setSlideAnim] = React.useState(false);

  React.useEffect(() => { rememberRoute(route); }, [route]);

  // Reprise de la session locale au chargement. Une modification d'écran ne doit
  // coûter ni une nouvelle organisation de démonstration, ni la conversation en
  // cours : le serveur garde les deux, seul le jeton avait besoin de survivre.
  React.useEffect(() => {
    if (!window.VascoApi?.hasResumableSession?.()) return undefined;
    let cancelled = false;
    setBusy('Reprise de la session locale…');
    (async () => {
      try {
        const resumed = await window.VascoApi.resumeSession();
        if (!resumed || cancelled) return;
        await window.VascoApi.loadMvpData();
        if (cancelled) return;
        // L'écran retenu a besoin de ses propres données avant d'être montré.
        const loaded = await loadRouteData(route);
        if (cancelled) return;
        if (!loaded) setRoute('dashboard');
        setOnboarded(Boolean(window.VascoApi.currentUser()?.onboarding?.completed));
        setAuthed(true);
      } catch {
        // La session locale n'est plus valable : l'écran de connexion reprend
        // la main, ce qui est la réponse honnête plutôt qu'une page à moitié
        // chargée.
        window.VascoApi.clearLocalSession();
      } finally {
        if (!cancelled) setBusy(null);
      }
    })();
    return () => { cancelled = true; };
  }, []);

  const connect = async () => {
    const provider = (() => {
      try { return window.localStorage.getItem('vasco_auth'); } catch { return null; }
    })();
    setBusy('Connexion sécurisée au serveur…');
    try {
      if (provider === 'psc') {
        const capabilities = await window.VascoApi.getAuthCapabilities();
        const psc = capabilities?.providers?.find((item) => item.id === 'pro_sante_connect');
        if (!psc?.enabled) throw new window.VascoApi.VascoApiError(
          'Pro Santé Connect n’est pas encore disponible sur cet environnement', 503, 'provider_unavailable', null,
        );
        throw new window.VascoApi.VascoApiError(
          'Le parcours Pro Santé Connect sera activé avec le fournisseur', 503, 'provider_unavailable', null,
        );
      }
      const result = await window.VascoApi.createDemoSession();
      await window.VascoApi.loadMvpData();
      // Une connexion fraîche reprend elle aussi l'onglet retenu — `route` porte
      // déjà cette valeur, lue de sessionStorage au premier rendu. Sans ce
      // chargement, se connecter sur l'onglet Patients ouvrait le répertoire de
      // la maquette.
      const loaded = await loadRouteData(route);
      if (!loaded) setRoute('dashboard');
      setOnboarded(Boolean(result.user.onboarding.completed));
      setAuthed(true);
    } catch (error) {
      window.vascoAuthClear();
      window.VascoApi.clearLocalSession();
      setLoginKey((value) => value + 1);
      showToast(error?.message || 'Connexion impossible', 'error');
    } finally {
      setBusy(null);
    }
  };

  // Une session démo suivante crée une organisation neuve. Les caches de la
  // session précédente doivent donc disparaître, sinon le prochain login
  // afficherait les patients et l'agenda d'un autre espace.
  const handleLoggedOut = () => {
    window.VASCO_RUNTIME = {};
    window.vascoAuthClear();
    window.VascoApi.clearLocalSession();
    setAuthed(false);
    setOnboarded(false);
    setAccountStats(false);
    setPatientId(null);
    setRoute('dashboard');
    // L'écran de connexion qui revient est la preuve de la déconnexion.
    setLoginKey((value) => value + 1);
  };

  const finishOnboarding = async () => {
    setBusy('Enregistrement de vos préférences…');
    try {
      await window.VascoApi.completeOnboarding();
      setOnboarded(true);
    } catch (error) {
      showToast(error?.message || 'Enregistrement impossible', 'error');
    } finally {
      setBusy(null);
    }
  };

  const slideToChat = () => {
    if (route === 'aichat') return;
    setPrevRoute(route);
    setSlideAnim(true);
    setRoute('aichat');
    setTimeout(() => { setSlideAnim(false); setPrevRoute(null); }, 1200);
  };

  // Le dossier est chargé avant d'être montré : arriver sur l'écran puis découvrir
  // qu'il n'y a rien à lire donnerait un dossier vide sous un nom. Rend `false`
  // quand le chargement a échoué — l'appelant qui enchaîne un geste, comme
  // « Ouvrir » sur une fiche de l'Assistant, saurait sinon dire qu'il a ouvert
  // un dossier que personne n'a vu.
  const openPatient = async (id, tab = 'resume') => {
    setBusy('Chargement du dossier patient…');
    try {
      await window.VascoApi.loadPatient(id);
      setPatientId(id);
      setPatientTab(tab);
      setRoute('patient');
      return true;
    } catch (error) {
      showToast(error?.message || 'Dossier patient indisponible', 'error');
      return false;
    } finally {
      setBusy(null);
    }
  };

  // Dossier désigné par un QR : le code ne donne que l'IPP, il faut donc le
  // répertoire pour le traduire en dossier. `loadPatients` sert le cache quand
  // il est déjà chargé, l'appel ne coûte rien de plus au geste.
  const openRecordByIpp = async (ipp) => {
    setBusy('Ouverture du dossier scanné…');
    let target = null;
    try {
      const patients = await window.VascoApi.loadPatients();
      target = (patients || []).find((patient) => patient.ipp === ipp) || null;
    } catch (error) {
      showToast(error?.message || 'Répertoire indisponible', 'error');
      setBusy(null);
      return;
    }
    setBusy(null);
    if (!target) {
      // Aucun dossier ne porte cet IPP ici. On ouvre quand même l'écran du
      // dossier : PatientDetail y rend « Dossier introuvable », qui reste
      // affiché avec son retour au répertoire. Un toast seul aurait disparu
      // avant d'être lu, en laissant le tableau de bord comme réponse — et un
      // tableau de bord ne se lit pas comme un refus. Le toast le double pour
      // nommer l'IPP en cause, que l'écran ne connaît pas.
      showToast(`Aucun dossier ne porte l’IPP ${ipp}`, 'error');
      setPatientId(`ipp-introuvable:${ipp}`);
      setPatientTab('resume');
      setRoute('patient');
      return;
    }
    await openPatient(target.id);
  };

  // Le geste attend l'authentification, quel qu'en soit le chemin : reprise de
  // session ou connexion fraîche passent toutes les deux par `authed`. Le lien
  // dit où aller, l'authentification garde la porte. Une seule fois : se
  // déconnecter puis se reconnecter ne rouvre pas le dossier scanné.
  // Un objet plutôt que l'IPP nu : rescanner le même dossier est un nouveau
  // geste, et doit rouvrir le dossier même si la valeur n'a pas changé.
  const [scannedRecord, setScannedRecord] = React.useState(
    SCANNED_RECORD_IPP ? { ipp: SCANNED_RECORD_IPP } : null,
  );
  const scannedRecordDone = React.useRef(false);
  // Un scan peut aussi arriver alors que l'app est déjà ouverte : un simple
  // changement de fragment ne recharge pas la page, donc la lecture du
  // chargement ne suffit pas. On écoute le fragment en continu — même geste,
  // même porte.
  React.useEffect(() => {
    const onHashChange = () => {
      const ipp = takeRecordIppFromHash();
      if (!ipp) return;
      scannedRecordDone.current = false;
      setScannedRecord({ ipp });
    };
    window.addEventListener('hashchange', onHashChange);
    return () => window.removeEventListener('hashchange', onHashChange);
  }, []);
  React.useEffect(() => {
    if (!authed || !scannedRecord || scannedRecordDone.current) return;
    scannedRecordDone.current = true;
    openRecordByIpp(scannedRecord.ipp);
  }, [authed, scannedRecord]);

  // Prendre un rendez-vous depuis un dossier : c'est l'agenda qui crée, avec le
  // patient déjà désigné. L'agenda doit être chargé avant d'y arriver, sinon sa
  // semaine est vide le temps de la requête.
  const openAgendaFor = async (id) => {
    setBusy('Chargement de l’agenda…');
    try {
      await window.VascoApi.loadAgenda();
      setNewRdvFor(id);
      setRoute('agenda');
    } catch (error) {
      showToast(error?.message || 'Agenda indisponible', 'error');
    } finally {
      setBusy(null);
    }
  };

  const handleResumeHandled = React.useCallback(() => setResumeReportSave(null), []);
  const handleNewRdvHandled = React.useCallback(() => setNewRdvFor(null), []);

  const handleTab = async (id) => {
    if (id === 'MOI') setAccountStats(false);
    const target = { DICT: 'dashboard', PAT: 'patients', AGE: 'agenda', IA: 'aichat', MOI: 'account' }[id];
    if (!target) return;
    // Le tableau de bord passe par la même porte que les trois autres onglets.
    // Il était le seul à s'afficher sans rien demander : après un archivage ou
    // un renommage fait au répertoire, il gardait ses chiffres du jour, son
    // prochain rendez-vous et le nom d'avant. `loadDashboard` ne rappelle le
    // serveur que si une mutation l'a marqué périmé.
    const loaders = {
      DICT: window.VascoApi.loadDashboard, PAT: window.VascoApi.loadPatients,
      AGE: window.VascoApi.loadAgenda, MOI: window.VascoApi.loadAccount,
    };
    const labels = {
      DICT: 'Actualisation du tableau de bord…', PAT: 'Chargement des patients…',
      AGE: 'Chargement de l’agenda…', MOI: 'Chargement du compte…',
    };
    const loader = loaders[id];
    if (!loader) { setRoute(target); return; }
    setBusy(labels[id]);
    try {
      await loader();
      setRoute(target);
    } catch (error) {
      showToast(error?.message || 'Chargement impossible', 'error');
    } finally {
      setBusy(null);
    }
  };

  const baseScreen = (() => {
    if (route === 'patient') {
      return <PatientDetail patientId={patientId} accent={accent}
        runtime={window.VascoApi.patient}
        initialTab={patientTab}
        onNewAppointment={openAgendaFor}
        onBack={() => setRoute('patients')} onOpenChat={slideToChat} />;
    }
    if (route === 'patients') {
      return <PatientFolders accent={accent}
        onOpenPatient={openPatient}
        onNewPatient={() => setRoute('wizard')}
        onNewAppointment={openAgendaFor}
        onNewNote={(id) => openPatient(id, 'notes')}
        // Retour au tableau de bord par la même porte que l'onglet : c'est le
        // chemin le plus court entre une suppression de dossier et l'écran qui
        // le comptait encore.
        onBack={() => handleTab('DICT')} onTabChange={handleTab} />;
    }
    if (route === 'agenda') return <AgendaScreen accent={accent} onTabChange={handleTab}
      runtime={window.VascoApi.agenda}
      newAppointmentFor={newRdvFor}
      onNewAppointmentHandled={handleNewRdvHandled} />;
    if (route === 'aichat') return <AIChatScreen accent={accent} onTabChange={handleTab}
      runtime={window.VascoApi.assistant}
      resumeSave={resumeReportSave}
      onResumeHandled={handleResumeHandled}
      // « Ouvrir » sur une fiche patient de l'Assistant mène au dossier, par le
      // même chemin que le répertoire ou le tableau de bord : c'est la coque qui
      // sait charger puis naviguer, la conversation ne fait que désigner.
      onOpenPatient={openPatient}
      onCreatePatient={(widget) => {
        setPendingReportSave(widget || null);
        setWizardReturn('aichat');
        setRoute('wizard');
      }} />;
    if (route === 'account') {
      return <AccountScreen accent={accent} onTabChange={handleTab}
        startOnStats={accountStats} onStatsClosed={() => setAccountStats(false)}
        runtime={window.VascoApi.account} onLoggedOut={handleLoggedOut} />;
    }
    return <Dashboard accent={accent} accentName={currentName}
      onOpenChat={slideToChat} onChangeTab={handleTab}
      onOpenPatient={openPatient}
      onSlideToChat={slideToChat}
      runtime={window.VascoApi.dashboard}
      dictationRuntime={window.VascoApi.dictation}
      onOpenStats={() => { setAccountStats(true); setRoute('account'); }} />;
  })();

  return (
    <div style={{
      position: 'absolute', inset: 0, width: '100%', height: '100%', overflow: 'hidden',
      background: '#f6f5f1', display: 'flex', flexDirection: 'column',
    }}>
      {!authed && <LoginScreen key={loginKey} onDone={connect} />}

      {authed && !onboarded && <OnboardingScreen accent={accent} onDone={finishOnboarding} />}

      {authed && <>
        <div style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
          {slideAnim && prevRoute && (
            <div style={{
              position: 'absolute', inset: 0, zIndex: 1,
              animation: 'slideOutLeft 1s cubic-bezier(.16,1,.3,1) forwards', pointerEvents: 'none',
            }}>
              <Dashboard accent={accent} accentName={currentName} onOpenChat={() => {}} onChangeTab={() => {}} />
            </div>
          )}
          <div style={{
            position: 'absolute', inset: 0, zIndex: 2, display: 'flex', flexDirection: 'column',
            animation: slideAnim ? 'slideInRight 1s cubic-bezier(.16,1,.3,1) forwards' : 'none',
          }}>
            <div style={{ flex: 1, minHeight: 0, position: 'relative', overflow: 'hidden' }}>{baseScreen}</div>
            {route === 'patient' && <TabBar active="PAT" onChange={handleTab} accent={accent} />}
          </div>
        </div>
        <style>{`
          @keyframes slideOutLeft { from { transform: translateX(0); opacity: 1; } to { transform: translateX(-30%); opacity: .3; } }
          @keyframes slideInRight { from { transform: translateX(100%); } to { transform: translateX(0); } }
        `}</style>
        {route === 'wizard' && <NewPatientWizard accent={accent}
          runtime={window.VascoApi.patient}
          // On revient là d'où l'on vient : créer un patient depuis une conversation
          // est une parenthèse dans un geste commencé ailleurs, et finir dans le
          // répertoire obligerait à retrouver le fil pour terminer ce geste.
          onClose={() => {
            setRoute(wizardReturn || 'patients');
            setWizardReturn(null);
            // Création abandonnée : il n'y a pas de dossier où écrire, l'enregistrement
            // en attente est oublié plutôt que rejoué au hasard.
            setPendingReportSave(null);
          }}
          onComplete={async (patient) => {
            // Le répertoire est en cache : après une création il doit être
            // relu, sinon le nouveau dossier n'apparaît pas dans la liste.
            await window.VascoApi.loadPatients().catch(() => {});
            if (pendingReportSave?.id && patient?.id) {
              setResumeReportSave({ widgetId: pendingReportSave.id, patientId: patient.id });
            }
            setPendingReportSave(null);
            setRoute(wizardReturn || 'patients');
            setWizardReturn(null);
          }} />}
      </>}

      {errorView && (
        <div style={{ position: 'absolute', inset: 0, zIndex: 6 }}>
          <ErrorScreen variant={errorView}
            onAction={() => {
              if (errorView === '404') { setErrorView(null); setRoute('dashboard'); return; }
              showToast('Nouvelle tentative…', 'info');
              setTimeout(() => setErrorView(null), 900);
            }}
            onSecondary={errorView === '404' ? undefined : () => { setErrorView(null); setRoute('dashboard'); }} />
        </div>
      )}

      <ToastHost />
      <CallHost />
      {busy && <VascoConnectionOverlay label={busy} />}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<AppMobileMvp />);
