/* ============================================================
   $GCA live engine.

   Central tick clock that drives:
   - rank deltas (suspects climb / fall live)
   - market cap + volume drift
   - BOLO feed (new wanted notices roll in)
   - claimed nations (new crews claim turf)
   - live-match clock (minute counter + score events)

   Plus utilities:
   - useLive()           → React hook for any of the above
   - claimTurf(c)        → mutate claimed set with confetti hook
   - withViewTransition  → cross-section morph helper
   - SoundtrackController + button
   - useTiltParallax     → DeviceOrientation parallax for mobile

   Zero deps. Pure browser APIs.
   ============================================================ */

const {
  useState: useS_eng,
  useEffect: useE_eng,
  useSyncExternalStore: useSyncStore_eng,
  useRef: useR_eng,
  useCallback: useCb_eng,
} = React;

/* ─────────────────────────────────────────────────────────────
   LiveStore — single source of truth that components subscribe to.
   We avoid React Context to keep this drop-in: any component can
   useLive() without an ancestor provider.
   ───────────────────────────────────────────────────────────── */
function makeLiveStore() {
  const data = window.GCA_DATA;
  const initialClaimed = new Set(["BRA", "ARG", "USA", "ESP", "GER"]);

  /* deterministic-ish alias pool */
  const aliasPool = (data?.aliases || []).slice();
  const crimePool = (data?.crimePool || []).slice();
  const crewPool  = ["🇧🇷","🇦🇷","🇲🇽","🇪🇸","🇩🇪","🇫🇷","🇮🇹","🇺🇸","🇯🇵","🇨🇴","🇵🇹","🇳🇱"];

  let state = {
    tick: 0,
    epoch: Date.now(),

    /* market */
    marketCap: data?.price?.marketCap || 84000,
    volume24h: data?.price?.volume24h || 124000,
    solPrice:  data?.price?.solPrice  || 240.0,

    /* leaderboard */
    rankDeltas: {},  // by rank → -3..3
    rowDrift:   {},  // by rank → bounty drift float
    sparkSeed:  {},  // by rank → bumped each major tick to re-roll spark

    /* live match */
    matchMinute: 67,
    matchSecond: 0,
    matchHome: 2,
    matchAway: 1,
    matchEvents: [],

    /* BOLO feed */
    bolos: [], // newest first
    boloCounter: 2417,

    /* claimed turf */
    claimedSet: initialClaimed,
    claimCounter: initialClaimed.size,

    /* audio */
    muted: true,
  };

  const subs = new Set();
  const subscribe = (fn) => { subs.add(fn); return () => subs.delete(fn); };
  const get = () => state;
  const emit = () => {
    // produce a new shallow object so React notices change
    state = { ...state };
    subs.forEach(fn => fn());
  };

  /* ── bolo seeder so we always have a few rolling messages ── */
  function pushBolo(b) {
    state.bolos = [b, ...state.bolos].slice(0, 60);
    state.boloCounter += 1;
  }
  function rngBolo() {
    const alias = aliasPool[Math.floor(Math.random() * aliasPool.length)] || "ghost_alias";
    const tmpl  = crimePool[Math.floor(Math.random() * crimePool.length)] || "wire fraud %X%";
    const amount = `${Math.floor(Math.random()*9+1)}.${Math.floor(Math.random()*9)}M`;
    const crime = tmpl.replace("%X%", amount);
    const crew  = crewPool[Math.floor(Math.random() * crewPool.length)];
    return { id: Math.random().toString(36).slice(2,9), alias, crime, crew, ts: Date.now() };
  }
  for (let i = 0; i < 6; i++) pushBolo(rngBolo());

  /* ── public mutators ── */
  function claim(c, opts = {}) {
    if (state.claimedSet.has(c)) return false;
    const set = new Set(state.claimedSet);
    set.add(c);
    state.claimedSet = set;
    state.claimCounter = set.size;
    pushBolo({
      id: Math.random().toString(36).slice(2,9),
      alias: opts.alias || aliasPool[Math.floor(Math.random()*aliasPool.length)] || "crew",
      crime: `claimed turf · ${c}`,
      crew: opts.crew || "🏴‍☠️",
      ts: Date.now(),
      kind: "claim",
    });
    emit();
    // notify particle layer
    window.dispatchEvent(new CustomEvent("gca:claim", { detail: { c, ...opts } }));
    return true;
  }

  function unclaim(c) {
    if (!state.claimedSet.has(c)) return false;
    const set = new Set(state.claimedSet);
    set.delete(c);
    state.claimedSet = set;
    state.claimCounter = set.size;
    emit();
    return true;
  }

  function setMuted(m) { state.muted = !!m; emit(); }

  /* ── master tick — 1.2s slow tick, 100ms match tick ── */
  let slowTimer = null;
  let matchTimer = null;
  function start() {
    if (slowTimer) return;
    slowTimer = setInterval(() => {
      state.tick++;

      // drift market
      state.marketCap += Math.round((Math.random() - 0.42) * 380);
      state.marketCap = Math.max(50000, state.marketCap);
      state.volume24h += Math.round((Math.random() - 0.3) * 620);

      // re-roll a couple of rank deltas
      const ranks = (data?.leaderboard || []).map(r => r.rank);
      for (let i = 0; i < 3; i++) {
        const rk = ranks[Math.floor(Math.random() * ranks.length)];
        state.rankDeltas[rk] = Math.round((Math.random() - 0.5) * 6);
        state.rowDrift[rk] = (state.rowDrift[rk] || 0) + (Math.random() - 0.5) * 4;
      }
      // every ~6 ticks bump a spark seed so sparkline subtly re-rolls
      if (state.tick % 6 === 0) {
        const rk = ranks[Math.floor(Math.random() * ranks.length)];
        state.sparkSeed[rk] = (state.sparkSeed[rk] || 0) + 1;
      }

      // every ~4 ticks push a new BOLO
      if (state.tick % 4 === 0) pushBolo(rngBolo());

      // occasionally auto-claim a new nation to feel alive
      if (state.tick % 11 === 0) {
        const all = (data?.nations || []).map(n => n.c);
        const free = all.filter(c => !state.claimedSet.has(c));
        if (free.length) {
          const pick = free[Math.floor(Math.random() * free.length)];
          claim(pick, { alias: aliasPool[Math.floor(Math.random()*aliasPool.length)] });
        }
      }
      emit();
    }, 1200);

    matchTimer = setInterval(() => {
      // advance match
      state.matchSecond += 1;
      if (state.matchSecond >= 60) {
        state.matchSecond = 0;
        state.matchMinute += 1;
        if (state.matchMinute > 90) { state.matchMinute = 1; state.matchHome = 0; state.matchAway = 0; }
        // tiny chance of a goal
        if (Math.random() < 0.04) {
          if (Math.random() < 0.55) state.matchHome++;
          else state.matchAway++;
          state.matchEvents = [
            { id: Math.random().toString(36).slice(2,9), m: state.matchMinute, t: (Math.random() < 0.55 ? "home" : "away") },
            ...state.matchEvents,
          ].slice(0, 6);
        }
      }
      emit();
    }, 1000);
  }
  function stop() {
    clearInterval(slowTimer); clearInterval(matchTimer);
    slowTimer = matchTimer = null;
  }

  return { subscribe, get, emit, start, stop, claim, unclaim, setMuted };
}

/* singleton */
window.LiveStore = window.LiveStore || makeLiveStore();
window.LiveStore.start();

/* ─────────────────────────────────────────────────────────────
   useLive() — selector hook with shallow-equal gating.
   We avoid useSyncExternalStore because object-returning selectors
   create new refs each call and trip its strict-equality check.
   ───────────────────────────────────────────────────────────── */
function shallowEq_eng(a, b) {
  if (a === b) return true;
  if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
  if (a instanceof Set && b instanceof Set) {
    if (a.size !== b.size) return false;
    for (const v of a) if (!b.has(v)) return false;
    return true;
  }
  const ak = Object.keys(a), bk = Object.keys(b);
  if (ak.length !== bk.length) return false;
  for (const k of ak) if (a[k] !== b[k]) return false;
  return true;
}
function useLive(selector) {
  const sel = selector || (s => s);
  const [, force] = React.useReducer(x => x + 1, 0);
  const lastRef = useR_eng();
  if (lastRef.current === undefined) {
    lastRef.current = sel(window.LiveStore.get());
  }
  useE_eng(() => {
    // sync once on mount in case state changed between render and effect
    const fresh = sel(window.LiveStore.get());
    if (!shallowEq_eng(fresh, lastRef.current)) {
      lastRef.current = fresh; force();
    }
    return window.LiveStore.subscribe(() => {
      const next = sel(window.LiveStore.get());
      if (!shallowEq_eng(next, lastRef.current)) {
        lastRef.current = next;
        force();
      }
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return lastRef.current;
}

/* ─────────────────────────────────────────────────────────────
   withViewTransition — wrap a DOM-mutating callback in a
   CSS view transition if supported. Falls back to immediate call.
   ───────────────────────────────────────────────────────────── */
function withViewTransition(fn, name) {
  if (typeof document.startViewTransition === "function") {
    // tag root for scoped transition
    if (name) document.documentElement.dataset.vt = name;
    const t = document.startViewTransition(() => {
      const r = fn();
      return r;
    });
    if (name) t.finished.finally(() => { delete document.documentElement.dataset.vt; });
    return t;
  }
  fn();
  return null;
}
window.withViewTransition = withViewTransition;

/* ─────────────────────────────────────────────────────────────
   SoundtrackController — Web Audio synthesized hum & stinger.
   No assets shipped; everything is generated on the fly.
   - Subtle synth-bass pulse loop
   - Stinger on claim event
   Respects prefers-reduced-motion (silent by default).
   ───────────────────────────────────────────────────────────── */
function SoundtrackController() {
  const live = useLive(s => ({ muted: s.muted }));
  const audio = useR_eng({ ctx: null, master: null, loopGain: null, lfo: null, started: false });

  // start/stop audio in response to muted
  useE_eng(() => {
    if (live.muted) {
      // pause
      if (audio.current.ctx) {
        audio.current.ctx.suspend();
      }
      return;
    }

    // create or resume context
    let { ctx, master, loopGain } = audio.current;
    if (!ctx) {
      try { ctx = new (window.AudioContext || window.webkitAudioContext)(); }
      catch (e) { return; }
      audio.current.ctx = ctx;

      // master with gentle compressor
      const comp = ctx.createDynamicsCompressor();
      comp.threshold.value = -22; comp.knee.value = 30;
      comp.ratio.value = 6; comp.attack.value = 0.01; comp.release.value = 0.2;
      master = ctx.createGain(); master.gain.value = 0.18;
      comp.connect(master); master.connect(ctx.destination);
      audio.current.master = master;
      audio.current.comp = comp;

      // bass pad — 2 detuned sawtooths low-passed
      const a = ctx.createOscillator(); a.type = "sawtooth"; a.frequency.value = 55;
      const b = ctx.createOscillator(); b.type = "sawtooth"; b.frequency.value = 55.4;
      const lp = ctx.createBiquadFilter(); lp.type = "lowpass"; lp.frequency.value = 320; lp.Q.value = 6;

      // LFO sweep on filter cutoff for that "synthwave" pump
      const lfo = ctx.createOscillator(); lfo.frequency.value = 0.22;
      const lfoGain = ctx.createGain(); lfoGain.gain.value = 180;
      lfo.connect(lfoGain); lfoGain.connect(lp.frequency);

      loopGain = ctx.createGain(); loopGain.gain.value = 0.0;
      a.connect(lp); b.connect(lp); lp.connect(loopGain); loopGain.connect(comp);
      a.start(); b.start(); lfo.start();
      audio.current.loopGain = loopGain;
      audio.current.lfo = lfo;

      // soft pad on top — pink noise filtered
      const noiseBuffer = ctx.createBuffer(1, ctx.sampleRate * 1, ctx.sampleRate);
      const ch = noiseBuffer.getChannelData(0);
      let lastOut = 0;
      for (let i = 0; i < ch.length; i++) {
        const white = Math.random() * 2 - 1;
        lastOut = (lastOut + 0.02 * white) / 1.02;
        ch[i] = lastOut * 3.5;
      }
      const noise = ctx.createBufferSource();
      noise.buffer = noiseBuffer; noise.loop = true;
      const noiseFilt = ctx.createBiquadFilter();
      noiseFilt.type = "bandpass"; noiseFilt.frequency.value = 950; noiseFilt.Q.value = 0.8;
      const noiseGain = ctx.createGain(); noiseGain.gain.value = 0.06;
      noise.connect(noiseFilt); noiseFilt.connect(noiseGain); noiseGain.connect(comp);
      noise.start();
    }
    ctx.resume();
    // fade in
    const now = ctx.currentTime;
    loopGain.gain.cancelScheduledValues(now);
    loopGain.gain.setValueAtTime(loopGain.gain.value, now);
    loopGain.gain.linearRampToValueAtTime(0.42, now + 0.4);
  }, [live.muted]);

  // stinger on claim
  useE_eng(() => {
    const onClaim = () => {
      const { ctx, comp } = audio.current;
      if (!ctx || live.muted) return;
      const t0 = ctx.currentTime;
      // descending blip
      const o = ctx.createOscillator(); o.type = "square";
      const g = ctx.createGain();
      o.frequency.setValueAtTime(880, t0);
      o.frequency.exponentialRampToValueAtTime(440, t0 + 0.25);
      g.gain.setValueAtTime(0.0001, t0);
      g.gain.exponentialRampToValueAtTime(0.22, t0 + 0.01);
      g.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.32);
      o.connect(g); g.connect(comp);
      o.start(t0); o.stop(t0 + 0.35);

      // metallic click
      const o2 = ctx.createOscillator(); o2.type = "triangle";
      const g2 = ctx.createGain();
      o2.frequency.value = 2200;
      g2.gain.setValueAtTime(0.0001, t0);
      g2.gain.exponentialRampToValueAtTime(0.18, t0 + 0.005);
      g2.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.12);
      o2.connect(g2); g2.connect(comp);
      o2.start(t0); o2.stop(t0 + 0.14);
    };
    window.addEventListener("gca:claim", onClaim);
    return () => window.removeEventListener("gca:claim", onClaim);
  }, [live.muted]);

  return null;
}
window.SoundtrackController = SoundtrackController;

/* ─────────────────────────────────────────────────────────────
   MuteButton — bottom-left floating control.
   First click unlocks audio (browser policy needs gesture).
   ───────────────────────────────────────────────────────────── */
function MuteButton() {
  const muted = useLive(s => s.muted);
  return (
    <button
      className={`gx-mute ${muted ? "off" : "on"}`}
      onClick={() => window.LiveStore.setMuted(!muted)}
      aria-label={muted ? "Unmute soundtrack" : "Mute soundtrack"}
      title={muted ? "Click for Grand Cup Auto soundtrack" : "Mute"}
    >
      <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
        {muted ? (
          <>
            <path d="M3 9v6h4l5 4V5L7 9H3z" fill="currentColor" />
            <path d="M19 5l-4 4M15 5l4 4M19 15l-4 4M15 15l4 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" fill="none" />
          </>
        ) : (
          <>
            <path d="M3 9v6h4l5 4V5L7 9H3z" fill="currentColor" />
            <path d="M16 8.5c1.5 1.5 1.5 5.5 0 7M19 6c3 3 3 9 0 12" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" fill="none" />
          </>
        )}
      </svg>
      <span>{muted ? "audio off" : "audio · live"}</span>
    </button>
  );
}
window.MuteButton = MuteButton;

/* ─────────────────────────────────────────────────────────────
   useTiltParallax — DeviceOrientation hook.
   Sets --tilt-x, --tilt-y CSS vars on :root (range -1..1).
   Gated behind a one-tap permission prompt on iOS.
   ───────────────────────────────────────────────────────────── */
function TiltGate() {
  const [granted, setGranted] = useS_eng(false);
  const [needsAsk, setNeedsAsk] = useS_eng(false);

  useE_eng(() => {
    if (typeof DeviceOrientationEvent === "undefined") return;
    if (typeof DeviceOrientationEvent.requestPermission === "function") {
      setNeedsAsk(true);
    } else {
      // android / desktop — start immediately
      attach();
    }
    function attach() {
      setGranted(true);
      const onO = (e) => {
        const gx = Math.max(-1, Math.min(1, (e.gamma || 0) / 30));
        const gy = Math.max(-1, Math.min(1, (e.beta  || 0) / 30));
        document.documentElement.style.setProperty("--tilt-x", gx.toFixed(3));
        document.documentElement.style.setProperty("--tilt-y", gy.toFixed(3));
      };
      window.addEventListener("deviceorientation", onO);
      return () => window.removeEventListener("deviceorientation", onO);
    }
    if (typeof DeviceOrientationEvent.requestPermission !== "function") {
      return attach();
    }
  }, []);

  if (!needsAsk || granted) return null;
  return (
    <button
      className="gx-tilt-ask"
      onClick={async () => {
        try {
          const r = await DeviceOrientationEvent.requestPermission();
          if (r === "granted") {
            setGranted(true); setNeedsAsk(false);
            window.addEventListener("deviceorientation", (e) => {
              const gx = Math.max(-1, Math.min(1, (e.gamma || 0) / 30));
              const gy = Math.max(-1, Math.min(1, (e.beta  || 0) / 30));
              document.documentElement.style.setProperty("--tilt-x", gx.toFixed(3));
              document.documentElement.style.setProperty("--tilt-y", gy.toFixed(3));
            });
          }
        } catch (e) {}
      }}
    >tilt mode</button>
  );
}
window.TiltGate = TiltGate;

window.useLive = useLive;
window.withViewTransition = withViewTransition;
