// shared utilities, BeatLeader API hook, icons, common data
// BeatLeader's public API has no CORS headers — fetched via our /api/bl proxy
const BL_API = '/api/bl';
const BL_REFRESH_MS = 60_000; // poll every minute for "live" feel

function useBeatLeader(playerId) {
  const [state, setState] = React.useState({
    scores: [],
    player: null,
    prevPlayer: null,
    loading: true,
    source: 'init', // 'init' | 'live' | 'offline'
    lastUpdate: null,
  });

  React.useEffect(() => {
    if (!playerId) {
      setState({ scores: [], player: null, prevPlayer: null, loading: false, source: 'offline', lastUpdate: null });
      return;
    }
    let cancelled = false;
    let timer = null;

    async function load(isInitial) {
      if (isInitial) setState((s) => ({ ...s, loading: true }));
      try {
        const [scoresRes, playerRes] = await Promise.all([
          fetch(`${BL_API}/player/${playerId}/scores?sortBy=date&order=desc&count=6&page=1`),
          fetch(`${BL_API}/player/${playerId}`),
        ]);
        if (!scoresRes.ok) throw new Error('scores fetch failed: ' + scoresRes.status);
        const scoresData = await scoresRes.json();
        const playerData = playerRes.ok ? await playerRes.json() : null;
        if (cancelled) return;
        const normalized = (scoresData.data || []).map(normalizeScore).filter(Boolean);
        if (normalized.length === 0) throw new Error('no scores');
        const nextPlayer = playerData ? normalizePlayer(playerData) : null;
        setState((prev) => ({
          scores: normalized,
          player: nextPlayer,
          prevPlayer: prev.source === 'live' ? prev.player : null,
          loading: false,
          source: 'live',
          lastUpdate: Date.now(),
        }));
      } catch (e) {
        if (cancelled) return;
        // keep last good live data if we ever had it; otherwise show offline
        setState((s) => s.source === 'live'
          ? { ...s, loading: false }
          : { scores: [], player: null, prevPlayer: null, loading: false, source: 'offline', lastUpdate: null }
        );
      }
    }
    load(true);
    timer = setInterval(() => load(false), BL_REFRESH_MS);
    return () => { cancelled = true; if (timer) clearInterval(timer); };
  }, [playerId]);

  return state;
}

function normalizeScore(s) {
  if (!s) return null;
  const lb = s.leaderboard || {};
  const song = lb.song || {};
  const diff = lb.difficulty || {};
  return {
    id: s.id,
    songName: song.name || 'Unknown',
    songAuthor: song.author || '',
    mapper: song.mapper || '',
    coverImage: song.coverImage || song.fullCoverImage || null,
    bpm: song.bpm || null,
    difficulty: diff.difficultyName || 'Expert',
    mode: diff.modeName || 'Standard',
    stars: diff.stars || 0,
    accuracy: s.accuracy || (s.baseScore && diff.maxScore ? s.baseScore / diff.maxScore : 0),
    pp: s.pp || 0,
    rank: s.rank || 0,
    timeset: Number(s.timeset || s.timepost) || (Date.now() / 1000),
    fc: !!s.fullCombo,
    misses: s.missedNotes || 0,
    modifiers: s.modifiers || '',
  };
}

function normalizePlayer(p) {
  return {
    name: p.name || 'notnic',
    avatar: p.avatar,
    pp: p.pp || 0,
    country: p.country || 'US',
    rank: p.rank || 0,
    countryRank: p.countryRank || 0,
  };
}

function relativeTime(timestamp) {
  const now = Date.now() / 1000;
  const diff = Math.max(0, now - timestamp);
  if (diff < 60) return 'just now';
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
  if (diff < 86400 * 30) return `${Math.floor(diff / 86400)}d ago`;
  return `${Math.floor(diff / 86400 / 30)}mo ago`;
}

function pct(x, digits = 2) { return (x * 100).toFixed(digits) + '%'; }

const DIFF_SHORT = { ExpertPlus: 'EX+', Expert: 'EXP', Hard: 'HARD', Normal: 'NRM', Easy: 'EZ' };
function diffShort(d) { return DIFF_SHORT[d] || d; }

const DIFF_COLOR_DEFAULT = { ExpertPlus: '#a855f7', Expert: '#dc2626', Hard: '#f97316', Normal: '#3b82f6', Easy: '#22c55e' };
function diffColor(d) { return DIFF_COLOR_DEFAULT[d] || '#888'; }

// percentile rank from a player's rank vs an assumed BL active pop
function rankPercentile(rank, total = 60000) {
  if (!rank) return 0;
  return Math.max(0, Math.min(1, 1 - rank / total));
}

// ── hooks ──────────────────────────────────────────────────────────────
function useTicker(intervalMs = 1000) {
  const [, force] = React.useState(0);
  React.useEffect(() => {
    const t = setInterval(() => force((n) => n + 1), intervalMs);
    return () => clearInterval(t);
  }, [intervalMs]);
}

// Konami: ↑↑↓↓←→←→BA — onActivate fires once per match
function useKonami(onActivate) {
  React.useEffect(() => {
    const seq = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
    let buf = [];
    function handler(e) {
      const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
      buf.push(k);
      if (buf.length > seq.length) buf = buf.slice(-seq.length);
      if (buf.length === seq.length && buf.every((c, i) => c === seq[i])) {
        buf = [];
        onActivate && onActivate();
      }
    }
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [onActivate]);
}

// keyboard up/down nav for a list of length n; returns {idx, setIdx}
function useArrowNav(n, onEnter) {
  const [idx, setIdx] = React.useState(-1);
  React.useEffect(() => {
    function h(e) {
      // ignore when typing
      const tag = (e.target.tagName || '').toLowerCase();
      if (tag === 'input' || tag === 'textarea') return;
      if (e.key === 'ArrowDown') { e.preventDefault(); setIdx((i) => Math.min(n - 1, i < 0 ? 0 : i + 1)); }
      else if (e.key === 'ArrowUp') { e.preventDefault(); setIdx((i) => Math.max(0, i < 0 ? 0 : i - 1)); }
      else if (e.key === 'Enter' && idx >= 0 && onEnter) { onEnter(idx); }
    }
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [n, idx, onEnter]);
  return { idx, setIdx };
}

// ─── icons ─── strokeable line icons sized via currentColor ───
function Icon({ name, size = 18, strokeWidth = 2, style }) {
  const props = {
    width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
    stroke: 'currentColor', strokeWidth, strokeLinecap: 'round', strokeLinejoin: 'round',
    style,
  };
  switch (name) {
    case 'instagram':
      return (<svg {...props}><rect x="3" y="3" width="18" height="18" rx="5"/><circle cx="12" cy="12" r="4"/><circle cx="17.5" cy="6.5" r=".7" fill="currentColor" stroke="none"/></svg>);
    case 'snapchat':
      return (<svg {...props}><path d="M12 3c2.8 0 4.5 2.2 4.5 4.8 0 .8-.1 1.6-.2 2.3.6.3 1.4.3 2 .2.5-.1.7.4.4.8-.6.7-1.5 1-2.3 1.3.2.6.5 1.2 1 1.7.8.8 1.8 1 2.8 1.1.5 0 .6.7.1.9-.9.4-2 .7-2.6.7-.2.4-.1 1-.4 1.2-.4.2-1-.1-1.5-.1-1 0-1.7.3-2.5 1-.5.4-1.4.7-2.3.7-.9 0-1.8-.3-2.3-.7-.8-.7-1.5-1-2.5-1-.5 0-1.1.3-1.5.1-.3-.2-.2-.8-.4-1.2-.6 0-1.7-.3-2.6-.7-.5-.2-.4-.9.1-.9 1-.1 2-.3 2.8-1.1.5-.5.8-1.1 1-1.7-.8-.3-1.7-.6-2.3-1.3-.3-.4-.1-.9.4-.8.6.1 1.4.1 2-.2-.1-.7-.2-1.5-.2-2.3C7.5 5.2 9.2 3 12 3z"/></svg>);
    case 'linkedin':
      return (<svg {...props}><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 10v7M8 7v.01M12 17v-4a2 2 0 0 1 4 0v4M12 10v7"/></svg>);
    case 'facebook':
      return (<svg {...props}><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/></svg>);
    case 'gamepad':
      return (<svg {...props}><line x1="6" y1="11" x2="10" y2="11"/><line x1="8" y1="9" x2="8" y2="13"/><line x1="15" y1="12" x2="15.01" y2="12"/><line x1="18" y1="10" x2="18.01" y2="10"/><path d="M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z"/></svg>);
    case 'heart':
      return (<svg {...props} fill="currentColor" stroke="none"><path d="M12 21s-7-4.5-9.5-9C.8 8.5 2.5 4 6.5 4c2 0 3.5 1 5.5 3 2-2 3.5-3 5.5-3 4 0 5.7 4.5 4 8-2.5 4.5-9.5 9-9.5 9z"/></svg>);
    case 'external':
      return (<svg {...props}><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>);
    case 'play':
      return (<svg {...props} fill="currentColor" stroke="none"><path d="M8 5v14l11-7z"/></svg>);
    case 'trophy':
      return (<svg {...props}><path d="M8 21h8M12 17v4M7 4h10v5a5 5 0 0 1-10 0z"/><path d="M17 4h3v3a3 3 0 0 1-3 3M7 4H4v3a3 3 0 0 0 3 3"/></svg>);
    case 'flag':
      return (<svg {...props}><path d="M4 21V4a1 1 0 0 1 1-1h13l-2 5 2 5H5"/></svg>);
    case 'sparkle':
      return (<svg {...props} fill="currentColor" stroke="none"><path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z"/></svg>);
    case 'cartridge':
      return (<svg {...props}><path d="M4 5h12l3 3v11H4z"/><path d="M7 9h6M7 12h5"/></svg>);
    case 'chip':
      return (<svg {...props}><rect x="6" y="6" width="12" height="12" rx="1"/><path d="M9 3v3M12 3v3M15 3v3M9 18v3M12 18v3M15 18v3M3 9h3M3 12h3M3 15h3M18 9h3M18 12h3M18 15h3"/></svg>);
    case 'wifi':
      return (<svg {...props}><path d="M5 12.55a11 11 0 0 1 14 0M8.5 16a6.5 6.5 0 0 1 7 0M12 20h.01"/></svg>);
    case 'arrow-up':
      return (<svg {...props}><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>);
    case 'arrow-down':
      return (<svg {...props}><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>);
    case 'pause':
      return (<svg {...props}><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>);
    default:
      return null;
  }
}

// ─── common data ───
const SOCIALS = [
  { id: 'instagram', label: 'instagram', short: 'IG', url: 'https://s2.tracemyip.org/vLg/lkh.php?c=4&s=2&l=https%3A%2F%2Finstagram.com%2Fitisnic&p=2&d=MTAzODU4&i=MQ==&lVer=2', icon: 'instagram' },
  { id: 'snapchat',  label: 'snapchat',  short: 'SC', url: 'https://s2.tracemyip.org/vLg/lkh.php?c=4&s=2&l=https%3A%2F%2Fsnapchat.com%2Ft%2FEC6uZOzz&p=2&d=MTAzODU4&i=Mg==&lVer=2', icon: 'snapchat' },
  { id: 'linkedin',  label: 'linkedin',  short: 'LI', url: 'https://s2.tracemyip.org/vLg/lkh.php?c=4&s=2&l=https%3A%2F%2Flinkedin.com%2Fin%2Fnichilde&p=2&d=MTAzODU4&i=Mw==&lVer=2', icon: 'linkedin' },
  { id: 'facebook',  label: 'facebook',  short: 'FB', url: 'https://s2.tracemyip.org/vLg/lkh.php?c=4&s=2&l=https%3A%2F%2Ffacebook.com%2Fshare%2F14MYgCvL3sU%2F%3Fmibextid%3DwwXIfr&p=2&d=MTAzODU4&i=NA==&lVer=2', icon: 'facebook' },
  { id: 'beatleader',label: 'beatleader',short: 'BL', url: 'https://s2.tracemyip.org/vLg/lkh.php?c=4&s=2&l=https%3A%2F%2Fbeatleader.com%2Fu%2Fnic%2F&p=2&d=MTAzODU4&i=NQ==&lVer=2', icon: 'gamepad' },
];

const APPS = [
  { id: 'wnrs',      label: "we're not really strangers",   desc: 'browser port of the iconic card game',           status: 'SHIPPED',     url: 'https://s2.tracemyip.org/vLg/lkh.php?c=4&s=2&l=https%3A%2F%2Fnotreallynic.com%2F&p=2&d=MTAzODU4&i=Ng==&lVer=2',          icon: 'heart' },
  { id: 'codenames', label: 'codenames online',             desc: 'realtime multiplayer codenames clone',           status: 'SHIPPED',     url: 'https://s2.tracemyip.org/vLg/lkh.php?c=4&s=2&l=https%3A%2F%2Fnotcodenames.com%2F&p=2&d=MTAzODU4&i=Nw==&lVer=2',         icon: 'gamepad' },
  { id: 'bl-replay', label: 'beat leader replay downloader',desc: 'grab .bsor files for any BL score',              status: 'SHIPPED',     url: 'https://s2.tracemyip.org/vLg/lkh.php?c=4&s=2&l=https%3A%2F%2Fnotbeatsaber.com%2F&p=2&d=MTAzODU4&i=OA==&lVer=2',        icon: 'cartridge' },
  { id: 'bl-pp',     label: 'beatleader pp optimizer',      desc: 'finds the maps that will gain you the most pp',  status: 'WIP',         url: 'https://s2.tracemyip.org/vLg/lkh.php?c=4&s=2&l=https%3A%2F%2Fgivemepp.com%2F&p=2&d=MTAzODU4&i=OQ==&lVer=2',          icon: 'chip' },
];

Object.assign(window, {
  useBeatLeader, useTicker, useKonami, useArrowNav,
  SOCIALS, APPS,
  relativeTime, pct, diffShort, diffColor, rankPercentile,
  Icon,
});
