// queue-times-fetch.jsx
// Browser fetch for live wait times → merges into window.PH365.ATTRACTIONS.
//
// Hits our same-origin /api/queue-times proxy (a Vercel serverless function)
// which forwards to queue-times.com with a proper User-Agent. We can't go
// direct from the browser: queue-times responds 503 to browser-issued
// requests, so the proxy is required. On non-Vercel hosts (local dev with
// `python -m http.server`) the proxy 404s and we fall back to direct fetch
// — which works in some networks even though Vercel-deployed users hit 503.
(function () {
  const PARK_IDS = [16, 17];   // 16 = Disneyland, 17 = California Adventure
  const REFRESH_MS = 5 * 60 * 1000;
  const PROXY_BASE = '/api/queue-times';
  const DIRECT_BASE = 'https://queue-times.com/en-US/parks';

  // ── Fetch one park's queue snapshot ──────────────────────────────────
  // 8s per-park timeout — Vercel's serverless function caps at 10s, and a
  // hung fetch with no abort would leave the UI stuck in "Refreshing…" forever.
  const FETCH_TIMEOUT_MS = 8000;

  async function fetchWithTimeout(url, opts = {}) {
    const ac = new AbortController();
    const timer = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS);
    try {
      return await fetch(url, { ...opts, signal: ac.signal });
    } finally {
      clearTimeout(timer);
    }
  }

  // queue-times lists single-rider queues as their own rides ("Radiator
  // Springs Racers Single Rider", and four others across the two parks).
  // Disney publishes no single-rider wait, so every one of them arrives as
  // wait_time: 0 — that 0 is missing data, not a walk-on. Dropping them here
  // keeps them out of the ranker, where a phantom 0 would otherwise sail to
  // the top of Go To as the best pick on the board, and out of the UI, where
  // it would read as a real posted wait. They are dropped at ingestion rather
  // than filtered later so no downstream code has to know about them.
  const SINGLE_RIDER_RE = /\bsingle\s+rider\b/i;
  const isSingleRider = name => typeof name === 'string' && SINGLE_RIDER_RE.test(name);

  async function fetchPark(parkId) {
    let res = await fetchWithTimeout(`${PROXY_BASE}?park=${parkId}`, {
      headers: { Accept: 'application/json' },
    });
    // 404 → no proxy deployed (local file server). Fall through to direct.
    if (res.status === 404) {
      res = await fetchWithTimeout(`${DIRECT_BASE}/${parkId}/queue_times.json`, {
        headers: { Accept: 'application/json' },
      });
    }
    if (!res.ok) throw new Error(`queue-times park ${parkId} → ${res.status}`);
    const json = await res.json();
    // Normalize: rides live in either `lands[].rides[]` or top-level `rides[]`.
    const rides = [];
    for (const land of (json.lands ?? [])) {
      for (const r of (land.rides ?? [])) {
        if (isSingleRider(r.name)) continue;
        rides.push({ qt_id: r.id, name: r.name, wait: r.wait_time, isOpen: r.is_open, last: r.last_updated });
      }
    }
    for (const r of (json.rides ?? [])) {
      if (isSingleRider(r.name)) continue;
      rides.push({ qt_id: r.id, name: r.name, wait: r.wait_time, isOpen: r.is_open, last: r.last_updated });
    }
    return rides;
  }

  // ── Apply fresh waits to window.PH365.ATTRACTIONS in place ──────────
  function applyWaits(allRides) {
    if (!window.PH365?.ATTRACTIONS) return 0;
    const byId = new Map(allRides.map(r => [r.qt_id, r]));
    let updated = 0, unmapped = 0;

    const fetchedAt = new Date().toISOString();
    const next = window.PH365.ATTRACTIONS.map(a => {
      if (a.queueTimesId == null) return a;
      const live = byId.get(a.queueTimesId);
      if (!live) { unmapped++; return a; }
      updated++;
      // wait_time from queue-times is always a number (0 when "walk-on" or
      // unknown). Closed rides keep wait=0 but isOpen=false. We stamp
      // lastUpdated unconditionally so the UI can tell "live 0" from "no data".
      const liveWait = (typeof live.wait === 'number' ? live.wait : 0);
      return {
        ...a,
        wait: live.isOpen ? liveWait : 0,
        isOpen: !!live.isOpen,
        lastUpdated: live.last || fetchedAt,
      };
    });

    window.PH365 = { ...window.PH365, ATTRACTIONS: next };
    window.dispatchEvent(new CustomEvent('ph365-waits-refreshed', {
      detail: { updated, unmapped, source: 'queue-times' }
    }));
    return { updated, unmapped };
  }

  // ── Refresh loop ────────────────────────────────────────────────────
  let timer = null;
  let lastFetchedAt = 0;
  let inFlight = null;  // dedupe: avoid overlapping fetches
  // Public debug state — inspect via `window.PH365_QT` in the console.
  window.PH365_QT = {
    lastFetchedAt: null,
    lastError: null,
    parkErrors: {},
    updated: 0,
    unmapped: 0,
    refresh: null,    // populated below
    start: null,
  };

  async function refresh() {
    // Dedupe overlapping calls — if a fetch is already in flight, return its
    // promise instead of starting a parallel one. Otherwise rapid refreshes
    // (manual click + interval + visibility change) stack up and the UI's
    // "Refreshing…" disable can outlast the most-recent fetch.
    if (inFlight) return inFlight;
    inFlight = (async () => {
      console.info('[queue-times] fetching DLR + DCA…');
      const parks = await Promise.allSettled(PARK_IDS.map(fetchPark));
      const parkErrors = {};
      parks.forEach((p, i) => {
        if (p.status === 'rejected') {
          parkErrors[PARK_IDS[i]] = String(p.reason?.message ?? p.reason);
          console.warn(`[queue-times] park ${PARK_IDS[i]} failed:`, p.reason);
        }
      });
      window.PH365_QT.parkErrors = parkErrors;

      const all = parks.flatMap(p => p.status === 'fulfilled' ? p.value : []);
      if (!all.length) {
        window.PH365_QT.lastError = 'all park fetches failed';
        console.warn('[queue-times] all park fetches failed — see network tab');
        return;
      }

      const result = applyWaits(all);
      lastFetchedAt = Date.now();
      Object.assign(window.PH365_QT, {
        lastFetchedAt: new Date(lastFetchedAt).toISOString(),
        lastError: null,
        updated: result.updated,
        unmapped: result.unmapped,
        ridesFetched: all.length,
      });
      console.info(`[queue-times] ✓ refreshed ${result.updated} of ${window.PH365.ATTRACTIONS.length} attractions (${result.unmapped} unmapped, ${all.length} rides from API)`);
    })();
    try {
      return await inFlight;
    } finally {
      inFlight = null;
    }
  }

  function start() {
    if (timer) return;  // already running
    console.info('[queue-times] starting auto-refresh every 5 min');
    refresh().catch(err => {
      window.PH365_QT.lastError = String(err?.message ?? err);
      console.warn('[queue-times] initial refresh threw:', err);
    });
    timer = setInterval(() => {
      if (document.visibilityState === 'visible') {
        refresh().catch(err => console.warn('[queue-times] periodic refresh threw:', err));
      }
    }, REFRESH_MS);

    // Refresh when tab becomes visible if we haven't pulled in >2 min
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible' && Date.now() - lastFetchedAt > 2 * 60 * 1000) {
        refresh().catch(err => console.warn('[queue-times] visibility refresh threw:', err));
      }
    });
  }

  window.PH365_QT.refresh = refresh;
  window.PH365_QT.start = start;

  // Re-apply waits whenever live-data finishes a (re)load.
  //
  // live-data.jsx's loadAll() rebuilds window.PH365.ATTRACTIONS from Supabase,
  // which clobbers any wait/isOpen we wrote here. Without this listener, if
  // queue-times finishes BEFORE live-data's first loadAll resolves (the common
  // case — Vercel CDN-cached proxy beats 6 parallel Supabase queries), every
  // ride snaps back to wait=0/isOpen=false and the home screen sticks on
  // "Loading live waits…" until the next 5-min interval fires. This also
  // covers loadAll() re-runs triggered by auth state changes.
  window.addEventListener('ph365-data-ready', () => {
    refresh().catch(err => console.warn('[queue-times] refresh after data-ready threw:', err));
  });

  // Wait for window.PH365 to be populated before kicking off.
  // data.js is a regular <script> so it should already be loaded, but the
  // type="text/babel" execution order is best-effort — guard against that.
  if (window.PH365?.ATTRACTIONS) {
    start();
  } else {
    console.warn('[queue-times] window.PH365 not ready, waiting for ph365-data-ready');
    window.addEventListener('ph365-data-ready', start, { once: true });
    setTimeout(() => { if (window.PH365?.ATTRACTIONS && !timer) start(); }, 200);
  }
})();
