/* Lightning client — connects directly to Blitzortung's public WebSocket
   from the browser.

   We originally tried to proxy this through a Vercel serverless
   WS → SSE fan-out so one upstream connection could serve many
   browsers. In practice Blitzortung silently drops connections
   from cloud IP ranges (Vercel/AWS): the handshake hangs with no error
   or close frame, the function times out at 5 minutes, no strikes ever
   arrive. Their own map.blitzortung.org client connects directly from
   each visitor's browser, so matching that topology is the reliable
   path — their WS is built for thousands of concurrent browser
   subscribers and that's exactly the load it expects.

   Protocol (canonical, mirrors what their own client does):
     1. Open wss://ws{1..8}.blitzortung.org/ (rotate for load spread)
     2. Send {"a": 111} to subscribe to the global strike feed
     3. Receive TEXT frames. Some servers send plain JSON; others use
        an LZW-compressed form. Decoder below handles both.

   Classifying LZW vs raw JSON: an earlier version peeked at the first
   character and assumed raw JSON if it was '{'. That's broken — LZW
   always preserves the first literal byte of the decoded output, so
   every compressed Blitzortung strike *also* starts with '{' and the
   heuristic never tripped the decode path. Dictionary references are
   emitted as code units ≥ 256, and valid JSON can't legitimately
   contain those above U+00FF in unescaped form, so scanning for any
   charCode ≥ 256 is a deterministic tell.
*/

(function () {
  const STRIKES = [];
  const STRIKE_TTL_MS = 3000;
  const MAX = 3000;

  let ws = null;
  let reconnectTimer = null;
  let reconnectDelay = 3000;
  const RECONNECT_CAP = 60000;

  // Blitzortung runs ws1–ws8, but not all are up at any one time (Oct
  // 2026: ws3–ws6 refuse connections). Rather than a random pick that can
  // land on a dead host and then wait out a backoff, walk a queue: the
  // last host that worked first, the rest shuffled. A connection that dies
  // before opening moves straight to the next host; backoff only applies
  // once every host has failed in a row.
  const HOSTS = [1, 2, 3, 4, 5, 6, 7, 8].map(n => `wss://ws${n}.blitzortung.org/`);
  const GOOD_HOST_KEY = 'ge-lightning-host';
  let hostQueue = [];

  function buildHostQueue() {
    let good = null;
    try { good = localStorage.getItem(GOOD_HOST_KEY); } catch {}
    const rest = HOSTS.filter(h => h !== good);
    for (let i = rest.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [rest[i], rest[j]] = [rest[j], rest[i]];
    }
    return HOSTS.includes(good) ? [good, ...rest] : rest;
  }

  // Blitzortung LZW decoder — bytes ≥ 256 reference phrases built up
  // during decoding. Canonical impl shared by their own JS client.
  function decode(msg) {
    const e = {};
    const chars = msg.split('');
    let prev = chars[0];
    let curr = prev;
    const out = [curr];
    let code = 256;
    for (let i = 1; i < chars.length; i++) {
      const cc = chars[i].charCodeAt(0);
      const phrase = cc < 256 ? chars[i] : (e[cc] || (prev + curr));
      out.push(phrase);
      curr = phrase[0];
      e[code] = prev + curr;
      code++;
      prev = phrase;
    }
    return out.join('');
  }

  function prune() {
    const cutoff = Date.now() - STRIKE_TTL_MS;
    while (STRIKES.length && STRIKES[0].t < cutoff) STRIKES.shift();
  }

  function connect() {
    try { if (ws) ws.close(); } catch {}
    if (!hostQueue.length) hostQueue = buildHostQueue();
    const url = hostQueue.shift();
    let opened = false;
    console.log('[lightning] opening', url);
    try {
      ws = new WebSocket(url);
    } catch (e) {
      console.warn('[lightning] WS construct failed', e);
      scheduleReconnect();
      return;
    }
    let msgsSinceLog = 0;
    let strikesSinceLog = 0;
    let statsTimer = null;
    ws.onopen = () => {
      opened = true;
      reconnectDelay = 3000;
      hostQueue = [];  // next reconnect starts again from the remembered host
      try { localStorage.setItem(GOOD_HOST_KEY, url); } catch {}
      ws.send(JSON.stringify({ a: 111 }));
      console.log(`[lightning] WS open → ${url} — subscribed {a:111}`);
      // Periodic stats so the user can see in DevTools whether
      // strikes are actually flowing through.
      if (statsTimer) clearInterval(statsTimer);
      statsTimer = setInterval(() => {
        if (msgsSinceLog || strikesSinceLog) {
          console.log(`[lightning] 30s: ${msgsSinceLog} msgs, ${strikesSinceLog} strikes`);
        } else {
          console.warn('[lightning] 30s: NO messages — Blitzortung WS silent');
        }
        msgsSinceLog = 0;
        strikesSinceLog = 0;
      }, 30000);
    };
    ws.onmessage = (ev) => {
      msgsSinceLog++;
      const text = ev.data;
      if (!text || typeof text !== 'string') return;
      // LZW classifier: any codepoint ≥ 256 is a dictionary reference,
      // which can't appear in raw JSON. Scan up to a short window — for
      // compressed frames we hit a high byte in the first dozen chars,
      // and for raw-JSON frames (all ASCII) we bail at end of string.
      let isLZW = false;
      const scanLen = Math.min(text.length, 64);
      for (let i = 0; i < scanLen; i++) {
        if (text.charCodeAt(i) >= 256) { isLZW = true; break; }
      }
      const raw = isLZW ? decode(text) : text;
      let j;
      try {
        j = JSON.parse(raw);
      } catch (err) {
        if (msgsSinceLog <= 2) {
          console.warn('[lightning] parse fail, lzw=' + isLZW + ', first bytes:',
                       String(raw).slice(0, 60));
        }
        return;
      }
      if (typeof j.lat !== 'number' || typeof j.lon !== 'number') {
        if (msgsSinceLog <= 2) console.warn('[lightning] msg without lat/lon, keys:', Object.keys(j).slice(0, 8));
        return;
      }
      strikesSinceLog++;
      STRIKES.push({ t: Date.now(), lat: +j.lat.toFixed(3), lon: +j.lon.toFixed(3), pol: j.pol || 0 });
      if (STRIKES.length > MAX) STRIKES.splice(0, STRIKES.length - MAX);
    };
    ws.onerror = (e) => {
      console.warn('[lightning] WS error', e?.message || '(no detail)');
    };
    ws.onclose = (e) => {
      if (statsTimer) clearInterval(statsTimer);
      statsTimer = null;
      console.warn(`[lightning] WS close code=${e.code} clean=${e.wasClean} reason=${e.reason || '(none)'}`);
      // Host never opened and others are untried — fail over immediately.
      if (!opened && hostQueue.length) { clearTimeout(reconnectTimer); reconnectTimer = setTimeout(connect, 0); return; }
      hostQueue = [];
      scheduleReconnect();
    };
  }

  function scheduleReconnect() {
    clearTimeout(reconnectTimer);
    reconnectTimer = setTimeout(connect, reconnectDelay);
    reconnectDelay = Math.min(RECONNECT_CAP, reconnectDelay * 2);
  }

  setInterval(prune, 500);
  connect();

  window.getLightningStrikes = () => STRIKES;
})();
