// The Zen Den aquarium — raw Three.js, lazy-loaded only on /zen (keeps three's
// ~150KB off every other page). Phase A: get UB alive on screen (Idle_swim),
// soft light, simple backdrop, capped pixel ratio, reduced-motion aware, clean
// teardown. The behavior engine (steering / spine-bend turns / darts) lands in
// Phase B; the structure here (kept animation actions, a single update loop)
// is set up to grow into it.
//
// UB ships (ub-split.glb) as THREE materials — UB_Body, UB_Fins, UB_Tail — so each
// renders differently. The single-mesh approach hit a hard ceiling: a transparent,
// double-sided whole fish can't depth-sort its own triangles (tail/eye drew through
// the face), and any alpha cutoff that cleaned the fin fringe also chewed the fins.
// Thin midline fans (tail especially) also self-overlap as they fold/sweep, so even
// single-sided *transparent* tail triangles bleed through each other → "two tails".
// The render is fully parameter-driven (see DEFAULTS) so it can be tuned live via
// /zen?debug=1 — tuning a blind WebGL fish through redeploys was the slow path.
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const MODEL_URL = '/models/ub-split.glb';

// Converged Phase-A starting point (Claude + Codex): a hair off strict profile so
// the mouth/chin reads naturally; tail OPAQUE alpha-tested (one coherent tail, no
// blend bleed); fins translucent + single-sided (kills the double-sided ghosting
// that read as an off-center dorsal). All live-tunable at /zen?debug=1.
export const DEFAULTS = {
  yaw: Math.PI / 2 + 0.10,   // ub.rotation.y — slight 3/4 view
  pitch: 0,                  // ub.rotation.x — nose up/down
  tailTranslucent: false,    // false = opaque alpha-tested (coherent); true = blended
  tailSide: 'front',         // front | back | double
  tailAlphaTest: 0.035,
  tailOpacity: 1.0,          // only when translucent
  finSide: 'front',          // front | back | double
  finOpacity: 0.75,
  finAlphaTest: 0.02,
  paused: false,
  frame: 0,                  // 0..1 scrub position when paused
};

const SIDE = { front: THREE.FrontSide, back: THREE.BackSide, double: THREE.DoubleSide };

export async function createAquarium(canvas, initial = {}) {
  const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
  const params = { ...DEFAULTS, ...initial };

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5));  // mobile thermal guard
  renderer.outputColorSpace = THREE.SRGBColorSpace;

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
  camera.position.set(0, 0.25, 4.6);
  camera.lookAt(0, 0, 0);

  // Soft, warm light — calm, gently dimensional (no harsh shadows in Phase A).
  scene.add(new THREE.HemisphereLight(0xffffff, 0x8aa0b0, 1.05));
  const key = new THREE.DirectionalLight(0xfff2e0, 1.25);
  key.position.set(2.5, 4, 3);
  scene.add(key);

  function resize() {
    const w = canvas.clientWidth || 1;
    const h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }

  // Load UB.
  const gltf = await new GLTFLoader().loadAsync(MODEL_URL);
  const ub = gltf.scene;

  // Center at origin and scale to a consistent on-screen size, regardless of the
  // model's native units.
  const box = new THREE.Box3().setFromObject(ub);
  const size = box.getSize(new THREE.Vector3());
  const center = box.getCenter(new THREE.Vector3());
  ub.position.sub(center);
  const maxDim = Math.max(size.x, size.y, size.z) || 1;
  ub.scale.setScalar(2.6 / maxDim);

  // Collect the three named materials/meshes so applyMaterials can retarget them
  // live without re-traversing semantics.
  const part = {};
  ub.traverse((o) => { if (o.isMesh && o.material?.name) part[o.material.name] = o; });

  function applyMaterials() {
    const body = part.UB_Body, fins = part.UB_Fins, tail = part.UB_Tail;
    if (body) {
      const m = body.material;
      m.transparent = false; m.opacity = 1; m.alphaTest = 0;
      m.depthWrite = true; m.depthTest = true; m.side = THREE.FrontSide;
      body.renderOrder = 1; m.needsUpdate = true;
    }
    if (fins) {
      const m = fins.material;
      m.transparent = true; m.opacity = params.finOpacity; m.alphaTest = params.finAlphaTest;
      m.alphaToCoverage = true; m.depthWrite = false; m.depthTest = true;
      m.side = SIDE[params.finSide] ?? THREE.FrontSide;
      fins.renderOrder = 3; m.needsUpdate = true;
    }
    if (tail) {
      const m = tail.material;
      m.alphaTest = params.tailAlphaTest; m.alphaToCoverage = true; m.depthTest = true;
      m.side = SIDE[params.tailSide] ?? THREE.FrontSide;
      if (params.tailTranslucent) {
        m.transparent = true; m.opacity = params.tailOpacity; m.depthWrite = false;
      } else {
        m.transparent = false; m.opacity = 1; m.depthWrite = true;  // opaque, coherent
      }
      tail.renderOrder = 2; m.needsUpdate = true;
    }
  }

  function applyTransform() {
    ub.rotation.set(params.pitch, params.yaw, 0);
  }

  applyMaterials();
  applyTransform();
  scene.add(ub);

  // Animation — keep every action by name so Phase B can crossfade (Idle_swim →
  // Eat_Up / Roll). Idle_swim is the base loop.
  const mixer = new THREE.AnimationMixer(ub);
  const actions = {};
  for (const clip of gltf.animations) actions[clip.name] = mixer.clipAction(clip);
  const baseClip = (actions.Idle_swim ? actions.Idle_swim : mixer.clipAction(gltf.animations[0]));
  baseClip.play();
  const baseDuration = baseClip.getClip().duration || 1;
  mixer.timeScale = reduced ? 0.6 : 1;   // calmer when reduced-motion

  resize();
  // The canvas lives in a responsive container; a ResizeObserver catches layout
  // shifts (mobile browser-chrome show/hide, future side panels) a bare 'resize' misses.
  const onResize = () => resize();
  window.addEventListener('resize', onResize);
  const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(onResize) : null;
  ro?.observe(canvas);

  const clock = new THREE.Clock();
  renderer.setAnimationLoop(() => {
    const dt = clock.getDelta();
    if (params.paused) {
      mixer.setTime(params.frame * baseDuration);  // scrub to a frozen frame
    } else {
      mixer.update(dt);
    }
    renderer.render(scene, camera);
  });

  return {
    // exposed for Phase B tuning + the /zen?debug=1 panel
    ub, actions, mixer, scene, camera, params, baseDuration,
    // live setter: merge new values, re-apply materials + transform
    setParams(next = {}) {
      Object.assign(params, next);
      applyMaterials();
      applyTransform();
      return { ...params };
    },
    getParams() { return { ...params }; },
    dispose() {
      renderer.setAnimationLoop(null);
      window.removeEventListener('resize', onResize);
      ro?.disconnect();
      scene.traverse((o) => {
        o.geometry?.dispose?.();
        const mats = Array.isArray(o.material) ? o.material : o.material ? [o.material] : [];
        for (const m of mats) {
          for (const v of Object.values(m)) v?.isTexture && v.dispose();
          m.dispose?.();
        }
      });
      renderer.dispose();
    },
  };
}
