import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
import { useFrame } from '@react-three/fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import * as THREE from 'three';
import { SceneObject, useStudioStore } from '../store/useStudioStore';
import { getFramesAtTime, interpolateKF } from '../utils/timeline';

interface ModelProps {
  obj: SceneObject;
  isSelected: boolean;
  onClick: () => void;
}

export interface ModelHandle {
  group: THREE.Group | null;
}

export const Model = forwardRef<ModelHandle, ModelProps>(({ obj, isSelected, onClick }, ref) => {
  const groupRef = useRef<THREE.Group>(null!);
  const mixerRef = useRef<THREE.AnimationMixer | null>(null);
  const actionsRef = useRef<THREE.AnimationAction[]>([]);
  const { updateObject, addError, tracks, playhead } = useStudioStore();
  const [scene, setScene] = useState<THREE.Group | null>(null);
  const [clips, setClips] = useState<THREE.AnimationClip[]>([]);
  const [error, setError] = useState<string | null>(null);

  // ── Expose group ref to parent (TransformControls needs it) ──
  useImperativeHandle(ref, () => ({ group: groupRef.current }), [scene]);

  // ── 1. Load GLB ──────────────────────────────────────────────
  useEffect(() => {
    let cancelled = false;
    const loader = new GLTFLoader();
    loader.load(
      obj.url,
      (loaded) => {
        if (cancelled) return;
        setScene(loaded.scene);
        setClips(loaded.animations || []);
        if (loaded.animations?.length > 0) {
          updateObject(obj.id, {
            animations: loaded.animations.map((a: THREE.AnimationClip, i: number) => ({
              name: a.name || `Anim ${i + 1}`,
              duration: a.duration,
              index: i,
            })),
          });
        }
      },
      undefined,
      (err: any) => {
        if (cancelled) return;
        const msg = `Failed to load: ${err?.message || String(err)}`;
        setError(msg);
        addError(msg);
      }
    );
    return () => { cancelled = true; };
  }, [obj.url]); // eslint-disable-line

  // ── 2. Apply materials ────────────────────────────────────────
  useEffect(() => {
    if (!scene) return;
    scene.traverse((child: THREE.Object3D) => {
      if ((child as THREE.Mesh).isMesh) {
        const mesh = child as THREE.Mesh;
        const mats = Array.isArray(mesh.material)
          ? mesh.material as THREE.MeshStandardMaterial[]
          : [mesh.material as THREE.MeshStandardMaterial];
        mats.forEach((m) => {
          if (!m) return;
          m.color = new THREE.Color(obj.color);
          m.metalness = obj.metalness;
          m.roughness = obj.roughness;
          m.envMapIntensity = obj.envMapIntensity;
          if (obj.textureUrl) {
            new THREE.TextureLoader().load(obj.textureUrl, (tex) => {
              m.map = tex;
              m.needsUpdate = true;
            });
          }
          m.needsUpdate = true;
        });
      }
    });
  }, [scene, obj.color, obj.metalness, obj.roughness, obj.envMapIntensity, obj.textureUrl]);

  // ── 3. Setup mixer — bound to SAME scene used by primitive ───
  useEffect(() => {
    if (!scene || !clips.length) return;
    // Dispose old mixer first
    mixerRef.current?.stopAllAction();
    mixerRef.current?.uncacheRoot(scene);
    const mixer = new THREE.AnimationMixer(scene);
    mixerRef.current = mixer;
    actionsRef.current = clips.map((clip) => mixer.clipAction(clip));
    return () => {
      mixer.stopAllAction();
      mixer.uncacheRoot(scene);
    };
  }, [scene, clips]);

  // ── 4. Play / pause / switch GLB animation clip ──────────────
  useEffect(() => {
    if (!actionsRef.current.length) return;
    // Stop all first
    actionsRef.current.forEach((a) => {
      a.stop();
      a.reset();
    });
    if (!obj.animPlaying) return;
    const action = actionsRef.current[obj.selectedAnimIndex];
    if (!action) return;
    action.setLoop(
      obj.animLoop ? THREE.LoopRepeat : THREE.LoopOnce,
      Infinity
    );
    action.clampWhenFinished = !obj.animLoop;
    action.timeScale = obj.animSpeed;
    action.reset();
    action.play();
  }, [obj.animPlaying, obj.selectedAnimIndex, obj.animLoop, obj.animSpeed]);

  // ── 5. Per-frame: mixer tick + timeline interpolation ────────
  useFrame((_state, delta) => {
    // Tick GLB animation mixer using R3F's delta (no separate clock)
    if (mixerRef.current && obj.animPlaying) {
      mixerRef.current.update(delta);
    }

    // Apply keyframe timeline interpolation
    if (!groupRef.current) return;
    const track = tracks.find(t => t.objectId === obj.id);
    if (track && track.keyframes.length >= 1) {
      const result = getFramesAtTime(track.keyframes, playhead);
      if (result) {
        const interp = interpolateKF(result.a, result.b, result.t);
        groupRef.current.position.set(...interp.position);
        groupRef.current.rotation.set(
          THREE.MathUtils.degToRad(interp.rotation[0]),
          THREE.MathUtils.degToRad(interp.rotation[1]),
          THREE.MathUtils.degToRad(interp.rotation[2]),
        );
        groupRef.current.scale.set(...interp.scale);
      }
    }
  });

  // ── Fallback box while loading / on error ────────────────────
  if (error || !scene) {
    return (
      <mesh position={obj.position} onClick={onClick}>
        <boxGeometry args={[1, 1, 1]} />
        <meshStandardMaterial color={error ? '#ff4444' : '#444466'} wireframe={!!error} />
      </mesh>
    );
  }

  // If this object has timeline keyframes, position is driven by
  // useFrame interpolation above — don't set it as a prop too
  const hasTimeline = tracks.find(t => t.objectId === obj.id)?.keyframes.length;

  return (
    <group
      ref={groupRef}
      position={hasTimeline ? undefined : obj.position}
      rotation={
        hasTimeline
          ? undefined
          : obj.rotation.map(THREE.MathUtils.degToRad) as [number, number, number]
      }
      scale={hasTimeline ? undefined : obj.scale}
      onClick={(e) => { e.stopPropagation(); onClick(); }}
    >
      {/* Original scene — NOT cloned, so mixer bindings stay valid */}
      <primitive object={scene} />
      {isSelected && (
        <mesh>
          <boxGeometry args={[2, 2, 2]} />
          <meshBasicMaterial color="#00d4ff" wireframe transparent opacity={0.15} />
        </mesh>
      )}
    </group>
  );
});

Model.displayName = 'Model';