/* FREE MUSIC PACK 1 v2.1 — Free forever offline AAC
   Link admission · download rights · manual update schema
   NOT Freshest Frequencies. No autoplay. No silent network. */
(function () {
  'use strict';

  var VERSION = '2.1.0';
  var SHORT_CODE = 'FM1';
  var CARTRIDGE_ID = 'FREE_MUSIC_CARTRIDGE';
  var STORAGE_PREFIX = 'fm1_v2_1::';
  var SPOTIFY_HOSTS = { 'open.spotify.com': true };
  var YOUTUBE_HOSTS = { 'www.youtube.com': true, 'youtube.com': true, 'music.youtube.com': true };
  var MAX_CATALOG_BYTES = 65536;
  var MAX_TRACKS = 3;

  var root = document.getElementById('cartridge-root');
  var audio = null;
  var tracks = [];
  var rightsMap = { files: {} };
  var packMeta = {};
  var bitrate = '96';
  var current = 0;
  var playing = false;
  var filterQ = '';
  var lang = 'en';
  var lyricsCache = {};
  var lastUpdateResult = null;
  var statusMsg = '';

  var STR = {
    en: {
      free: 'FREE FOREVER',
      quality: 'Quality:',
      search: 'Search tracks',
      play: 'Play',
      pause: 'Pause',
      playAll: 'Play All',
      stop: 'Stop',
      lyrics: 'Karaoke / Lyrics',
      stream: 'Stream (explicit open only)',
      spotify: 'Open on Spotify',
      youtube: 'Find on YouTube',
      download: 'Download song',
      noDownload: 'Download not admitted',
      favorites: 'Favorite',
      unfavorite: 'Unfavorite',
      updates: 'Check for updates',
      noCatalog: 'No update catalog configured (fail closed)',
      favs: 'Favorites',
      offlineNote: 'Core player works offline. Streaming links need network after you click.'
    },
    es: {
      free: 'GRATIS PARA SIEMPRE',
      quality: 'Calidad:',
      search: 'Buscar canciones',
      play: 'Reproducir',
      pause: 'Pausa',
      playAll: 'Todo',
      stop: 'Parar',
      lyrics: 'Karaoke / Letras',
      stream: 'Streaming (solo con clic)',
      spotify: 'Abrir en Spotify',
      youtube: 'Buscar en YouTube',
      download: 'Descargar canción',
      noDownload: 'Descarga no admitida',
      favorites: 'Favorito',
      unfavorite: 'Quitar favorito',
      updates: 'Buscar actualizaciones',
      noCatalog: 'Sin catálogo de actualizaciones (cerrado)',
      favs: 'Favoritos',
      offlineNote: 'El reproductor funciona sin red. Los enlaces de streaming requieren red tras el clic.'
    }
  };

  function t(k) {
    return (STR[lang] || STR.en)[k] || STR.en[k] || k;
  }

  function sanitizeLang(c) {
    return c === 'es' || c === 'en' ? c : 'en';
  }

  function safeGet(k, d) {
    try {
      var v = localStorage.getItem(STORAGE_PREFIX + k);
      return v == null ? d : v;
    } catch (e) {
      return d;
    }
  }

  function safeSet(k, v) {
    try {
      localStorage.setItem(STORAGE_PREFIX + k, v);
      return true;
    } catch (e) {
      return false;
    }
  }

  function getFavorites() {
    try {
      var arr = JSON.parse(safeGet('favorites', '[]'));
      if (!Array.isArray(arr)) return [];
      return arr.filter(function (x) { return typeof x === 'string' && x.length < 80; }).slice(0, 50);
    } catch (e) {
      return [];
    }
  }

  function setFavorites(arr) {
    safeSet('favorites', JSON.stringify(arr.slice(0, 50)));
  }

  function el(tag, attrs, kids) {
    var n = document.createElement(tag);
    if (attrs) {
      Object.keys(attrs).forEach(function (k) {
        if (k === 'class' || k === 'className') n.className = attrs[k];
        else if (k === 'text') n.textContent = attrs[k];
        else if (k.indexOf('on') === 0 && typeof attrs[k] === 'function') {
          n.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
        } else if (attrs[k] != null) n.setAttribute(k, attrs[k]);
      });
    }
    (kids || []).forEach(function (c) {
      if (c == null) return;
      n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
    });
    return n;
  }

  /** Admit only https Spotify/YouTube official hosts — no credentials, no ports, no lookalikes. */
  function admitStreamUrl(url, kind) {
    if (!url || typeof url !== 'string' || url.length > 2000) return null;
    var u;
    try {
      u = new URL(url);
    } catch (e) {
      return null;
    }
    if (u.protocol !== 'https:') return null;
    if (u.username || u.password) return null;
    if (u.port) return null;
    var host = u.hostname.toLowerCase();
    if (kind === 'spotify') {
      if (!SPOTIFY_HOSTS[host]) return null;
    } else if (kind === 'youtube') {
      if (!YOUTUBE_HOSTS[host]) return null;
    } else return null;
    // reject encoded controls / backslash tricks in path
    if (/[\u0000-\u001f\\]/.test(url)) return null;
    return u.toString();
  }

  function isDownloadAdmitted(fileKey) {
    if (!fileKey || typeof fileKey !== 'string') return false;
    if (fileKey.indexOf('..') >= 0 || fileKey.indexOf('/') >= 0 || fileKey.indexOf('\\') >= 0) return false;
    var rec = rightsMap.files && rightsMap.files[fileKey];
    return !!(rec && rec.download_admitted === true && rec.sha256 && rec.size > 0);
  }

  function audioUrl(track) {
    return bitrate === '64' ? track.audio_64 : track.audio_96;
  }

  function stop() {
    if (audio) {
      try { audio.pause(); } catch (e) {}
      audio = null;
    }
    playing = false;
  }

  function playAt(i) {
    if (!tracks[i]) return;
    stop();
    current = i;
    var url = audioUrl(tracks[i]);
    if (!url || url.indexOf('://') >= 0) return; // only relative same-origin
    audio = new Audio(url);
    audio.setAttribute('playsinline', 'true');
    audio.onended = function () {
      if (current + 1 < tracks.length) playAt(current + 1);
      else {
        playing = false;
        render();
      }
    };
    // user-initiated play only
    audio.play().then(function () {
      playing = true;
      render();
    }).catch(function () {
      playing = false;
      statusMsg = 'Playback blocked by browser or missing file';
      render();
    });
  }

  function loadLyrics(track, cb) {
    if (!track || !track.lyrics_text) return cb('');
    if (lyricsCache[track.id]) return cb(lyricsCache[track.id]);
    fetch(track.lyrics_text)
      .then(function (r) { return r.text(); })
      .then(function (txt) {
        lyricsCache[track.id] = String(txt).slice(0, 100000);
        cb(lyricsCache[track.id]);
      })
      .catch(function () { cb(''); });
  }

  function filteredTracks() {
    var q = (filterQ || '').toLowerCase().trim();
    if (!q) return tracks.map(function (t, i) { return { t: t, i: i }; });
    return tracks.map(function (t, i) { return { t: t, i: i }; }).filter(function (x) {
      return (x.t.title || '').toLowerCase().indexOf(q) >= 0 || (x.t.id || '').toLowerCase().indexOf(q) >= 0;
    });
  }

  function hasProtoKey(obj) {
    if (!obj || typeof obj !== 'object') return false;
    return Object.prototype.hasOwnProperty.call(obj, '__proto__') ||
      Object.prototype.hasOwnProperty.call(obj, 'constructor') ||
      Object.prototype.hasOwnProperty.call(obj, 'prototype');
  }

  function validateUpdateCatalog(obj, rawLen) {
    if (rawLen > MAX_CATALOG_BYTES) return { ok: false, error: 'catalog too large' };
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return { ok: false, error: 'not object' };
    if (hasProtoKey(obj)) return { ok: false, error: 'prototype key rejected' };
    var allowed = {
      format: 1, catalog_version: 1, cartridge_id: 1, tracks: 1,
      generated_at: 1, advertised_cartridges: 1, notes: 1
    };
    for (var k in obj) {
      if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
      if (!allowed[k]) return { ok: false, error: 'unknown field: ' + k };
    }
    if (obj.format !== 'YOURIQ_MUSIC_UPDATE_CATALOG_v1') return { ok: false, error: 'bad format' };
    if (obj.cartridge_id !== CARTRIDGE_ID) return { ok: false, error: 'wrong cartridge_id' };
    if (typeof obj.catalog_version !== 'string' || !obj.catalog_version || obj.catalog_version.length > 32) {
      return { ok: false, error: 'bad catalog_version' };
    }
    if (!Array.isArray(obj.tracks) || obj.tracks.length > MAX_TRACKS) return { ok: false, error: 'bad tracks' };
    for (var i = 0; i < obj.tracks.length; i++) {
      var tr = obj.tracks[i];
      if (!tr || typeof tr !== 'object' || hasProtoKey(tr)) return { ok: false, error: 'bad track' };
      if (typeof tr.id !== 'string' || typeof tr.title !== 'string') return { ok: false, error: 'track fields' };
      if (tr.spotify && !admitStreamUrl(tr.spotify, 'spotify')) return { ok: false, error: 'bad spotify link' };
      if (tr.youtube && !admitStreamUrl(tr.youtube, 'youtube')) return { ok: false, error: 'bad youtube link' };
    }
    if (obj.advertised_cartridges != null) {
      if (!Array.isArray(obj.advertised_cartridges) || obj.advertised_cartridges.length > 20) {
        return { ok: false, error: 'bad advertised' };
      }
      for (var j = 0; j < obj.advertised_cartridges.length; j++) {
        var a = obj.advertised_cartridges[j];
        if (!a || typeof a !== 'object' || hasProtoKey(a)) return { ok: false, error: 'bad adv item' };
        if (typeof a.id !== 'string' || typeof a.title !== 'string') return { ok: false, error: 'adv fields' };
        if (a.url != null && typeof a.url !== 'string') return { ok: false, error: 'adv url type' };
        if (a.url) {
          // relative same-path OR https allowlist only
          if (a.url.charAt(0) === '/') {
            if (a.url.indexOf('//') >= 0 || a.url.indexOf('\\') >= 0) return { ok: false, error: 'adv url' };
          } else {
            try {
              var uu = new URL(a.url);
              if (uu.protocol !== 'https:' || uu.username || uu.password || uu.port) {
                return { ok: false, error: 'adv url scheme' };
              }
            } catch (e) {
              return { ok: false, error: 'adv url parse' };
            }
          }
        }
      }
    }
    // semver-ish: reject rollback if lower than installed pack version
    if (compareLooseSemver(obj.catalog_version, VERSION) < 0) {
      return { ok: false, error: 'rollback rejected' };
    }
    return { ok: true, catalog: obj };
  }

  function compareLooseSemver(a, b) {
    function parts(s) {
      return String(s).split(/[^0-9]+/).filter(Boolean).map(function (n) { return parseInt(n, 10) || 0; });
    }
    var pa = parts(a);
    var pb = parts(b);
    var n = Math.max(pa.length, pb.length);
    for (var i = 0; i < n; i++) {
      var x = pa[i] || 0;
      var y = pb[i] || 0;
      if (x < y) return -1;
      if (x > y) return 1;
    }
    return 0;
  }

  function checkForUpdates() {
    var url = packMeta.update_catalog_url;
    if (!url) {
      lastUpdateResult = { ok: false, error: 'no_catalog_configured' };
      statusMsg = t('noCatalog');
      render();
      return Promise.resolve(lastUpdateResult);
    }
    // same-origin relative only for safety
    if (typeof url !== 'string' || url.indexOf('://') >= 0) {
      lastUpdateResult = { ok: false, error: 'update url not same-origin relative' };
      statusMsg = lastUpdateResult.error;
      render();
      return Promise.resolve(lastUpdateResult);
    }
    return fetch(url, { credentials: 'same-origin', cache: 'no-store' })
      .then(function (r) {
        if (!r.ok) throw new Error('HTTP ' + r.status);
        return r.text();
      })
      .then(function (text) {
        if (text.length > MAX_CATALOG_BYTES) throw new Error('catalog too large');
        var obj = JSON.parse(text);
        var v = validateUpdateCatalog(obj, text.length);
        lastUpdateResult = v;
        statusMsg = v.ok ? ('Update catalog OK v' + obj.catalog_version) : ('Update rejected: ' + v.error);
        render();
        return v;
      })
      .catch(function (e) {
        lastUpdateResult = { ok: false, error: String(e.message || e) };
        statusMsg = 'Update failed: ' + lastUpdateResult.error;
        render();
        return lastUpdateResult;
      });
  }

  function openExternal(url) {
    // user gesture only — open without opener
    var w = window.open(url, '_blank', 'noopener,noreferrer');
    if (w) try { w.opener = null; } catch (e) {}
  }

  function render() {
    if (!root) return;
    while (root.firstChild) root.removeChild(root.firstChild);

    root.appendChild(el('span', { class: 'fm-tag', id: 'fm-free-tag', text: t('free') }));
    root.appendChild(el('h2', { class: 'fm-title', text: 'Free Music · Pack 1' }));
    root.appendChild(el('p', {
      class: 'fm-sub',
      text: '3 songs · AAC offline · Separate from Golden Ticket · Not Freshest Frequencies · © 2026 Bret Fencl'
    }));
    root.appendChild(el('p', { class: 'fm-hint', text: t('offlineNote') }));

    var search = el('input', {
      class: 'fm-search',
      id: 'fm-search',
      type: 'search',
      'aria-label': t('search'),
      placeholder: t('search'),
      value: filterQ
    });
    search.addEventListener('input', function () {
      filterQ = search.value.slice(0, 100);
      render();
      var s2 = document.getElementById('fm-search');
      if (s2) {
        s2.focus();
        try { s2.setSelectionRange(filterQ.length, filterQ.length); } catch (e) {}
      }
    });
    root.appendChild(search);

    // Bitrate
    var br = el('div', { class: 'fm-bitrate' });
    br.appendChild(el('span', { text: t('quality') + ' ' }));
    ['64', '96'].forEach(function (b) {
      br.appendChild(el('button', {
        class: 'fm-btn' + (bitrate === b ? ' on' : ''),
        type: 'button',
        text: b + ' kbps AAC',
        'aria-pressed': bitrate === b ? 'true' : 'false',
        onClick: function () {
          bitrate = b;
          safeSet('bitrate', b);
          var was = playing;
          var idx = current;
          stop();
          render();
          if (was) playAt(idx);
        }
      }));
    });
    root.appendChild(br);

    var list = el('div', { class: 'fm-list', role: 'list' });
    var favs = getFavorites();
    filteredTracks().forEach(function (item) {
      var track = item.t;
      var i = item.i;
      var card = el('div', { class: 'fm-card' + (current === i ? ' cur' : ''), role: 'listitem' });
      if (track.cover) {
        var img = el('img', {
          class: 'fm-cov',
          src: track.cover,
          alt: 'Cover art for ' + track.title
        });
        img.onerror = function () { img.style.display = 'none'; };
        card.appendChild(img);
      }
      card.appendChild(el('div', { class: 'fm-tt', text: track.title }));
      card.appendChild(el('button', {
        class: 'fm-btn',
        type: 'button',
        'aria-label': (current === i && playing ? t('pause') : t('play')) + ' ' + track.title,
        text: current === i && playing ? t('pause') : t('play'),
        onClick: function () {
          if (current === i && playing) {
            stop();
            render();
          } else playAt(i);
        }
      }));
      var isFav = favs.indexOf(track.id) >= 0;
      card.appendChild(el('button', {
        class: 'fm-btn ghost',
        type: 'button',
        text: isFav ? t('unfavorite') : t('favorites'),
        onClick: function () {
          var f = getFavorites();
          var ix = f.indexOf(track.id);
          if (ix >= 0) f.splice(ix, 1);
          else f.push(track.id);
          setFavorites(f);
          render();
        }
      }));
      list.appendChild(card);
    });
    root.appendChild(list);

    var controls = el('div', { class: 'fm-row' });
    controls.appendChild(el('button', {
      class: 'fm-btn', type: 'button', text: t('playAll'), id: 'fm-play-all',
      onClick: function () { playAt(0); }
    }));
    controls.appendChild(el('button', {
      class: 'fm-btn ghost', type: 'button', text: t('stop'), id: 'fm-stop',
      onClick: function () { stop(); render(); }
    }));
    root.appendChild(controls);

    var track = tracks[current] || tracks[0];
    var kara = el('div', { class: 'fm-karaoke' });
    kara.appendChild(el('h2', { class: 'fm-h2', text: t('lyrics') + ' — ' + (track ? track.title : '') }));
    var box = el('div', { class: 'fm-lyrics-box', id: 'fm-lyrics-box' });
    box.textContent = '…';
    kara.appendChild(box);
    if (track && track.lyrics_image) {
      kara.appendChild(el('img', {
        class: 'fm-lyrics-img',
        src: track.lyrics_image,
        alt: 'Lyric sheet for ' + track.title
      }));
    }
    root.appendChild(kara);
    if (track) {
      loadLyrics(track, function (txt) {
        var b = document.getElementById('fm-lyrics-box');
        if (!b) return;
        while (b.firstChild) b.removeChild(b.firstChild);
        if (txt && txt.trim()) {
          txt.split(/\r?\n/).forEach(function (line) {
            b.appendChild(el('div', {
              class: 'fm-line' + (line.trim() ? '' : ' blank'),
              text: line.length ? line : ' '
            }));
          });
        } else {
          b.textContent = 'Lyrics unavailable offline.';
        }
      });
    }

    // Stream + download
    var links = el('div', { class: 'fm-links' });
    links.appendChild(el('h2', { class: 'fm-h2', text: t('stream') }));
    if (track) {
      var sp = admitStreamUrl(track.spotify, 'spotify');
      if (sp) {
        var aSp = el('a', {
          class: 'fm-link',
          href: sp,
          target: '_blank',
          rel: 'noopener noreferrer',
          text: t('spotify') + ' — ' + track.title,
          id: 'fm-spotify'
        });
        aSp.addEventListener('click', function (e) {
          e.preventDefault();
          openExternal(sp);
        });
        links.appendChild(aSp);
      }
      var yt = admitStreamUrl(track.youtube, 'youtube');
      if (yt) {
        var aYt = el('a', {
          class: 'fm-link',
          href: yt,
          target: '_blank',
          rel: 'noopener noreferrer',
          text: t('youtube') + ' — ' + track.title,
          id: 'fm-youtube'
        });
        aYt.addEventListener('click', function (e) {
          e.preventDefault();
          openExternal(yt);
        });
        links.appendChild(aYt);
      }
      // Download song — only admitted local rights file
      var dKey = track.download_file_key || track.audio_96;
      if (track.download_admitted && isDownloadAdmitted(dKey)) {
        links.appendChild(el('a', {
          class: 'fm-link',
          href: dKey,
          download: dKey,
          id: 'fm-download',
          text: t('download') + ' — ' + track.title,
          rel: 'noopener'
        }));
      } else {
        links.appendChild(el('p', {
          class: 'fm-hint',
          id: 'fm-download-blocked',
          text: t('noDownload')
        }));
      }
    }
    root.appendChild(links);

    // Updates
    var adv = el('div', { class: 'fm-adv' });
    adv.appendChild(el('button', {
      class: 'fm-btn',
      type: 'button',
      id: 'fm-check-updates',
      text: t('updates'),
      onClick: function () { checkForUpdates(); }
    }));
    adv.appendChild(el('div', {
      class: 'fm-status',
      id: 'fm-status',
      role: 'status',
      'aria-live': 'polite',
      text: statusMsg || ''
    }));
    // advertised cartridges from last good catalog
    if (lastUpdateResult && lastUpdateResult.ok && lastUpdateResult.catalog && lastUpdateResult.catalog.advertised_cartridges) {
      var wrap = el('div', { id: 'fm-advertised' });
      wrap.appendChild(el('h3', { class: 'fm-h2', text: 'Newly advertised (text only)' }));
      lastUpdateResult.catalog.advertised_cartridges.forEach(function (a) {
        var line = el('div', { class: 'fm-hint' });
        line.appendChild(document.createTextNode(a.id + ' — ' + a.title));
        if (a.url && a.url.charAt(0) === '/') {
          // relative same-origin link only
          line.appendChild(document.createTextNode(' · '));
          line.appendChild(el('a', {
            class: 'fm-link',
            href: a.url,
            rel: 'noopener noreferrer',
            text: a.url
          }));
        }
        wrap.appendChild(line);
      });
      adv.appendChild(wrap);
    }
    root.appendChild(adv);

    root.appendChild(el('p', {
      class: 'fm-copy',
      text: '© 2026 Bret Fencl · BRET.FENCL.1 · All Rights Reserved · FM1 v' + VERSION
    }));
  }

  function setLang(code) {
    lang = sanitizeLang(code);
    safeSet('lang', lang);
    document.documentElement.lang = lang;
    var en = document.getElementById('lang-en');
    var es = document.getElementById('lang-es');
    if (en) {
      en.className = 'lang-btn' + (lang === 'en' ? ' on' : '');
      en.setAttribute('aria-pressed', lang === 'en' ? 'true' : 'false');
    }
    if (es) {
      es.className = 'lang-btn' + (lang === 'es' ? ' on' : '');
      es.setAttribute('aria-pressed', lang === 'es' ? 'true' : 'false');
    }
    render();
  }

  function getState() {
    return {
      version: VERSION,
      shortCode: SHORT_CODE,
      cartridgeId: CARTRIDGE_ID,
      lang: lang,
      bitrate: bitrate,
      trackCount: tracks.length,
      current: current,
      playing: playing,
      favorites: getFavorites(),
      lastUpdate: lastUpdateResult,
      storagePrefix: STORAGE_PREFIX
    };
  }

  window.FM = {
    VERSION: VERSION,
    SHORT_CODE: SHORT_CODE,
    admitStreamUrl: admitStreamUrl,
    isDownloadAdmitted: isDownloadAdmitted,
    validateUpdateCatalog: validateUpdateCatalog,
    checkForUpdates: checkForUpdates,
    setLang: setLang,
    getState: getState,
    playAt: playAt,
    stop: stop,
    getTracks: function () { return tracks.slice(); },
    getRights: function () { return rightsMap; }
  };

  function init() {
    lang = sanitizeLang(safeGet('lang', 'en'));
    bitrate = safeGet('bitrate', '96') === '64' ? '64' : '96';
    var en = document.getElementById('lang-en');
    var es = document.getElementById('lang-es');
    if (en) en.addEventListener('click', function () { setLang('en'); });
    if (es) es.addEventListener('click', function () { setLang('es'); });

    Promise.all([
      fetch('tracks.json').then(function (r) { return r.json(); }),
      fetch('download_rights.json').then(function (r) { return r.json(); })
    ]).then(function (pair) {
      packMeta = pair[0] || {};
      rightsMap = pair[1] || { files: {} };
      var rawTracks = Array.isArray(packMeta.tracks) ? packMeta.tracks.slice(0, MAX_TRACKS) : [];
      tracks = rawTracks.filter(function (tr) {
        return tr && typeof tr.id === 'string' && typeof tr.title === 'string';
      });
      setLang(lang);
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('service-worker.js').catch(function () {});
      }
    }).catch(function () {
      if (root) root.textContent = 'Could not load tracks/rights.';
    });
  }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
  else init();
})();
