import React, { useEffect, useMemo, useRef, useState } from 'react'
import professeurTopImage from './assets/professeur-top.png'

const API_BASE = '/api'
const AUTO_STOP_SILENCE_MS = 2500
const SPEECH_START_THRESHOLD = 0.02
const SILENCE_THRESHOLD = 0.012
const DEBUG_AUDIO = false
const TTS_PROFILE_STORAGE_KEY = 'professeur-top-tts-profile'

async function parseApiResponse(res) {
  const contentType = res.headers.get('content-type') || ''
  const bodyText = await res.text()
  const isJson = contentType.includes('application/json')
  const data = isJson && bodyText ? JSON.parse(bodyText) : null

  if (!res.ok) {
    const detail =
      data?.detail ||
      data?.message ||
      bodyText.trim() ||
      `Erreur API (${res.status})`
    throw new Error(detail)
  }

  if (!isJson) {
    throw new Error(`Réponse API invalide (${res.status})`)
  }

  return data
}

async function apiFetch(path, options = {}) {
  const res = await fetch(`${API_BASE}${path}`, {
    credentials: 'same-origin',
    ...options,
  })
  return parseApiResponse(res)
}

async function fetchAudio(path, options) {
  const res = await fetch(`${API_BASE}${path}`, {
    credentials: 'same-origin',
    ...options,
  })
  if (!res.ok) {
    let detail = `Erreur API (${res.status})`
    const contentType = res.headers.get('content-type') || ''
    const bodyText = await res.text()

    if (contentType.includes('application/json') && bodyText) {
      try {
        const data = JSON.parse(bodyText)
        detail = data?.detail || data?.message || detail
      } catch {
        if (bodyText.trim()) detail = bodyText.trim()
      }
    } else if (bodyText.trim()) {
      detail = bodyText.trim()
    }
    throw new Error(detail)
  }
  return res.blob()
}

function Avatar({ speaking }) {
  return (
    <div className="avatar-shell">
      <div className={`avatar ${speaking ? 'speaking' : ''}`}>
        <img
          className="avatar-image"
          src={professeurTopImage}
          alt="Professeur TOP"
        />
      </div>
      <div className="avatar-caption">Professeur TOP</div>
    </div>
  )
}

function ProgressCard({ item }) {
  const pct = Math.round(item.mastery_score)
  return (
    <div className="progress-card">
      <div className="progress-head">
        <strong>{item.label}</strong>
        <span>{pct}%</span>
      </div>
      <div className="progress-bar">
        <div className="progress-fill" style={{ width: `${pct}%` }} />
      </div>
      <small>{item.subject} · preuves: {item.evidence_count}</small>
    </div>
  )
}

function flattenProgramTree(node, depth = 0, rows = []) {
  if (!node) return rows
  rows.push({ node, depth })
  ;(node.children || []).forEach((child) => flattenProgramTree(child, depth + 1, rows))
  return rows
}

function flattenVisibleProgramTree(node, collapsedKeys, depth = 0, rows = []) {
  if (!node) return rows
  rows.push({ node, depth })
  if (!collapsedKeys.has(node.key)) {
    ;(node.children || []).forEach((child) => flattenVisibleProgramTree(child, collapsedKeys, depth + 1, rows))
  }
  return rows
}

function collectCollapsibleProgramKeys(node, depth = 0, keys = []) {
  if (!node) return keys
  if (depth > 0 && node.children?.length) keys.push(node.key)
  ;(node.children || []).forEach((child) => collectCollapsibleProgramKeys(child, depth + 1, keys))
  return keys
}

function ProgramReviewPanel({ selectedNode, reviewForm, onReviewChange, onSaveReview, readOnly = false, onOpenValidation }) {
  const [isFullscreen, setIsFullscreen] = useState(false)

  if (!selectedNode) {
    return (
      <section className="admin-empty">
        <h2>Programme</h2>
        <p className="muted">Choisis un élément de l’arborescence pour le relire, le valider ou laisser un commentaire.</p>
      </section>
    )
  }

  const status = selectedNode.status || {}
  const review = selectedNode.review || {}
  return (
    <section className="admin-program-review">
      <div className="admin-log-detail-head">
        <div>
          <span className="stage-label">{selectedNode.type}</span>
          <h2>{selectedNode.label}</h2>
          <p className="muted">{selectedNode.path || selectedNode.key}</p>
        </div>
        {review.status && <span className={`review-pill ${review.status}`}>{review.status}</span>}
      </div>

      {Object.keys(status).length > 0 && (
        <div className="program-status-grid">
          <span>Micro-fiches: {status.micro_fiches ?? 0}</span>
          <span>SVG leçon: {status.lesson_svg ?? 0}</span>
          <span>Cards: {status.cards ?? 0}</span>
          <span>Contextes: {status.contexts ?? 0}</span>
          <span>Exercices: {status.text_exercises ?? 0}</span>
          <span>SVG exercices: {status.exercise_svg ?? 0}</span>
        </div>
      )}

      {selectedNode.url && (
        <button type="button" className="program-node-preview" onClick={() => setIsFullscreen(true)}>
          <img src={selectedNode.url} alt={selectedNode.label} />
          <span>{readOnly ? 'Cliquer pour inspecter en plein écran' : 'Cliquer pour valider en plein écran'}</span>
        </button>
      )}

      {review.comment && (
        <div className="admin-review-note">
          <strong>Dernier commentaire</strong>
          <p>{review.comment}</p>
          <span>{review.reviewer} · {review.updated_at}</span>
        </div>
      )}

      {readOnly ? (
        <div className="admin-review-form">
          <strong>Validation centralisée</strong>
          <p className="muted">Cet onglet sert à explorer le programme. Les décisions éditoriales se font dans l’onglet Validation contenus.</p>
          <button type="button" onClick={onOpenValidation}>Ouvrir dans Validation contenus</button>
        </div>
      ) : (
        <form className="admin-review-form" onSubmit={onSaveReview}>
          <label>
            <span>Statut de validation</span>
            <select
              value={reviewForm.status}
              onChange={(event) => onReviewChange({ ...reviewForm, status: event.target.value })}
            >
              <option value="validated">Validé</option>
              <option value="needs_changes">À modifier</option>
              <option value="blocked">Bloqué</option>
              <option value="draft">Brouillon</option>
            </select>
          </label>
          <label>
            <span>Commentaire visible par Codex</span>
            <textarea
              value={reviewForm.comment}
              onChange={(event) => onReviewChange({ ...reviewForm, comment: event.target.value })}
              placeholder="Note ce qui dépasse, ce qui est incorrect, ou ce qui doit être régénéré."
              rows={5}
            />
          </label>
          <button type="submit">Enregistrer la revue</button>
        </form>
      )}

      {isFullscreen && (
        <div className="review-fullscreen" role="dialog" aria-modal="true" aria-label={`Revue plein écran ${selectedNode.label}`}>
          <div className="review-fullscreen-stage">
            <div className="review-fullscreen-head">
              <div>
                <span className="stage-label">{selectedNode.type}</span>
                <h2>{selectedNode.label}</h2>
                <p className="muted">{selectedNode.path || selectedNode.key}</p>
              </div>
              <button type="button" className="secondary-button" onClick={() => setIsFullscreen(false)}>
                Fermer
              </button>
            </div>
            <div className="review-fullscreen-image">
              <img src={selectedNode.url} alt={selectedNode.label} />
            </div>
          </div>

          <aside className="review-fullscreen-panel">
            <h2>{readOnly ? 'Inspection' : 'Validation'}</h2>
            {readOnly ? (
              <div className="admin-review-form">
                <p className="muted">Pour modifier le statut ou laisser une remarque, ouvre cet élément dans la file de validation.</p>
                <button type="button" onClick={() => { setIsFullscreen(false); onOpenValidation?.() }}>
                  Ouvrir dans Validation contenus
                </button>
              </div>
            ) : (
              <form className="admin-review-form" onSubmit={onSaveReview}>
                <label>
                  <span>Statut</span>
                  <select
                    value={reviewForm.status}
                    onChange={(event) => onReviewChange({ ...reviewForm, status: event.target.value })}
                  >
                    <option value="validated">Validé</option>
                    <option value="needs_changes">À modifier</option>
                    <option value="blocked">Bloqué</option>
                    <option value="draft">Brouillon</option>
                  </select>
                </label>
                <label>
                  <span>Commentaire</span>
                  <textarea
                    value={reviewForm.comment}
                    onChange={(event) => onReviewChange({ ...reviewForm, comment: event.target.value })}
                    placeholder="Note précisément ce qui doit être corrigé."
                    rows={8}
                  />
                </label>
                <button type="submit">Enregistrer la revue</button>
              </form>
            )}
          </aside>
        </div>
      )}
    </section>
  )
}

function getSupportedRecordingMimeType() {
  if (typeof MediaRecorder === 'undefined') return ''
  const candidates = [
    'audio/webm;codecs=opus',
    'audio/webm',
    'audio/mp4',
    'audio/ogg;codecs=opus',
  ]
  return candidates.find((type) => MediaRecorder.isTypeSupported(type)) || ''
}

function formatVoiceLabel(profile) {
  if (!profile?.label) return ''
  return profile.label.toLowerCase().startsWith('voix ')
    ? profile.label
    : `Voix ${profile.label}`
}

function LoginPage({ onLogin }) {
  const [credentials, setCredentials] = useState({ username: '', password: '' })
  const [status, setStatus] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)

  async function submitLogin(e) {
    e.preventDefault()
    if (!credentials.username.trim() || !credentials.password) return

    setIsSubmitting(true)
    setStatus('')
    try {
      const data = await apiFetch('/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          username: credentials.username.trim(),
          password: credentials.password,
        }),
      })
      onLogin(data.user)
    } catch (error) {
      setStatus(error.message || 'Connexion impossible.')
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <main className="login-page">
      <section className="login-panel card">
        <Avatar speaking={false} />
        <div className="login-copy">
          <h1>Professeur TOP</h1>
          <p className="muted">Connexion élève, professeur ou maintenance.</p>
        </div>

        <form onSubmit={submitLogin} className="stack login-form">
          <input
            autoComplete="username"
            placeholder="Identifiant"
            value={credentials.username}
            onChange={(event) =>
              setCredentials({ ...credentials, username: event.target.value })
            }
          />
          <input
            autoComplete="current-password"
            placeholder="Mot de passe"
            type="password"
            value={credentials.password}
            onChange={(event) =>
              setCredentials({ ...credentials, password: event.target.value })
            }
          />
          <button type="submit" disabled={isSubmitting}>
            {isSubmitting ? 'Connexion...' : 'Se connecter'}
          </button>
        </form>
        {status && <p className="muted voice-status">{status}</p>}
      </section>
    </main>
  )
}

function TutorApp({ currentUser, onLogout }) {
  const isStudentUser = currentUser.role === 'student'
  const [students, setStudents] = useState([])
  const [selectedStudentId, setSelectedStudentId] = useState('')
  const [form, setForm] = useState({
    first_name: '',
    age: 8,
    grade: 'CM1',
    username: '',
    password: '',
  })
  const [messages, setMessages] = useState([])
  const [currentInstruction, setCurrentInstruction] = useState('')
  const [lastStudentTransmission, setLastStudentTransmission] = useState('')
  const [sessionActive, setSessionActive] = useState(false)
  const [currentFocusSkill, setCurrentFocusSkill] = useState(null)
  const [exerciseStatus, setExerciseStatus] = useState('not-started')
  const [input, setInput] = useState('')
  const [progress, setProgress] = useState([])
  const [assessment, setAssessment] = useState(null)
  const [assessmentAnswer, setAssessmentAnswer] = useState('')
  const [speaking, setSpeaking] = useState(false)
  const [isRecording, setIsRecording] = useState(false)
  const [isAutoListening, setIsAutoListening] = useState(false)
  const [isTranscribing, setIsTranscribing] = useState(false)
  const [voiceStatus, setVoiceStatus] = useState('')
  const [errorMessage, setErrorMessage] = useState('')
  const [micLevel, setMicLevel] = useState(0)
  const [audioDebug, setAudioDebug] = useState([])
  const [voiceProfiles, setVoiceProfiles] = useState([])
  const [selectedVoiceProfileId, setSelectedVoiceProfileId] = useState('')
  const [programLessons, setProgramLessons] = useState([])
  const [programStatus, setProgramStatus] = useState(null)
  const [programTree, setProgramTree] = useState(null)
  const [selectedProgramNodeKey, setSelectedProgramNodeKey] = useState('')
  const [programReviewForm, setProgramReviewForm] = useState({ status: 'needs_changes', comment: '' })
  const [activeAdminTab, setActiveAdminTab] = useState('overview')
  const [collapsedProgramKeys, setCollapsedProgramKeys] = useState(() => new Set())
  const [studentProgram, setStudentProgram] = useState(null)
  const [activeAssetIndex, setActiveAssetIndex] = useState(0)
  const [activeSectionIndex, setActiveSectionIndex] = useState(0)
  const [programStart, setProgramStart] = useState({ asset_index: 0, section_index: 0 })
  const [cardProgress, setCardProgress] = useState([])
  const [sessionCardStates, setSessionCardStates] = useState({})
  const [sessionLogs, setSessionLogs] = useState([])
  const [selectedSessionLog, setSelectedSessionLog] = useState(null)
  const [sessionMessageStartIndex, setSessionMessageStartIndex] = useState(0)
  const [progressPanelOpen, setProgressPanelOpen] = useState(false)
  const [settingsPanelOpen, setSettingsPanelOpen] = useState(false)
  const mediaRecorderRef = useRef(null)
  const mediaStreamRef = useRef(null)
  const recordedChunksRef = useRef([])
  const recordingMimeTypeRef = useRef('')
  const audioContextRef = useRef(null)
  const analyserRef = useRef(null)
  const sourceNodeRef = useRef(null)
  const processorNodeRef = useRef(null)
  const monitorGainRef = useRef(null)
  const silenceStartedAtRef = useRef(null)
  const hasSpeechInSegmentRef = useRef(false)
  const activeAssetIndexRef = useRef(0)
  const activeSectionIndexRef = useRef(0)
  const isRecordingRef = useRef(false)
  const isAutoListeningRef = useRef(false)
  const isTranscribingRef = useRef(false)
  const speakingRef = useRef(false)
  const levelFrameCountRef = useRef(0)
  const audioPlaybackRef = useRef(null)
  const audioPlaybackUrlRef = useRef('')

  const selectedStudent = useMemo(
    () => students.find((student) => String(student.id) === String(selectedStudentId)),
    [students, selectedStudentId]
  )

  const selectedVoiceProfile = useMemo(
    () => voiceProfiles.find((profile) => profile.id === selectedVoiceProfileId) || null,
    [voiceProfiles, selectedVoiceProfileId]
  )

  const averageScore = useMemo(() => {
    if (!progress.length) return 0
    const total = progress.reduce((sum, item) => sum + item.mastery_score, 0)
    return Math.round(total / progress.length)
  }, [progress])

  const lessonFocus = useMemo(() => {
    if (currentFocusSkill) return currentFocusSkill
    if (!progress.length) return null
    return [...progress].sort((a, b) => a.mastery_score - b.mastery_score)[0]
  }, [currentFocusSkill, progress])

  const lessonPercent = lessonFocus ? Math.round(lessonFocus.mastery_score) : averageScore
  const programRows = useMemo(() => flattenProgramTree(programTree).filter((row) => row.node.type !== 'root'), [programTree])
  const visibleProgramRows = useMemo(
    () => flattenVisibleProgramTree(programTree, collapsedProgramKeys).filter((row) => row.node.type !== 'root'),
    [programTree, collapsedProgramKeys]
  )
  const selectedProgramNode = useMemo(
    () => programRows.find((row) => row.node.key === selectedProgramNodeKey)?.node || null,
    [programRows, selectedProgramNodeKey]
  )

  const exerciseLabel = {
    'not-started': 'Pas encore lance',
    'in-progress': 'En cours',
    resolved: 'Resolu',
    'needs-review': 'A revoir',
  }[exerciseStatus]

  useEffect(() => {
    loadStudents()
    if (!isStudentUser) {
      loadProgramTree()
      loadProgramStatus()
    }
  }, [currentUser])

  useEffect(() => {
    if (selectedProgramNode) {
      setProgramReviewForm({
        status: selectedProgramNode.review?.status || 'needs_changes',
        comment: selectedProgramNode.review?.comment || '',
      })
    }
  }, [selectedProgramNodeKey, selectedProgramNode?.review?.updated_at])

  function pushAudioDebug(message) {
    if (!DEBUG_AUDIO) return
    const timestamp = new Date().toLocaleTimeString('fr-FR', { hour12: false })
    setAudioDebug((prev) => [`${timestamp} ${message}`, ...prev].slice(0, 12))
  }

  function setLessonPosition(assetIndex, sectionIndex) {
    activeAssetIndexRef.current = assetIndex
    activeSectionIndexRef.current = sectionIndex
    setActiveAssetIndex(assetIndex)
    setActiveSectionIndex(sectionIndex)
  }

  function toggleProgramNode(node) {
    if (!node?.children?.length) {
      setSelectedProgramNodeKey(node?.key || '')
      return
    }
    setSelectedProgramNodeKey(node.key)
    setCollapsedProgramKeys((current) => {
      const next = new Set(current)
      if (next.has(node.key)) next.delete(node.key)
      else next.add(node.key)
      return next
    })
  }

  useEffect(() => {
    isRecordingRef.current = isRecording
  }, [isRecording])

  useEffect(() => {
    activeAssetIndexRef.current = activeAssetIndex
  }, [activeAssetIndex])

  useEffect(() => {
    activeSectionIndexRef.current = activeSectionIndex
  }, [activeSectionIndex])

  useEffect(() => {
    setInput('')
    setAssessmentAnswer('')
    setLastStudentTransmission('')
  }, [activeAssetIndex, activeSectionIndex])

  useEffect(() => {
    isAutoListeningRef.current = isAutoListening
  }, [isAutoListening])

  useEffect(() => {
    isTranscribingRef.current = isTranscribing
  }, [isTranscribing])

  useEffect(() => {
    speakingRef.current = speaking
  }, [speaking])

  useEffect(() => {
    setCurrentInstruction('')
    setSessionActive(false)
    setAssessment(null)
    setAssessmentAnswer('')
    setLastStudentTransmission('')
    setCurrentFocusSkill(null)
    setExerciseStatus('not-started')
    setStudentProgram(null)
    setProgramLessons([])
    setProgramStart({ asset_index: 0, section_index: 0 })
    setCardProgress([])
    setSessionCardStates({})
    setSessionLogs([])
    setSelectedSessionLog(null)
    if (selectedStudentId) {
      loadProgress(selectedStudentId)
      loadMessages(selectedStudentId)
      loadStudentProgram(selectedStudentId)
      loadCardProgress(selectedStudentId)
      if (!isStudentUser) {
        loadProgramStatus()
        loadSessionLogs(selectedStudentId)
        const student = students.find((item) => String(item.id) === String(selectedStudentId))
        if (student) loadProgramLessons(student.grade)
      }
    } else {
      setMessages([])
    }
  }, [selectedStudentId, students])

  useEffect(() => {
    if (selectedVoiceProfileId) {
      window.localStorage.setItem(TTS_PROFILE_STORAGE_KEY, selectedVoiceProfileId)
    }
  }, [selectedVoiceProfileId])

  useEffect(() => {
    loadVoiceProfiles()
    return () => {
      stopSpeechPlayback()
      deactivateAutoListening(false)
    }
  }, [])

  useEffect(() => {
    if (selectedStudentId && studentProgram?.id) {
      loadCardProgress(selectedStudentId, studentProgram.id)
    }
  }, [selectedStudentId, studentProgram?.id])

  async function loadStudents() {
    if (isStudentUser) {
      if (currentUser.student) {
        setStudents([currentUser.student])
        setSelectedStudentId(String(currentUser.student.id))
      } else {
        setStudents([])
        setSelectedStudentId('')
        setErrorMessage('Ce compte élève n’est pas encore rattaché à une fiche élève.')
      }
      return
    }

    try {
      setErrorMessage('')
      const data = await apiFetch('/students')
      setStudents(data)
      if (data.length && !selectedStudentId) {
        setSelectedStudentId(String(data[0].id))
      }
    } catch (error) {
      setErrorMessage(error.message || 'Impossible de charger les élèves.')
    }
  }

  async function loadVoiceProfiles() {
    try {
      const data = await apiFetch('/tts/profiles')
      const nextProfiles = data.profiles || []
      setVoiceProfiles(nextProfiles)
      setSelectedVoiceProfileId((currentId) => {
        if (currentId && nextProfiles.some((profile) => profile.id === currentId)) {
          return currentId
        }

        const storedId = window.localStorage.getItem(TTS_PROFILE_STORAGE_KEY)
        if (storedId && nextProfiles.some((profile) => profile.id === storedId)) {
          return storedId
        }

        return nextProfiles[0]?.id || ''
      })
    } catch (error) {
      setErrorMessage(error.message || 'Impossible de charger les voix du professeur.')
    }
  }

  async function createStudent(e) {
    e.preventDefault()
    if (isStudentUser) return
    if (!form.first_name.trim()) {
      setErrorMessage('Le prénom est obligatoire.')
      return
    }
    if (!form.username.trim() || !form.password) {
      setErrorMessage('Identifiant et mot de passe élève obligatoires.')
      return
    }

    try {
      setErrorMessage('')
      const data = await apiFetch('/admin/student-accounts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          ...form,
          first_name: form.first_name.trim(),
          username: form.username.trim(),
          age: Number(form.age),
        }),
      })
      await loadStudents()
      setSelectedStudentId(String(data.student.id))
      setForm({ first_name: '', age: 8, grade: 'CM1', username: '', password: '' })
    } catch (error) {
      setErrorMessage(error.message || 'Impossible de créer le compte élève.')
    }
  }

  async function startSession() {
    if (!selectedStudentId) return
    try {
      setErrorMessage('')
      setInput('')
      setAssessmentAnswer('')
      setLastStudentTransmission('')
      setVoiceStatus('')
      setSessionMessageStartIndex(messages.length)
      setSessionCardStates({})
      const data = await apiFetch(`/session/start?student_id=${selectedStudentId}`, { method: 'POST' })
      appendMessage('assistant', data.reply)
      setCurrentInstruction(data.reply)
      setSessionActive(true)
      setExerciseStatus('not-started')
      setCurrentFocusSkill((current) => current || lessonFocus)
      speak(data.reply)
      await loadProgress(selectedStudentId)
    } catch (error) {
      setErrorMessage(error.message || 'Impossible de démarrer la séance.')
    }
  }

  function appendMessage(role, content) {
    setMessages((prev) => [...prev, { role, content, id: crypto.randomUUID() }])
  }

  async function loadProgress(studentId) {
    try {
      setErrorMessage('')
      const data = await apiFetch(`/progress/${studentId}`)
      setProgress(data.progress || [])
    } catch (error) {
      setErrorMessage(error.message || 'Impossible de charger la progression.')
    }
  }

  async function fetchAssessment() {
    if (!selectedStudentId) return
    try {
      setErrorMessage('')
      const data = await apiFetch(`/assessment/next/${selectedStudentId}`)
      setAssessment(data)
      setAssessmentAnswer('')
      setCurrentInstruction(data.question)
      setSessionActive(true)
      setExerciseStatus('in-progress')
      setCurrentFocusSkill(
        progress.find((item) => item.code === data.skill_code) || {
          code: data.skill_code,
          label: data.skill_label,
          mastery_score: lessonPercent,
        }
      )
    } catch (error) {
      setErrorMessage(error.message || 'Impossible de charger le mini-test.')
    }
  }

  async function submitAssessment(e) {
    e.preventDefault()
    if (!assessment || !assessmentAnswer.trim()) return
    try {
      setErrorMessage('')
      const data = await apiFetch('/assessment/answer', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          student_id: Number(selectedStudentId),
          skill_code: assessment.skill_code,
          answer: assessmentAnswer,
        }),
      })
      appendMessage('user', `Reponse au mini-test: ${assessmentAnswer}`)
      setLastStudentTransmission(assessmentAnswer)
      appendMessage('assistant', data.feedback)
      setCurrentInstruction(data.feedback)
      setExerciseStatus(data.correct ? 'resolved' : 'needs-review')
      speak(data.feedback)
      setAssessment(null)
      setAssessmentAnswer('')
      await loadProgress(selectedStudentId)
    } catch (error) {
      setErrorMessage(error.message || 'Impossible de valider la réponse.')
    }
  }

  function stopSpeechPlayback() {
    if (audioPlaybackRef.current) {
      audioPlaybackRef.current.pause()
      audioPlaybackRef.current.src = ''
      audioPlaybackRef.current = null
    }
    if (audioPlaybackUrlRef.current) {
      URL.revokeObjectURL(audioPlaybackUrlRef.current)
      audioPlaybackUrlRef.current = ''
    }
    setSpeaking(false)
  }

  async function saveLessonSession() {
    if (!selectedStudentId || !studentProgram) return
    const currentCardState = getActiveCardState(40, false)
    const sessionMessages = messages.slice(sessionMessageStartIndex)
    const conversation = sessionMessages.map((message) => ({
      id: message.id,
      role: message.role,
      content: message.content,
      created_at: message.created_at || null,
    }))
    const lastConversationItem = conversation[conversation.length - 1]
    if (currentInstruction && lastConversationItem?.content !== currentInstruction) {
      conversation.push({
        id: 'current-instruction',
        role: 'assistant',
        content: currentInstruction,
        created_at: null,
      })
    }
    await apiFetch('/session/end', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        student_id: Number(selectedStudentId),
        lesson_id: studentProgram.id,
        lesson_title: studentProgram.title,
        conversation,
        card_states: getMergedCardStates(currentCardState),
      }),
    })
    await loadCardProgress(selectedStudentId, studentProgram.id)
    await loadStudentProgram(selectedStudentId)
    if (!isStudentUser) await loadSessionLogs(selectedStudentId)
  }

  async function endSession() {
    try {
      await saveLessonSession()
    } catch (error) {
      setErrorMessage(error.message || 'Impossible d’enregistrer la séance.')
    }
    stopSpeechPlayback()
    deactivateAutoListening(false)
    setSessionActive(false)
    setProgressPanelOpen(false)
    setSettingsPanelOpen(false)
    setCurrentInstruction('')
    setAssessment(null)
    setAssessmentAnswer('')
    setInput('')
    setLastStudentTransmission('')
    setSessionCardStates({})
  }

  async function toggleSession() {
    if (sessionActive) {
      endSession()
      return
    }
    await startSession()
  }

  async function loadMessages(studentId) {
    try {
      setErrorMessage('')
      const data = await apiFetch(`/students/${studentId}/messages?limit=80`)
      setMessages(
        (data || []).map((message) => ({
          id: message.id,
          role: message.role,
          content: message.content,
          created_at: message.created_at,
        }))
      )
    } catch (error) {
      setErrorMessage(error.message || 'Impossible de charger la conversation.')
    }
  }

  async function loadProgramLessons(grade) {
    if (!grade) return
    try {
      const data = await apiFetch(`/program/lessons?grade=${encodeURIComponent(grade)}`)
      setProgramLessons(data.lessons || [])
    } catch (error) {
      setErrorMessage(error.message || 'Impossible de charger le programme.')
    }
  }

  async function loadProgramStatus() {
    try {
      const data = await apiFetch('/program/status')
      setProgramStatus(data)
    } catch {
      setProgramStatus(null)
    }
  }

  async function loadProgramTree() {
    if (isStudentUser) return
    try {
      const data = await apiFetch('/admin/program/tree')
      setProgramTree(data)
      setSelectedProgramNodeKey((current) => current || data.children?.[0]?.key || '')
      setCollapsedProgramKeys(new Set(collectCollapsibleProgramKeys(data)))
    } catch (error) {
      setErrorMessage(error.message || 'Impossible de charger l’arborescence du programme.')
    }
  }

  async function saveProgramReview(event) {
    event.preventDefault()
    if (!selectedProgramNode) return
    try {
      await apiFetch('/admin/program/review', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          node_key: selectedProgramNode.key,
          status: programReviewForm.status,
          comment: programReviewForm.comment,
        }),
      })
      await loadProgramTree()
    } catch (error) {
      setErrorMessage(error.message || 'Impossible d’enregistrer la revue du programme.')
    }
  }

  async function loadStudentProgram(studentId) {
    try {
      const data = await apiFetch(`/students/${studentId}/program`)
      setStudentProgram(data.lesson || null)
      const nextStart = {
        asset_index: data.start_asset_index || 0,
        section_index: data.start_section_index || 0,
      }
      setProgramStart(nextStart)
      setLessonPosition(nextStart.asset_index, nextStart.section_index)
    } catch (error) {
      setErrorMessage(error.message || 'Impossible de charger la leçon de l’élève.')
    }
  }

  async function loadCardProgress(studentId, lessonId = studentProgram?.id) {
    if (!studentId) return
    try {
      const suffix = lessonId ? `?lesson_id=${encodeURIComponent(lessonId)}` : ''
      const data = await apiFetch(`/students/${studentId}/card-progress${suffix}`)
      setCardProgress(data || [])
    } catch {
      setCardProgress([])
    }
  }

  async function loadSessionLogs(studentId) {
    if (!studentId || isStudentUser) return
    try {
      const data = await apiFetch(`/admin/students/${studentId}/session-logs`)
      setSessionLogs(data || [])
    } catch {
      setSessionLogs([])
    }
  }

  async function assignProgramLesson(lessonId) {
    if (!selectedStudentId || !lessonId) return
    try {
      setErrorMessage('')
      const data = await apiFetch(`/admin/students/${selectedStudentId}/program`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ lesson_id: lessonId }),
      })
      setStudentProgram(data.lesson || null)
      const nextStart = {
        asset_index: data.start_asset_index || 0,
        section_index: data.start_section_index || 0,
      }
      setProgramStart(nextStart)
      setLessonPosition(nextStart.asset_index, nextStart.section_index)
    } catch (error) {
      setErrorMessage(error.message || 'Impossible d’affecter cette leçon.')
    }
  }

  async function updateProgramStart(assetIndex, sectionIndex) {
    if (!selectedStudentId || !studentProgram) return
    try {
      const data = await apiFetch(`/admin/students/${selectedStudentId}/program/start`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          asset_index: Number(assetIndex),
          section_index: Number(sectionIndex),
        }),
      })
      setStudentProgram(data.lesson || null)
      const nextStart = {
        asset_index: data.start_asset_index || 0,
        section_index: data.start_section_index || 0,
      }
      setProgramStart(nextStart)
      setLessonPosition(nextStart.asset_index, nextStart.section_index)
    } catch (error) {
      setErrorMessage(error.message || 'Impossible de programmer la reprise.')
    }
  }

  async function deleteSessionLog(logId) {
    try {
      await apiFetch(`/admin/session-logs/${logId}`, { method: 'DELETE' })
      setSelectedSessionLog((current) => (current?.id === logId ? null : current))
      await loadSessionLogs(selectedStudentId)
    } catch (error) {
      setErrorMessage(error.message || 'Impossible d’effacer ce log.')
    }
  }

  function parseLogJson(value) {
    try {
      const parsed = JSON.parse(value || '[]')
      return Array.isArray(parsed) ? parsed : []
    } catch {
      return []
    }
  }

  function getLessonPosition() {
    const assetIndex = activeAssetIndexRef.current
    const sectionIndex = activeSectionIndexRef.current
    const asset = lessonAssets[assetIndex] || lessonAssets[0] || null
    const sections = asset?.sections || []
    const section = sections[sectionIndex] || null
    return { assetIndex, sectionIndex, asset, sections, section }
  }

  function goToPreviousLessonStep() {
    const position = getLessonPosition()
    if (position.sectionIndex > 0) {
      setLessonPosition(position.assetIndex, position.sectionIndex - 1)
      return
    }
    if (position.assetIndex > 0) {
      const previousAsset = lessonAssets[position.assetIndex - 1]
      setLessonPosition(position.assetIndex - 1, Math.max((previousAsset?.sections?.length || 1) - 1, 0))
    }
  }

  function goToNextLessonStep() {
    const nextStep = getNextLessonStep()
    if (!nextStep) return
    setLessonPosition(nextStep.assetIndex, nextStep.sectionIndex)
  }

  function getNextLessonStep() {
    const position = getLessonPosition()
    const sectionCount = position.sections.length || 1
    if (position.sectionIndex < sectionCount - 1) {
      return {
        assetIndex: position.assetIndex,
        sectionIndex: position.sectionIndex + 1,
        asset: position.asset,
        section: position.sections[position.sectionIndex + 1],
      }
    }
    if (position.assetIndex < lessonAssets.length - 1) {
      const nextAsset = lessonAssets[position.assetIndex + 1]
      return {
        assetIndex: position.assetIndex + 1,
        sectionIndex: 0,
        asset: nextAsset,
        section: nextAsset?.sections?.[0] || null,
      }
    }
    return null
  }

  function buildNextCardInstruction(nextStep) {
    const title = nextStep?.section?.title || nextStep?.asset?.title || 'la suite'
    const key = nextStep?.section?.context_key || ''
    const prompts = {
      Fiche1Card2: 'Très bien, on passe à Exemple. Lis le nombre 345 208.',
      Fiche1Card3: 'Très bien, on passe à À retenir. Dis-moi ce qu’on lit en premier.',
      Fiche1Card4: 'Très bien, on passe à Exemple 1. Lis le nombre 7 425.',
      Fiche1Card5: 'Très bien, on passe à Exemple 2. Lis le nombre 38 090.',
      Fiche1Card6: 'Très bien, on passe à Exemple 3. Lis le nombre 506 012.',
    }
    if (prompts[key]) return prompts[key]
    if (title.toLowerCase().startsWith('exemple')) {
      return `Très bien, on passe à ${title}. Lis seulement le nombre affiché.`
    }
    return `Très bien, on passe à ${title}. Regarde cette card.`
  }

  function normalizeAnswer(text) {
    return text
      .toLowerCase()
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .replace(/[’']/g, ' ')
  }

  function answerValidatesCurrentCard(text) {
    const answer = normalizeAnswer(text)
    const { section } = getLessonPosition()
    const key = section?.context_key || ''
    if (/^(ok|oui|d accord|daccord|j ai compris|compris)[.!?\s]*$/.test(answer.trim())) return true
    if (/\b(oui|ok|d accord)\b/.test(answer) && /compr/.test(answer)) return true
    if (key === 'Fiche1Card1') {
      return /compr/.test(answer) || (answer.includes('345') && answer.includes('208'))
    }
    if (key === 'Fiche1Card2') {
      const digitRead = answer.replace(/\D/g, '') === '345208'
      const wordRead = answer.includes('trois') && answer.includes('quarante') && answer.includes('mille') && answer.includes('huit')
      return digitRead || wordRead
    }
    if (key === 'Fiche1Card3') {
      return answer.includes('milliers') && (answer.includes('unites') || answer.includes('simples'))
    }
    if (key === 'Fiche1Card4') {
      return answer.includes('sept') && answer.includes('mille') && answer.includes('vingt') && answer.includes('cinq')
    }
    if (key === 'Fiche1Card5') {
      return answer.includes('trente') && answer.includes('huit') && answer.includes('mille') && answer.includes('quatre') && answer.includes('dix')
    }
    if (key === 'Fiche1Card6') {
      return answer.includes('cinq') && answer.includes('six') && answer.includes('mille') && answer.includes('douze')
    }
    return false
  }

  function getSectionKey(asset, section) {
    return section?.context_key || `${asset?.path || 'asset'}:${section?.index ?? 0}`
  }

  function getActiveCardState(score = 0, validated = false) {
    const { asset, section } = getLessonPosition()
    if (!studentProgram || !asset) return null
    const sectionTitle = section?.title || asset.title
    const sectionKey = getSectionKey(asset, section)
    return {
      lesson_id: studentProgram.id,
      asset_path: asset.path,
      asset_title: asset.title,
      section_key: sectionKey,
      section_title: sectionTitle,
      comprehension_score: score,
      attempts: 1,
      validated,
    }
  }

  function rememberActiveCard({ score = 100, validated = true } = {}) {
    const state = getActiveCardState(score, validated)
    if (!state) return
    setSessionCardStates((current) => {
      const previous = current[state.section_key] || {}
      return {
        ...current,
        [state.section_key]: {
          ...state,
          comprehension_score: Math.max(previous.comprehension_score || 0, state.comprehension_score),
          attempts: Math.max(previous.attempts || 0, state.attempts),
          validated: Boolean(previous.validated || state.validated),
        },
      }
    })
  }

  function getMergedCardStates(extraState = null) {
    const fromProgress = cardProgress
      .filter((item) => item.lesson_id === studentProgram?.id)
      .map((item) => ({
        lesson_id: item.lesson_id,
        asset_path: item.asset_path,
        asset_title: item.asset_title,
        section_key: item.section_key,
        section_title: item.section_title,
        comprehension_score: item.comprehension_score,
        attempts: item.attempts,
        validated: Boolean(item.validated_at),
      }))
    const merged = {}
    for (const item of fromProgress) merged[item.section_key] = item
    for (const item of Object.values(sessionCardStates)) merged[item.section_key] = item
    if (extraState) merged[extraState.section_key] = extraState
    return Object.values(merged)
  }

  function getLessonContextPayload() {
    const { asset, section } = getLessonPosition()
    return {
      lesson_title: activeLessonTitle,
      asset_title: asset?.title || null,
      section_title: section?.title || asset?.title || null,
      section_context_key: section?.context_key || null,
      section_context: section?.context || null,
      step_percent: lessonStepPercent,
    }
  }

  function compactInstruction(text) {
    const cleanText = text.trim()
    if (!cleanText) return ''
    const sentences = cleanText.match(/[^.!?]+[.!?]+|[^.!?]+$/g) || [cleanText]
    return sentences.slice(0, 3).join(' ').trim()
  }

  async function finishSpeechPlayback(debugMessage) {
    setSpeaking(false)
    if (audioPlaybackUrlRef.current) {
      URL.revokeObjectURL(audioPlaybackUrlRef.current)
      audioPlaybackUrlRef.current = ''
    }
    audioPlaybackRef.current = null

    if (debugMessage) {
      pushAudioDebug(debugMessage)
    }

    if (isAutoListeningRef.current && !isRecordingRef.current && !isTranscribingRef.current) {
      await startSegmentRecording()
    }
  }

  async function speak(text) {
    const cleanText = text.trim()
    if (!cleanText) return
    if (!selectedVoiceProfileId) {
      setVoiceStatus('Aucune voix Professeur TOP disponible.')
      return
    }

    stopSpeechPlayback()
    setVoiceStatus(`Professeur TOP parle en mode ${selectedVoiceProfile?.label || selectedVoiceProfileId}...`)

    try {
      const audioBlob = await fetchAudio('/tts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: cleanText,
          profile_id: selectedVoiceProfileId,
        }),
      })

      const objectUrl = URL.createObjectURL(audioBlob)
      const audio = new Audio(objectUrl)
      audioPlaybackRef.current = audio
      audioPlaybackUrlRef.current = objectUrl

      audio.onplay = () => setSpeaking(true)
      audio.onended = () => {
        setVoiceStatus('')
        void finishSpeechPlayback('Fin de voix prof, réarmement du segment micro')
      }
      audio.onerror = () => {
        setVoiceStatus('Lecture audio impossible.')
        void finishSpeechPlayback('Erreur voix prof, réarmement du segment micro')
      }

      await audio.play()
    } catch (error) {
      setVoiceStatus(error.message || 'Impossible de générer la voix du professeur.')
      await finishSpeechPlayback('Erreur synthèse vocale, réarmement du segment micro')
    }
  }

  function stopMediaStream() {
    pushAudioDebug('Arrêt et nettoyage du flux micro')
    if (sourceNodeRef.current) {
      sourceNodeRef.current.disconnect()
      sourceNodeRef.current = null
    }
    if (processorNodeRef.current) {
      processorNodeRef.current.disconnect()
      processorNodeRef.current.onaudioprocess = null
      processorNodeRef.current = null
    }
    if (monitorGainRef.current) {
      monitorGainRef.current.disconnect()
      monitorGainRef.current = null
    }
    if (audioContextRef.current) {
      audioContextRef.current.close().catch(() => {})
      audioContextRef.current = null
    }
    analyserRef.current = null
    silenceStartedAtRef.current = null
    hasSpeechInSegmentRef.current = false
    levelFrameCountRef.current = 0
    setMicLevel(0)
    if (mediaStreamRef.current) {
      mediaStreamRef.current.getTracks().forEach((track) => track.stop())
      mediaStreamRef.current = null
    }
    mediaRecorderRef.current = null
    recordedChunksRef.current = []
    recordingMimeTypeRef.current = ''
  }

  async function submitUserMessage(text) {
    if (!selectedStudentId) {
      throw new Error('Choisis un élève avant d’envoyer un message.')
    }
    appendMessage('user', text)
    setLastStudentTransmission(text)
    setInput('')
    setErrorMessage('')
    if (answerValidatesCurrentCard(text)) {
      rememberActiveCard({ score: 100, validated: true })
      const nextStep = getNextLessonStep()
      const transitionReply = nextStep
        ? buildNextCardInstruction(nextStep)
        : 'Très bien, cette fiche est terminée.'
      if (nextStep) {
        setLessonPosition(nextStep.assetIndex, nextStep.sectionIndex)
      }
      appendMessage('assistant', transitionReply)
      setCurrentInstruction(transitionReply)
      speak(transitionReply)
      return
    }
    const data = await apiFetch('/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        student_id: Number(selectedStudentId),
        message: text,
        ...getLessonContextPayload(),
      }),
    })
    const shouldAdvance = data.advance_lesson_step
    if (shouldAdvance) {
      rememberActiveCard({ score: 100, validated: true })
      const nextStep = getNextLessonStep()
      const transitionReply = nextStep
        ? buildNextCardInstruction(nextStep)
        : 'Très bien, cette fiche est terminée.'
      if (nextStep) {
        setLessonPosition(nextStep.assetIndex, nextStep.sectionIndex)
      }
      appendMessage('assistant', transitionReply)
      setCurrentInstruction(transitionReply)
      speak(transitionReply)
    } else {
      rememberActiveCard({ score: 40, validated: false })
      appendMessage('assistant', data.reply)
      setCurrentInstruction(data.reply)
      speak(data.reply)
    }
  }

  async function transcribeRecording(audioBlob, mimeType, { autoSend = false } = {}) {
    const extension = mimeType.includes('mp4') ? 'mp4' : mimeType.includes('ogg') ? 'ogg' : 'webm'
    const formData = new FormData()
    formData.append('file', new File([audioBlob], `voice-input.${extension}`, { type: mimeType || 'audio/webm' }))
    pushAudioDebug(`Transcription demandée, taille=${audioBlob.size}, mime=${mimeType || 'inconnu'}`)

    setIsTranscribing(true)
    setVoiceStatus('Transcription en cours...')

    try {
      const data = await apiFetch('/transcribe', {
        method: 'POST',
        body: formData,
      })
      const transcript = (data.text || '').trim()
      setInput(transcript)
      pushAudioDebug(`Transcription reçue, longueur=${transcript.length}`)

      if (!transcript) {
        setVoiceStatus('Aucun texte reconnu.')
        return
      }

      setLastStudentTransmission(transcript)
      if (autoSend && selectedStudentId) {
        setVoiceStatus('Texte reconnu, envoi automatique...')
        pushAudioDebug('Envoi automatique du message transcrit')
        await submitUserMessage(transcript)
        setVoiceStatus('Message vocal envoyé automatiquement.')
      } else {
        setVoiceStatus('Texte dicté prêt à être envoyé.')
      }
    } catch (error) {
      pushAudioDebug(`Erreur transcription: ${error.message || 'inconnue'}`)
      setVoiceStatus(error.message || 'Impossible de transcrire cet enregistrement.')
    } finally {
      setIsTranscribing(false)
    }
  }

  function calculateVolume(floatData) {
    let sumSquares = 0
    for (const value of floatData) {
      sumSquares += value * value
    }

    return Math.sqrt(sumSquares / floatData.length)
  }

  async function startSegmentRecording() {
    if (!mediaStreamRef.current || isRecordingRef.current || isTranscribingRef.current) return

    const mimeType = getSupportedRecordingMimeType()
    const recorder = mimeType
      ? new MediaRecorder(mediaStreamRef.current, { mimeType })
      : new MediaRecorder(mediaStreamRef.current)

    mediaRecorderRef.current = recorder
    recordedChunksRef.current = []
    recordingMimeTypeRef.current = mimeType || recorder.mimeType || 'audio/webm'
    silenceStartedAtRef.current = null
    hasSpeechInSegmentRef.current = false
    pushAudioDebug(`Démarrage enregistrement segment, mime=${recordingMimeTypeRef.current}`)

    recorder.ondataavailable = (event) => {
      if (event.data && event.data.size > 0) {
        recordedChunksRef.current.push(event.data)
        pushAudioDebug(`Chunk audio reçu, taille=${event.data.size}`)
      }
    }

    recorder.onstop = async () => {
      const finalMimeType = recordingMimeTypeRef.current || recorder.mimeType || 'audio/webm'
      const audioBlob = new Blob(recordedChunksRef.current, { type: finalMimeType })
      recordedChunksRef.current = []
      mediaRecorderRef.current = null
      setIsRecording(false)
      pushAudioDebug(`Enregistrement arrêté, taille totale=${audioBlob.size}, parole détectée=${hasSpeechInSegmentRef.current ? 'oui' : 'non'}`)

      if (audioBlob.size > 0 && hasSpeechInSegmentRef.current) {
        await transcribeRecording(audioBlob, finalMimeType, { autoSend: true })
      } else if (isAutoListeningRef.current) {
        setVoiceStatus('Micro actif. Parle quand tu veux.')
      }

      hasSpeechInSegmentRef.current = false
      if (isAutoListeningRef.current && !isTranscribingRef.current && !speakingRef.current) {
        pushAudioDebug('Segment terminé, relance écoute micro')
        await startSegmentRecording()
      }
    }

    recorder.onerror = () => {
      pushAudioDebug('Erreur MediaRecorder')
      setVoiceStatus('Le navigateur a rencontré une erreur pendant l’enregistrement.')
      setIsRecording(false)
    }

    recorder.start()
    setIsRecording(true)
    setVoiceStatus('Je t’écoute...')
  }

  function stopSegmentRecording(statusMessage = 'Silence détecté, transcription...') {
    const recorder = mediaRecorderRef.current
    if (recorder && recorder.state !== 'inactive') {
      silenceStartedAtRef.current = null
      pushAudioDebug('Arrêt du segment demandé après silence')
      recorder.stop()
      setVoiceStatus(statusMessage)
    }
  }

  function handleMicrophoneLevel(volume) {
    if (!isAutoListeningRef.current) return

    const now = Date.now()
    setMicLevel(volume)
    levelFrameCountRef.current += 1

    if (levelFrameCountRef.current === 1) {
      pushAudioDebug(`Premier niveau audio reçu=${volume.toFixed(4)}`)
    } else if (levelFrameCountRef.current % 50 === 0) {
      pushAudioDebug(`Niveau audio=${volume.toFixed(4)}`)
    }

    if (volume >= SPEECH_START_THRESHOLD) {
      if (!hasSpeechInSegmentRef.current) {
        pushAudioDebug(`Parole détectée, niveau=${volume.toFixed(4)}`)
      }
      hasSpeechInSegmentRef.current = true
      silenceStartedAtRef.current = null
    } else if (isRecordingRef.current && hasSpeechInSegmentRef.current && volume <= SILENCE_THRESHOLD) {
      if (!silenceStartedAtRef.current) {
        pushAudioDebug(`Début silence, niveau=${volume.toFixed(4)}`)
        silenceStartedAtRef.current = now
      } else if (now - silenceStartedAtRef.current >= AUTO_STOP_SILENCE_MS) {
        pushAudioDebug('Silence confirmé, arrêt du segment')
        stopSegmentRecording()
      }
    } else if (volume > SILENCE_THRESHOLD) {
      silenceStartedAtRef.current = null
    }
  }

  async function activateAutoListening() {
    if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
      setVoiceStatus('Le micro automatique n’est pas pris en charge par ce navigateur.')
      pushAudioDebug('Navigateur incompatible micro auto')
      return
    }

    try {
      pushAudioDebug('Demande accès micro')
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: {
          echoCancellation: true,
          noiseSuppression: true,
          autoGainControl: true,
        },
      })
      const AudioContextClass = window.AudioContext || window.webkitAudioContext
      const audioContext = new AudioContextClass()
      if (audioContext.state === 'suspended') {
        await audioContext.resume()
      }
      pushAudioDebug(`Micro autorisé, AudioContext=${audioContext.state}`)
      const analyser = audioContext.createAnalyser()
      analyser.fftSize = 2048
      analyser.smoothingTimeConstant = 0.85
      const sourceNode = audioContext.createMediaStreamSource(stream)
      const processorNode = audioContext.createScriptProcessor(2048, 1, 1)
      const monitorGain = audioContext.createGain()
      monitorGain.gain.value = 0
      processorNode.onaudioprocess = (event) => {
        const channelData = event.inputBuffer.getChannelData(0)
        const volume = calculateVolume(channelData)
        handleMicrophoneLevel(volume)
      }

      sourceNode.connect(analyser)
      sourceNode.connect(processorNode)
      processorNode.connect(monitorGain)
      analyser.connect(monitorGain)
      monitorGain.connect(audioContext.destination)

      audioContextRef.current = audioContext
      analyserRef.current = analyser
      sourceNodeRef.current = sourceNode
      processorNodeRef.current = processorNode
      monitorGainRef.current = monitorGain
      mediaStreamRef.current = stream
      setIsAutoListening(true)
      setVoiceStatus('Micro actif. Parle quand tu veux, j’enverrai après 2,5 s de silence.')
      pushAudioDebug(`Piste micro active=${stream.getAudioTracks()[0]?.readyState || 'inconnue'}`)
      await startSegmentRecording()
    } catch {
      pushAudioDebug('Accès micro refusé ou indisponible')
      setVoiceStatus('Accès au micro refusé ou indisponible.')
      stopMediaStream()
    }
  }

  function deactivateAutoListening(resetStatus = true) {
    pushAudioDebug('Désactivation micro auto')
    setIsAutoListening(false)
    if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
      mediaRecorderRef.current.onstop = null
      mediaRecorderRef.current.stop()
    }
    setIsRecording(false)
    stopMediaStream()
    if (resetStatus) {
      setVoiceStatus('Micro automatique désactivé.')
    }
  }

  async function startVoiceInput() {
    if (isAutoListening) {
      deactivateAutoListening()
      return
    }
    await activateAutoListening()
  }

  async function sendMessage(e) {
    e.preventDefault()
    if (!selectedStudentId || !input.trim()) return
    const text = input.trim()
    try {
      await submitUserMessage(text)
    } catch (error) {
      setInput(text)
      setErrorMessage(error.message || 'Impossible d’envoyer le message.')
    }
  }

  const lastAssistantMessage = [...messages].reverse().find((message) => message.role === 'assistant')
  const lastUserMessage = [...messages].reverse().find((message) => message.role === 'user')
  const activeLessonTitle = studentProgram?.title || lessonFocus?.label || 'Cours du jour'
  const lessonAssets = studentProgram?.assets || []
  const activeAsset = lessonAssets[activeAssetIndex] || lessonAssets[0] || null
  const activeSections = activeAsset?.sections || []
  const activeSection = activeSections[activeSectionIndex] || null
  const totalLessonSteps = lessonAssets.reduce(
    (total, asset) => total + Math.max(asset.sections?.length || 0, 1),
    0
  )
  const completedBeforeAsset = lessonAssets
    .slice(0, activeAssetIndex)
    .reduce((total, asset) => total + Math.max(asset.sections?.length || 0, 1), 0)
  const activeLessonStep = totalLessonSteps
    ? completedBeforeAsset + (activeSection ? activeSectionIndex + 1 : 1)
    : 0
  const lessonStepPercent = totalLessonSteps
    ? Math.min(100, Math.round((activeLessonStep / totalLessonSteps) * 100))
    : 0
  const mergedCardStates = getMergedCardStates()
  const validatedLessonCards = mergedCardStates.filter((item) => item.validated).length
  const lessonComprehensionPercent = totalLessonSteps
    ? Math.min(100, Math.round((validatedLessonCards / totalLessonSteps) * 100))
    : lessonPercent
  const displayedLessonPercent = studentProgram ? lessonComprehensionPercent : lessonPercent
  const lessonSummary = lastAssistantMessage?.content || 'Aucune ancienne séance trouvée pour le moment. Professeur TOP commencera par une reprise courte.'
  const lastStudentWork = lastUserMessage?.content || 'Aucune réponse récente enregistrée.'
  const nextLessonAdvice = studentProgram
    ? `Conseil: démarrer la leçon ${studentProgram.title} avec les fiches choisies par le professeur.`
    : lessonFocus
    ? `Conseil: reprendre ${lessonFocus.label}, puis lancer un exercice court pour équilibrer la progression.`
    : 'Conseil: démarrer une séance pour laisser Professeur TOP choisir la prochaine leçon.'

  if (isStudentUser) {
    return (
      <div className={`student-screen ${sessionActive ? 'lesson-active' : 'lesson-ready'}`}>
        {!sessionActive ? (
          <main className="student-home">
            <section className="student-home-hero card">
              <div>
                <p className="eyebrow">Bonjour</p>
                <h1>{selectedStudent?.first_name || currentUser.username}</h1>
                <p className="muted">{selectedStudent?.grade || 'Classe non renseignée'}</p>
              </div>
              <div className="student-home-actions">
                <button onClick={toggleSession} disabled={!selectedStudentId}>
                  Démarrer la leçon
                </button>
                <button type="button" className="secondary-button" onClick={onLogout}>
                  Déconnexion
                </button>
              </div>
            </section>

            <section className="student-home-grid">
              <article className="student-info-card card">
                <span className="stage-label">Dernière séance</span>
                <p>{lessonSummary}</p>
              </article>

              <article className="student-info-card card">
                <span className="stage-label">Dernière réponse</span>
                <p>{lastStudentWork}</p>
              </article>

              <article className="student-info-card card">
                <div className="today-card-head">
                  <span className="stage-label">Avancement</span>
                  <strong>{displayedLessonPercent}%</strong>
                </div>
                <h2>{activeLessonTitle}</h2>
                <div className="progress-bar">
                  <div className="progress-fill" style={{ width: `${displayedLessonPercent}%` }} />
                </div>
              </article>

              <article className="student-info-card card advice-card">
                <span className="stage-label">Prochaine leçon conseillée</span>
                <p>{nextLessonAdvice}</p>
              </article>
            </section>

            {voiceStatus && <p className="muted voice-status visible-status">{voiceStatus}</p>}
            {errorMessage && <p className="muted voice-status visible-status">{errorMessage}</p>}
          </main>
        ) : (
          <main className="student-lesson">
            <header className="lesson-topbar">
              <Avatar speaking={speaking} />
              <div>
                <p className="eyebrow">Leçon en cours</p>
                <h1>{activeLessonTitle}</h1>
              </div>
              <div className="lesson-actions">
                <button
                  type="button"
                  className="icon-button"
                  aria-label="Paramètres"
                  title="Paramètres"
                  onClick={() => setSettingsPanelOpen(true)}
                >
                  ...
                </button>
                <button type="button" className="secondary-button" onClick={() => setProgressPanelOpen(true)}>
                  Progression
                </button>
                <button type="button" className="secondary-button" onClick={endSession}>
                  Fin
                </button>
              </div>
            </header>

            <section className="lesson-display">
              <div className="lesson-display-head">
                <span className="stage-label">Professeur TOP</span>
                {activeAsset && (
                  <span className="asset-counter">
                    {lessonStepPercent}% · Fiche {activeAssetIndex + 1}/{lessonAssets.length}
                  </span>
                )}
              </div>
              {currentInstruction ? (
                <p className="teacher-transcript">{compactInstruction(currentInstruction)}</p>
              ) : (
                <p className="muted teacher-transcript">Professeur TOP prépare la suite de la leçon.</p>
              )}
              {activeAsset && (
                <div className="lesson-asset-view" aria-label="Fiche de leçon active">
                  <div className="lesson-asset-toolbar">
                    <div className="lesson-asset-actions">
                      <button
                        type="button"
                        className="secondary-button"
                        onClick={goToPreviousLessonStep}
                        disabled={activeLessonStep <= 1}
                      >
                        Précédente
                      </button>
                      <button
                        type="button"
                        className="secondary-button"
                        onClick={goToNextLessonStep}
                        disabled={activeLessonStep >= totalLessonSteps}
                      >
                        Suivante
                      </button>
                    </div>
                  </div>
                  <div className="lesson-asset-open">
                    <img
                      src={activeSection?.url || activeAsset.url}
                      alt={activeSection?.title || activeAsset.title}
                    />
                  </div>
                </div>
              )}
            </section>

            <form onSubmit={assessment ? submitAssessment : sendMessage} className="lesson-composer">
              <input
                value={assessment ? assessmentAnswer : input}
                onChange={(event) =>
                  assessment ? setAssessmentAnswer(event.target.value) : setInput(event.target.value)
                }
                placeholder={
                  assessment
                    ? 'Ta réponse au mini-test...'
                    : 'Dicte ou écris ta réponse...'
                }
                autoComplete="off"
              />
              <button type="button" onClick={startVoiceInput} disabled={isTranscribing}>
                {isAutoListening ? 'Stop micro' : isTranscribing ? 'Transcription...' : 'Dicter'}
              </button>
              <button type="submit">Envoyer</button>
            </form>

            {(voiceStatus || errorMessage || isAutoListening) && (
              <p className="lesson-status">
                {errorMessage || voiceStatus || `Niveau micro: ${Math.round(Math.min(micLevel * 1200, 100))}%`}
              </p>
            )}

            {progressPanelOpen && (
              <div className="progress-panel-backdrop" role="presentation" onClick={() => setProgressPanelOpen(false)}>
                <aside className="progress-panel card" role="dialog" aria-label="Progression" onClick={(event) => event.stopPropagation()}>
                  <div className="progress-panel-head">
                    <h2>Progression</h2>
                    <button type="button" className="secondary-button" onClick={() => setProgressPanelOpen(false)}>
                      Fermer
                    </button>
                  </div>
                  <div className="stack small-gap">
                    {progress.map((item) => <ProgressCard key={item.code} item={item} />)}
                  </div>
                </aside>
              </div>
            )}

            {settingsPanelOpen && (
              <div className="progress-panel-backdrop" role="presentation" onClick={() => setSettingsPanelOpen(false)}>
                <aside className="progress-panel card" role="dialog" aria-label="Paramètres" onClick={(event) => event.stopPropagation()}>
                  <div className="progress-panel-head">
                    <h2>Paramètres</h2>
                    <button type="button" className="secondary-button" onClick={() => setSettingsPanelOpen(false)}>
                      Fermer
                    </button>
                  </div>
                  <label className="settings-field">
                    <span>Voix de Professeur TOP</span>
                    <select
                      value={selectedVoiceProfileId}
                      onChange={(event) => setSelectedVoiceProfileId(event.target.value)}
                    >
                      <option value="">Voix de Professeur TOP</option>
                      {voiceProfiles.map((profile) => (
                        <option key={profile.id} value={profile.id}>
                          {formatVoiceLabel(profile)}
                        </option>
                      ))}
                    </select>
                  </label>
                  {selectedVoiceProfile && (
                    <p className="muted voice-note visible-status">
                      Voix IA non humaine: {selectedVoiceProfile.label} - {selectedVoiceProfile.description}
                    </p>
                  )}
                </aside>
              </div>
            )}

          </main>
        )}
      </div>
    )
  }

  if (!isStudentUser) {
    const validatedCards = cardProgress.filter((item) => item.validated).length
    const averageComprehension = cardProgress.length
      ? Math.round(cardProgress.reduce((sum, item) => sum + item.comprehension_score, 0) / cardProgress.length)
      : 0
    const reviewRows = programRows.filter(({ node }) => node.type === 'lesson' || node.type === 'fiche' || node.type === 'card')
    const reviewQueue = reviewRows.filter(({ node }) => ['needs_changes', 'blocked', 'draft', undefined].includes(node.review?.status))
    const selectedNodeStatus = selectedProgramNode?.status || {}
    const totalLessonCards = studentProgram?.assets?.reduce((sum, asset) => sum + (asset.sections?.length || 0), 0) || 0
    const remainingCards = Math.max(totalLessonCards - validatedCards, 0)
    const adminTabs = [
      { id: 'overview', label: 'Vue d’ensemble' },
      { id: 'students', label: 'Élèves' },
      { id: 'program', label: 'Programme global' },
      { id: 'validation', label: 'Validation contenus' },
      { id: 'factory', label: 'Atelier génération' },
      { id: 'settings', label: 'Paramètres globaux' },
    ]

    return (
      <div className="admin-console">
        <header className="admin-console-header">
          <div>
            <p className="eyebrow">Poste de pilotage</p>
            <h1>Professeur TOP</h1>
            <p className="muted">Suivi des élèves, validation des contenus et pilotage du programme.</p>
          </div>
          <div className="admin-user-box">
            <span>{currentUser.username} · {currentUser.role}</span>
            <button type="button" className="secondary-button" onClick={onLogout}>Déconnexion</button>
          </div>
        </header>

        <nav className="admin-tabs" aria-label="Navigation admin">
          {adminTabs.map((tab) => (
            <button
              key={tab.id}
              type="button"
              className={activeAdminTab === tab.id ? 'active' : ''}
              onClick={() => setActiveAdminTab(tab.id)}
            >
              {tab.label}
            </button>
          ))}
        </nav>

        <main className="admin-workspace">
          {activeAdminTab === 'overview' && (
            <>
              <section className="admin-metrics-grid">
                <article className="admin-metric">
                  <span>Élèves actifs</span>
                  <strong>{students.length}</strong>
                  <p>{selectedStudent ? `${selectedStudent.first_name} sélectionné` : 'Aucun élève sélectionné'}</p>
                </article>
                <article className="admin-metric">
                  <span>Leçon active</span>
                  <strong>{studentProgram?.title || 'À affecter'}</strong>
                  <p>{studentProgram ? `${studentProgram.asset_count} fiche(s) · ${totalLessonCards} cards` : 'Passe par l’onglet Élèves'}</p>
                </article>
                <article className="admin-metric">
                  <span>Compréhension</span>
                  <strong>{averageComprehension}%</strong>
                  <p>{validatedCards} card(s) validée(s), {remainingCards} restante(s)</p>
                </article>
                <article className="admin-metric warning">
                  <span>File de revue</span>
                  <strong>{reviewQueue.length}</strong>
                  <p>Contenus à relire, modifier ou intégrer</p>
                </article>
              </section>

              <section className="admin-dashboard-grid">
                <div className="admin-panel">
                  <div className="admin-panel-head">
                    <div>
                      <span className="stage-label">Priorités</span>
                      <h2>À traiter maintenant</h2>
                    </div>
                  </div>
                  <div className="admin-action-list">
                    <button type="button" onClick={() => setActiveAdminTab('validation')}>
                      Relire les SVG d’exercices fractions
                      <span>Phase 6B attendue</span>
                    </button>
                    <button type="button" onClick={() => setActiveAdminTab('students')}>
                      Vérifier le programme de l’élève
                      <span>{selectedStudent?.first_name || 'Choisir un élève'}</span>
                    </button>
                    <button type="button" onClick={() => setActiveAdminTab('factory')}>
                      Préparer les prochaines générations
                      <span>Kits adaptatifs et intégration</span>
                    </button>
                  </div>
                </div>

                <div className="admin-panel">
                  <div className="admin-panel-head">
                    <div>
                      <span className="stage-label">Production</span>
                      <h2>État du programme</h2>
                    </div>
                    <button type="button" className="secondary-button" onClick={loadProgramTree}>Rafraîchir</button>
                  </div>
                  <div className="program-status-grid">
                    <span>Leçons: {programStatus?.lesson_count ?? 0}</span>
                    <span>CM1: {programStatus?.lessons_by_grade?.CM1 ?? 0}</span>
                    <span>Dossier: {programStatus?.content_root_exists === false ? 'absent' : 'OK'}</span>
                  </div>
                </div>
              </section>
            </>
          )}

          {activeAdminTab === 'students' && (
            <section className="admin-students-layout">
              <aside className="admin-panel">
                <div className="admin-panel-head">
                  <div>
                    <span className="stage-label">Élèves</span>
                    <h2>Sélection et création</h2>
                  </div>
                </div>
                <div className="student-roster">
                  {students.map((student) => (
                    <button
                      key={student.id}
                      type="button"
                      className={String(student.id) === String(selectedStudentId) ? 'active' : ''}
                      onClick={() => setSelectedStudentId(String(student.id))}
                    >
                      <strong>{student.first_name}</strong>
                      <span>{student.grade} · {student.age} ans</span>
                    </button>
                  ))}
                </div>

                <form onSubmit={createStudent} className="student-create-form">
                  <h3>Créer un compte élève</h3>
                  <input placeholder="Prénom" value={form.first_name} onChange={(event) => setForm({ ...form, first_name: event.target.value })} />
                  <input type="number" min="8" max="12" value={form.age} onChange={(event) => setForm({ ...form, age: event.target.value })} />
                  <select value={form.grade} onChange={(event) => setForm({ ...form, grade: event.target.value })}>
                    <option>CE2</option>
                    <option>CM1</option>
                    <option>CM2</option>
                    <option>6e</option>
                  </select>
                  <input placeholder="Identifiant élève" value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} />
                  <input placeholder="Mot de passe élève" type="password" value={form.password} onChange={(event) => setForm({ ...form, password: event.target.value })} />
                  <button type="submit">Créer le compte</button>
                </form>
              </aside>

              <section className="admin-panel student-report-panel">
                {selectedStudent ? (
                  <>
                    <div className="admin-panel-head">
                      <div>
                        <span className="stage-label">Fiche élève</span>
                        <h2>{selectedStudent.first_name}</h2>
                        <p className="muted">{selectedStudent.grade} · {selectedStudent.age} ans</p>
                      </div>
                    </div>

                    <div className="student-report-grid">
                      <article>
                        <span>Leçon courante</span>
                        <strong>{studentProgram?.title || 'Aucune leçon'}</strong>
                        <p>{studentProgram ? `${studentProgram.subject} · ${studentProgram.asset_count} fiche(s)` : 'Affecte une leçon ci-dessous.'}</p>
                      </article>
                      <article>
                        <span>Résultats</span>
                        <strong>{averageComprehension}%</strong>
                        <p>{cardProgress.length} card(s) observée(s)</p>
                      </article>
                      <article>
                        <span>Contenu fait</span>
                        <strong>{validatedCards}</strong>
                        <p>{remainingCards} card(s) encore à travailler</p>
                      </article>
                    </div>

                    <div className="student-settings-grid">
                      <label>
                        <span>Leçon active</span>
                        <select value={studentProgram?.id || ''} onChange={(event) => assignProgramLesson(event.target.value)}>
                          <option value="">Choisir une leçon pour {selectedStudent.grade}</option>
                          {programLessons.map((lesson) => (
                            <option key={lesson.id} value={lesson.id}>{lesson.subject} · {lesson.title} · {lesson.asset_count} fiche(s)</option>
                          ))}
                        </select>
                      </label>
                      {studentProgram?.assets?.length > 0 && (
                        <>
                          <label>
                            <span>Reprendre à la fiche</span>
                            <select value={programStart.asset_index} onChange={(event) => updateProgramStart(event.target.value, 0)}>
                              {studentProgram.assets.map((asset, index) => (
                                <option key={asset.path} value={index}>{index + 1}. {asset.title}</option>
                              ))}
                            </select>
                          </label>
                          <label>
                            <span>Card de départ</span>
                            <select value={programStart.section_index} onChange={(event) => updateProgramStart(programStart.asset_index, event.target.value)}>
                              {(studentProgram.assets[programStart.asset_index]?.sections || []).map((section, index) => (
                                <option key={section.context_key || index} value={index}>{index + 1}. {section.title}</option>
                              ))}
                            </select>
                          </label>
                        </>
                      )}
                      <label>
                        <span>Rythme pédagogique</span>
                        <select defaultValue="normal">
                          <option value="slow">Très guidé</option>
                          <option value="normal">Normal</option>
                          <option value="fast">Autonome</option>
                        </select>
                      </label>
                      <label className="wide-field">
                        <span>Instructions propres à l’enfant</span>
                        <textarea placeholder="Exemple: privilégier les exemples concrets, reformuler les consignes longues, valoriser les réponses orales." />
                      </label>
                    </div>

                    <div className="admin-dashboard-grid">
                      <section className="admin-preview">
                        <h2>Compréhension des cards</h2>
                        {cardProgress.length ? (
                          <div className="admin-log-list">
                            {cardProgress.map((item) => (
                              <article key={item.id} className="admin-log-card">
                                <strong>{item.section_title}</strong>
                                <span>{Math.round(item.comprehension_score)}% · essais: {item.attempts}</span>
                              </article>
                            ))}
                          </div>
                        ) : <p className="muted">Aucune card validée pour le moment.</p>}
                      </section>

                      <section className="admin-preview">
                        <h2>Logs de séance</h2>
                        {sessionLogs.length ? (
                          <div className="admin-log-list">
                            {sessionLogs.map((log) => (
                              <article key={log.id} className="admin-log-card">
                                <div>
                                  <strong>{log.lesson_title}</strong>
                                  <span>{new Date(log.ended_at).toLocaleString('fr-FR')}</span>
                                </div>
                                <div className="admin-log-actions">
                                  <button type="button" className="secondary-button" onClick={() => setSelectedSessionLog(log)}>Voir</button>
                                  <button type="button" className="secondary-button" onClick={() => deleteSessionLog(log.id)}>Effacer</button>
                                </div>
                              </article>
                            ))}
                          </div>
                        ) : <p className="muted">Aucun log enregistré.</p>}
                      </section>
                    </div>

                    {selectedSessionLog && (
                      <section className="admin-preview">
                        <div className="admin-log-detail-head">
                          <h2>Détail de séance</h2>
                          <button type="button" className="secondary-button" onClick={() => setSelectedSessionLog(null)}>Fermer</button>
                        </div>
                        <div className="admin-log-detail">
                          <h3>Résultats des cards</h3>
                          {parseLogJson(selectedSessionLog.card_results).map((item) => (
                            <article key={item.section_key} className="admin-log-card">
                              <strong>{item.section_title}</strong>
                              <span>{Math.round(item.comprehension_score)}% · {item.validated ? 'validée' : 'vue/tentée'}</span>
                            </article>
                          ))}
                          <h3>Conversation</h3>
                          {parseLogJson(selectedSessionLog.conversation).map((message, index) => (
                            <p key={`${message.id || index}`} className="admin-log-message">
                              <strong>{message.role === 'assistant' ? 'Professeur TOP' : 'Élève'}:</strong> {message.content}
                            </p>
                          ))}
                        </div>
                      </section>
                    )}
                  </>
                ) : (
                  <section className="admin-empty">
                    <h2>Aucun élève sélectionné</h2>
                    <p className="muted">Choisis un élève ou crée un compte pour configurer son programme.</p>
                  </section>
                )}
              </section>
            </section>
          )}

          {activeAdminTab === 'program' && (
            <section className="admin-program-browser program-tab">
              <div className="program-tree-panel">
                <div className="admin-log-detail-head">
                  <h2>Arborescence du programme</h2>
                  <button type="button" className="secondary-button" onClick={loadProgramTree}>Rafraîchir</button>
                </div>
                <div className="program-tree-list">
                  {visibleProgramRows.map(({ node, depth }) => (
                    <button key={node.key} type="button" className={`program-tree-row ${selectedProgramNodeKey === node.key ? 'active' : ''}`} style={{ paddingLeft: `${0.65 + depth * 1.05}rem` }} onClick={() => toggleProgramNode(node)}>
                      <span className="tree-label">
                        <b>{node.children?.length ? (collapsedProgramKeys.has(node.key) ? '+' : '-') : ''}</b>
                        {node.label}
                      </span>
                      <em>{node.type}</em>
                      {node.review?.status && <strong>{node.review.status}</strong>}
                    </button>
                  ))}
                </div>
              </div>
              <ProgramReviewPanel
                selectedNode={selectedProgramNode}
                reviewForm={programReviewForm}
                onReviewChange={setProgramReviewForm}
                onSaveReview={saveProgramReview}
                readOnly
                onOpenValidation={() => setActiveAdminTab('validation')}
              />
            </section>
          )}

          {activeAdminTab === 'validation' && (
            <section className="validation-layout">
              <aside className="admin-panel review-queue">
                <div className="admin-panel-head">
                  <div>
                    <span className="stage-label">File éditoriale</span>
                    <h2>Contenus à valider</h2>
                  </div>
                </div>
                <div className="review-filters">
                  <span>À relire</span>
                  <span>À modifier</span>
                  <span>Validé</span>
                  <span>Intégré</span>
                </div>
                <div className="program-tree-list">
                  {reviewRows.map(({ node, depth }) => (
                    <button key={node.key} type="button" className={`program-tree-row ${selectedProgramNodeKey === node.key ? 'active' : ''}`} style={{ paddingLeft: `${0.65 + Math.min(depth, 3) * 0.75}rem` }} onClick={() => setSelectedProgramNodeKey(node.key)}>
                      <span>{node.label}</span>
                      <em>{node.type}</em>
                      <strong>{node.review?.status || 'à relire'}</strong>
                    </button>
                  ))}
                </div>
              </aside>

              <section className="admin-panel validation-stage">
                <div className="admin-panel-head">
                  <div>
                    <span className="stage-label">{selectedProgramNode?.type || 'Sélection'}</span>
                    <h2>{selectedProgramNode?.label || 'Choisis une card ou une fiche'}</h2>
                    <p className="muted">{selectedProgramNode?.path || selectedProgramNode?.key || 'La zone centrale affiche le contenu à analyser en grand.'}</p>
                  </div>
                  {selectedProgramNode?.review?.status && <span className={`review-pill ${selectedProgramNode.review.status}`}>{selectedProgramNode.review.status}</span>}
                </div>
                <div className="validation-main-preview">
                  {selectedProgramNode?.url ? (
                    <img src={selectedProgramNode.url} alt={selectedProgramNode.label} />
                  ) : (
                    <div className="empty-preview">Aucun visuel direct pour cet élément.</div>
                  )}
                </div>
              </section>

              <aside className="admin-panel validation-inspector">
                <h2>Revue</h2>
                <div className="program-status-grid compact">
                  <span>Micro: {selectedNodeStatus.micro_fiches ?? '-'}</span>
                  <span>SVG: {selectedNodeStatus.lesson_svg ?? '-'}</span>
                  <span>Cards: {selectedNodeStatus.cards ?? '-'}</span>
                  <span>Contextes: {selectedNodeStatus.contexts ?? '-'}</span>
                  <span>Exercices: {selectedNodeStatus.text_exercises ?? '-'}</span>
                  <span>SVG ex.: {selectedNodeStatus.exercise_svg ?? '-'}</span>
                </div>
                <form className="admin-review-form" onSubmit={saveProgramReview}>
                  <label>
                    <span>Statut</span>
                    <select value={programReviewForm.status} onChange={(event) => setProgramReviewForm({ ...programReviewForm, status: event.target.value })}>
                      <option value="validated">Validé</option>
                      <option value="needs_changes">À modifier</option>
                      <option value="blocked">Bloqué</option>
                      <option value="draft">Brouillon</option>
                    </select>
                  </label>
                  <label>
                    <span>Remarque pour modification</span>
                    <textarea value={programReviewForm.comment} onChange={(event) => setProgramReviewForm({ ...programReviewForm, comment: event.target.value })} placeholder="Explique ce qui doit être corrigé avant assimilation au programme." rows={8} />
                  </label>
                  <button type="submit" disabled={!selectedProgramNode}>Enregistrer la revue</button>
                </form>
              </aside>
            </section>
          )}

          {activeAdminTab === 'factory' && (
            <section className="admin-dashboard-grid">
              <div className="admin-panel">
                <span className="stage-label">Atelier</span>
                <h2>Planifier une génération</h2>
                <div className="student-settings-grid single">
                  <label><span>Niveau</span><select defaultValue="CM1"><option>CM1</option><option>CM2</option><option>6e</option></select></label>
                  <label><span>Production</span><select defaultValue="exercise_svg"><option value="micro">Micro-fiches</option><option value="lesson_svg">SVG leçon</option><option value="contexts">Contextes de cards</option><option value="exercises">Exercices textuels</option><option value="exercise_svg">SVG exercices</option><option value="adaptive">Kit adaptatif</option></select></label>
                  <label><span>Créneau</span><select defaultValue="night"><option value="now">Lancer maintenant</option><option value="night">Cette nuit</option></select></label>
                  <button type="button" disabled>Planifier après branchement backend</button>
                </div>
              </div>
              <div className="admin-panel">
                <span className="stage-label">Pipeline</span>
                <h2>Assimilation des contenus</h2>
                <div className="pipeline-list">
                  <span>1. Génération nocturne</span>
                  <span>2. File de validation</span>
                  <span>3. Remarque ou validation</span>
                  <span>4. Assimilation au programme</span>
                  <span>5. Disponibilité dans le mode élève</span>
                </div>
              </div>
            </section>
          )}

          {activeAdminTab === 'settings' && (
            <section className="admin-dashboard-grid">
              <div className="admin-panel">
                <span className="stage-label">Application</span>
                <h2>Paramètres globaux</h2>
                <div className="student-settings-grid single">
                  <label><span>Voix par défaut</span><select value={selectedVoiceProfileId} onChange={(event) => setSelectedVoiceProfileId(event.target.value)}>{voiceProfiles.map((profile) => <option key={profile.id} value={profile.id}>{formatVoiceLabel(profile)}</option>)}</select></label>
                  <label><span>Longueur des tours</span><select defaultValue="short"><option value="short">Courte</option><option value="normal">Normale</option></select></label>
                  <label className="wide-field"><span>Règles pédagogiques globales</span><textarea placeholder="Règles communes appliquées à Professeur TOP pour tous les élèves." /></label>
                </div>
              </div>
              <div className="admin-panel">
                <span className="stage-label">Système</span>
                <h2>Diagnostic</h2>
                <div className="program-status-grid">
                  <span>API: active</span>
                  <span>Contenus: {programStatus?.content_root_exists === false ? 'à vérifier' : 'OK'}</span>
                  <span>Leçons: {programStatus?.lesson_count ?? 0}</span>
                </div>
              </div>
            </section>
          )}

          {errorMessage && <p className="muted voice-status visible-status">{errorMessage}</p>}
        </main>
      </div>
    )
  }

  return (
    <div className={`layout ${isStudentUser ? 'student-layout' : 'admin-layout'}`}>
      <aside className="sidebar card">
        <h2>{isStudentUser ? 'Ma séance' : 'Élèves'}</h2>
        {!isStudentUser && (
          <>
            <select value={selectedStudentId} onChange={(event) => setSelectedStudentId(event.target.value)}>
              <option value="">Choisir un élève</option>
              {students.map((student) => (
                <option key={student.id} value={student.id}>
                  {student.first_name} · {student.grade}
                </option>
              ))}
            </select>

            <form onSubmit={createStudent} className="stack">
              <input
                placeholder="Prénom"
                value={form.first_name}
                onChange={(event) => setForm({ ...form, first_name: event.target.value })}
              />
              <input
                type="number"
                min="8"
                max="12"
                value={form.age}
                onChange={(event) => setForm({ ...form, age: event.target.value })}
              />
              <select value={form.grade} onChange={(event) => setForm({ ...form, grade: event.target.value })}>
                <option>CE2</option>
                <option>CM1</option>
                <option>CM2</option>
                <option>6e</option>
              </select>
              <input
                placeholder="Identifiant élève"
                value={form.username}
                onChange={(event) => setForm({ ...form, username: event.target.value })}
              />
              <input
                placeholder="Mot de passe élève"
                type="password"
                value={form.password}
                onChange={(event) => setForm({ ...form, password: event.target.value })}
              />
              <button type="submit">Créer le compte élève</button>
            </form>

            {selectedStudent && (
              <section className="admin-program-panel">
                <h3>Programme de l’élève</h3>
                <p className="muted">
                  {studentProgram
                    ? `Leçon active: ${studentProgram.title}`
                    : 'Aucune leçon active pour cet élève.'}
                </p>
                <select
                  value={studentProgram?.id || ''}
                  onChange={(event) => assignProgramLesson(event.target.value)}
                >
                  <option value="">Choisir une leçon pour {selectedStudent.grade}</option>
                  {programLessons.map((lesson) => (
                    <option key={lesson.id} value={lesson.id}>
                      {lesson.subject} · {lesson.title} · {lesson.asset_count} fiche(s)
                    </option>
                  ))}
                </select>
                {programLessons.length === 0 && (
                  <p className="program-diagnostic">
                    Aucune leçon trouvée pour {selectedStudent.grade}. Dossier lu: {programStatus?.content_root || 'inconnu'}.
                    {programStatus?.content_root_exists === false
                      ? ' Le dossier de programme n’est pas accessible par le backend.'
                      : ' Vérifie le niveau ou le montage du dossier Programme.'}
                  </p>
                )}
              </section>
            )}
          </>
        )}

        <button type="button" className="secondary-button" onClick={onLogout}>
          Déconnexion
        </button>
      </aside>

      <main className="admin-main card">
        <header className="admin-header">
          <div>
            <h1>Gestion du programme</h1>
            <p className="muted">
              Configure les élèves, leur niveau et la leçon active visible dans le mode élève.
            </p>
          </div>
          <span>{currentUser.username} · {currentUser.role}</span>
        </header>

        <section className="admin-program-browser">
          <div className="program-tree-panel">
            <div className="admin-log-detail-head">
              <h2>Arborescence du programme</h2>
              <button type="button" className="secondary-button" onClick={loadProgramTree}>
                Rafraîchir
              </button>
            </div>
            <div className="program-tree-list">
              {programRows.map(({ node, depth }) => (
                <button
                  key={node.key}
                  type="button"
                  className={`program-tree-row ${selectedProgramNodeKey === node.key ? 'active' : ''}`}
                  style={{ paddingLeft: `${0.65 + depth * 1.05}rem` }}
                  onClick={() => setSelectedProgramNodeKey(node.key)}
                >
                  <span>{node.label}</span>
                  <em>{node.type}</em>
                  {node.review?.status && <strong>{node.review.status}</strong>}
                </button>
              ))}
            </div>
          </div>
          <ProgramReviewPanel
            selectedNode={selectedProgramNode}
            reviewForm={programReviewForm}
            onReviewChange={setProgramReviewForm}
            onSaveReview={saveProgramReview}
          />
        </section>

        {selectedStudent ? (
          <section className="admin-detail">
            <div className="admin-student-card">
              <span className="stage-label">Élève sélectionné</span>
              <h2>{selectedStudent.first_name}</h2>
              <p>{selectedStudent.grade} · {selectedStudent.age} ans</p>
            </div>

            <div className="admin-student-card">
              <span className="stage-label">Leçon active</span>
              <h2>{studentProgram?.title || 'Aucune leçon'}</h2>
              <p>
                {studentProgram
                  ? `${studentProgram.subject} · ${studentProgram.asset_count} fiche(s)`
                  : 'Choisis une leçon dans la colonne de gauche.'}
              </p>
              {studentProgram?.assets?.length > 0 && (
                <div className="admin-start-controls">
                  <label>
                    <span>Reprendre à la fiche</span>
                    <select
                      value={programStart.asset_index}
                      onChange={(event) => updateProgramStart(event.target.value, 0)}
                    >
                      {studentProgram.assets.map((asset, index) => (
                        <option key={asset.path} value={index}>
                          {index + 1}. {asset.title}
                        </option>
                      ))}
                    </select>
                  </label>
                  <label>
                    <span>Card de départ</span>
                    <select
                      value={programStart.section_index}
                      onChange={(event) => updateProgramStart(programStart.asset_index, event.target.value)}
                    >
                      {(studentProgram.assets[programStart.asset_index]?.sections || []).map((section, index) => (
                        <option key={section.context_key || index} value={index}>
                          {index + 1}. {section.title}
                        </option>
                      ))}
                    </select>
                  </label>
                </div>
              )}
            </div>
          </section>
        ) : (
          <section className="admin-empty">
            <h2>Aucun élève sélectionné</h2>
            <p className="muted">Choisis un élève ou crée un compte pour configurer son programme.</p>
          </section>
        )}

        {studentProgram?.assets?.length > 0 && (
          <section className="admin-preview">
            <h2>Fiches de la leçon</h2>
            <div className="admin-asset-grid">
              {studentProgram.assets.map((asset) => (
                <article key={asset.path} className="admin-asset-card">
                  <img src={asset.url} alt={asset.title} />
                  <strong>{asset.title}</strong>
                </article>
              ))}
            </div>
          </section>
        )}

        {selectedStudent && (
          <section className="admin-preview">
            <h2>Compréhension des cards</h2>
            {cardProgress.length ? (
              <div className="admin-log-list">
                {cardProgress.map((item) => (
                  <article key={item.id} className="admin-log-card">
                    <strong>{item.section_title}</strong>
                    <span>{Math.round(item.comprehension_score)}% · essais: {item.attempts}</span>
                  </article>
                ))}
              </div>
            ) : (
              <p className="muted">Aucune card validée pour le moment.</p>
            )}
          </section>
        )}

        {selectedStudent && (
          <section className="admin-preview">
            <h2>Logs de séance</h2>
            {sessionLogs.length ? (
              <div className="admin-log-list">
                {sessionLogs.map((log) => (
                  <article key={log.id} className="admin-log-card">
                    <div>
                      <strong>{log.lesson_title}</strong>
                      <span>{new Date(log.ended_at).toLocaleString('fr-FR')}</span>
                    </div>
                    <div className="admin-log-actions">
                      <button type="button" className="secondary-button" onClick={() => setSelectedSessionLog(log)}>
                        Voir
                      </button>
                      <button type="button" className="secondary-button" onClick={() => deleteSessionLog(log.id)}>
                        Effacer
                      </button>
                    </div>
                  </article>
                ))}
              </div>
            ) : (
              <p className="muted">Aucun log enregistré.</p>
            )}
          </section>
        )}

        {selectedSessionLog && (
          <section className="admin-preview">
            <div className="admin-log-detail-head">
              <h2>Détail de séance</h2>
              <button type="button" className="secondary-button" onClick={() => setSelectedSessionLog(null)}>
                Fermer
              </button>
            </div>
            <div className="admin-log-detail">
              <h3>Résultats des cards</h3>
              {parseLogJson(selectedSessionLog.card_results).map((item) => (
                <article key={item.section_key} className="admin-log-card">
                  <strong>{item.section_title}</strong>
                  <span>{Math.round(item.comprehension_score)}% · {item.validated ? 'validée' : 'vue/tentée'}</span>
                </article>
              ))}
              <h3>Conversation</h3>
              {parseLogJson(selectedSessionLog.conversation).map((message, index) => (
                <p key={`${message.id || index}`} className="admin-log-message">
                  <strong>{message.role === 'assistant' ? 'Professeur TOP' : 'Élève'}:</strong> {message.content}
                </p>
              ))}
            </div>
          </section>
        )}

        {errorMessage && <p className="muted voice-status visible-status">{errorMessage}</p>}
      </main>
    </div>
  )
}

export default function App() {
  const [currentUser, setCurrentUser] = useState(null)
  const [isCheckingSession, setIsCheckingSession] = useState(true)

  useEffect(() => {
    let isMounted = true

    async function loadSession() {
      try {
        const user = await apiFetch('/auth/me')
        if (isMounted) setCurrentUser(user)
      } catch {
        if (isMounted) setCurrentUser(null)
      } finally {
        if (isMounted) setIsCheckingSession(false)
      }
    }

    loadSession()
    return () => {
      isMounted = false
    }
  }, [])

  async function logout() {
    try {
      await apiFetch('/auth/logout', { method: 'POST' })
    } finally {
      setCurrentUser(null)
    }
  }

  if (isCheckingSession) {
    return (
      <main className="login-page">
        <section className="login-panel card">
          <Avatar speaking={false} />
          <p className="muted">Verification de la session...</p>
        </section>
      </main>
    )
  }

  if (!currentUser) {
    return <LoginPage onLogin={setCurrentUser} />
  }

  return <TutorApp currentUser={currentUser} onLogout={logout} />
}
