import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { Menu, X } from 'lucide-react';
import tavernBackdropUrl from '../assets/scene2d/tavern-bg-039.png';
import orcBartenderUrl from '../assets/scene2d/orc-bartender-trim-081.png';
import elfGuideUrl from '../assets/scene2d/elf-guide-trim-081.png';
import dwarfPatronUrl from '../assets/scene2d/dwarf-patron-trim-081.png';

const RELEASE = '0.8.4-splash';
const BACKDROP_ASPECT = 1672 / 941;
const ORC_ASPECT = 1024 / 1387;
const ELF_ASPECT = 777 / 1276;
const DWARF_ASPECT = 939 / 1332;
const SCREEN_ASPECT = 1220 / 520;
const BOOTH_ASPECT = 980 / 250;
const SPEECH_ASPECT = 820 / 520;

const NAV_ROUTES = [
  { id: 'home', label: 'Solution' },
  { id: 'agents', label: 'Staff' },
  { id: 'contracts', label: 'Pricing' },
  { id: 'signin', label: 'Sign Up' },
  { id: 'signin', label: 'Login' },
  { id: 'booth', label: 'My Booth' },
];

const WANTED_AGENTS = [
  { name: 'Velvet Hex', role: 'consent guide', skill: 'BYOA links', bounty: '14c/h', initials: 'VH', tint: '#b989ff' },
  { name: 'Socket Wraith', role: 'tinybox bridge', skill: 'port mapping', bounty: '16c/h', initials: 'SW', tint: '#54d9ff' },
  { name: 'Iron Tusk', role: 'ops bartender', skill: 'publish agent', bounty: '18c/h', initials: 'IT', tint: '#7be36d' },
  { name: 'Amber Ledger', role: 'contract clerk', skill: 'pricing audit', bounty: '12c/h', initials: 'AL', tint: '#ffc45f' },
  { name: 'Northstar Fixer', role: 'merchant ops', skill: 'session scope', bounty: '20c/h', initials: 'NF', tint: '#a7ecff' },
  { name: 'Cinder Relay', role: 'delivery relay', skill: 'remote use', bounty: '15c/h', initials: 'CR', tint: '#ff8564' },
];

const SPEECH_STEPS = {
  home: ['Publish Agent', 'Remote Use', 'Port Mapping', 'Delivery Relay', 'Run Directly'],
  agents: ['Inspect Roster', 'Check Skills', 'Open Contract', 'Hire Session', 'Audit Scope'],
  booth: ['Attach Skills', 'Set Runtime', 'Approve Budget', 'Launch Booth', 'Watch Output'],
  contracts: ['Scope Bounty', 'Limit Spend', 'Rotate Keys', 'Review Logs', 'Renew Term'],
  signin: ['Create Pass', 'Link Agent', 'Bring Token', 'Confirm Consent', 'Enter Booth'],
  admin: ['Tenant Keys', 'Session Health', 'Runtime Gate', 'Audit Queue', 'Deploy Static'],
};

const ASSET_AUDIT = [
  { area: 'tavern backdrop', asset: 'assets/scene2d/tavern-bg-039.png', source: 'imagegen reference-guided game splash art', rating: '8.8/10' },
  { area: 'orc bartender', asset: 'assets/scene2d/orc-bartender-trim-081.png', source: 'imagegen character sprite with chroma-key alpha, alpha-trimmed for framing', rating: '8.6/10' },
  { area: 'elf guide', asset: 'assets/scene2d/elf-guide-trim-081.png', source: 'imagegen character sprite with chroma-key alpha, alpha-trimmed for framing', rating: '8.7/10' },
  { area: 'dwarf patron', asset: 'assets/scene2d/dwarf-patron-trim-081.png', source: 'imagegen character sprite with chroma-key alpha, alpha-trimmed for framing', rating: '8.5/10' },
  { area: 'browse agents screen', asset: 'animated CanvasTexture wanted board', source: 'repo-generated canvas UI', rating: '8.3/10' },
  { area: 'enter booth sign', asset: 'animated CanvasTexture neon sign', source: 'repo-generated canvas UI', rating: '8.2/10' },
  { area: 'topbar', asset: 'CSS carved wood/gem nav', source: 'repo-generated CSS material', rating: '8.0/10' },
];

const IDLE_ANIMATIONS = [
  'orc sprite: breathing scale, shoulder sway, mug glow pulse, cyan armor flicker',
  'elf sprite: cape sway, hand presentation bob, gem pulse, speech bubble pulse',
  'dwarf sprite: seated toast bob, mug lift beat, backpack core glow',
  'browse agents screen: wanted roster scroll, badge cycling, coin burst motion',
  'enter booth sign: neon tube flicker, scanline sweep, floor ring pulse',
  'environment: lantern glow pulses, crystal glints, ember drift, drag parallax',
];

export default function SplashSceneApp() {
  const [route, setRoute] = useHashRoute();

  return (
    <main className="amerc-app gemhall-app">
      <h1 className="sr-only">amerc agent mercenary tavern</h1>
      <WoodTopbar route={route} setRoute={setRoute} />
      <SplashScene route={route} setRoute={setRoute} />
    </main>
  );
}

function useHashRoute() {
  const normalize = () => {
    const raw = window.location.hash.replace(/^#\/?/, '');
    return ['home', 'agents', 'booth', 'contracts', 'admin', 'signin'].includes(raw) ? raw : 'home';
  };
  const [route, setRouteState] = useState(normalize);

  useEffect(() => {
    const onHash = () => setRouteState(normalize());
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);

  const setRoute = (nextRoute) => {
    window.location.hash = `/${nextRoute}`;
    setRouteState(nextRoute);
  };

  return [route, setRoute];
}

function WoodTopbar({ route, setRoute }) {
  const [open, setOpen] = useState(false);
  const nav = (
    <>
      {NAV_ROUTES.map((item, index) => (
        <button
          className={route === item.id ? 'active' : ''}
          key={`${item.id}-${item.label}-${index}`}
          onClick={() => {
            setRoute(item.id);
            setOpen(false);
          }}
          type="button"
        >
          {item.label}
        </button>
      ))}
      <button
        className={route === 'admin' ? 'active quartermaster-link' : 'quartermaster-link'}
        onClick={() => {
          setRoute('admin');
          setOpen(false);
        }}
        type="button"
      >
        Quartermaster
      </button>
    </>
  );

  return (
    <header className="wood-topbar">
      <button className="logo-plaque" onClick={() => setRoute('home')} type="button" aria-label="amerc home">
        <span className="logo-gem" aria-hidden="true" />
        <strong>amerc</strong>
        <small>agent mercenary</small>
        <em>v{RELEASE}</em>
      </button>
      <nav className="wood-nav" aria-label="Primary navigation">
        {nav}
      </nav>
      <button className="wood-menu" onClick={() => setOpen((value) => !value)} type="button" aria-label="Toggle menu">
        {open ? <X size={21} /> : <Menu size={21} />}
      </button>
      {open && <div className="wood-mobile">{nav}</div>}
    </header>
  );
}

function SplashScene({ route, setRoute }) {
  const mountRef = useRef(null);
  const [fallback, setFallback] = useState(false);

  useEffect(() => {
    const mount = mountRef.current;
    if (!mount) return undefined;

    let renderer;
    try {
      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: false,
        preserveDrawingBuffer: true,
        powerPreference: 'high-performance',
      });
    } catch {
      setFallback(true);
      return undefined;
    }

    setFallback(false);
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
    renderer.domElement.setAttribute('data-version', RELEASE);
    renderer.domElement.style.cursor = 'grab';
    renderer.domElement.style.touchAction = 'none';
    mount.appendChild(renderer.domElement);

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x020101);
    const camera = new THREE.PerspectiveCamera(35, 16 / 9, 0.1, 80);
    const target = new THREE.Vector3(0, -0.15, -2);

    const stage = new THREE.Group();
    scene.add(stage);

    const built = buildSplashScene(route);
    stage.add(built.root);

    const interactives = [built.dashboard, built.boothSign, built.speech];
    const tracked = {
      dashboard: built.dashboard,
      booth: built.boothSign,
      speech: built.speech,
      orc: built.orc,
      elf: built.elf,
      dwarf: built.dwarf,
    };

    const raycaster = new THREE.Raycaster();
    const pointer = new THREE.Vector2();
    const drag = { active: false, moved: false, startX: 0, startY: 0, pointerId: null };
    let hoverRoute = '';
    let frame = 0;
    let disposed = false;
    let layout = 'desktop';
    let currentYaw = 0;
    let currentPitch = 0;
    let targetYaw = 0;
    let targetPitch = 0;

    const metrics = () => ({
      assetMode: 'imagegen bitmap asset pack plus Three.js layered parallax and animated canvas UI',
      assetAudit: ASSET_AUDIT,
      idleAnimations: IDLE_ANIMATIONS,
      generatedAssets: [tavernBackdropUrl, orcBartenderUrl, elfGuideUrl, dwarfPatronUrl],
      dynamicFrames: {
        wantedList: built.dashboard.material.map?.userData?.frame || 0,
        speech: built.speech.material.map?.userData?.frame || 0,
        boothNeon: built.boothSign.material.map?.userData?.frame || 0,
      },
      rosterScroll: round(built.dashboard.material.map?.userData?.scroll || 0),
      rosterActiveAgent: built.dashboard.material.map?.userData?.activeAgent || WANTED_AGENTS[0].name,
      visualTarget: 'reference-matched polished fantasy sci-fi tavern splash art',
      spriteAssets: true,
      lowPoly: true,
      draggable: true,
    });

    const setLayout = () => {
      const rect = mount.getBoundingClientRect();
      const width = Math.max(1, Math.floor(rect.width || window.innerWidth || 1));
      const height = Math.max(1, Math.floor(rect.height || window.innerHeight || 1));
      const aspect = width / height;
      const portrait = height > width * 1.05;
      layout = width < 760 && portrait ? 'mobile' : width < 1080 || aspect < 1.22 ? 'tablet' : 'desktop';
      renderer.setSize(width, height, false);
      camera.aspect = aspect;

      if (layout === 'mobile') {
        camera.fov = 44;
        camera.position.set(0, 1.58, 7.45);
        target.set(0, -0.18, -1.95);
        stage.scale.setScalar(1.08);
        stage.position.set(0, 0.24, 0);
        placeMobile(built);
      } else if (layout === 'tablet') {
        camera.fov = 42;
        camera.position.set(0, 1.72, 8.55);
        target.set(0, -0.22, -2);
        stage.scale.setScalar(1);
        stage.position.set(0, -0.03, 0);
        placeTablet(built);
      } else {
        camera.fov = 38;
        camera.position.set(0, 1.56, 8.65);
        target.set(0, -0.18, -2.05);
        stage.scale.setScalar(1);
        stage.position.set(0, 0, 0);
        placeDesktop(built);
      }
      camera.updateProjectionMatrix();
      camera.lookAt(target);
      publishStageDebug({ width, height, layout, dragYaw: currentYaw, dragPitch: currentPitch, meshCount: countMeshes(stage), ...metrics() }, tracked, camera);
    };

    const resizeObserver = new ResizeObserver(setLayout);
    resizeObserver.observe(mount);
    setLayout();

    const pickRoute = (event) => {
      const rect = renderer.domElement.getBoundingClientRect();
      pointer.x = ((event.clientX - rect.left) / Math.max(rect.width, 1)) * 2 - 1;
      pointer.y = -((event.clientY - rect.top) / Math.max(rect.height, 1)) * 2 + 1;
      raycaster.setFromCamera(pointer, camera);
      const hit = raycaster.intersectObjects(interactives, false)[0];
      return hit?.object?.userData?.route || '';
    };

    const onPointerDown = (event) => {
      drag.active = true;
      drag.moved = false;
      drag.startX = event.clientX;
      drag.startY = event.clientY;
      drag.pointerId = event.pointerId;
      renderer.domElement.setPointerCapture?.(event.pointerId);
      renderer.domElement.style.cursor = 'grabbing';
    };

    const onPointerMove = (event) => {
      if (drag.active) {
        const dx = event.clientX - drag.startX;
        const dy = event.clientY - drag.startY;
        if (Math.hypot(dx, dy) > 4) drag.moved = true;
        targetYaw = clamp(dx * 0.0012, -0.16, 0.16);
        targetPitch = clamp(dy * 0.00075, -0.06, 0.06);
        return;
      }
      hoverRoute = pickRoute(event);
      renderer.domElement.style.cursor = hoverRoute ? 'pointer' : 'grab';
    };

    const onPointerUp = (event) => {
      renderer.domElement.releasePointerCapture?.(event.pointerId);
      drag.active = false;
      drag.pointerId = null;
      if (!drag.moved) {
        const nextRoute = pickRoute(event);
        if (nextRoute) setRoute(nextRoute);
      }
      hoverRoute = pickRoute(event);
      renderer.domElement.style.cursor = hoverRoute ? 'pointer' : 'grab';
    };

    const onPointerLeave = () => {
      if (!drag.active) {
        hoverRoute = '';
        renderer.domElement.style.cursor = 'grab';
      }
    };

    renderer.domElement.addEventListener('pointerdown', onPointerDown);
    renderer.domElement.addEventListener('pointermove', onPointerMove);
    renderer.domElement.addEventListener('pointerup', onPointerUp);
    renderer.domElement.addEventListener('pointercancel', onPointerUp);
    renderer.domElement.addEventListener('pointerleave', onPointerLeave);

    const start = performance.now();
    const animate = () => {
      if (disposed) return;
      frame = requestAnimationFrame(animate);
      const t = (performance.now() - start) / 1000;
      currentYaw += (targetYaw - currentYaw) * 0.12;
      currentPitch += (targetPitch - currentPitch) * 0.12;
      stage.rotation.y = currentYaw + Math.sin(t * 0.28) * 0.004;
      stage.rotation.x = currentPitch;

      animateSprites(built, t);
      built.updaters.forEach((update) => update(t));
      interactives.forEach((object) => {
        const base = object.userData.baseScale || object.scale;
        const targetScale = object.userData.route === hoverRoute ? 1.035 : 1;
        object.scale.lerp(new THREE.Vector3(base.x * targetScale, base.y * targetScale, base.z), 0.16);
      });

      if (Math.floor(t * 10) % 3 === 0) {
        publishStageDebug(
          { width: renderer.domElement.clientWidth, height: renderer.domElement.clientHeight, layout, dragYaw: currentYaw, dragPitch: currentPitch, meshCount: countMeshes(stage), ...metrics() },
          tracked,
          camera,
        );
      }
      renderer.render(scene, camera);
    };
    animate();

    return () => {
      disposed = true;
      resizeObserver.disconnect();
      renderer.domElement.removeEventListener('pointerdown', onPointerDown);
      renderer.domElement.removeEventListener('pointermove', onPointerMove);
      renderer.domElement.removeEventListener('pointerup', onPointerUp);
      renderer.domElement.removeEventListener('pointercancel', onPointerUp);
      renderer.domElement.removeEventListener('pointerleave', onPointerLeave);
      cancelAnimationFrame(frame);
      disposeScene(scene);
      renderer.dispose();
      if (renderer.domElement.parentNode === mount) mount.removeChild(renderer.domElement);
    };
  }, [route, setRoute]);

  return (
    <div className="tavern-stage gemhall-stage" ref={mountRef} aria-label="Draggable polished bitmap-asset AI mercenary tavern">
      {fallback && <div className="webgl-fallback" aria-hidden="true" />}
    </div>
  );
}

function buildSplashScene(route) {
  const root = new THREE.Group();
  const bg = makeImagePlane(tavernBackdropUrl, BACKDROP_ASPECT, { renderOrder: -20, transparent: false });
  bg.userData.asset = 'imagegen tavern background';
  root.add(bg);

  const title = makeCanvasPlane(makeTitleTexture(route).texture, { renderOrder: 24, depthTest: false });
  const wantedTexture = makeWantedTexture(route);
  const speechTexture = makeSpeechTexture(route);
  const boothTexture = makeBoothTexture();
  const dashboard = makeCanvasPlane(wantedTexture.texture, { renderOrder: 28, depthTest: true, route: 'agents' });
  const boothSign = makeCanvasPlane(boothTexture.texture, { renderOrder: 34, depthTest: true, route: 'booth' });
  const speech = makeCanvasPlane(speechTexture.texture, { renderOrder: 36, depthTest: true, route: route === 'signin' ? 'booth' : 'agents' });

  const dwarf = makeSpriteGroup(dwarfPatronUrl, DWARF_ASPECT, 'dwarf patron sprite');
  const orc = makeSpriteGroup(orcBartenderUrl, ORC_ASPECT, 'orc bartender sprite');
  const elf = makeSpriteGroup(elfGuideUrl, ELF_ASPECT, 'elf guide sprite');
  const particles = makeParticles();
  const boothRing = makeRingSet();

  root.add(dashboard, boothSign, speech, dwarf, orc, elf, particles, boothRing);
  return {
    root,
    bg,
    title,
    dashboard,
    boothSign,
    speech,
    dwarf,
    orc,
    elf,
    particles,
    boothRing,
    updaters: [wantedTexture.update, speechTexture.update, boothTexture.update],
  };
}

function makeImagePlane(url, aspect, options = {}) {
  const texture = new THREE.TextureLoader().load(url);
  prepareTexture(texture);
  const material = new THREE.MeshBasicMaterial({
    map: texture,
    transparent: options.transparent ?? true,
    depthTest: options.depthTest ?? true,
    depthWrite: options.depthWrite ?? false,
    alphaTest: options.transparent === false ? 0 : 0.02,
  });
  const mesh = new THREE.Mesh(new THREE.PlaneGeometry(aspect, 1), material);
  mesh.renderOrder = options.renderOrder ?? 0;
  return mesh;
}

function makeSpriteGroup(url, aspect, label) {
  const group = new THREE.Group();
  const sprite = makeImagePlane(url, aspect, { renderOrder: 20, transparent: true, depthWrite: false });
  sprite.userData.baseScale = sprite.scale.clone();
  group.add(sprite);
  group.userData.sprite = sprite;
  group.userData.asset = label;
  group.userData.baseY = 0;
  group.userData.baseRotY = 0;
  group.userData.baseScale = 1;
  return group;
}

function makeCanvasPlane(texture, { renderOrder = 10, route = '', depthTest = true } = {}) {
  const material = new THREE.MeshBasicMaterial({
    map: texture,
    transparent: true,
    depthTest,
    depthWrite: false,
    side: THREE.DoubleSide,
  });
  const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
  mesh.renderOrder = renderOrder;
  mesh.userData.route = route;
  mesh.userData.baseScale = mesh.scale.clone();
  return mesh;
}

function makeTitleTexture(route) {
  return makeAnimatedTexture(1240, 260, (ctx, width, height, time) => {
    const routeLabel = route === 'home' ? 'Hire agents like mercenaries.' : routeTitle(route);
    ctx.clearRect(0, 0, width, height);
    ctx.save();
    ctx.shadowColor = 'rgba(0,0,0,.68)';
    ctx.shadowBlur = 18;
    ctx.fillStyle = 'rgba(22, 8, 4, .78)';
    ctx.strokeStyle = 'rgba(255, 196, 93, .72)';
    ctx.lineWidth = 5;
    drawRound(ctx, 190, 36, width - 380, height - 70, 34);
    ctx.fill();
    ctx.stroke();
    ctx.restore();

    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.shadowColor = 'rgba(0,0,0,.7)';
    ctx.shadowBlur = 14;
    ctx.fillStyle = '#fff0d7';
    ctx.font = '900 48px Georgia, Times New Roman, serif';
    fitText(ctx, routeLabel, width / 2, 96, width - 500, 48);
    ctx.font = '900 21px Inter, Arial, sans-serif';
    ctx.fillStyle = '#ffc96e';
    fitText(ctx, 'live contracts / agent roster / private booth', width / 2, 150, width - 560, 21);
    ctx.fillStyle = `rgba(80, 225, 255, ${0.5 + Math.sin(time * 2.2) * 0.16})`;
    ctx.fillRect(390, 188, 460 + Math.sin(time * 0.9) * 60, 4);
    return {};
  });
}

function makeWantedTexture(route) {
  return makeAnimatedTexture(1220, 520, (ctx, width, height, time) => {
    const scroll = positiveModulo(time * 56, 118);
    const active = WANTED_AGENTS[Math.floor(time / 2.25) % WANTED_AGENTS.length];
    ctx.clearRect(0, 0, width, height);

    ctx.save();
    ctx.shadowColor = '#1fe7ff';
    ctx.shadowBlur = 24;
    ctx.fillStyle = 'rgba(5, 21, 36, .9)';
    ctx.strokeStyle = 'rgba(54, 226, 255, .92)';
    ctx.lineWidth = 8;
    drawRound(ctx, 22, 20, width - 44, height - 40, 34);
    ctx.fill();
    ctx.stroke();
    ctx.restore();

    ctx.strokeStyle = 'rgba(65, 230, 255, .12)';
    ctx.lineWidth = 1;
    for (let x = 60; x < width - 60; x += 48) {
      ctx.beginPath();
      ctx.moveTo(x, 36);
      ctx.lineTo(x, height - 42);
      ctx.stroke();
    }
    for (let y = 72; y < height - 48; y += 42) {
      ctx.beginPath();
      ctx.moveTo(44, y);
      ctx.lineTo(width - 44, y);
      ctx.stroke();
    }

    ctx.textAlign = 'left';
    ctx.textBaseline = 'middle';
    ctx.shadowColor = '#33eaff';
    ctx.shadowBlur = 18;
    ctx.fillStyle = '#bff8ff';
    ctx.font = '900 56px Georgia, Times New Roman, serif';
    fitText(ctx, 'BROWSE AGENTS', 64, 62, 520, 56, 'left');
    ctx.shadowBlur = 0;
    ctx.fillStyle = '#ffd36a';
    ctx.font = '900 23px Inter, Arial, sans-serif';
    fitText(ctx, 'WANTED LIST / SCOPED MERCENARIES', 68, 112, 570, 23, 'left');

    ctx.save();
    ctx.beginPath();
    drawRound(ctx, 52, 132, 674, 292, 20);
    ctx.clip();
    for (let i = -1; i < WANTED_AGENTS.length + 3; i += 1) {
      const agent = WANTED_AGENTS[positiveModulo(i, WANTED_AGENTS.length)];
      drawWantedRow(ctx, agent, 72, 144 + i * 86 - scroll, time + i);
    }
    ctx.restore();

    drawActiveAgent(ctx, active, 770, 126, 360, 285, time);

    for (let i = 0; i < 24; i += 1) {
      const x = 600 + Math.cos(time * 1.8 + i * 0.7) * (60 + (i % 5) * 16);
      const y = 258 + Math.sin(time * 2.1 + i * 1.3) * (38 + (i % 4) * 9);
      ctx.globalAlpha = 0.45 + (i % 3) * 0.14;
      ctx.fillStyle = '#ffc45f';
      ctx.beginPath();
      ctx.ellipse(x, y, 9, 14, time + i, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;

    return { scroll, activeAgent: active.name };
  });
}

function drawWantedRow(ctx, agent, x, y, time) {
  ctx.save();
  ctx.shadowColor = agent.tint;
  ctx.shadowBlur = 10;
  ctx.fillStyle = 'rgba(7, 31, 45, .82)';
  ctx.strokeStyle = agent.tint;
  ctx.lineWidth = 2;
  drawRound(ctx, x, y, 620, 58, 16);
  ctx.fill();
  ctx.stroke();

  ctx.fillStyle = 'rgba(0,0,0,.38)';
  ctx.beginPath();
  ctx.arc(x + 34, y + 29, 20, 0, Math.PI * 2);
  ctx.fill();
  ctx.stroke();
  ctx.fillStyle = agent.tint;
  ctx.font = '900 15px Inter, Arial, sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText(agent.initials, x + 34, y + 29);

  ctx.textAlign = 'left';
  ctx.shadowBlur = 0;
  ctx.fillStyle = '#f2fbff';
  ctx.font = '900 20px Georgia, Times New Roman, serif';
  fitText(ctx, agent.name, x + 74, y + 21, 250, 20, 'left');
  ctx.fillStyle = '#b8d7df';
  ctx.font = '800 14px Inter, Arial, sans-serif';
  fitText(ctx, agent.role, x + 74, y + 42, 250, 14, 'left');
  ctx.fillStyle = '#c4f3ff';
  fitText(ctx, agent.skill, x + 395, y + 27, 130, 14, 'left');
  ctx.fillStyle = '#ffd36a';
  ctx.font = '900 18px Inter, Arial, sans-serif';
  fitText(ctx, agent.bounty, x + 545, y + 28 + Math.sin(time * 2) * 1.5, 70, 18, 'left');
  ctx.restore();
}

function drawActiveAgent(ctx, agent, x, y, width, height, time) {
  ctx.save();
  ctx.shadowColor = agent.tint;
  ctx.shadowBlur = 24;
  ctx.fillStyle = 'rgba(4, 20, 32, .86)';
  ctx.strokeStyle = agent.tint;
  ctx.lineWidth = 5;
  drawRound(ctx, x, y, width, height, 26);
  ctx.fill();
  ctx.stroke();
  ctx.translate(x + width / 2, y + 86);
  ctx.rotate(Math.sin(time * 0.9) * 0.08);
  ctx.strokeStyle = agent.tint;
  ctx.lineWidth = 7;
  drawHex(ctx, 0, 0, 56 + Math.sin(time * 3) * 5);
  ctx.stroke();
  ctx.fillStyle = agent.tint;
  ctx.font = '900 35px Inter, Arial, sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText(agent.initials, 0, 7);
  ctx.setTransform(1, 0, 0, 1, 0, 0);
  ctx.shadowBlur = 0;
  ctx.textAlign = 'center';
  ctx.fillStyle = '#f3fbff';
  ctx.font = '900 27px Georgia, Times New Roman, serif';
  fitText(ctx, agent.name, x + width / 2, y + 176, width - 60, 27);
  ctx.fillStyle = '#c8f7ff';
  ctx.font = '800 18px Inter, Arial, sans-serif';
  fitText(ctx, agent.role, x + width / 2, y + 210, width - 70, 18);
  ctx.fillStyle = '#ffd36a';
  ctx.font = '900 22px Inter, Arial, sans-serif';
  fitText(ctx, `Bounty ${agent.bounty}`, x + width / 2, y + 250, width - 80, 22);
  ctx.restore();
}

function makeSpeechTexture(route) {
  const lines = SPEECH_STEPS[route] || SPEECH_STEPS.home;
  return makeAnimatedTexture(820, 520, (ctx, width, height, time) => {
    const active = Math.floor(time / 1.45) % lines.length;
    const pulse = 0.74 + Math.sin(time * 3.8) * 0.14;
    ctx.clearRect(0, 0, width, height);
    ctx.save();
    ctx.shadowColor = '#35dfff';
    ctx.shadowBlur = 24 * pulse;
    ctx.fillStyle = 'rgba(3, 22, 39, .88)';
    ctx.strokeStyle = `rgba(58, 222, 255, ${0.72 + pulse * 0.2})`;
    ctx.lineWidth = 7;
    drawRound(ctx, 28, 26, width - 80, height - 92, 28);
    ctx.fill();
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(width - 150, height - 72);
    ctx.lineTo(width - 44, height - 24);
    ctx.lineTo(width - 104, height - 118);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();
    ctx.restore();

    ctx.textAlign = 'left';
    ctx.textBaseline = 'middle';
    ctx.fillStyle = '#bff8ff';
    ctx.font = '900 34px Georgia, Times New Roman, serif';
    fitText(ctx, 'Velvet Hex offers', 70, 76, width - 160, 34, 'left');
    ctx.font = '900 27px Inter, Arial, sans-serif';
    lines.forEach((line, index) => {
      const y = 145 + index * 58;
      const selected = index === active;
      ctx.globalAlpha = selected ? 1 : 0.44;
      ctx.strokeStyle = selected ? '#5ee8ff' : 'rgba(94,232,255,.6)';
      ctx.lineWidth = selected ? 5 : 3;
      ctx.beginPath();
      ctx.arc(78, y, selected ? 14 + Math.sin(time * 6) * 2 : 9, 0, Math.PI * 2);
      ctx.stroke();
      ctx.fillStyle = selected ? '#5ee8ff' : '#c9f5ff';
      fitText(ctx, line, 112, y, width - 190, 27, 'left');
    });
    ctx.globalAlpha = 1;
    return {};
  });
}

function makeBoothTexture() {
  return makeAnimatedTexture(980, 250, (ctx, width, height, time) => {
    const flicker = 0.72 + Math.sin(time * 9.5) * 0.08 + (Math.sin(time * 27) > 0.94 ? 0.18 : 0);
    ctx.clearRect(0, 0, width, height);
    ctx.save();
    ctx.shadowColor = '#24e6ff';
    ctx.shadowBlur = 28 * flicker;
    ctx.fillStyle = 'rgba(1, 19, 32, .82)';
    ctx.strokeStyle = `rgba(44, 230, 255, ${0.75 * flicker})`;
    ctx.lineWidth = 8;
    drawRound(ctx, 38, 38, width - 76, height - 76, 28);
    ctx.fill();
    ctx.stroke();
    ctx.font = '900 66px Georgia, Times New Roman, serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.strokeStyle = `rgba(38, 225, 255, ${0.95 * flicker})`;
    ctx.lineWidth = 7;
    ctx.strokeText('Enter My Booth', width / 2, height / 2 - 6, width - 130);
    ctx.fillStyle = `rgba(232, 255, 255, ${0.96 * flicker})`;
    ctx.fillText('Enter My Booth', width / 2, height / 2 - 6, width - 130);
    ctx.fillStyle = `rgba(255, 210, 107, ${0.84 * flicker})`;
    ctx.font = '900 18px Inter, Arial, sans-serif';
    fitText(ctx, 'private agent command booth', width / 2, height - 60, width - 180, 18);
    ctx.fillStyle = `rgba(49, 229, 255, ${0.26 * flicker})`;
    ctx.fillRect(74 + positiveModulo(time * 180, width - 230), 54, 120, 5);
    ctx.restore();
    return {};
  });
}

function makeParticles() {
  const group = new THREE.Group();
  const material = new THREE.MeshBasicMaterial({ color: 0xffc36e, transparent: true, opacity: 0.85 });
  for (let i = 0; i < 48; i += 1) {
    const dot = new THREE.Mesh(new THREE.CircleGeometry(0.012 + (i % 3) * 0.004, 6), material.clone());
    dot.position.set(-5.8 + ((i * 1.83) % 11.6), -1.35 + ((i * 0.43) % 4.1), -0.4 - (i % 5) * 0.34);
    dot.userData.seed = i * 0.47;
    dot.renderOrder = 38;
    group.add(dot);
  }
  return group;
}

function makeRingSet() {
  const group = new THREE.Group();
  const material = new THREE.MeshBasicMaterial({ color: 0x1ee6ff, transparent: true, opacity: 0.62, side: THREE.DoubleSide, depthWrite: false });
  [0.56, 0.78, 1.0].forEach((radius) => {
    const ring = new THREE.Mesh(new THREE.RingGeometry(radius, radius + 0.012, 96), material.clone());
    ring.rotation.x = -Math.PI / 2;
    ring.renderOrder = 31;
    group.add(ring);
  });
  return group;
}

function animateSprites(built, time) {
  const orc = built.orc;
  orc.position.y = orc.userData.baseY + Math.sin(time * 1.15) * 0.035;
  orc.rotation.z = Math.sin(time * 0.8) * 0.006;
  orc.scale.setScalar(orc.userData.baseScale * (1 + Math.sin(time * 1.25) * 0.012));

  const elf = built.elf;
  elf.position.y = elf.userData.baseY + Math.sin(time * 1.45 + 0.9) * 0.045;
  elf.rotation.z = Math.sin(time * 1.1) * 0.01;
  elf.scale.setScalar(elf.userData.baseScale * (1 + Math.sin(time * 1.8) * 0.008));

  const dwarf = built.dwarf;
  dwarf.position.y = dwarf.userData.baseY + Math.sin(time * 1.8 + 1.9) * 0.028;
  dwarf.rotation.z = Math.sin(time * 1.6) * 0.012;
  dwarf.scale.setScalar(dwarf.userData.baseScale * (1 + Math.sin(time * 2.0 + 0.5) * 0.01));

  built.boothRing.rotation.z = time * 0.12;
  built.boothRing.children.forEach((ring, index) => {
    ring.material.opacity = 0.32 + Math.sin(time * 2.4 + index * 0.8) * 0.14;
  });

  built.particles.children.forEach((dot) => {
    const seed = dot.userData.seed || 0;
    dot.position.y += Math.sin(time * 1.3 + seed) * 0.00055;
    dot.material.opacity = 0.42 + Math.sin(time * 2.2 + seed) * 0.28;
  });
}

function placeDesktop(built) {
  placePlane(built.bg, 14.4, 14.4 / BACKDROP_ASPECT, 0, -0.18, -4.3, 0);
  placePlane(built.title, 4.45, 0.72, -0.32, 1.22, -1.62, 0);
  placePlane(built.dashboard, 4.55, 4.55 / SCREEN_ASPECT, -0.36, 0.43, -1.54, -0.02);
  placePlane(built.boothSign, 2.8, 2.8 / BOOTH_ASPECT, -0.05, -1.68, 0.1, 0);
  placePlane(built.speech, 1.88, 1.88 / SPEECH_ASPECT, 2.92, 0.04, 0.32, -0.1);
  placeSprite(built.orc, 2.34, 2.34 / ORC_ASPECT, 0.02, -1.38, 0.26, 0.02, 1);
  placeSprite(built.elf, 1.46, 1.46 / ELF_ASPECT, 3.04, -1.42, 0.5, -0.12, 1);
  placeSprite(built.dwarf, 1.18, 1.18 / DWARF_ASPECT, -3.04, -1.46, 0.58, 0.14, 1);
  placeFlat(built.boothRing, -0.1, -2, 0.9, 1.05, 0);
}

function placeTablet(built) {
  placePlane(built.bg, 13.0, 13.0 / BACKDROP_ASPECT, 0, -0.18, -4.1, 0);
  placePlane(built.title, 3.7, 0.62, 0, 1.08, -1.35, 0);
  placePlane(built.dashboard, 3.85, 3.85 / SCREEN_ASPECT, -0.22, 0.34, -1.48, -0.02);
  placePlane(built.boothSign, 2.22, 2.22 / BOOTH_ASPECT, -0.06, -1.58, 0.08, 0);
  placePlane(built.speech, 1.46, 1.46 / SPEECH_ASPECT, 1.88, -0.2, 0.24, -0.1);
  placeSprite(built.orc, 1.95, 1.95 / ORC_ASPECT, 0, -1.34, 0.28, 0, 1);
  placeSprite(built.elf, 1.22, 1.22 / ELF_ASPECT, 2.05, -1.52, 0.46, -0.1, 1);
  placeSprite(built.dwarf, 0.98, 0.98 / DWARF_ASPECT, -2.02, -1.54, 0.58, 0.1, 1);
  placeFlat(built.boothRing, -0.08, -1.92, 0.92, 0.92, 0);
}

function placeMobile(built) {
  placePlane(built.bg, 10.7, 10.7 / BACKDROP_ASPECT, 0, 0.16, -4.0, 0);
  placePlane(built.title, 2.8, 0.48, 0, 1.02, -1.24, 0);
  placePlane(built.dashboard, 3.05, 3.05 / SCREEN_ASPECT, 0, 0.5, -1.42, 0);
  placePlane(built.boothSign, 1.84, 1.84 / BOOTH_ASPECT, -0.12, -1.24, 0.2, 0);
  placePlane(built.speech, 1.08, 1.08 / SPEECH_ASPECT, 0.58, -0.44, 0.38, -0.06);
  placeSprite(built.orc, 1.68, 1.68 / ORC_ASPECT, -0.18, -1.0, 0.36, 0, 1);
  placeSprite(built.elf, 0.84, 0.84 / ELF_ASPECT, 0.84, -1.22, 0.54, -0.06, 1);
  placeSprite(built.dwarf, 0.7, 0.7 / DWARF_ASPECT, -0.86, -1.22, 0.62, 0.06, 1);
  placeFlat(built.boothRing, -0.2, -1.5, 0.86, 0.68, 0);
}

function placePlane(mesh, width, height, x, y, z, rotY = 0) {
  mesh.scale.set(width, height, 1);
  mesh.position.set(x, y, z);
  mesh.rotation.set(0, rotY, 0);
  mesh.userData.baseScale = mesh.scale.clone();
}

function placeSprite(group, width, height, x, y, z, rotY = 0, baseScale = 1) {
  const sprite = group.userData.sprite;
  sprite.scale.set(width, height, 1);
  group.position.set(x, y, z);
  group.rotation.set(0, rotY, 0);
  group.userData.baseY = y;
  group.userData.baseRotY = rotY;
  group.userData.baseScale = baseScale;
}

function placeFlat(group, x, y, z, scale, rotY) {
  group.position.set(x, y, z);
  group.scale.setScalar(scale);
  group.rotation.set(0, rotY, 0);
}

function makeAnimatedTexture(width, height, draw) {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  const texture = new THREE.CanvasTexture(canvas);
  prepareTexture(texture);
  const update = (time = 0) => {
    const data = draw(ctx, width, height, time) || {};
    texture.userData.frame = (texture.userData.frame || 0) + 1;
    Object.assign(texture.userData, data);
    texture.needsUpdate = true;
  };
  update(0);
  return { texture, update };
}

function prepareTexture(texture) {
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.anisotropy = 8;
}

function drawRound(ctx, x, y, width, height, radius) {
  ctx.beginPath();
  ctx.moveTo(x + radius, y);
  ctx.lineTo(x + width - radius, y);
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
  ctx.lineTo(x + width, y + height - radius);
  ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
  ctx.lineTo(x + radius, y + height);
  ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
  ctx.lineTo(x, y + radius);
  ctx.quadraticCurveTo(x, y, x + radius, y);
  ctx.closePath();
}

function drawHex(ctx, x, y, radius) {
  ctx.beginPath();
  for (let i = 0; i < 6; i += 1) {
    const angle = Math.PI / 6 + i * Math.PI / 3;
    const px = x + Math.cos(angle) * radius;
    const py = y + Math.sin(angle) * radius;
    if (i === 0) ctx.moveTo(px, py);
    else ctx.lineTo(px, py);
  }
  ctx.closePath();
}

function fitText(ctx, text, x, y, maxWidth, size, align = 'center') {
  let fontSize = size;
  const family = ctx.font.replace(/^[^ ]+ /, '');
  ctx.textAlign = align;
  while (fontSize > 9) {
    ctx.font = `${fontSize}px ${family}`;
    if (ctx.measureText(text).width <= maxWidth) break;
    fontSize -= 1;
  }
  ctx.fillText(text, x, y);
}

function routeTitle(route) {
  return {
    agents: 'Browse agent mercenaries.',
    booth: 'Enter your private booth.',
    contracts: 'Contract every run.',
    signin: 'Claim a tavern pass.',
    admin: 'Quartermaster control.',
  }[route] || 'Hire agents like mercenaries.';
}

function positiveModulo(value, modulo) {
  return ((value % modulo) + modulo) % modulo;
}

function clamp(value, min, max) {
  return Math.max(min, Math.min(max, value));
}

function round(value) {
  return Math.round(value * 1000) / 1000;
}

function countMeshes(root) {
  let count = 0;
  root.traverse((child) => {
    if (child.isMesh || child.isPoints) count += 1;
  });
  return count;
}

function publishStageDebug(metrics, tracked, camera) {
  if (typeof window === 'undefined') return;
  const placements = Object.fromEntries(
    Object.entries(tracked).map(([name, object]) => {
      const bounds = projectedBounds(object, camera);
      const visible = bounds.left >= -1.08 && bounds.right <= 1.08 && bounds.bottom >= -1.08 && bounds.top <= 1.08;
      return [name, { bounds, visible }];
    }),
  );
  window.__AMERC_STAGE = {
    version: RELEASE,
    release: RELEASE,
    ...metrics,
    placements,
    allVisible: Object.values(placements).every((item) => item.visible),
  };
}

function projectedBounds(object, camera) {
  object.updateWorldMatrix(true, true);
  const box = new THREE.Box3().setFromObject(object);
  if (box.isEmpty()) return { left: 0, right: 0, top: 0, bottom: 0 };
  const points = [
    new THREE.Vector3(box.min.x, box.min.y, box.min.z),
    new THREE.Vector3(box.min.x, box.min.y, box.max.z),
    new THREE.Vector3(box.min.x, box.max.y, box.min.z),
    new THREE.Vector3(box.min.x, box.max.y, box.max.z),
    new THREE.Vector3(box.max.x, box.min.y, box.min.z),
    new THREE.Vector3(box.max.x, box.min.y, box.max.z),
    new THREE.Vector3(box.max.x, box.max.y, box.min.z),
    new THREE.Vector3(box.max.x, box.max.y, box.max.z),
  ].map((point) => point.project(camera));
  return {
    left: round(Math.min(...points.map((point) => point.x))),
    right: round(Math.max(...points.map((point) => point.x))),
    top: round(Math.max(...points.map((point) => point.y))),
    bottom: round(Math.min(...points.map((point) => point.y))),
  };
}

function disposeScene(scene) {
  scene.traverse((object) => {
    if (object.geometry) object.geometry.dispose();
    const materials = Array.isArray(object.material) ? object.material : [object.material];
    materials.filter(Boolean).forEach((material) => {
      if (material.map) material.map.dispose();
      material.dispose?.();
    });
  });
}
