// boot.js (Lane F) — the shell. Creates renderer/scene/loop, picks the world (real or
// stub), constructs assets + player + combat, pumps level events, hosts DBG.
// Lanes: request wiring via NOTES, don't edit here. Round 2: A/B/C/D snippets integrated.

import * as THREE from 'three';
import { parseFlags } from './core/flags.js';
import { createRng } from './core/rng.js';
import { createBus } from './core/bus.js';
import { createAssets } from './core/assets.js';
import { createWorld as createStubWorld, STUB_LEVEL } from './stub/world_stub.js';
import { createPlayer } from './flight/player.js';
import { createCombat } from './combat/index.js';

const flags = parseFlags();
const bus = createBus();

const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.getElementById('game').appendChild(renderer.domElement);

const assets = await createAssets({ flags, renderer });

async function loadLevel(id) {
  try {
    const mod = await import('./levels/index.js');           // Lane C's registry
    return mod.getLevel(id);                                 // default level lives there
  } catch (e) {
    console.info('[boot] Lane C registry not available —', e.message);
    return null;
  }
}

async function pickWorld() {
  const level = (await loadLevel(flags.lvl)) ?? STUB_LEVEL;
  const rng = createRng(flags.seed ?? level.seed);
  if (!flags.stub) {
    try {
      const mod = await import('./world/index.js');          // Lane A's world
      return mod.createWorld(level, { rng, assets });
    } catch (e) {
      console.info('[boot] Lane A world not available, using stub —', e.message);
    }
  }
  return createStubWorld(level, { rng, assets });            // ?stub=1&lvl=<id> composes (beat 1 only — stub is single-segment)
}

const world = await pickWorld();
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 600);
scene.add(world.group);
scene.background = new THREE.Color(world.biomeAt(0).palette.void);

// --- player + combat (Lane B). ?fly=1 keeps the noclip/drift camera instead. ---
const rng = createRng(flags.seed ?? world.level?.seed ?? 0);
const player = flags.fly ? null
  : createPlayer({ scene, world, bus, rng, flags, camera, dom: renderer.domElement });
const combat = player ? createCombat({ scene, world, bus, rng, flags, player }) : null;

// --- UI (Lane E). Constructed here because #ui needs an owner and E is bus-only: it never
// sees player/combat, just the events they emit. After combat so every subscription exists
// before the first state emit; skipped under ?fly=1, which has no player to instrument.
let ui = null;
if (player) {
  try {
    const [hudMod, commsMod, fbMod, cardsMod, audioMod] = await Promise.all([
      import('./ui/hud.js'), import('./ui/comms.js'), import('./ui/feedback.js'),
      import('./ui/cards.js'), import('./audio/engine.js'),
    ]);
    const args = { bus, flags, assets, level: world.level, world };
    // Each module is constructed independently: one that throws must not cost us the rest.
    // A missing score readout is a blemish; a UI layer that fails to build in a game whose
    // only other feedback is the wall rushing past is an unplayable build.
    const parts = {};
    for (const [key, make] of [
      ['hud', () => hudMod.createHUD({ bus, flags, level: world.level })],
      ['comms', () => commsMod.createComms({ bus, flags })],
      ['feedback', () => fbMod.createFeedback(args)],
      ['cards', () => cardsMod.createCards(args)],
      ['audio', () => audioMod.createAudio(args)],
    ]) {
      try { parts[key] = make(); } catch (e) { console.info(`[boot] Lane E ${key} failed —`, e.message); }
    }
    const ticking = Object.values(parts).filter((p) => typeof p?.update === 'function');
    ui = {
      ...parts,
      // The HUD is bus-driven and needs no tick; the audio heartbeat, the corruption decay
      // and the card animations do. Called from frame(), never from step(): step() is the
      // deterministic sim that stepped-sim harnesses drive thousands of times a second, and
      // it must not do DOM or WebAudio work.
      update(dt) { for (const p of ticking) p.update(dt); },
      dispose() { for (const p of Object.values(parts)) { try { p?.dispose?.(); } catch { /* keep going */ } } },
    };
  } catch (e) {
    console.info('[boot] Lane E UI not available —', e.message);
  }
}

// Pause. cards.js raises ui:pause; somebody has to honour it or the button is a lie (its
// reviewer flagged exactly that, and it is boot's to fix — E cannot reach the loop).
// Gated in frame(), NOT in step(), so a paused game still renders and animates its overlay
// while the simulation holds still — and so stepped sims, which call step() directly, are
// unaffected by a UI state they have no business knowing about.
let paused = false;
bus.on('ui:pause', (e) => { paused = !!e?.paused; });

// --- level-event pump (owned by boot per round-2 ruling): emits C's events as the
// player crosses their s. Each event fires ONCE per run — respawn does not rewind the pump
// (no double-spawns, collected pickups stay collected); hazards re-arm via combat.reset(),
// which is the one replay a death actually needs (the thing that killed you works again).
let _evts = [...(world.level?.events ?? [])].sort((a, b) => a.s - b.s), _ei = 0;
const pumpTo = (s) => { while (_ei < _evts.length && s >= _evts[_ei].s) bus.emit('level:event', _evts[_ei++]); };

// --- run state: checkpoint -> death -> respawn, gate -> level:complete ------------------
// C's authoring laws carry this: checkpoints sit outside hazard zones and <=30s apart, so
// "respawn at the last checkpoint crossed" is safe by construction. Stats accumulate across
// deaths (a run's story includes its deaths); E renders the medal card off level:complete.
const run = { t: 0, deaths: 0, checkpointS: player?.state.s ?? 0, respawnT: 0, done: false };
bus.on('level:event', (ev) => {
  if (ev.type === 'checkpoint') {
    run.checkpointS = ev.s;
    bus.emit('audio:cue', { name: 'checkpoint' });
  } else if (ev.type === 'gate' && !run.done) {
    run.done = true;
    bus.emit('level:complete', {
      id: world.level?.id ?? null, to: ev.to ?? null, gate: ev.id ?? null,
      stats: {
        time: run.t, deaths: run.deaths,
        score: combat?.score.value ?? 0, kills: combat?.score.kills ?? 0,
        samples: combat?.score.samples ?? 0,
      },
      par: world.level?.par ?? null,
    });
  }
});
bus.on('player:death', () => { run.deaths++; run.respawnT = 2.0; });   // 2s of feed-drop, then back
function updateRun(dt) {
  if (!run.done) run.t += dt;
  if (run.respawnT > 0 && player) {
    run.respawnT -= dt;
    if (run.respawnT <= 0) {
      combat?.reset(run.checkpointS);          // re-arm hazards at/ahead of the checkpoint
      player.respawn(run.checkpointS);
    }
  }
}

// --- debug drift/fly camera (?fly=1, or fallback when B's player is absent) ---
let sCam = 5, look = { yaw: 0, pitch: 0 }, dragging = false, speed = world.biomeAt(0).flow * 0.6;
const keys = new Set();
addEventListener('keydown', (e) => keys.add(e.code));
addEventListener('keyup', (e) => keys.delete(e.code));
addEventListener('mousedown', () => (dragging = true));
addEventListener('mouseup', () => (dragging = false));
addEventListener('mousemove', (e) => {
  if (!dragging || player) return;
  look.yaw -= e.movementX * 0.003;
  look.pitch = THREE.MathUtils.clamp(look.pitch - e.movementY * 0.003, -1.2, 1.2);
});

function updateCamera(dt) {
  if (flags.fly) {
    if (keys.has('KeyW')) sCam += speed * 2 * dt;
    if (keys.has('KeyS')) sCam -= speed * 2 * dt;
  } else {
    sCam += speed * dt;                                        // title-screen auto-drift
  }
  sCam = ((sCam % world.length) + world.length) % world.length;
  const f = world.sample(sCam), ahead = world.sample(Math.min(sCam + 12, world.length));
  camera.position.copy(f.pos).addScaledVector(f.nor, -f.radius * 0.25);
  const target = ahead.pos.clone()
    .addScaledVector(f.bin, Math.sin(look.yaw) * 8)
    .addScaledVector(f.nor, Math.sin(look.pitch) * 8);
  camera.up.copy(f.nor);
  camera.lookAt(target);
}

// --- DBG (?dbg=1) — cite these numbers in NOTES; DBG.shot('name') saves a frame ---
const dbgEl = document.getElementById('dbg');
let frames = 0, fpsT = 0, fps = 0;
renderer.info.autoReset = false;                 // three resets info at render end — cache per-frame instead
const stats = { draws: 0, tris: 0 };
window.DBG = {
  get draws() { return stats.draws; },
  get tris() { return stats.tris; },
  get fps() { return fps; },
  world, player, combat, assets, ui,
  shot(name = 'shot') {
    const a = document.createElement('a');
    a.download = `${name}.png`;
    a.href = renderer.domElement.toDataURL('image/png');
    a.click();
  },
};

addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

// One deterministic simulation step — the whole game minus rendering. Exported so the
// stepped-sim harness (f-progress documents it; rAF never fires in a hidden pane) exercises
// the REAL loop — pump, run state and all — instead of a hand-rolled copy that drifts.
function step(dt) {
  if (player) {
    player.update(dt);          // must run first: combat hit-tests this frame's ship position
    pumpTo(player.state.s);
    combat.update(dt);
    updateRun(dt);
    world.update(dt, player.state.s);
  } else {
    updateCamera(dt);           // ?fly=1 noclip / title drift
    world.update(dt, sCam);
  }
}

let last = performance.now();
function frame(now) {
  const dt = Math.min((now - last) / 1000, 0.05);
  last = now;
  if (!paused) step(dt);
  ui?.update?.(dt);            // overlay keeps animating while paused — the pause card moves
  renderer.render(scene, camera);
  stats.draws = renderer.info.render.calls;
  stats.tris = renderer.info.render.triangles;
  renderer.info.reset();
  frames++; fpsT += dt;
  if (fpsT >= 0.5) {
    fps = Math.round(frames / fpsT); frames = 0; fpsT = 0;
    if (flags.dbg && !flags.shots) {
      const s = player?.state.s ?? sCam;
      dbgEl.textContent = `${fps} fps · ${DBG.draws} draws · ${(DBG.tris / 1000) | 0}k tris · s=${s.toFixed(0)} · ${world.level?.id ?? '?'} ${world.hash?.() ?? ''}`;
    }
  }
  requestAnimationFrame(frame);
}
if (flags.dbg && !flags.shots) dbgEl.style.display = 'block';
requestAnimationFrame(frame);

export { scene, camera, renderer, bus, flags, world, player, combat, assets, ui, step };
