// Shared Azuracast audio + now-playing engine for all variations.
// Exposes: window.useEphemeralRadio() — React hook returning {state, track, listeners, toggle, audioRef, analyserData}

// Use flac as the primary stream — leaning into the lossless ethos.
// mp3 is a fallback for browsers that can't decode flac (Safari).
const STREAM_URL_FLAC = "https://listen.ephemeral.club/listen/ephemeral/radio.flac";
const STREAM_URL_MP3  = "https://listen.ephemeral.club/listen/ephemeral/radio.mp3";

function pickStream() {
  try {
    const a = document.createElement('audio');
    const ok = a.canPlayType('audio/flac') || a.canPlayType('audio/x-flac');
    return ok ? STREAM_URL_FLAC : STREAM_URL_MP3;
  } catch {
    return STREAM_URL_MP3;
  }
}
const STREAM_URL = pickStream();
const STREAM_FORMAT = STREAM_URL === STREAM_URL_FLAC ? "FLAC · 24-bit · 44.1 kHz" : "MP3 · 320 kbps";
window.EPHEMERAL_STREAM_FORMAT = STREAM_FORMAT;
const NOWPLAYING_URL = "https://listen.ephemeral.club/api/nowplaying/1";

// Local placeholder art for tracks that don't have artwork or whose art doesn't fit the aesthetic
const PLACEHOLDER_ART = "placeholder-art.png";

function isGenericArt(url) {
  if (!url) return true;
  // Azuracast default art is usually served from /static/ and contains "default"
  return /default|generic|missing|no[-_]?art/i.test(url);
}

// ---------------------------------------------------------------------------
// Purchase link — operator-set, per track.
// AzuraCast lets you attach Custom Fields to media; when one holds a store URL
// it flows through the now-playing API on song.custom_fields. We surface it as
// a "buy" button ONLY when present, so the button appears exactly when a track
// is actually for sale — there is no reliable client-side way to guess this.
// custom_fields arrives in one of two shapes depending on AzuraCast version:
//   array : [{ name, value }, ...]
//   object: { name: value, ... }
// We accept either and match a few likely field names. To light up the button,
// add a Custom Field in AzuraCast (Administration → Custom Fields) named one of
// the below and set it to the Bandcamp (or any store) URL on purchasable media.
// ---------------------------------------------------------------------------
const BUY_FIELD_NAMES = ["buy_url","buy","bandcamp","bandcamp_url","purchase","purchase_url","buy_link","store","store_url"];

// Opt-in preview switch: append ?pvbuy=1 (or ?pvbuy=<url>) to the page URL to
// force the buy button on for design/QA without configuring AzuraCast. Never
// active for normal visitors — the param must be explicitly present.
const _previewBuy = (() => {
  try {
    const v = new URLSearchParams(location.search).get("pvbuy");
    if (!v) return null;
    return /^https?:\/\//i.test(v) ? v : "https://bandcamp.com";
  } catch { return null; }
})();

function extractBuyUrl(song) {
  if (!song) return null;
  const cf = song.custom_fields;
  let val = null;
  if (Array.isArray(cf)) {
    const hit = cf.find(f => f && BUY_FIELD_NAMES.includes(String(f.name || "").toLowerCase()) && f.value);
    val = hit ? hit.value : null;
  } else if (cf && typeof cf === "object") {
    for (const k of Object.keys(cf)) {
      if (BUY_FIELD_NAMES.includes(k.toLowerCase()) && cf[k]) { val = cf[k]; break; }
    }
  }
  if (!val || typeof val !== "string") return _previewBuy;
  val = val.trim();
  return /^https?:\/\//i.test(val) ? val : _previewBuy;
}

// Multi-artist tags often come as "Artist A; Artist B; Artist C".
// For external lookups, take only the primary (first) artist.
function primaryArtist(artist) {
  if (!artist) return '';
  return artist.split(';')[0].trim();
}

// ---------------------------------------------------------------------------
// iTunes Search API fallback for missing/generic embedded art.
// No API key required. Search by album when available (best art quality),
// otherwise fall back to song search. Cache hits AND misses to avoid
// hammering the API on every poll cycle.
// ---------------------------------------------------------------------------
const _itunesCache = new Map(); // `${primaryArtist}|${album||title}` -> URL or null

async function fetchITunesArt(artist, album, title) {
  const primary = primaryArtist(artist);
  if (!primary) return null;
  const queryAlbum = !!album;
  const term = queryAlbum ? `${primary} ${album}` : `${primary} ${title || ''}`;
  if (!term.trim()) return null;

  const key = `${primary}|${album || title || ''}`;
  if (_itunesCache.has(key)) return _itunesCache.get(key);

  const entity = queryAlbum ? 'album' : 'song';
  const url = `https://itunes.apple.com/search?term=${encodeURIComponent(term)}&media=music&entity=${entity}&limit=1`;

  try {
    const r = await fetch(url);
    if (!r.ok) throw new Error('itunes fetch');
    const j = await r.json();
    const small = j.results?.[0]?.artworkUrl100;
    // Upscale 100x100 → 600x600 (iTunes URLs include the size in the path)
    const big = small ? small.replace(/100x100/g, '600x600') : null;
    _itunesCache.set(key, big); // cache misses (null) too — avoids retrying failures
    return big;
  } catch {
    _itunesCache.set(key, null);
    return null;
  }
}

// Singleton audio + analyser shared across all artboards on the canvas
let _sharedAudio = null;
let _sharedCtx = null;
let _sharedAnalyser = null;
let _sharedSource = null;

function getSharedAudio() {
  if (_sharedAudio) return _sharedAudio;
  const a = new Audio();
  a.crossOrigin = "anonymous";
  a.preload = "none";
  a.src = STREAM_URL;
  _sharedAudio = a;
  return a;
}

function ensureAnalyser() {
  if (_sharedAnalyser) return _sharedAnalyser;
  try {
    const Ctx = window.AudioContext || window.webkitAudioContext;
    _sharedCtx = new Ctx();
    const audio = getSharedAudio();
    _sharedSource = _sharedCtx.createMediaElementSource(audio);
    _sharedAnalyser = _sharedCtx.createAnalyser();
    _sharedAnalyser.fftSize = 128;
    _sharedAnalyser.smoothingTimeConstant = 0.82;
    _sharedSource.connect(_sharedAnalyser);
    _sharedAnalyser.connect(_sharedCtx.destination);
  } catch (e) {
    console.warn("Analyser unavailable — CORS or autoplay restriction", e);
  }
  return _sharedAnalyser;
}

function useEphemeralRadio() {
  const [state, setState] = React.useState("idle"); // idle | loading | playing | error
  const [track, setTrack] = React.useState(null);
  const [listeners, setListeners] = React.useState(null);
  const [history, setHistory] = React.useState([]);
  const [live, setLive] = React.useState({ isLive: false, streamer: null });
  const [levels, setLevels] = React.useState(() => new Uint8Array(64));
  // Volume: persisted to localStorage (1.0 default), only applied on desktop UI
  const [volume, _setVolume] = React.useState(() => {
    try { const v = parseFloat(localStorage.getItem('eph.volume')); return isFinite(v) ? Math.min(1, Math.max(0, v)) : 1; } catch { return 1; }
  });
  const audioRef = React.useRef(null);

  // Mount: get singleton audio
  React.useEffect(() => {
    audioRef.current = getSharedAudio();
    const a = audioRef.current;
    const onPlay = () => setState("playing");
    const onPlaying = () => setState("playing");
    const onCanPlay = () => { if (!a.paused) setState("playing"); };
    const onPause = () => setState("idle");
    const onWaiting = () => { if (!a.paused) setState("loading"); };
    const onError = () => setState("error");
    // timeupdate is the ground-truth heartbeat: if currentTime is advancing,
    // we're actually playing regardless of what waiting/playing/canplay said.
    // This recovers from buffer hiccups where `playing` fails to re-fire after `waiting`.
    const onTimeUpdate = () => {
      if (!a.paused) setState(s => s === "playing" ? s : "playing");
    };
    a.addEventListener("play", onPlay);
    a.addEventListener("playing", onPlaying);
    a.addEventListener("canplay", onCanPlay);
    a.addEventListener("pause", onPause);
    a.addEventListener("waiting", onWaiting);
    a.addEventListener("error", onError);
    a.addEventListener("timeupdate", onTimeUpdate);
    // sync state on mount
    if (!a.paused) setState("playing");
    return () => {
      a.removeEventListener("play", onPlay);
      a.removeEventListener("playing", onPlaying);
      a.removeEventListener("canplay", onCanPlay);
      a.removeEventListener("pause", onPause);
      a.removeEventListener("waiting", onWaiting);
      a.removeEventListener("error", onError);
      a.removeEventListener("timeupdate", onTimeUpdate);
    };
  }, []);

  // Poll now-playing every 12s
  React.useEffect(() => {
    let cancelled = false;
    async function fetchNP() {
      try {
        const r = await fetch(NOWPLAYING_URL, { cache: "no-store" });
        if (!r.ok) throw new Error("np fetch");
        const j = await r.json();
        const data = Array.isArray(j) ? j[0] : j;
        if (cancelled || !data) return;
        const np = data.now_playing || {};
        const song = np.song || {};

        // ART RESOLUTION ORDER:
        //   1. Embedded art (Azuracast) — if present and not generic
        //   2. iTunes Search API   — async, updates track once resolved
        //   3. Local placeholder   — used until/unless iTunes resolves
        const embedded = isGenericArt(song.art) ? null : song.art;

        setTrack({
          title: song.title || "untitled",
          artist: song.artist || "unknown",
          album: song.album || "",
          art: embedded || PLACEHOLDER_ART,
          buyUrl: extractBuyUrl(song),
          startedAt: np.played_at ? np.played_at * 1000 : Date.now(),
          duration: np.duration || 0,
          elapsed: np.elapsed || 0,
        });
        setListeners(data.listeners?.current ?? null);

        // Azuracast live streamer info — set when a DJ takes over from AutoDJ
        const liveInfo = data.live || {};
        setLive({
          isLive: !!liveInfo.is_live,
          streamer: liveInfo.streamer_name || null,
        });

        // History items — same three-tier resolution
        const historyItems = (data.song_history || []).slice(0, 6).map(h => {
          const hsong = h.song || {};
          const hembedded = isGenericArt(hsong.art) ? null : hsong.art;
          return {
            title: hsong.title || "",
            artist: hsong.artist || "",
            album: hsong.album || "",
            art: hembedded || PLACEHOLDER_ART,
          };
        });
        setHistory(historyItems);

        // Async-resolve iTunes art for the current track if embedded was missing.
        // Guard against the track having changed before the lookup completes.
        if (!embedded && song.artist && (song.album || song.title)) {
          fetchITunesArt(song.artist, song.album, song.title).then(it => {
            if (cancelled || !it) return;
            setTrack(t => {
              if (!t || t.title !== song.title || t.artist !== song.artist) return t;
              return { ...t, art: it };
            });
          });
        }

        // Async-resolve iTunes art for any history items missing embedded art.
        // Cache means same-album repeats are free after the first hit.
        historyItems.forEach((h, idx) => {
          if (h.art !== PLACEHOLDER_ART) return;
          if (!h.artist || (!h.album && !h.title)) return;
          fetchITunesArt(h.artist, h.album, h.title).then(it => {
            if (cancelled || !it) return;
            setHistory(prev => {
              const slot = prev[idx];
              if (!slot || slot.title !== h.title || slot.artist !== h.artist) return prev;
              const next = [...prev];
              next[idx] = { ...slot, art: it };
              return next;
            });
          });
        });
      } catch (e) {
        if (!cancelled) {
          // If the API is unreachable, leave previous track in place (or empty on first load)
          setTrack(t => t || {
            title: "—",
            artist: "—",
            album: "",
            art: PLACEHOLDER_ART,
            buyUrl: _previewBuy,
            startedAt: Date.now(),
            duration: 0,
            elapsed: 0,
          });
        }
      }
    }
    fetchNP();
    const id = setInterval(fetchNP, 12000);
    return () => { cancelled = true; clearInterval(id); };
  }, []);

  // Analyser RAF — only run while playing
  React.useEffect(() => {
    if (state !== "playing") return;
    let raf;
    const an = ensureAnalyser();
    if (!an) return;
    const buf = new Uint8Array(an.frequencyBinCount);
    const tick = () => {
      an.getByteFrequencyData(buf);
      setLevels(new Uint8Array(buf));
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [state]);

  const toggle = React.useCallback(async () => {
    const a = audioRef.current;
    if (!a) return;
    if (a.paused) {
      // Activate AudioContext on user gesture so analyser works
      try { ensureAnalyser(); if (_sharedCtx?.state === "suspended") await _sharedCtx.resume(); } catch {}
      setState("loading");
      // Force fresh fetch of live stream
      try { a.src = STREAM_URL + "?t=" + Date.now(); a.load(); } catch {}
      try { await a.play(); } catch (e) { setState("error"); console.warn(e); }
    } else {
      a.pause();
    }
  }, []);

  // Volume: apply to audio element + persist
  const setVolume = React.useCallback((v) => {
    const clamped = Math.min(1, Math.max(0, v));
    _setVolume(clamped);
    const a = audioRef.current;
    if (a) a.volume = clamped;
    try { localStorage.setItem('eph.volume', String(clamped)); } catch {}
  }, []);
  // Apply current volume whenever audio element is ready
  React.useEffect(() => {
    const a = audioRef.current;
    if (a) a.volume = volume;
  }, [volume]);

  return { state, track, listeners, history, toggle, audioRef, levels, live, volume, setVolume };
}

window.useEphemeralRadio = useEphemeralRadio;
