import React, { useEffect, useMemo, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { ChevronLeft, Menu, Wifi, WifiOff, Save, RotateCcw, Play, ScrollText, Volume2, VolumeX } from 'lucide-react';
import './styles.css';
import { story } from './story/story.js';
import { createInitialState, loadState } from './engine/avgEngine.js';
import { chooseMilestone, computeProgress } from './engine/avgRules.js';
import { startNewGame, loadSavedGame, persistGameState, resolvePlayerInput } from './engine/runtimeController.js';
import { hasLLMProvider } from './engine/llmProviders.js';
import { ensureCohubRuntime } from './engine/cohubRuntime.js';
import { preloadGate, warmRest, resolveAsset, isAssetReady, toAbsoluteAssetUrl, loadOne } from './engine/preload.js';
import { reportStoryValidation, validateStory } from './engine/storyValidator.js';

const PHASE = { BOOT: 'boot', TITLE: 'title', PLAY: 'play' };

function useTypewriter(text, speed = 24) {
  const [shown, setShown] = useState('');
  const [done, setDone] = useState(false);
  const timerRef = useRef(null);
  useEffect(() => {
    setShown('');
    setDone(!text);
    let i = 0;
    if (timerRef.current) clearInterval(timerRef.current);
    if (!text) return undefined;
    timerRef.current = setInterval(() => {
      i += 1;
      setShown(text.slice(0, i));
      if (i >= text.length) {
        setDone(true);
        clearInterval(timerRef.current);
        timerRef.current = null;
      }
    }, speed);
    return () => { clearInterval(timerRef.current); timerRef.current = null; };
  }, [text, speed]);
  return {
    shown,
    done,
    skip: () => {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
      setShown(text);
      setDone(true);
    }
  };
}

function useLLMProviderStatus() {
  const [ready, setReady] = useState(false);
  const [available, setAvailable] = useState(false);
  useEffect(() => {
    let live = true;
    ensureCohubRuntime();
    const tick = async () => {
      const hasProvider = await hasLLMProvider().catch(() => false);
      if (!live) return;
      setReady(true);
      setAvailable(hasProvider);
    };
    tick();
    const timer = setInterval(tick, 1200);
    const onMessage = (event) => {
      if (event.origin !== window.location.origin) return;
      if (event.data?.source === 'avg-neta-auth' || event.data?.source === 'world-sim-neta-auth') tick();
    };
    window.addEventListener('message', onMessage);
    return () => { live = false; clearInterval(timer); window.removeEventListener('message', onMessage); };
  }, []);
  return { ready, available, refresh: async () => setAvailable(await hasLLMProvider().catch(() => false)) };
}

function LoadingScreen({ progress }) {
  const pct = Math.round(Math.max(0, Math.min(1, progress)) * 100);
  return <main className="boot">
    <div className="boot-inner">
      <span className="boot-label">Preparing story</span>
      <div className="boot-bar"><span className="boot-fill" style={{ width: `${pct}%` }} /></div>
    </div>
  </main>;
}

function Sprite({ src, tint, name }) {
  // Hold the last fully-decoded sprite so expression switches don't blank the stage
  // while the next URL is still coming from disk/network cache.
  const [displaySrc, setDisplaySrc] = useState(() => (src ? resolveAsset(src) : null));
  const [loaded, setLoaded] = useState(() => (src ? isAssetReady(src) : false));
  const pendingRef = useRef(null);

  useEffect(() => {
    if (!src) {
      setDisplaySrc(null);
      setLoaded(false);
      return undefined;
    }
    const abs = toAbsoluteAssetUrl(src);
    pendingRef.current = abs;
    let live = true;

    const show = (url) => {
      if (!live || pendingRef.current !== abs) return;
      setDisplaySrc(url || abs);
      setLoaded(true);
    };

    if (isAssetReady(src)) {
      show(resolveAsset(src));
      return () => { live = false; };
    }

    // Keep previous displaySrc visible (loaded stays true) until the next image is ready.
    loadOne(src).then((url) => {
      if (!live || pendingRef.current !== abs) return;
      // Decode via a probe image first, then swap — avoids empty <img> frame.
      const probe = new Image();
      probe.onload = () => show(url || abs);
      probe.onerror = () => show(abs);
      probe.src = url || abs;
      if (probe.complete && probe.naturalWidth) show(url || abs);
    });

    return () => { live = false; };
  }, [src]);

  if (!src && !displaySrc) {
    return <div className="sprite sprite-tint" style={{ background: tint || '#2c3e54' }}><span>{String(name || '?').slice(0, 1)}</span></div>;
  }

  return <img
    className={`sprite ${loaded ? 'is-loaded' : ''}`}
    src={displaySrc || resolveAsset(src)}
    alt={name || ''}
    decoding="async"
    // No key={src}: remounting forces a blank frame even when the file is cached.
    onLoad={() => setLoaded(true)}
    ref={(el) => {
      if (el?.complete && el.naturalWidth) setLoaded(true);
    }}
  />;
}

function TurnLogPanel({ open, entries, onClose }) {
  if (!open) return null;
  return <aside className="turn-log-panel" aria-label="Turn log">
    <div className="panel-head">
      <strong>Turn Log</strong>
      <button type="button" onClick={onClose} aria-label="Close turn log">×</button>
    </div>
    <div className="log-list">
      {entries.length ? entries.map((entry) => <article className="log-card" key={entry.id}>
        <div className="log-meta">
          <b>{String(entry.turn).padStart(2, '0')}</b>
          <span>{entry.actorName}</span>
          <em>{entry.actionLabel}</em>
        </div>
        <p className="log-player">You: {entry.input || 'Continue'}</p>
        <p className="log-response">{entry.actorName}: {entry.response}</p>
        <div className="log-rule">
          <span>{entry.ruleSummary}</span>
          {entry.source && entry.source !== 'unknown' && <i>{entry.source === 'llm' ? 'AI' : 'Local'}</i>}
        </div>
        {entry.meterDelta?.length > 0 && <div className="log-deltas">
          {entry.meterDelta.map((m) => <span key={m.id}>{m.label} {m.delta > 0 ? '+' : ''}{m.delta}</span>)}
        </div>}
      </article>) : <div className="log-empty">No turns yet.</div>}
    </div>
  </aside>;
}

function TitleScreen({ hasSave, onStart, onContinue, musicEnabled, onToggleMusic }) {
  const titleImage = story.titleImage || story.coverImage || null;
  return <main className="title-screen">
    <div
      className={`title-bg ${titleImage ? 'has-title-image' : ''}`}
      style={{ backgroundImage: titleImage ? `url(${resolveAsset(titleImage)})` : undefined }}
    />
    <div className="title-inner">
      <p className="eyebrow">{story.subtitle}</p>
      <h1>{story.title}</h1>
      <p className="premise">{story.premise}</p>
      <div className="title-actions">
        <button className="primary" onClick={onStart}><Play size={18} /><span>Start New Game</span></button>
        {hasSave && <button onClick={onContinue}><RotateCcw size={18} /><span>Continue Save</span></button>}
        <button onClick={onToggleMusic} title={musicEnabled ? 'Mute music' : 'Play music'}>{musicEnabled ? <Volume2 size={18} /> : <VolumeX size={18} />}</button>
      </div>
    </div>
  </main>;
}

function App() {
  const validation = useMemo(() => validateStory(story), []);
  const [phase, setPhase] = useState(PHASE.BOOT);
  const [bootProgress, setBootProgress] = useState(0);
  const [hasSave, setHasSave] = useState(() => !!loadState(story.id, story));
  const [state, setState] = useState(() => createInitialState(story));
  const [page, setPage] = useState(0);
  const [menuOpen, setMenuOpen] = useState(false);
  const [turnLogOpen, setTurnLogOpen] = useState(false);
  const [freeText, setFreeText] = useState('');
  const [busy, setBusy] = useState(false);
  const [statusMode, setStatusMode] = useState('idle');
  const [choicesHidden, setChoicesHidden] = useState(false);
  const [toast, setToast] = useState(null);
  const [keyboardLift, setKeyboardLift] = useState(0);
  const [keyboardOpen, setKeyboardOpen] = useState(false);
  const [musicEnabled, setMusicEnabled] = useState(false);
  const audioRef = useRef(null);
  const requestRef = useRef(0);
  const toastTimerRef = useRef(null);
  const inputRef = useRef(null);
  const keyboardTimersRef = useRef([]);
  const { ready: authReady, available: authed, refresh } = useLLMProviderStatus();

  useEffect(() => {
    reportStoryValidation(validation);
    let live = true;
    preloadGate(story, { onProgress: (p) => { if (live) setBootProgress(p); } })
      .finally(() => {
        if (!live) return;
        warmRest(story);
        setPhase(PHASE.TITLE);
      });
    return () => { live = false; };
  }, [validation]);

  useEffect(() => {
    const preventGesture = (event) => event.preventDefault();
    document.addEventListener('gesturestart', preventGesture, { passive: false });
    document.addEventListener('gesturechange', preventGesture, { passive: false });
    document.addEventListener('gestureend', preventGesture, { passive: false });
    return () => {
      document.removeEventListener('gesturestart', preventGesture);
      document.removeEventListener('gesturechange', preventGesture);
      document.removeEventListener('gestureend', preventGesture);
    };
  }, []);

  useEffect(() => {
    const allowScrollSelector = '.text, .actor-list, .menu-panel, .log-list';
    const onTouchMove = (event) => {
      if (event.target.closest?.(allowScrollSelector)) return;
      event.preventDefault();
    };
    document.addEventListener('touchmove', onTouchMove, { passive: false });
    return () => document.removeEventListener('touchmove', onTouchMove);
  }, []);

  useEffect(() => {
    const clearKeyboardTimers = () => {
      keyboardTimersRef.current.forEach((id) => window.clearTimeout(id));
      keyboardTimersRef.current = [];
    };
    const sync = () => {
      const vv = window.visualViewport;
      const viewportHeight = window.innerHeight;
      const keyboardHeight = vv ? Math.max(0, Math.round(viewportHeight - (vv.height + vv.offsetTop))) : 0;
      const visible = keyboardHeight > 80;
      setKeyboardLift(visible ? keyboardHeight : 0);
      setKeyboardOpen(visible);
    };
    const scheduleSync = () => {
      clearKeyboardTimers();
      [0, 80, 180, 320, 520].forEach((delay) => {
        keyboardTimersRef.current.push(window.setTimeout(sync, delay));
      });
    };
    const onFocus = () => {
      setTurnLogOpen(false);
      scheduleSync();
    };
    const onBlur = () => {
      clearKeyboardTimers();
      keyboardTimersRef.current.push(window.setTimeout(sync, 80));
      keyboardTimersRef.current.push(window.setTimeout(sync, 260));
    };
    const input = inputRef.current;
    input?.addEventListener('focus', onFocus);
    input?.addEventListener('blur', onBlur);
    window.addEventListener('resize', scheduleSync);
    const vv = window.visualViewport;
    vv?.addEventListener('resize', sync);
    vv?.addEventListener('scroll', sync);
    sync();
    return () => {
      clearKeyboardTimers();
      input?.removeEventListener('focus', onFocus);
      input?.removeEventListener('blur', onBlur);
      window.removeEventListener('resize', scheduleSync);
      vv?.removeEventListener('resize', sync);
      vv?.removeEventListener('scroll', sync);
    };
  }, [phase]);

  const actorId = state.actorId;
  const actor = story.cast[actorId];
  const baseLines = actor.lines || [];
  const lines = state.dynamicLines[actorId]?.length ? state.dynamicLines[actorId] : baseLines;
  const line = lines[Math.min(page, lines.length - 1)] || '';
  const typewriter = useTypewriter(line, 22);
  const textDone = typewriter.done && typewriter.shown === line;
  const isLastPage = page >= lines.length - 1;
  const canChoose = isLastPage && textDone && !choicesHidden && !busy && !state.ended;
  const choices = state.dynamicChoices[actorId]?.length ? state.dynamicChoices[actorId] : (actor.choices || []);

  const scene = story.scenes[state.sceneId] || story.scenes[story.defaultSceneId] || {};
  const sceneImage = scene.image || scene.bg || null;
  const currentBgm = phase === PHASE.TITLE
    ? (story.titleBgm || null)
    : (scene.bgm || story.titleBgm || null);
  const spriteSrc = actor.sprites?.[state.expression] || actor.sprite || null;
  const milestone = chooseMilestone(state, actorId, story);
  const progress = computeProgress(state, actorId, story);
  const meterValues = state.actors?.[actorId] || {};
  const turnLog = state.turnLog || [];

  // BGM: story.titleBgm for title screen, scene.bgm for gameplay scenes
  useEffect(() => {
    if (!currentBgm) return;
    if (!audioRef.current) {
      audioRef.current = new Audio();
      audioRef.current.loop = true;
      audioRef.current.volume = 0.35;
    }
    const audio = audioRef.current;
    const nextSrc = new URL(resolveAsset(currentBgm), window.location.href).href;
    if (audio.src !== nextSrc) audio.src = nextSrc;
    if (!musicEnabled) { audio.pause(); return; }
    audio.play().catch(() => setMusicEnabled(false));
  }, [currentBgm, musicEnabled]);

  useEffect(() => () => { audioRef.current?.pause(); }, []);

  const showToast = (message) => {
    setToast(message);
    if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
    toastTimerRef.current = setTimeout(() => setToast(null), 1800);
  };

  const beginPlay = (nextState) => {
    requestRef.current += 1;
    setBusy(false);
    setStatusMode('idle');
    setState(nextState);
    setPage(0);
    setFreeText('');
    setChoicesHidden(false);
    setMenuOpen(false);
    setTurnLogOpen(false);
    setPhase(PHASE.PLAY);
  };

  const onStart = () => beginPlay(startNewGame(story));
  const onContinue = () => beginPlay(loadSavedGame(story, createInitialState(story)) || startNewGame(story));

  const advancePage = () => {
    if (!textDone) {
      typewriter.skip();
      return;
    }
    if (!isLastPage) setPage((value) => value + 1);
  };

  // No <form>: the runtime ships inside a Cohub Work sandbox iframe whose
  // sandbox attribute lacks `allow-forms`, so native form submission is
  // blocked. Send is a plain button + Enter key handler routed here.
  const commitFreeText = () => {
    const input = freeText.trim();
    if (busy || !input) return false;
    setFreeText('');
    handleChoose(input);
    return true;
  };

  const handleChoose = async (input) => {
    if (busy) return;
    setBusy(true);
    setChoicesHidden(true);
    const token = requestRef.current + 1;
    requestRef.current = token;
    setStatusMode(authed ? 'thinking' : 'fallback');
    const result = await resolvePlayerInput({
      story,
      state,
      actor,
      visibleLine: line,
      playerInput: input,
      requestToken: token,
      isCurrent: (value) => requestRef.current === value
    });
    if (result.stale) {
      if (requestRef.current === token) {
        setChoicesHidden(false);
        setBusy(false);
        setStatusMode('idle');
      }
      return;
    }
    setState(result.state);
    persistGameState(story, result.state);
    setHasSave(true);
    setPage(Math.max((result.state.dynamicLines?.[actorId]?.length || 1) - 1, 0));
    setChoicesHidden(false);
    setBusy(false);
    setStatusMode(result.source === 'llm' ? 'online' : 'offline');
    if (result.errorCode) showToast('AI did not respond; switched to local performance');
    refresh();
  };

  if (phase === PHASE.BOOT) return <LoadingScreen progress={bootProgress} />;
  if (phase === PHASE.TITLE) return <TitleScreen hasSave={hasSave} onStart={onStart} onContinue={onContinue} musicEnabled={musicEnabled} onToggleMusic={() => setMusicEnabled((v) => !v)} />;

  const aiStatusClass = statusMode === 'thinking'
    ? 'thinking'
    : authed && statusMode !== 'offline'
      ? 'on'
      : 'fallback';

  return <main className={`game-shell ${keyboardOpen ? 'keyboard-open' : ''}`} style={{ '--keyboard-lift': `${keyboardLift}px` }}>
    <div
      className={`bg ${sceneImage ? '' : 'bg-tint'}`}
      style={{
        backgroundImage: sceneImage ? `url(${resolveAsset(sceneImage)})` : 'none',
        backgroundColor: scene.color || '#14161d'
      }}
    />
    <div className="shade" />

    <header className="toolbar">
      <button className="tool" type="button" onClick={() => setPhase(PHASE.TITLE)} aria-label="Back to title" title="Back to title"><ChevronLeft size={18} /></button>
      <button className="tool" type="button" onClick={() => { persistGameState(story, state); showToast('Saved'); }} aria-label="Save progress" title="Save progress"><Save size={18} /></button>
      <button className="tool" type="button" onClick={() => setMenuOpen((value) => !value)} aria-label="Open cast panel" title="Cast panel"><Menu size={18} /></button>
      <button className="tool" type="button" onClick={() => { setTurnLogOpen((value) => !value); setMenuOpen(false); }} aria-label="Open turn log" title="Turn log"><ScrollText size={18} /></button>
      <button className="tool" type="button" onClick={() => setMusicEnabled((v) => !v)} aria-label={musicEnabled ? 'Mute music' : 'Play music'} title={musicEnabled ? 'Mute music' : 'Play music'}>{musicEnabled ? <Volume2 size={16} /> : <VolumeX size={16} />}</button>
      <span className="tool-sep" aria-hidden="true" />
      <div
        className={`tool ai ${aiStatusClass}`}
        aria-live="polite"
        aria-label={statusMode === 'thinking' ? 'AI writing' : authed ? 'AI connected' : authReady ? 'Local performance' : 'Connecting'}
        title={statusMode === 'thinking' ? 'AI writing' : authed ? 'AI connected' : authReady ? 'Local performance' : 'Connecting'}
      >
        {authed ? <Wifi size={16} /> : <WifiOff size={16} />}
      </div>
    </header>

    <section className={`stage ${busy ? 'thinking' : ''}`} onClick={advancePage}>
      <Sprite src={spriteSrc} tint={actor?.accent} name={actor?.name} />
    </section>

    {menuOpen && <aside className="menu-panel" aria-label="Cast panel">
      <div className="panel-head">
        <strong>{scene.name || story.title}</strong>
        <button type="button" onClick={() => setMenuOpen(false)} aria-label="Close cast panel">×</button>
      </div>
      <div className="meter-board">
        {(story.meters || []).map((meter) => {
          const value = Number(meterValues[meter.id] || 0);
          return <div className="meter-row" key={meter.id}>
            <span>{meter.label}</span>
            <em>{value}</em>
          </div>;
        })}
      </div>
      <button className="menu-reset" type="button" onClick={() => beginPlay(startNewGame(story))}><RotateCcw size={16} /><span>Restart</span></button>
    </aside>}

    <section className="dock">
      {canChoose && <div className="choice-float">
        {choices.map((choice, index) => <button key={`${choice}-${index}`} type="button" onClick={(event) => { event.stopPropagation(); handleChoose(choice); }}>
          <i>{String(index + 1).padStart(2, '0')}</i>
          <span>{choice}</span>
        </button>)}
      </div>}

      <section className="dialogue" onClick={advancePage}>
        <div className="meta">
          <span className="name">{actor?.name}</span>
          <span className="arc">{milestone?.label || actor?.role || story.tone} · {progress}%</span>
          <div className="pager" onClick={(event) => event.stopPropagation()}>
            <button type="button" disabled={page <= 0} onClick={() => setPage((value) => Math.max(0, value - 1))} aria-label="Previous page">‹</button>
            <button type="button" disabled={isLastPage && textDone} onClick={() => {
              if (!textDone) {
                typewriter.skip();
                return;
              }
              if (!isLastPage) setPage((value) => value + 1);
            }} aria-label="Next page">›</button>
          </div>
        </div>
        <div className={`text ${textDone ? 'done' : ''}`}>
          {busy ? (
            <span className="thinking-line" aria-label="Loading">
              <span className="dots" aria-hidden="true" />
            </span>
          ) : (
            <>
              {typewriter.shown}
              {!textDone && <span className="caret" />}
            </>
          )}
        </div>
        <div className="input-row" onClick={(event) => event.stopPropagation()}>
          <input
            ref={inputRef}
            value={freeText}
            onChange={(event) => setFreeText(event.target.value)}
            onKeyDown={(event) => {
              if (event.key !== 'Enter' || event.nativeEvent?.isComposing) return;
              event.preventDefault();
              event.stopPropagation();
              commitFreeText();
            }}
            placeholder="输入你的下一步行动……"
            maxLength={48}
            enterKeyHint="send"
            disabled={busy}
          />
          <span className="input-hint">Enter 发送</span>
          <button
            className="send"
            type="button"
            disabled={busy || !freeText.trim()}
            aria-label="Send input"
            onPointerDown={(event) => { event.preventDefault(); event.stopPropagation(); }}
            onClick={(event) => { event.preventDefault(); event.stopPropagation(); commitFreeText(); }}
          >→</button>
        </div>
      </section>
    </section>

    {state.ended && <section className="ending-panel" aria-live="polite">
      <p className="ending-kicker">夜班记录已封存</p>
      <h2>{state.ended.title}</h2>
      <p>{state.ended.summary}</p>
      <button type="button" onClick={() => beginPlay(startNewGame(story))}><RotateCcw size={16} /><span>重新进入乔家超市</span></button>
    </section>}

    <TurnLogPanel open={turnLogOpen} entries={turnLog} onClose={() => setTurnLogOpen(false)} />
    {toast && <div className="toast">{toast}</div>}
  </main>;
}

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

