// core/assets.js (Lane D) — the manifest loader. Implements the asset contract in docs/TECH.md.
//
// THE LAW THIS FILE EXISTS TO ENFORCE: every asset is optional at runtime. Nothing in here
// throws, ever. A missing manifest, a 404, a corrupt WebP, a box with no network — all resolve
// to `null`, and the consumer draws its procedural fallback. GUTS must be fully playable with
// an empty assets/gen/, and `?localassets=0` exists so you can prove it on demand.
//
// Usage (boot constructs it once and passes it to every lane factory):
//   const assets = await createAssets({ flags, renderer });
//   const e = assets.get('textures', 'wall_esophagus_a');   // raw manifest entry | null
//   const t = assets.texture('wall_esophagus_a');           // {map, normalMap, tile} | null
//   if (t) mat.uniforms.uDetail.value = t.map;              // else: keep the procedural path

import * as THREE from 'three';

const MANIFEST_URL = new URL('../../assets/manifest.json', import.meta.url);
const ASSETS_BASE = new URL('../../assets/', import.meta.url);
// The 3GOD depot (house shared mesh store). `depot:foo.glb` in a manifest resolves here.
// Nothing SHIPPED may use it — the game must run offline (README law) — but it lets Lane D
// point at depot meshes while prototyping without copying them into the repo.
const DEPOT_BASE = 'https://digalot.fyi/3god/';

const EMPTY = { textures: {}, matcaps: {}, models: {}, audio: { beds: {}, sfx: {} } };

/** `depot:x` -> depot URL; anything else -> resolved against web/assets/. */
function resolve(url) {
  if (typeof url !== 'string' || !url) return null;
  if (url.startsWith('depot:')) return DEPOT_BASE + url.slice(6);
  if (/^https?:\/\//.test(url)) return url;
  return new URL(url, ASSETS_BASE).href;
}

export async function createAssets({ flags = {}, renderer = null } = {}) {
  const loader = new THREE.TextureLoader();
  const cache = new Map();               // url -> THREE.Texture (one GPU upload per file)
  const maxAniso = renderer?.capabilities?.getMaxAnisotropy?.() ?? 1;
  let manifest = EMPTY;

  // ?localassets=0 => pretend the manifest is empty. This is the fallback-path test switch:
  // if the game looks broken with it on, someone has violated the optional-asset law.
  if (flags.localassets === false) {
    console.info('[assets] ?localassets=0 — empty manifest, procedural fallbacks only');
  } else {
    try {
      const r = await fetch(MANIFEST_URL, { cache: 'no-cache' });
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      manifest = { ...EMPTY, ...(await r.json()) };
    } catch (e) {
      // Not an error condition — an asset-free checkout is a supported way to run GUTS.
      console.info('[assets] no manifest, running asset-free —', e.message);
      manifest = EMPTY;
    }
  }

  function loadTexture(url, { srgb = true, repeat = null } = {}) {
    const abs = resolve(url);
    if (!abs) return null;
    if (cache.has(abs)) return cache.get(abs);
    let tex;
    try {
      // TextureLoader is async under the hood; a decode failure lands in onError, where we
      // just mark the texture dead rather than letting it reject into nothing.
      tex = loader.load(abs, undefined, undefined, () => {
        console.info(`[assets] failed to decode ${url} — consumer should fall back`);
        tex.userData.failed = true;
      });
    } catch (e) {
      console.info(`[assets] failed to load ${url} —`, e.message);
      return null;
    }
    tex.colorSpace = srgb ? THREE.SRGBColorSpace : THREE.NoColorSpace;
    tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
    tex.anisotropy = maxAniso;
    if (repeat) tex.repeat.set(repeat[0], repeat[1]);
    cache.set(abs, tex);
    return tex;
  }

  const api = {
    manifest,

    /** THE contract (TECH.md): raw manifest entry, or null on any miss. Never throws. */
    get(category, name) {
      const cat = manifest?.[category];
      if (!cat) return null;
      if (category === 'audio') return null;       // audio is nested; use audioUrl()
      return cat[name] ?? null;
    },

    /**
     * Wall texture ready to plug into a material, or null.
     * `tile` = [repeats around theta, units of s per repeat] (LANE_D_NOTES §tiling).
     * The stub/world UV is (theta/2pi, s-in-units), so repeat = (tile[0], 1/tile[1])
     * is pre-applied here — Lane A shouldn't have to rediscover the convention.
     */
    texture(name) {
      const e = api.get('textures', name);
      if (!e) return null;
      const tile = Array.isArray(e.tile) && e.tile.length === 2 ? e.tile : [4, 16];
      const repeat = [tile[0], 1 / tile[1]];
      const map = loadTexture(e.url, { srgb: true, repeat });
      if (!map) return null;
      return {
        map,
        normalMap: e.normal ? loadTexture(e.normal, { srgb: false, repeat }) : null,
        tile,
        repeat,
      };
    },

    /** Matcap texture (clamped, non-tiling), or null. */
    matcap(name) {
      const e = api.get('matcaps', name);
      if (!e) return null;
      const t = loadTexture(e.url, { srgb: true });
      if (t) t.wrapS = t.wrapT = THREE.ClampToEdgeWrapping;
      return t;
    },

    /** Model manifest entry (url resolved), or null. Lane D ships GLBs from round 2. */
    model(name) {
      const e = api.get('models', name);
      if (!e) return null;
      return { ...e, url: resolve(e.url) };
    },

    /**
     * Best playable audio URL for this browser, or null.
     * cat: 'beds' | 'sfx'. Safari can't do Opus-in-Ogg, so we dual-ship and probe.
     */
    audioUrl(cat, name) {
      const e = manifest?.audio?.[cat]?.[name];
      if (!e) return null;
      const probe = document.createElement('audio');
      const canOgg = !!probe.canPlayType('audio/ogg; codecs="opus"');
      const pick = (canOgg && e.ogg) ? e.ogg : (e.m4a || e.ogg);
      return pick ? resolve(pick) : null;
    },

    /** Raw audio entry ({ogg, m4a, loop, gain, seconds}) or null — for Lane E's engine. */
    audio(cat, name) {
      return manifest?.audio?.[cat]?.[name] ?? null;
    },

    /** Names present in a category — lets a lane iterate what actually shipped. */
    list(category) {
      if (category === 'audio') return Object.keys(manifest?.audio ?? {});
      return Object.keys(manifest?.[category] ?? {});
    },

    dispose() {
      for (const t of cache.values()) t.dispose();
      cache.clear();
    },
  };
  return api;
}
