// Preview-режим emote-анимации. 2026-05-27.
//
// URL: https://player.rublox.pro/_preview-emote/<itemId>#team_jwt=<JWT>
//
// Грузит дефолтного бекона + указанный GLB-emote, retargeting на скелет
// бекона по именам костей (Mixamo). Запускает анимацию в цикле.
// Кнопка «Проиграть ещё раз» — рестартует.

import { useEffect, useRef, useState, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useAuth } from '../auth/PlayerAuth';
import { BabylonScene } from '../engine/BabylonScene';
import LoadingScreen from '../LoadingScreen';
import { SceneLoader } from '@babylonjs/core/Loading/sceneLoader';
import '@babylonjs/loaders/glTF';
import { parseEmoteGlbToSpec } from '../engine/EmoteGlbParser';

export default function PreviewEmoteRoute() {
  const { itemId } = useParams();
  const navigate = useNavigate();
  const { isAuthenticated, isLoading: authLoading, error: authError } = useAuth();

  const canvasRef = useRef(null);
  const sceneRef = useRef(null);
  const animGroupRef = useRef(null);
  const [emote, setEmote] = useState(null);
  const [loadErr, setLoadErr] = useState(null);
  const [phase, setPhase] = useState('auth');

  // 1) Грузим метаданные emote через публичный эндпоинт.
  // Используем JWT (если есть) — для draft/testing.
  useEffect(() => {
    if (!isAuthenticated) return;
    setPhase('loading');
    // JWT плеера лежит под ключом 'player_jwt' (см. ticketExchange.js).
    // team_jwt из hash тоже сохраняется в этот же ключ через saveJWT.
    const headers = {};
    try {
      const jwt = localStorage.getItem('player_jwt');
      if (jwt) headers['Authorization'] = 'Bearer ' + jwt;
    } catch (e) {}
    fetch('/api-storys/rublox/emotes/' + itemId, { headers })
      .then((r) => {
        if (!r.ok) throw new Error('HTTP ' + r.status);
        return r.json();
      })
      .then((e) => {
        if (!e || !e.file_path) throw new Error('Emote не найден');
        setEmote(e);
      })
      .catch((e) => {
        setLoadErr(e?.message || 'Не удалось загрузить emote');
        setPhase('error');
      });
  }, [isAuthenticated, itemId]);

  // 2) Сцена + бекон + retargeting emote на его скелет
  useEffect(() => {
    if (!emote || !canvasRef.current) return;
    if (sceneRef.current) return;
    let scene, destroyed = false;
    (async () => {
      try {
        scene = new BabylonScene(canvasRef.current);
        sceneRef.current = scene;
        scene.init();
        try { scene.setActiveTool?.('select'); } catch (e) {}
        try { scene.spawnPoint = { x: 0, y: 1, z: 0 }; } catch (e) {}
        // Бекон как дефолт
        try { scene.setPlayerModelType?.('skin_bacon-hair'); } catch (e) {}
        scene.enterPlayMode?.();

        // Дожидаемся появления PlayerController с инициализированным R15Animator
        const playerController = await waitForR15Animator(scene, 5000);
        if (destroyed) return;
        if (!playerController) {
          throw new Error('R15Animator не инициализировался у игрока — бекон не загрузился?');
        }

        // Грузим GLB-emote (только armature + AnimationGroup; меши скрываем).
        // Babylon SceneLoader не умеет читать API-эндпоинты, которые могут
        // требовать JWT — сначала загружаем blob через fetch, кладём в
        // memory File и подсовываем SceneLoader через blob-URL.
        const authHeader = (() => {
          try {
            const jwt = localStorage.getItem('player_jwt');
            return jwt ? 'Bearer ' + jwt : null;
          } catch (e) { return null; }
        })();
        const glbResp = await fetch(emote.file_path, {
          headers: authHeader ? { 'Authorization': authHeader } : {},
        });
        if (!glbResp.ok) {
          throw new Error(`Не удалось скачать GLB: HTTP ${glbResp.status}`);
        }
        const glbBlob = await glbResp.blob();
        const blobUrl = URL.createObjectURL(glbBlob);
        let result;
        try {
          // 4-аргументная форма: (rootUrl, sceneFilename, scene, onProgress).
          // sceneFilename начинается с 'http' / 'blob:' / 'data:' → SceneLoader
          // берёт его как абсолютный URL целиком. Для glTF-плагина нужно явное
          // pluginExtension ('.glb') — иначе он не понимает расширение
          // у blob://uuid URL.
          result = await SceneLoader.ImportMeshAsync(
            '',                              // мешами не фильтруем — берём все
            '',                              // rootUrl пустой (URL в sceneFilename)
            blobUrl,                         // sceneFilename = blob-URL
            scene._scene || scene.scene,
            null,                            // onProgress
            '.glb',                          // pluginExtension — обязательно для blob
          );
        } finally {
          URL.revokeObjectURL(blobUrl);
        }
        if (destroyed) return;
        for (const m of result.meshes || []) {
          if (m.setEnabled) m.setEnabled(false);
        }

        const groups = result.animationGroups || [];
        if (groups.length === 0) {
          throw new Error('В GLB нет анимаций (animationGroups[] пустой)');
        }
        const group = groups[0];
        group.stop();

        // Парсим AnimationGroup → delta-spec (одна запись на кость).
        // restQ берём ИЗ ТЕКУЩИХ значений target (= bind pose исходника).
        const spec = parseEmoteGlbToSpec(group, emote.is_loop);
        if (!spec || !spec.perBoneFrames) {
          throw new Error('Парсер не извлёк ни одного канала из GLB');
        }
        const boneCount = Object.keys(spec.perBoneFrames).length;
        console.log(`[PreviewEmote] parsed: ${boneCount} R15-bones, length=${spec.length.toFixed(2)}s, loop=${spec.loop}`);
        if (boneCount === 0) {
          throw new Error(
            'Ни одна кость GLB не сопоставилась с R15-скелетом. ' +
            'Ожидаются Mixamo-имена (Hips/Spine/RightArm/...).',
          );
        }

        // Удаляем импортированный armature чтобы он не «дёргался» сам
        // (его AnimationGroup мы парсенули и больше не нужен).
        try { group.stop(); group.dispose(); } catch (e) {}
        try {
          for (const m of result.meshes || []) {
            if (m.dispose) m.dispose();
          }
          for (const s of result.skeletons || []) {
            if (s.dispose) s.dispose();
          }
        } catch (e) {}

        // Запускаем кастомный emote через R15Animator
        const ok = playerController.playCustomEmote(spec);
        if (!ok) throw new Error('playCustomEmote вернул false');

        // Запомним spec чтобы кнопка «Проиграть ещё раз» работала
        animGroupRef.current = { spec, controller: playerController };
        if (!destroyed) setPhase('ready');
      } catch (e) {
        console.error('[PreviewEmote] failed', e);
        if (!destroyed) {
          setLoadErr(e?.message || 'Ошибка инициализации');
          setPhase('error');
        }
      }
    })();
    return () => {
      destroyed = true;
      try { animGroupRef.current?.stop(); } catch (e) {}
      try {
        if (scene && typeof scene.dispose === 'function') scene.dispose();
        else if (scene && scene.engine) scene.engine.dispose();
      } catch (e) {}
      sceneRef.current = null;
    };
  }, [emote]);

  const replay = useCallback(() => {
    const ref = animGroupRef.current;
    if (ref && ref.controller && ref.spec) {
      ref.controller.stopEmote?.();
      ref.controller.playCustomEmote(ref.spec);
    }
  }, []);

  if (authLoading) return <LoadingScreen text="Подключение" />;
  if (authError === 'no_jwt_local') {
    return <LoadingScreen text="Нужен JWT"
                          subText="Открой из team.rublox.pro кнопкой «Тестировать»." />;
  }
  if (!isAuthenticated) {
    return <LoadingScreen text="Сессия истекла"
                          subText="Вернись в team.rublox.pro и попробуй снова." />;
  }

  return (
    <div style={{ position: 'fixed', inset: 0, background: '#000' }}>
      <canvas ref={canvasRef}
              style={{ width: '100%', height: '100%', display: 'block',
                       touchAction: 'none', outline: 'none' }}/>

      <div style={bannerStyle}>
        <div style={{ fontWeight: 700, fontSize: 14 }}>
          Тест emote: «{emote ? emote.name : '...'}»
        </div>
        <div style={{ fontSize: 12, opacity: 0.7, marginTop: 2 }}>
          {emote ? `код ${emote.code} · ${emote.is_loop ? 'loop' : 'однократно'}` : ''}
          {emote && emote.status !== 'published' && (
            <span style={{ color: '#ffcc66', marginLeft: 8 }}>
              · {emote.status === 'draft' ? 'черновик' : 'на проверке'}
            </span>
          )}
        </div>
        <button onClick={() => { try { window.close(); } catch (e) {} navigate('/'); }}
                style={closeBtnStyle}>Закрыть</button>
      </div>

      <div style={{
        position: 'absolute', bottom: 16, left: '50%',
        transform: 'translateX(-50%)',
        display: 'flex', gap: 8, alignItems: 'center',
      }}>
        <button onClick={replay} style={primaryBtnStyle}>
          ⟳ Проиграть ещё раз
        </button>
        <div style={hintStyle}>
          WASD — двигать камеру · мышь — повернуть
        </div>
      </div>

      {phase === 'loading' && (
        <div style={overlayStyle}><div>Загрузка emote…</div></div>
      )}
      {phase === 'error' && (
        <div style={overlayStyle}>
          <div style={{ color: '#ff8888', fontWeight: 700, marginBottom: 8 }}>
            Ошибка
          </div>
          <div style={{ fontSize: 13, opacity: 0.85, maxWidth: 480,
                         textAlign: 'center', lineHeight: 1.5 }}>
            {loadErr}
          </div>
          <button onClick={() => window.close()}
                  style={{ ...closeBtnStyle, marginTop: 12, position: 'static' }}>
            Закрыть
          </button>
        </div>
      )}
    </div>
  );
}

/**
 * Ждать пока у PlayerController появится инициализированный R15Animator.
 * Возвращает PlayerController или null по таймауту.
 */
async function waitForR15Animator(scene, timeoutMs) {
  const start = Date.now();
  return new Promise((resolve) => {
    const tick = () => {
      try {
        // PlayerController хранится в scene.player (см. BabylonScene.js:196)
        const pc = scene.player || scene._playerController;
        if (pc && pc._r15Animator && pc._isR15) {
          return resolve(pc);
        }
      } catch (e) {}
      if (Date.now() - start > timeoutMs) return resolve(null);
      requestAnimationFrame(tick);
    };
    tick();
  });
}

const bannerStyle = {
  position: 'absolute', top: 12, left: 12,
  background: 'rgba(10, 14, 26, 0.88)', color: '#f1f5fb',
  padding: '10px 90px 10px 14px',
  borderRadius: 8, border: '1px solid rgba(255,255,255,0.10)',
  fontFamily: '"Roboto Condensed", system-ui, sans-serif',
  display: 'inline-block',
  maxWidth: 'calc(100vw - 40px)',
};
const closeBtnStyle = {
  position: 'absolute', top: 10, right: 10,
  background: 'rgba(255, 111, 122, 0.18)', color: '#ff6f7a',
  border: '1px solid rgba(255, 111, 122, 0.40)',
  borderRadius: 6, padding: '4px 10px',
  fontSize: 12, fontWeight: 700, cursor: 'pointer',
};
const primaryBtnStyle = {
  background: '#3357FF', color: '#fff',
  border: 'none', borderRadius: 8,
  padding: '8px 16px', fontSize: 13, fontWeight: 700,
  cursor: 'pointer',
};
const hintStyle = {
  background: 'rgba(10, 14, 26, 0.88)', color: '#f1f5fb',
  padding: '8px 14px', borderRadius: 8,
  border: '1px solid rgba(255,255,255,0.10)',
  fontSize: 12,
  fontFamily: '"Roboto Condensed", system-ui, sans-serif',
};
const overlayStyle = {
  position: 'absolute', inset: 0,
  background: 'rgba(0,0,0,0.65)', color: '#fff',
  display: 'flex', alignItems: 'center', justifyContent: 'center',
  flexDirection: 'column', fontFamily: 'system-ui, sans-serif',
};
