const {useEffect, useRef, useState} = React;

const API = '../api/index.php';

async function api(route, options = {}) {
  const [path, query = ''] = route.split('?');
  const headers = new Headers(options.headers || {});
  if (!(options.body instanceof FormData) && options.body !== undefined) headers.set('Content-Type', 'application/json');
  const response = await fetch(`${API}?route=${encodeURIComponent(path)}${query ? `&${query}` : ''}`, {
    ...options,
    headers,
    credentials: 'include'
  });
  const payload = await response.json().catch(() => ({ok: false, error: 'Invalid server response.'}));
  if (!response.ok || payload.ok === false) throw new Error(payload.error || `Request failed (${response.status})`);
  return payload;
}

function uid() {
  return window.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

function go(path) { window.location.hash = path; }

function useRoute() {
  const [route, setRoute] = useState(location.hash.slice(1) || '/');
  useEffect(() => {
    const handler = () => setRoute(location.hash.slice(1) || '/');
    addEventListener('hashchange', handler);
    return () => removeEventListener('hashchange', handler);
  }, []);
  return route;
}

function mergeTranscript(current, incoming) {
  const next = String(incoming || '').trim();
  if (!next) return current;
  if (!current) return next;
  if (next.startsWith(current)) return next;
  if (current.endsWith(next)) return current;
  return `${current} ${next}`.replace(/\s+/g, ' ').trim();
}

function QuotaBadge({quota}) {
  if (!quota) return null;
  if (quota.remaining === null) return <span className="quota-badge">Unlimited interactions</span>;
  return <span className={`quota-badge ${quota.remaining < 3 ? 'low' : ''}`}>
    {quota.remaining} of {quota.limit} {quota.period} interactions left
  </span>;
}

function SpritePlayer({character, state, className = ''}) {
  const animations = character?.animations || {};
  let animation = animations[state];
  if (!animation && ['talking', 'explaining', 'happy', 'encouraging', 'celebrating', 'warning'].includes(state)) animation = animations.talking;
  if (!animation && ['listening', 'thinking', 'confused', 'error'].includes(state)) animation = animations.idle;
  animation = animation || animations.idle || Object.values(animations)[0];
  const [frame, setFrame] = useState(0);

  useEffect(() => {
    setFrame(0);
    if (!animation || Number(animation.frame_count) <= 1) return;
    const timer = setInterval(() => setFrame(current => {
      const next = current + 1;
      if (next < Number(animation.frame_count)) return next;
      return animation.loop_enabled ? 0 : current;
    }), Math.max(16, 1000 / Math.max(1, Number(animation.fps))));
    return () => clearInterval(timer);
  }, [animation?.id, state]);

  if (!animation) return <div className={`sprite-empty ${className}`}>Upload idle and talking sprites</div>;
  const aspect = Number(animation.frame_width) > 0 && Number(animation.frame_height) > 0
    ? `${animation.frame_width} / ${animation.frame_height}` : '1 / 1';

  if (animation.animation_type === 'frames') {
    const frames = animation.frame_paths || [];
    const path = frames[Math.min(frame, frames.length - 1)];
    return <div className={`sprite-stage ${className}`} style={{aspectRatio: aspect}}>
      {path ? <img src={`..${path}`} alt={`${character.name} ${state}`} draggable="false"/> : null}
    </div>;
  }

  const columns = Math.max(1, Number(animation.columns_count));
  const rows = Math.max(1, Number(animation.rows_count));
  const col = frame % columns;
  const row = Math.floor(frame / columns);
  const x = columns === 1 ? 0 : (col / (columns - 1)) * 100;
  const y = rows === 1 ? 0 : (row / (rows - 1)) * 100;
  return <div className={`sprite-stage ${className}`} role="img" aria-label={`${character.name} ${state}`} style={{
    aspectRatio: aspect,
    backgroundImage: animation.file_path ? `url(..${animation.file_path})` : undefined,
    backgroundSize: `${columns * 100}% ${rows * 100}%`,
    backgroundPosition: `${x}% ${y}%`
  }}/>;
}

function Login({onLogin}) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [busy, setBusy] = useState(false);

  async function submit(e) {
    e.preventDefault();
    setBusy(true);
    setError('');
    try {
      const data = await api('auth/login', {method: 'POST', body: JSON.stringify({email, password})});
      onLogin(data.user);
    } catch (err) {
      setError(err.message);
    } finally {
      setBusy(false);
    }
  }

  return <main className="login-shell"><section className="login-card">
    <div className="brand-mark">AI</div>
    <p className="eyebrow">AI Tutor · SpatialNex</p>
    <h1>Welcome back</h1>
    <p className="muted">Sign in to learn with animated tutors and professional assistants.</p>
    <form onSubmit={submit} className="stack">
      <label>Email<input type="email" value={email} onChange={e => setEmail(e.target.value)} required/></label>
      <label>Password<input type="password" value={password} onChange={e => setPassword(e.target.value)} required/></label>
      {error && <div className="error-box">{error}</div>}
      <button className="primary" disabled={busy}>{busy ? 'Signing in…' : 'Sign in'}</button>
    </form>
  </section></main>;
}

function Home({user, onLogout}) {
  const [characters, setCharacters] = useState([]);
  const [error, setError] = useState('');
  const [quota, setQuota] = useState(user.quota || null);

  useEffect(() => {
    api('characters').then(d => setCharacters(d.characters)).catch(e => setError(e.message));
    api('quota').then(d => setQuota(d.quota)).catch(() => {});
  }, []);

  return <main className="home-shell">
    <header className="topbar"><div><p className="eyebrow">Animated AI Professionals</p><h1>Choose your expert</h1></div>
      <div className="top-actions"><QuotaBadge quota={quota}/>
        {user.role === 'admin' && <button className="ghost-button" onClick={() => go('/admin')}>⚙ Admin</button>}
        <button className="ghost-button" onClick={onLogout}>Logout</button>
      </div>
    </header>
    <section className="hero-panel"><div><span className="hero-icon">✦</span><h2>One engine. Any profession.</h2>
      <p>Teachers, tutors, technicians, doctors for general education, support agents, and other professionals can each use their own sprites, interface, model, voice, and limits.</p></div></section>
    {error && <div className="error-box">{error}</div>}
    <section className="character-grid">{characters.map(character => <button key={character.id} className="character-card" onClick={() => go(`/character/${character.id}`)}>
      <div className={`character-preview framing-${character.character_framing}`}><SpritePlayer character={character} state="idle"/></div>
      <div className="character-copy"><span className="pill">{character.profession}</span><h3>{character.name}</h3>
        <p>{character.description || 'AI professional ready to help.'}</p>
        <div className="card-meta"><strong>Start →</strong><small>{character.interaction_mode === 'standard' ? 'Text + Chirp' : character.interaction_mode === 'live' ? 'Gemini Live' : 'Chat + Live'}</small></div>
      </div>
    </button>)}</section>
    {!characters.length && !error && <div className="empty-panel">No published characters yet. Create one in Admin.</div>}
  </main>;
}

function Tutor({id}) {
  const [character, setCharacter] = useState(null);
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [state, setState] = useState('idle');
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState('');
  const [conversationId, setConversationId] = useState(null);
  const [listening, setListening] = useState(false);
  const [quota, setQuota] = useState(null);
  const [chatState, setChatState] = useState('open');
  const [liveConnected, setLiveConnected] = useState(false);
  const [liveMic, setLiveMic] = useState(false);
  const liveMicRef = useRef(false);
  const [liveCaption, setLiveCaption] = useState('');
  const audioRef = useRef(null);
  const endRef = useRef(null);
  const liveRef = useRef(null);
  const liveEventRef = useRef(null);
  const liveInputRef = useRef('');
  const liveOutputRef = useRef('');
  const liveCompletingRef = useRef(false);
  const liveAudioPlayingRef = useRef(false);
  const conversationRef = useRef(null);

  useEffect(() => {
    api(`character?id=${id}`).then(d => {
      setCharacter(d.character);
      setQuota(d.quota);
      setChatState(d.character.chat_default_state || 'open');
    }).catch(e => setError(e.message));
  }, [id]);

  useEffect(() => { conversationRef.current = conversationId; }, [conversationId]);
  useEffect(() => endRef.current?.scrollIntoView({behavior: 'smooth'}), [messages, busy, liveCaption, chatState]);
  useEffect(() => () => {
    if (audioRef.current) audioRef.current.pause();
    if (liveEventRef.current) api('live/cancel', {method: 'POST', body: JSON.stringify({eventUuid: liveEventRef.current})}).catch(() => {});
    liveRef.current?.close?.();
  }, []);

  function stopStandardAudio() {
    if (audioRef.current) {
      audioRef.current.pause();
      audioRef.current.currentTime = 0;
      audioRef.current = null;
    }
    setState('idle');
  }

  async function speak(text, animation) {
    if (!character?.voice_reply_enabled || !text.trim()) {
      setState('idle');
      return;
    }
    try {
      const data = await api('tts', {method: 'POST', body: JSON.stringify({characterId: character.id, text})});
      const audio = new Audio(`data:${data.mimeType};base64,${data.audioBase64}`);
      audioRef.current = audio;
      audio.onplay = () => setState(animation || 'talking');
      audio.onended = () => { audioRef.current = null; setState('idle'); };
      audio.onerror = () => { audioRef.current = null; setState('idle'); };
      await audio.play();
    } catch (err) {
      setState('idle');
      setError(`${err.message} The text answer is still available.`);
    }
  }

  async function sendStandardMessage(text = input) {
    if (!character || busy || !text.trim()) return;
    stopStandardAudio();
    const clean = text.trim();
    setInput('');
    setError('');
    setBusy(true);
    setState('thinking');
    setMessages(items => [...items, {id: uid(), role: 'user', text: clean}]);
    try {
      const data = await api('chat', {
        method: 'POST',
        body: JSON.stringify({characterId: character.id, conversationId, message: clean, eventUuid: uid()})
      });
      setConversationId(data.conversationId);
      setQuota(data.quota);
      setMessages(items => [...items, {id: uid(), role: 'assistant', text: data.reply.displayText}]);
      await speak(data.reply.speechText, data.reply.animation);
    } catch (err) {
      setState('error');
      setError(err.message);
      setTimeout(() => setState('idle'), 1200);
    } finally {
      setBusy(false);
    }
  }

  function startBrowserListening() {
    if (!character?.allow_voice_input) return;
    const Ctor = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!Ctor) {
      setError('Speech recognition is not available in this browser. You can still type your question.');
      return;
    }
    stopStandardAudio();
    const recognition = new Ctor();
    recognition.lang = character.voice_locale || 'en-US';
    recognition.interimResults = true;
    recognition.continuous = false;
    recognition.onresult = e => setInput(Array.from(e.results).map(result => result[0].transcript).join(' '));
    recognition.onend = () => { setListening(false); setState('idle'); };
    recognition.onerror = recognition.onend;
    setListening(true);
    setState('listening');
    recognition.start();
  }

  async function finalizeLiveTurn() {
    if (!liveEventRef.current || liveCompletingRef.current) return;
    liveCompletingRef.current = true;
    const eventUuid = liveEventRef.current;
    const inputText = liveInputRef.current.trim();
    const outputText = liveOutputRef.current.trim();
    try {
      const data = await api('live/complete', {
        method: 'POST',
        body: JSON.stringify({
          characterId: character.id,
          eventUuid,
          inputText,
          outputText,
          conversationId: conversationRef.current
        })
      });
      if (data.conversationId) setConversationId(data.conversationId);
      setQuota(data.quota);
      if (inputText) setMessages(items => [...items, {id: uid(), role: 'user', text: inputText}]);
      if (outputText) setMessages(items => [...items, {id: uid(), role: 'assistant', text: outputText}]);
      setLiveCaption('');
    } catch (err) {
      setError(err.message);
    } finally {
      liveEventRef.current = null;
      liveInputRef.current = '';
      liveOutputRef.current = '';
      liveCompletingRef.current = false;
      if (!liveAudioPlayingRef.current) setState('idle');
    }
  }

  async function ensureLiveConnected() {
    if (liveRef.current?.isReady?.()) return liveRef.current;
    if (!window.GeminiLiveSession) throw new Error('Gemini Live client did not load. Refresh the page and try again.');
    const tokenData = await api('live/token', {method: 'POST', body: JSON.stringify({characterId: character.id})});
    setQuota(tokenData.quota);
    const session = new window.GeminiLiveSession({
      onOpen: () => setLiveConnected(true),
      onClose: () => {
        setLiveConnected(false);
        liveMicRef.current = false;
        setLiveMic(false);
        if (liveEventRef.current) {
          api('live/cancel', {method: 'POST', body: JSON.stringify({eventUuid: liveEventRef.current})}).catch(() => {});
          liveEventRef.current = null;
        }
        setState('idle');
      },
      onError: err => setError(err.message),
      onNotice: message => setError(message),
      onMicStart: () => { liveMicRef.current = true; setLiveMic(true); setState('listening'); },
      onMicStop: () => { liveMicRef.current = false; setLiveMic(false); setState('thinking'); },
      onAudioStart: () => { liveAudioPlayingRef.current = true; setState('talking'); },
      onAudioEnd: () => { liveAudioPlayingRef.current = false; if (!liveMicRef.current) setState('idle'); },
      onInterrupted: () => setState(liveMicRef.current ? 'listening' : 'idle'),
      onInputTranscript: text => {
        liveInputRef.current = mergeTranscript(liveInputRef.current, text);
        setLiveCaption(liveInputRef.current);
      },
      onOutputTranscript: text => {
        liveOutputRef.current = mergeTranscript(liveOutputRef.current, text);
        setLiveCaption(liveOutputRef.current);
        if (!character.voice_reply_enabled) setState('explaining');
      },
      onTurnComplete: finalizeLiveTurn
    });
    await session.connect(tokenData.session.token, tokenData.session.setup);
    liveRef.current = session;
    return session;
  }

  async function startLiveTurn() {
    if (!character?.allow_voice_input || busy) return;
    setBusy(true);
    setError('');
    try {
      const session = await ensureLiveConnected();
      const eventUuid = uid();
      const reserve = await api('live/reserve', {method: 'POST', body: JSON.stringify({characterId: character.id, eventUuid})});
      setQuota(reserve.quota);
      liveEventRef.current = eventUuid;
      liveInputRef.current = '';
      liveOutputRef.current = '';
      setLiveCaption('Listening…');
      await session.startMicrophone();
    } catch (err) {
      if (liveEventRef.current) {
        await api('live/cancel', {method: 'POST', body: JSON.stringify({eventUuid: liveEventRef.current})}).catch(() => {});
        liveEventRef.current = null;
      }
      setError(err.message);
      setState('idle');
    } finally {
      setBusy(false);
    }
  }

  async function stopLiveTurn() {
    if (!liveRef.current || !liveMic) return;
    await liveRef.current.stopMicrophone(true);
  }

  async function sendLiveText(text = input) {
    if (!character || busy || !text.trim()) return;
    setBusy(true);
    setError('');
    const clean = text.trim();
    setInput('');
    try {
      const session = await ensureLiveConnected();
      const eventUuid = uid();
      const reserve = await api('live/reserve', {method: 'POST', body: JSON.stringify({characterId: character.id, eventUuid})});
      setQuota(reserve.quota);
      liveEventRef.current = eventUuid;
      liveInputRef.current = clean;
      liveOutputRef.current = '';
      setState('thinking');
      session.sendText(clean);
    } catch (err) {
      if (liveEventRef.current) {
        await api('live/cancel', {method: 'POST', body: JSON.stringify({eventUuid: liveEventRef.current})}).catch(() => {});
        liveEventRef.current = null;
      }
      setError(err.message);
      setState('idle');
    } finally {
      setBusy(false);
    }
  }

  function submitText(text = input) {
    if (character.interaction_mode === 'live') return sendLiveText(text);
    return sendStandardMessage(text);
  }

  if (!character) return <div className="page-center">{error || 'Loading character…'}</div>;

  const isLiveAvailable = ['live', 'hybrid'].includes(character.interaction_mode);
  const showChat = character.show_text_chat && chatState === 'open';
  const minimized = character.show_text_chat && chatState === 'minimized';
  const shellClasses = [
    'tutor-shell',
    character.layout_mode === 'character_focus' ? 'character-focus' : 'tutor-split',
    `chat-${chatState}`,
    `framing-${character.character_framing}`
  ].join(' ');

  return <main className={shellClasses}>
    <section className="character-pane">
      <button className="icon-text-button back-button" onClick={() => go('/')}>← Experts</button>
      <div className="character-stage"><SpritePlayer character={character} state={state}/><div className="state-chip">{state}</div></div>
      <div className="character-meta"><div><span className="pill">{character.profession}</span><h1>{character.name}</h1><p>{character.description}</p></div>
        <div className="character-controls"><QuotaBadge quota={quota}/>
          {liveConnected && <span className="live-status">● Live connected</span>}
          {character.show_text_chat && chatState !== 'open' && <button className="ghost-button" onClick={() => setChatState('open')}>Open chat</button>}
        </div>
      </div>

      {character.show_talk_button && isLiveAvailable && character.allow_voice_input && <button
        className={`floating-talk ${liveMic ? 'active' : ''}`}
        onClick={liveMic ? stopLiveTurn : startLiveTurn}
        disabled={busy || (quota?.remaining === 0)}>
        <span>{liveMic ? '■' : '🎤'}</span>{liveMic ? 'Finish speaking' : 'Talk to character'}
      </button>}
      {liveCaption && <div className="live-caption">{liveCaption}</div>}
    </section>

    {showChat && <section className="chat-pane">
      <header className="chat-header"><div><strong>{character.name}</strong><span>
        {character.interaction_mode === 'live' ? `Gemini Live · ${character.live_model}` : `Gemini chat · ${character.gemini_model}`}
      </span></div>
        <div className="chat-header-actions"><QuotaBadge quota={quota}/>
          <button className="ghost-button" onClick={() => setChatState('minimized')}>Minimize</button>
          {audioRef.current && <button className="ghost-button" onClick={stopStandardAudio}>■ Stop voice</button>}
        </div>
      </header>
      <div className="messages">
        {!messages.length && <div className="welcome-message"><h2>How can I help?</h2><p>Ask a question, request a lesson, or use the administrator-enabled talk button.</p>
          <div className="suggestions"><button onClick={() => submitText('Introduce yourself and explain how you can help me.')}>Introduce yourself</button>
            <button onClick={() => submitText('Teach me a useful beginner lesson in your field.')}>Start a lesson</button>
            <button onClick={() => submitText('Give me a short quiz and wait for my answer.')}>Test me</button>
          </div></div>}
        {messages.map(m => <article key={m.id} className={`message ${m.role}`}><span>{m.role === 'assistant' ? character.name : 'You'}</span><p>{m.text}</p></article>)}
        {busy && <article className="message assistant thinking-message"><span>{character.name}</span><p>{state === 'listening' ? 'Listening…' : 'Thinking…'}</p></article>}
        <div ref={endRef}/>
      </div>
      {error && <div className="error-box chat-error">{error}</div>}
      <form className="composer" onSubmit={e => { e.preventDefault(); submitText(); }}>
        {character.allow_voice_input && character.interaction_mode !== 'live' && <button type="button" className={`mic-button ${listening ? 'active' : ''}`} onClick={startBrowserListening}>🎤</button>}
        <textarea value={input} onChange={e => setInput(e.target.value)} placeholder={`Ask ${character.name} anything…`} rows="1"
          onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submitText(); } }}/>
        <button className="send-button" disabled={busy || !input.trim() || quota?.remaining === 0}>➤</button>
      </form>
    </section>}

    {minimized && <button className="chat-minimized" onClick={() => setChatState('open')}>
      <span>💬</span><div><strong>Open conversation</strong><small>{messages.length ? `${messages.length} messages` : 'Chat is minimized by default'}</small></div>
    </button>}
    {character.show_text_chat && chatState === 'hidden' && <button className="chat-open-fab" onClick={() => setChatState('open')}>💬 Open chat</button>}
    {!showChat && error && <div className="floating-error error-box">{error}</div>}
  </main>;
}

const emptyCharacter = {
  id: 0,
  name: '',
  slug: '',
  profession: '',
  description: '',
  system_prompt: '',
  knowledge_text: '',
  gemini_model: 'auto',
  interaction_mode: 'standard',
  live_model: 'gemini-3.1-flash-live-preview',
  live_voice: 'Kore',
  voice_reply_enabled: true,
  allow_voice_input: true,
  voice_locale: 'en-US',
  voice_name: 'Charon',
  speaking_rate: 1,
  layout_mode: 'tutor_split',
  chat_default_state: 'open',
  character_framing: 'waist_up',
  show_talk_button: true,
  show_text_chat: true,
  status: 'draft',
  animations: {}
};
const animationKeys = ['idle', 'talking', 'listening', 'thinking', 'explaining', 'happy', 'encouraging', 'celebrating', 'confused', 'warning', 'error'];

function CharacterEditor({characterId, onDone}) {
  const [character, setCharacter] = useState({...emptyCharacter});
  const [models, setModels] = useState([]);
  const [liveModels, setLiveModels] = useState([]);
  const [voices, setVoices] = useState([]);
  const [locales, setLocales] = useState([]);
  const [error, setError] = useState('');
  const [notice, setNotice] = useState('');
  const [saving, setSaving] = useState(false);
  const [animationKey, setAnimationKey] = useState('idle');
  const [animationType, setAnimationType] = useState('sheet');
  const [sheetFile, setSheetFile] = useState(null);
  const [frameFiles, setFrameFiles] = useState([]);
  const [rows, setRows] = useState(1);
  const [columns, setColumns] = useState(1);
  const [frameCount, setFrameCount] = useState(1);
  const [fps, setFps] = useState(12);
  const [loop, setLoop] = useState(true);

  useEffect(() => {
    api('bootstrap').then(d => {
      setModels(d.models);
      setLiveModels(d.liveModels);
      setVoices(d.voices);
      setLocales(d.locales);
    });
    if (characterId) {
      api('admin/characters').then(d => {
        const found = d.characters.find(x => Number(x.id) === Number(characterId));
        if (found) setCharacter(found);
      }).catch(e => setError(e.message));
    }
  }, [characterId]);

  function field(key, value) { setCharacter(c => ({...c, [key]: value})); }
  function changeMode(value) {
    setCharacter(c => ({...c, interaction_mode: value}));
  }

  async function save() {
    setSaving(true);
    setError('');
    setNotice('');
    try {
      const data = await api('admin/characters/save', {method: 'POST', body: JSON.stringify(character)});
      setCharacter(data.character);
      setNotice('Character and interface settings saved.');
      if (!characterId) onDone(data.character.id);
    } catch (err) {
      setError(err.message);
    } finally {
      setSaving(false);
    }
  }

  async function uploadAnimation(e) {
    e.preventDefault();
    if (!character.id) { setError('Save the character before uploading animations.'); return; }
    const form = new FormData();
    form.append('characterId', character.id);
    form.append('animationKey', animationKey);
    form.append('animationType', animationType);
    form.append('rows', rows);
    form.append('columns', columns);
    form.append('frameCount', frameCount);
    form.append('fps', fps);
    form.append('loopEnabled', loop ? '1' : '0');
    if (animationType === 'sheet' && sheetFile) form.append('sheet', sheetFile);
    frameFiles.forEach(file => form.append('frames[]', file));
    try {
      const data = await api('admin/animations/upload', {method: 'POST', body: form});
      setCharacter(data.character);
      setNotice(`${animationKey} animation uploaded.`);
    } catch (err) {
      setError(err.message);
    }
  }

  const hasStandard = ['standard', 'hybrid'].includes(character.interaction_mode);
  const hasLive = ['live', 'hybrid'].includes(character.interaction_mode);

  return <section><button className="icon-text-button" onClick={() => onDone(null)}>← Characters</button>
    <header className="section-header compact"><div><p className="eyebrow">Character Builder</p><h1>{character.id ? (character.name || 'Edit character') : 'New character'}</h1></div>
      <button className="primary" onClick={save} disabled={saving}>{saving ? 'Saving…' : 'Save character'}</button>
    </header>
    {error && <div className="error-box">{error}</div>}{notice && <div className="success-box">{notice}</div>}
    <div className="editor-layout"><div className="editor-main">
      <div className="panel"><div className="panel-title"><div><h2>Identity and role</h2><p>The prompt and knowledge stay private on the server.</p></div></div>
        <div className="form-grid">
          <label>Name<input value={character.name} onChange={e => field('name', e.target.value)}/></label>
          <label>Profession<input value={character.profession} onChange={e => field('profession', e.target.value)} placeholder="Mathematics teacher, electrician…"/></label>
          <label className="full">Description<textarea value={character.description || ''} onChange={e => field('description', e.target.value)} rows="3"/></label>
          <label>Status<select value={character.status} onChange={e => field('status', e.target.value)}><option value="draft">Draft</option><option value="published">Published</option><option value="disabled">Disabled</option></select></label>
          <label>Conversation engine<select value={character.interaction_mode} onChange={e => changeMode(e.target.value)}>
            <option value="standard">Standard chat + optional Chirp voice</option>
            <option value="live">Gemini Live streaming voice</option>
            <option value="hybrid">Standard chat + Gemini Live talk button</option>
          </select></label>
          <label className="full">Character instructions<textarea value={character.system_prompt || ''} onChange={e => field('system_prompt', e.target.value)} rows="6" placeholder="Teaching style, safety rules, response length, personality…"/></label>
          <label className="full">Approved knowledge<textarea value={character.knowledge_text || ''} onChange={e => field('knowledge_text', e.target.value)} rows="8"/></label>
        </div>
      </div>

      <div className="panel"><div className="panel-title"><div><h2>AI and voice</h2><p>Voice behavior is locked by the administrator; users do not receive an on/off switch.</p></div></div>
        <div className="form-grid">
          {hasStandard && <React.Fragment>
            <label>Standard Gemini model<select value={character.gemini_model} onChange={e => field('gemini_model', e.target.value)}>{models.map(m => <option key={m.id} value={m.id}>{m.name}{m.preview ? ' — Preview' : ''}</option>)}</select></label>
            <label className="check-card"><input type="checkbox" checked={!!character.voice_reply_enabled} onChange={e => field('voice_reply_enabled', e.target.checked)}/><span><strong>Voice replies enabled</strong><small>When off, replies remain text-only and the user cannot enable voice.</small></span></label>
            {character.voice_reply_enabled && <React.Fragment>
              <label>Chirp locale<select value={character.voice_locale} onChange={e => field('voice_locale', e.target.value)}>{locales.map(l => <option key={l.id} value={l.id}>{l.name}</option>)}</select></label>
              <label>Chirp 3 HD voice<select value={character.voice_name} onChange={e => field('voice_name', e.target.value)}>{voices.map(v => <option key={v.id} value={v.id}>{v.id} — {v.gender}</option>)}</select></label>
              <label className="full">Speaking rate ({Number(character.speaking_rate).toFixed(2)}×)<input type="range" min=".25" max="2" step=".05" value={character.speaking_rate} onChange={e => field('speaking_rate', Number(e.target.value))}/></label>
            </React.Fragment>}
          </React.Fragment>}
          {hasLive && <React.Fragment>
            <label>Gemini Live model<select value={character.live_model} onChange={e => field('live_model', e.target.value)}>{liveModels.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}</select></label>
            <label>Gemini Live native voice<select value={character.live_voice} onChange={e => field('live_voice', e.target.value)}>{voices.map(v => <option key={v.id} value={v.id}>{v.id} — {v.gender}</option>)}</select></label>
            <div className="note-box full">Gemini Live streams native audio when voice replies are enabled and streams text when they are disabled. The permanent Gemini API key stays on PHP; browsers receive only short-lived tokens.</div>
          </React.Fragment>}
          <label className="check-card full"><input type="checkbox" checked={!!character.allow_voice_input} onChange={e => field('allow_voice_input', e.target.checked)}/><span><strong>Allow microphone input</strong><small>Controls browser speech input and the Gemini Live talk button.</small></span></label>
        </div>
      </div>

      <div className="panel"><div className="panel-title"><div><h2>User interface</h2><p>Configure the screen independently for every professional.</p></div></div>
        <div className="form-grid">
          <label>Layout preset<select value={character.layout_mode} onChange={e => field('layout_mode', e.target.value)}>
            <option value="tutor_split">Tutor Lesson — character + full lesson text</option>
            <option value="character_focus">Character Focus — full character + drawer chat</option>
          </select></label>
          <label>Character framing<select value={character.character_framing} onChange={e => field('character_framing', e.target.value)}><option value="waist_up">Waist-up</option><option value="full_body">Full body</option></select></label>
          <label>Chat default state<select value={character.chat_default_state} onChange={e => field('chat_default_state', e.target.value)}><option value="open">Open</option><option value="minimized">Minimized</option><option value="hidden">Hidden</option></select></label>
          <label className="check-card"><input type="checkbox" checked={!!character.show_text_chat} onChange={e => field('show_text_chat', e.target.checked)}/><span><strong>Text chat available</strong><small>When disabled, the character can be voice-only.</small></span></label>
          <label className="check-card"><input type="checkbox" checked={!!character.show_talk_button} onChange={e => field('show_talk_button', e.target.checked)}/><span><strong>Show large talk button</strong><small>Best for engineers, doctors, and full-character screens.</small></span></label>
          <div className="interface-help full">
            <strong>Recommended:</strong> Tutors use <b>Tutor Lesson + waist-up + chat open</b>. Engineers or medical educators use <b>Character Focus + full body + chat minimized</b>.
          </div>
        </div>
      </div>

      <form className="panel" onSubmit={uploadAnimation}><div className="panel-title"><div><h2>Upload animation</h2><p>Idle and talking are required. Every other state is optional and falls back safely.</p></div><span>⬆</span></div>
        <div className="form-grid"><label>Animation state<select value={animationKey} onChange={e => setAnimationKey(e.target.value)}>{animationKeys.map(k => <option key={k}>{k}</option>)}</select></label>
          <label>Upload type<select value={animationType} onChange={e => setAnimationType(e.target.value)}><option value="sheet">Sprite sheet</option><option value="frames">Separate PNG/WebP frames</option></select></label>
          {animationType === 'sheet' ? <React.Fragment><label>Rows<input type="number" min="1" value={rows} onChange={e => setRows(e.target.value)}/></label>
            <label>Columns<input type="number" min="1" value={columns} onChange={e => setColumns(e.target.value)}/></label>
            <label>Usable frame count<input type="number" min="1" value={frameCount} onChange={e => setFrameCount(e.target.value)}/></label>
            <label className="full">Sprite sheet<input type="file" accept="image/png,image/webp" onChange={e => setSheetFile(e.target.files?.[0] || null)}/></label></React.Fragment>
            : <label className="full">Frames in filename order<input type="file" accept="image/png,image/webp" multiple onChange={e => setFrameFiles(Array.from(e.target.files || []))}/></label>}
          <label>FPS<input type="number" min="1" max="60" value={fps} onChange={e => setFps(e.target.value)}/></label>
          <label className="check-label"><input type="checkbox" checked={loop} onChange={e => setLoop(e.target.checked)}/> Loop animation</label>
          <div className="full"><button className="primary" type="submit">Upload {animationKey}</button></div>
        </div>
      </form>
    </div>

      <aside className="editor-preview panel"><p className="eyebrow">Live preview</p>
        <div className={`preview-stage framing-${character.character_framing}`}><SpritePlayer character={character} state={animationKey}/></div>
        <h3>{character.name || 'Character name'}</h3><p>{character.profession || 'Profession'}</p>
        <div className="preview-settings"><span>{character.layout_mode === 'tutor_split' ? 'Tutor Lesson' : 'Character Focus'}</span><span>Chat: {character.chat_default_state}</span><span>{character.interaction_mode}</span></div>
        <div className="animation-list">{animationKeys.map(k => <button type="button" key={k} className={animationKey === k ? 'active' : ''} onClick={() => setAnimationKey(k)}>{k}<span>{character.animations[k] ? 'Uploaded' : 'Fallback'}</span></button>)}</div>
      </aside>
    </div>
  </section>;
}

function UsersAdmin() {
  const blank = {role: 'user', status: 'active', interaction_period: 'daily', interaction_limit: 50};
  const [users, setUsers] = useState([]);
  const [draft, setDraft] = useState({...blank});
  const [error, setError] = useState('');
  const [notice, setNotice] = useState('');

  function load() { api('admin/users').then(d => setUsers(d.users)).catch(e => setError(e.message)); }
  useEffect(load, []);

  async function save() {
    setError('');
    setNotice('');
    try {
      await api('admin/users/save', {method: 'POST', body: JSON.stringify(draft)});
      setDraft({...blank});
      setNotice('User and interaction limit saved.');
      load();
    } catch (err) {
      setError(err.message);
    }
  }

  function edit(user) { setDraft({...user, password: '', interaction_limit: user.interaction_limit ?? ''}); }

  return <section><header className="section-header"><div><p className="eyebrow">Access control</p><h1>Users and interaction limits</h1><p>Limits apply to normal messages and each Gemini Live user turn.</p></div></header>
    {error && <div className="error-box">{error}</div>}{notice && <div className="success-box">{notice}</div>}
    <div className="users-layout"><div className="panel"><div className="panel-title"><h2>{draft.id ? 'Edit user' : 'New user'}</h2></div><div className="stack">
      <label>Name<input value={draft.name || ''} onChange={e => setDraft({...draft, name: e.target.value})}/></label>
      <label>Email<input type="email" value={draft.email || ''} onChange={e => setDraft({...draft, email: e.target.value})}/></label>
      <label>{draft.id ? 'New password (optional)' : 'Password'}<input type="password" value={draft.password || ''} onChange={e => setDraft({...draft, password: e.target.value})}/></label>
      <label>Role<select value={draft.role} onChange={e => setDraft({...draft, role: e.target.value})}><option value="user">User</option><option value="admin">Admin</option></select></label>
      <label>Status<select value={draft.status} onChange={e => setDraft({...draft, status: e.target.value})}><option value="active">Active</option><option value="disabled">Disabled</option></select></label>
      <label>Limit period<select value={draft.interaction_period} onChange={e => setDraft({...draft, interaction_period: e.target.value})}><option value="daily">Daily</option><option value="monthly">Monthly</option><option value="lifetime">Lifetime</option><option value="unlimited">Unlimited</option></select></label>
      {draft.interaction_period !== 'unlimited' && <label>Maximum interactions<input type="number" min="0" value={draft.interaction_limit ?? ''} onChange={e => setDraft({...draft, interaction_limit: e.target.value})}/></label>}
      <button className="primary" onClick={save}>Save user</button>
      {draft.id && <button className="ghost-button" onClick={() => setDraft({...blank})}>Cancel edit</button>}
    </div></div>
      <div className="panel table-wrap"><table><thead><tr><th>Name</th><th>Email</th><th>Role</th><th>Limit</th><th>Usage</th><th>Status</th><th></th></tr></thead><tbody>
        {users.map(u => <tr key={u.id}><td>{u.name}</td><td>{u.email}</td><td>{u.role}</td><td>{u.interaction_period === 'unlimited' ? 'Unlimited' : `${u.interaction_limit} / ${u.interaction_period}`}</td>
          <td>{u.quota.remaining === null ? '—' : `${u.quota.used} used · ${u.quota.remaining} left`}</td><td>{u.status}</td><td><button className="text-button" onClick={() => edit(u)}>Edit</button></td></tr>)}
      </tbody></table></div>
    </div>
  </section>;
}

function SettingsAdmin() {
  const [gemini, setGemini] = useState('');
  const [service, setService] = useState('');
  const [status, setStatus] = useState({geminiApiKeyConfigured: false, googleServiceAccountConfigured: false});
  const [notice, setNotice] = useState('');
  const [error, setError] = useState('');

  function load() { api('admin/settings').then(d => setStatus(d.settings)).catch(e => setError(e.message)); }
  useEffect(load, []);

  async function save(e) {
    e.preventDefault();
    setNotice('');
    setError('');
    try {
      await api('admin/settings/save', {method: 'POST', body: JSON.stringify({geminiApiKey: gemini, googleServiceAccountJson: service})});
      setGemini('');
      setService('');
      setNotice('API credentials saved securely. The Gemini key now supports standard chat and short-lived Live tokens.');
      load();
    } catch (err) {
      setError(err.message);
    }
  }

  return <section><header className="section-header"><div><p className="eyebrow">Integrations</p><h1>Gemini, Gemini Live, and Chirp</h1><p>The same Gemini API key is used to provision secure ephemeral Live tokens.</p></div></header>
    {error && <div className="error-box">{error}</div>}{notice && <div className="success-box">{notice}</div>}
    <form className="panel stack" onSubmit={save}>
      <div className="credential-status"><span>🔑</span><div><strong>Gemini API key</strong><span>{status.geminiApiKeyConfigured ? 'Configured for chat and Live' : 'Not configured'}</span></div></div>
      <label>New Gemini API key<input type="password" value={gemini} onChange={e => setGemini(e.target.value)} placeholder="Leave blank to keep existing key"/></label>
      <div className="credential-status"><span>🔑</span><div><strong>Google Cloud service account</strong><span>{status.googleServiceAccountConfigured ? 'Configured for Chirp 3 HD' : 'Not configured'}</span></div></div>
      <label>Service-account JSON<textarea value={service} onChange={e => setService(e.target.value)} rows="14" placeholder="Paste complete JSON. Leave blank to keep existing account."/></label>
      <div className="note-box">Chirp needs Cloud Text-to-Speech and billing enabled. Gemini Live uses the Gemini API directly and does not expose your permanent key to the browser.</div>
      <button className="primary">Save credentials</button>
    </form>
  </section>;
}

function Admin({initialRoute}) {
  const [section, setSection] = useState(initialRoute.startsWith('/admin/users') ? 'users' : initialRoute.startsWith('/admin/settings') ? 'settings' : 'characters');
  const match = initialRoute.match(/^\/admin\/character\/(.+)$/);
  const [editorId, setEditorId] = useState(match ? (match[1] === 'new' ? null : Number(match[1])) : undefined);
  const [characters, setCharacters] = useState([]);
  const [error, setError] = useState('');

  function load() { api('admin/characters').then(d => setCharacters(d.characters)).catch(e => setError(e.message)); }
  useEffect(() => { if (section === 'characters' && editorId === undefined) load(); }, [section, editorId]);

  function chooseSection(next) {
    setSection(next);
    setEditorId(undefined);
    go(`/admin${next === 'characters' ? '' : `/${next}`}`);
  }
  function done(id) {
    if (id) {
      setEditorId(Number(id));
      go(`/admin/character/${id}`);
    } else {
      setEditorId(undefined);
      go('/admin');
      load();
    }
  }

  let content;
  if (editorId !== undefined) content = <CharacterEditor characterId={editorId} onDone={done}/>;
  else if (section === 'users') content = <UsersAdmin/>;
  else if (section === 'settings') content = <SettingsAdmin/>;
  else content = <section><header className="section-header"><div><p className="eyebrow">Character Builder</p><h1>Your AI professionals</h1><p>Each character can have its own model, voice behavior, interface, sprites, and conversation mode.</p></div>
      <button className="primary" onClick={() => { setEditorId(null); go('/admin/character/new'); }}>＋ New character</button></header>
      {error && <div className="error-box">{error}</div>}<div className="admin-grid">{characters.map(c => <button className="admin-card" key={c.id} onClick={() => { setEditorId(Number(c.id)); go(`/admin/character/${c.id}`); }}>
        <div className="admin-card-icon">✦</div><div><span className={`status ${c.status}`}>{c.status}</span><h3>{c.name}</h3><p>{c.profession}</p></div>
        <small>{Object.keys(c.animations || {}).length} animations · {c.interaction_mode} · {c.layout_mode}</small>
      </button>)}</div>
    </section>;

  return <div className="admin-shell"><aside className="admin-sidebar"><button className="admin-brand" onClick={() => go('/')}>AI Tutor</button><nav>
    <button className={section === 'characters' ? 'active' : ''} onClick={() => chooseSection('characters')}>Characters</button>
    <button className={section === 'users' ? 'active' : ''} onClick={() => chooseSection('users')}>Users & limits</button>
    <button className={section === 'settings' ? 'active' : ''} onClick={() => chooseSection('settings')}>API settings</button>
    <button onClick={() => go('/')}>User app</button>
  </nav></aside><main className="admin-content">{content}</main></div>;
}

function App() {
  const route = useRoute();
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => { api('auth/me').then(d => setUser(d.user)).finally(() => setLoading(false)); }, []);
  async function logout() {
    await api('auth/logout', {method: 'POST', body: JSON.stringify({})});
    setUser(null);
    go('/');
  }

  if (loading) return <div className="page-center">Loading…</div>;
  if (!user) return <Login onLogin={u => { setUser(u); go('/'); }}/>;
  if (route.startsWith('/admin')) return user.role === 'admin' ? <Admin key={route} initialRoute={route}/> : <Home user={user} onLogout={logout}/>;
  const match = route.match(/^\/character\/(\d+)$/);
  if (match) return <Tutor id={match[1]}/>;
  return <Home user={user} onLogout={logout}/>;
}

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