<script lang="ts">
  import { onMount } from 'svelte';
  import MapCard from '$lib/components/MapCard.svelte';

  type BeatLeaderScore = {
    timeset?: string | number;
    accuracy?: number;
    acc?: number;
    rank?: number;
    leaderboard?: {
      id?: string | number | null;
      leaderboardId?: string | number | null;
      song?: { hash?: string | null };
      difficulty?: { value?: number | string | null; modeName?: string | null };
    };
  };

  type BeatLeaderScoresResponse = { data?: BeatLeaderScore[] };

  type MapMeta = {
    songName?: string;
    key?: string;
    coverURL?: string;
    mapper?: string;
  };

  type H2HItem = {
    hash: string;
    diffName: string;
    modeName: string;
    timeset: number; // most recent between the two
    accA: number | null;
    accB: number | null;
    rankA?: number;
    rankB?: number;
    leaderboardId?: string;
  };

  const difficulties = ['Easy', 'Normal', 'Hard', 'Expert', 'ExpertPlus'];
  const modes = ['Standard', 'Lawless', 'OneSaber', 'NoArrows', 'Lightshow'];
  const ONE_YEAR_SECONDS = 365 * 24 * 60 * 60;

  let playerA = '';
  let playerB = '';
  let months: number | string = 6;

  // UI state
  let loading = false;
  let errorMsg: string | null = null;

  // Results state
  let items: H2HItem[] = [];
  let sortDir: 'asc' | 'desc' = 'desc';
  let page = 1;
  let pageSize: number | string = 24;
  let filterWinMargin: string = 'all'; // 'all', '1', '2'
  $: pageSizeNum = Number(pageSize) || 24;
  $: filtered = (() => {
    if (filterWinMargin === 'all') return items;
    const margin = Number(filterWinMargin);
    return items.filter(i => {
      if (i.accA == null || i.accB == null) return false;
      return i.accA > i.accB && (i.accA - i.accB) > margin;
    });
  })();
  $: sorted = [...filtered].sort((a, b) => (sortDir === 'asc' ? a.timeset - b.timeset : b.timeset - a.timeset));
  $: totalPages = Math.max(1, Math.ceil(sorted.length / pageSizeNum));
  $: page = Math.min(page, totalPages);
  $: pageItems = sorted.slice((page - 1) * pageSizeNum, (page - 1) * pageSizeNum + pageSizeNum);

  // Meta cache
  let metaByHash: Record<string, MapMeta> = {};
  let loadingMeta = false;

  function normalizeAccuracy(value: number | undefined): number | null {
    if (value === undefined || value === null) return null;
    return value <= 1 ? value * 100 : value;
  }

  function normalizeDifficultyName(value: number | string | null | undefined): string {
    if (value === null || value === undefined) return 'ExpertPlus';
    if (typeof value === 'string') {
      const v = value.toLowerCase();
      if (v.includes('expertplus') || v === 'expertplus' || v === 'ex+' || v.includes('ex+')) return 'ExpertPlus';
      if (v.includes('expert')) return 'Expert';
      if (v.includes('hard')) return 'Hard';
      if (v.includes('normal')) return 'Normal';
      if (v.includes('easy')) return 'Easy';
      return value;
    }
    switch (value) {
      case 1: return 'Easy';
      case 3: return 'Normal';
      case 5: return 'Hard';
      case 7: return 'Expert';
      case 9: return 'ExpertPlus';
      default: return 'ExpertPlus';
    }
  }

  function parseTimeset(ts: string | number | undefined): number {
    if (ts === undefined) return 0;
    if (typeof ts === 'number') return ts;
    const n = Number(ts);
    return Number.isFinite(n) ? n : 0;
  }

  function getCutoffEpochFromMonths(m: number | string): number {
    const monthsNum = Number(m) || 0;
    const seconds = Math.max(0, monthsNum) * 30 * 24 * 60 * 60;
    return Math.floor(Date.now() / 1000) - seconds;
  }

  async function fetchBeatSaverMeta(inHash: string): Promise<MapMeta | null> {
    try {
      const res = await fetch(`https://api.beatsaver.com/maps/hash/${encodeURIComponent(inHash)}`);
      if (!res.ok) throw new Error(String(res.status));
      const data: any = await res.json();
      const cover = data?.versions?.[0]?.coverURL ?? `https://cdn.beatsaver.com/${inHash.toLowerCase()}.jpg`;
      return {
        songName: data?.metadata?.songName ?? data?.name ?? undefined,
        key: data?.id ?? undefined,
        coverURL: cover,
        mapper: data?.uploader?.name ?? undefined
      };
    } catch {
      return { coverURL: `https://cdn.beatsaver.com/${inHash.toLowerCase()}.jpg` };
    }
  }

  async function loadMetaForResults(list: H2HItem[]): Promise<void> {
    const needed = Array.from(new Set(list.map((i) => i.hash))).filter((h) => !metaByHash[h]);
    if (needed.length === 0) return;
    loadingMeta = true;
    for (const h of needed) {
      const m = await fetchBeatSaverMeta(h);
      if (m) metaByHash = { ...metaByHash, [h]: m };
    }
    loadingMeta = false;
  }

  async function fetchAllRecentScoresForDiff(playerId: string, cutoffEpoch: number, reqDiff: string, maxPages = 100): Promise<BeatLeaderScore[]> {
    const qs = new URLSearchParams({ diff: reqDiff, cutoffEpoch: String(cutoffEpoch), maxPages: String(maxPages) });
    const url = `/api/beatleader-cache/player/${encodeURIComponent(playerId)}?${qs.toString()}`;
    const res = await fetch(url);
    if (!res.ok) throw new Error(`Failed to fetch scores for ${playerId}: ${res.status}`);
    const data = (await res.json()) as BeatLeaderScoresResponse;
    return data.data ?? [];
  }

  async function fetchAllRecentScoresAllDiffs(playerId: string, cutoffEpoch: number): Promise<BeatLeaderScore[]> {
    const arrays = await Promise.all(
      difficulties.map((d) => fetchAllRecentScoresForDiff(playerId, cutoffEpoch, d))
    );
    // Merge and dedupe by leaderboard key (hash|diff|mode) and timeset
    const merged = new Map<string, BeatLeaderScore>();
    for (const arr of arrays) {
      for (const s of arr) {
        const rawHash = s.leaderboard?.song?.hash ?? undefined;
        const modeName = s.leaderboard?.difficulty?.modeName ?? 'Standard';
        if (!rawHash) continue;
        const hashLower = String(rawHash).toLowerCase();
        const diffName = normalizeDifficultyName(s.leaderboard?.difficulty?.value ?? undefined);
        const key = `${hashLower}|${diffName}|${modeName}`;
        const prev = merged.get(key);
        if (!prev || parseTimeset(prev.timeset) < parseTimeset(s.timeset)) merged.set(key, s);
      }
    }
    return Array.from(merged.values());
  }

  function buildLatestByKey(scores: BeatLeaderScore[], cutoffEpoch: number): Map<string, BeatLeaderScore> {
    const byKey = new Map<string, BeatLeaderScore>();
    for (const s of scores) {
      const t = parseTimeset(s.timeset);
      if (!t || t < cutoffEpoch - ONE_YEAR_SECONDS) continue; // sanity guard
      const rawHash = s.leaderboard?.song?.hash ?? undefined;
      const modeName = s.leaderboard?.difficulty?.modeName ?? 'Standard';
      if (!rawHash) continue;
      const hashLower = String(rawHash).toLowerCase();
      const diffName = normalizeDifficultyName(s.leaderboard?.difficulty?.value ?? undefined);
      const key = `${hashLower}|${diffName}|${modeName}`;
      const prev = byKey.get(key);
      if (!prev || parseTimeset(prev.timeset) < t) byKey.set(key, s);
    }
    return byKey;
  }

  function toH2HItems(aMap: Map<string, BeatLeaderScore>, bMap: Map<string, BeatLeaderScore>): H2HItem[] {
    const out: H2HItem[] = [];
    for (const [key, aScore] of aMap) {
      if (!bMap.has(key)) continue;
      const bScore = bMap.get(key)!;
      const [hashLower, diffName, modeName] = key.split('|');
      const rawHash = (aScore.leaderboard?.song?.hash ?? bScore.leaderboard?.song?.hash ?? hashLower);
      const hash = String(rawHash).toLowerCase();
      const tA = parseTimeset(aScore.timeset);
      const tB = parseTimeset(bScore.timeset);
      const accA = normalizeAccuracy((aScore.accuracy ?? aScore.acc) as number | undefined);
      const accB = normalizeAccuracy((bScore.accuracy ?? bScore.acc) as number | undefined);
      const lbIdRaw = (aScore.leaderboard as any)?.id ?? (aScore.leaderboard as any)?.leaderboardId ?? (bScore.leaderboard as any)?.id ?? (bScore.leaderboard as any)?.leaderboardId;
      const leaderboardId = lbIdRaw != null ? String(lbIdRaw) : undefined;
      out.push({
        hash,
        diffName,
        modeName,
        timeset: Math.max(tA, tB),
        accA,
        accB,
        rankA: aScore.rank,
        rankB: bScore.rank,
        leaderboardId
      });
    }
    return out;
  }

  async function onCompare(ev: SubmitEvent) {
    ev.preventDefault();
    // Push current params into URL on user action
    updateUrl(false);
    errorMsg = null; items = []; metaByHash = {};
    const a = playerA.trim();
    const b = playerB.trim();
    if (!a || !b) { errorMsg = 'Please enter both Player A and Player B IDs.'; return; }
    loading = true;
    try {
      const cutoff = getCutoffEpochFromMonths(months);
      const [aScores, bScores] = await Promise.all([
        fetchAllRecentScoresAllDiffs(a, cutoff),
        fetchAllRecentScoresAllDiffs(b, cutoff)
      ]);
      const aLatest = buildLatestByKey(aScores, cutoff - ONE_YEAR_SECONDS);
      const bLatest = buildLatestByKey(bScores, cutoff - ONE_YEAR_SECONDS);
      const combined = toH2HItems(aLatest, bLatest);
      combined.sort((x, y) => y.timeset - x.timeset);
      items = combined;
      page = 1;
      loadMetaForResults(combined);
    } catch (err) {
      errorMsg = err instanceof Error ? err.message : 'Unknown error';
    } finally {
      loading = false;
    }
  }

  onMount(() => {
    const sp = new URLSearchParams(location.search);
    playerA = sp.get('a') ?? '';
    playerB = sp.get('b') ?? '';
    const m = sp.get('m') ?? sp.get('months');
    if (m) months = Number(m);
    const p = sp.get('page');
    if (p) page = Math.max(1, Number(p) || 1);
    const dir = sp.get('dir');
    if (dir === 'asc' || dir === 'desc') sortDir = dir;
    const size = sp.get('size') ?? sp.get('ps');
    if (size) pageSize = Number(size) || pageSize;
    const filter = sp.get('filter');
    if (filter && ['all', '1', '2'].includes(filter)) filterWinMargin = filter;
    initialized = true;
  });

  // Short labels for players
  $: playerAId = playerA.trim();
  $: playerBId = playerB.trim();
  $: idShortA = playerAId ? playerAId.slice(0, 6) : 'A';
  $: idShortB = playerBId ? playerBId.slice(0, 6) : 'B';

  // URL param sync
  let initialized = false;
  let urlSyncInProgress = false;
  function updateUrl(replace = true) {
    if (!initialized || urlSyncInProgress) return;
    try {
      urlSyncInProgress = true;
      const sp = new URLSearchParams(location.search);
      if (playerAId) sp.set('a', playerAId); else sp.delete('a');
      if (playerBId) sp.set('b', playerBId); else sp.delete('b');
      sp.delete('diff');
      sp.delete('mode');
      const monthsVal = Number(months) || 0;
      if (monthsVal) sp.set('months', String(monthsVal)); else sp.delete('months');
      if (page > 1) sp.set('page', String(page)); else sp.delete('page');
      if (sortDir !== 'desc') sp.set('dir', sortDir); else sp.delete('dir');
      if (pageSizeNum !== 24) sp.set('size', String(pageSizeNum)); else sp.delete('size');
      if (filterWinMargin !== 'all') sp.set('filter', filterWinMargin); else sp.delete('filter');
      const qs = sp.toString();
      const url = location.pathname + (qs ? `?${qs}` : '');
      if (replace) history.replaceState(null, '', url); else history.pushState(null, '', url);
    } finally {
      urlSyncInProgress = false;
    }
  }

  // Sync URL on state changes
  $: updateUrl(true);

  // ===== Derived Stats for Visualizations =====
  function mean(values: number[]): number {
    if (!values.length) return 0;
    return values.reduce((a, b) => a + b, 0) / values.length;
  }
  function median(values: number[]): number {
    if (!values.length) return 0;
    const v = [...values].sort((a, b) => a - b);
    const mid = Math.floor(v.length / 2);
    return v.length % 2 ? v[mid] : (v[mid - 1] + v[mid]) / 2;
  }
  function percentile(values: number[], p: number): number {
    if (!values.length) return 0;
    const v = [...values].sort((a, b) => a - b);
    const idx = Math.min(v.length - 1, Math.max(0, Math.floor((p / 100) * (v.length - 1))));
    return v[idx];
  }

  $: comparable = items.filter((i) => i.accA != null && i.accB != null) as Array<{
    hash: string; diffName: string; modeName: string; timeset: number; accA: number; accB: number; rankA?: number; rankB?: number; leaderboardId?: string;
  }>;
  $: totalComparable = comparable.length;
  $: winsA = comparable.filter((i) => i.accA > i.accB).length;
  $: winsB = comparable.filter((i) => i.accB > i.accA).length;
  $: ties = comparable.filter((i) => i.accA === i.accB).length;

  $: accAList = comparable.map((i) => i.accA);
  $: accBList = comparable.map((i) => i.accB);
  $: avgAccA = mean(accAList);
  $: avgAccB = mean(accBList);
  $: medAccA = median(accAList);
  $: medAccB = median(accBList);

  $: deltas = comparable.map((i) => i.accA - i.accB);
  $: absMargins = deltas.map((x) => Math.abs(x));
  $: avgMargin = mean(absMargins);
  $: p95Margin = percentile(absMargins, 95);

  $: chronological = [...comparable].sort((a, b) => a.timeset - b.timeset);
  $: longestA = (() => {
    let cur = 0, best = 0;
    for (const i of chronological) {
      if (i.accA > i.accB) { cur += 1; best = Math.max(best, cur); } else if (i.accA === i.accB) { cur = 0; } else { cur = 0; }
    }
    return best;
  })();
  $: longestB = (() => {
    let cur = 0, best = 0;
    for (const i of chronological) {
      if (i.accB > i.accA) { cur += 1; best = Math.max(best, cur); } else if (i.accA === i.accB) { cur = 0; } else { cur = 0; }
    }
    return best;
  })();

  // Win share (bar + donut)
  $: shareA = totalComparable ? winsA / totalComparable : 0;
  $: shareT = totalComparable ? ties / totalComparable : 0;
  $: shareB = totalComparable ? winsB / totalComparable : 0;
  // Donut chart calculations
  const donutR = 44;
  $: donutCircumference = 2 * Math.PI * donutR;
  $: donutALen = donutCircumference * shareA;
  $: donutTLen = donutCircumference * shareT;
  $: donutBLen = donutCircumference * shareB;

  // Histogram of signed margins (A - B)
  $: maxAbsDelta = Math.max(0, ...deltas.map((x) => Math.abs(x)));
  $: histBins = 14;
  $: histRangeMin = -maxAbsDelta;
  $: histRangeMax = maxAbsDelta;
  $: histBinWidth = histBins ? (histRangeMax - histRangeMin) / histBins : 1;
  $: histogram = (() => {
    const bins = Array.from({ length: histBins }, () => 0);
    for (const x of deltas) {
      if (!Number.isFinite(x)) continue;
      let idx = Math.floor((x - histRangeMin) / histBinWidth);
      if (idx < 0) idx = 0; if (idx >= histBins) idx = histBins - 1;
      bins[idx] += 1;
    }
    return bins;
  })();
  $: histMaxCount = Math.max(1, ...histogram);

  // Cumulative wins over time sparkline
  $: cumSeries = (() => {
    const pts: { t: number; a: number; b: number }[] = [];
    let a = 0, b = 0;
    for (const i of chronological) {
      if (i.accA > i.accB) a += 1; else if (i.accB > i.accA) b += 1;
      pts.push({ t: i.timeset, a, b });
    }
    return pts;
  })();
  $: tMin = cumSeries.length ? cumSeries[0].t : 0;
  $: tMax = cumSeries.length ? cumSeries[cumSeries.length - 1].t : 1;
  function mapX(t: number, w: number): number {
    if (tMax === tMin) return 0;
    return ((t - tMin) / (tMax - tMin)) * w;
  }
  function mapY(v: number, h: number, vMax?: number): number {
    const maxV = vMax ?? Math.max(1, cumSeries.length ? Math.max(cumSeries[cumSeries.length - 1].a, cumSeries[cumSeries.length - 1].b) : 1);
    return h - (v / maxV) * h;
  }
</script>

<section class="py-8">
  <h1 class="font-display text-3xl sm:text-4xl">BeatLeader: Head-to-Head</h1>
  <p class="mt-2 text-muted">Paginated head-to-head results on the same map and difficulty.</p>

  <form class="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4 items-end" on:submit|preventDefault={onCompare}>
    <div>
      <label class="block text-sm text-muted">Player A ID
        <input class="mt-1 w-full rounded-md border border-white/10 bg-transparent px-3 py-2 text-sm outline-none" bind:value={playerA} placeholder="7656119... or BL ID" required />
      </label>
    </div>
    <div>
      <label class="block text-sm text-muted">Player B ID
        <input class="mt-1 w-full rounded-md border border-white/10 bg-transparent px-3 py-2 text-sm outline-none" bind:value={playerB} placeholder="7656119... or BL ID" required />
      </label>
    </div>
    <div>
      <label class="block text-sm text-muted">Lookback (months)
        <input class="mt-1 w-full rounded-md border border-white/10 bg-transparent px-3 py-2 text-sm outline-none" type="number" min="0" max="120" bind:value={months} />
      </label>
    </div>
    <div>
      <button class="btn-neon" disabled={loading}>
        {#if loading}Loading...{:else}Compare{/if}
      </button>
    </div>
  </form>

  {#if errorMsg}
    <div class="mt-4 text-danger">{errorMsg}</div>
  {/if}

  {#if items.length > 0}
    <!-- KPI Tiles -->
    <div class="mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
      <div class="kpi-tile">
        <div class="kpi-label">Shared Maps</div>
        <div class="kpi-value">{totalComparable}</div>
      </div>
      <div class="kpi-tile a">
        <div class="kpi-label">Wins {idShortA}</div>
        <div class="kpi-value">{winsA}</div>
      </div>
      <div class="kpi-tile b">
        <div class="kpi-label">Wins {idShortB}</div>
        <div class="kpi-value">{winsB}</div>
      </div>
      <div class="kpi-tile">
        <div class="kpi-label">Ties</div>
        <div class="kpi-value">{ties}</div>
      </div>
    </div>
    <div class="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
      <div class="kpi-tile a">
        <div class="kpi-sublabel">Avg Acc {idShortA}</div>
        <div class="kpi-subvalue">{avgAccA.toFixed(2)}%</div>
      </div>
      <div class="kpi-tile b">
        <div class="kpi-sublabel">Avg Acc {idShortB}</div>
        <div class="kpi-subvalue">{avgAccB.toFixed(2)}%</div>
      </div>
      <div class="kpi-tile a">
        <div class="kpi-sublabel">Median {idShortA}</div>
        <div class="kpi-subvalue">{medAccA.toFixed(2)}%</div>
      </div>
      <div class="kpi-tile b">
        <div class="kpi-sublabel">Median {idShortB}</div>
        <div class="kpi-subvalue">{medAccB.toFixed(2)}%</div>
      </div>
    </div>
    <div class="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
      <div class="kpi-tile">
        <div class="kpi-sublabel">Avg Win Margin</div>
        <div class="kpi-subvalue">{avgMargin.toFixed(2)}%</div>
      </div>
      <div class="kpi-tile">
        <div class="kpi-sublabel">95th %ile Margin</div>
        <div class="kpi-subvalue">{p95Margin.toFixed(2)}%</div>
      </div>
      <div class="kpi-tile a">
        <div class="kpi-sublabel">Longest Streak {idShortA}</div>
        <div class="kpi-subvalue">{longestA}</div>
      </div>
      <div class="kpi-tile b">
        <div class="kpi-sublabel">Longest Streak {idShortB}</div>
        <div class="kpi-subvalue">{longestB}</div>
      </div>
    </div>

    <!-- Win Share: Bar + Donut -->
    <div class="mt-6 grid gap-4 lg:grid-cols-2">
      <div class="card-surface p-4">
        <div class="text-sm text-muted mb-2">Win Share</div>
        <div class="winshare-bar">
          <div class="seg a" style={`width: ${(shareA * 100).toFixed(2)}%`}></div>
          <div class="seg t" style={`width: ${(shareT * 100).toFixed(2)}%`}></div>
          <div class="seg b" style={`width: ${(shareB * 100).toFixed(2)}%`}></div>
        </div>
        <div class="mt-2 text-xs text-muted flex gap-3">
          <span class="badge a"></span> {idShortA} {(shareA * 100).toFixed(1)}%
          <span class="badge t"></span> Ties {(shareT * 100).toFixed(1)}%
          <span class="badge b"></span> {idShortB} {(shareB * 100).toFixed(1)}%
        </div>
      </div>
      <div class="card-surface p-4 flex items-center justify-center">
        <svg viewBox="0 0 120 120" width="160" height="160">
          <!-- Donut background -->
          <circle cx="60" cy="60" r="44" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="16" />
          {#if totalComparable > 0}
            <!-- A -->
            <circle cx="60" cy="60" r="44" fill="none" stroke="rgba(0,255,204,0.9)" stroke-width="16" stroke-dasharray={`${donutALen} ${donutCircumference - donutALen}`} stroke-dashoffset={0} />
            <!-- Ties -->
            <circle cx="60" cy="60" r="44" fill="none" stroke="rgba(200,200,200,0.7)" stroke-width="16" stroke-dasharray={`${donutTLen} ${donutCircumference - donutTLen}`} stroke-dashoffset={-donutALen} />
            <!-- B -->
            <circle cx="60" cy="60" r="44" fill="none" stroke="rgba(255,0,170,0.9)" stroke-width="16" stroke-dasharray={`${donutBLen} ${donutCircumference - donutBLen}`} stroke-dashoffset={-(donutALen + donutTLen)} />
          {/if}
          <text x="60" y="64" text-anchor="middle" font-size="14" fill="#9ca3af">{totalComparable} maps</text>
        </svg>
      </div>
    </div>

    <!-- Win Margin Histogram & Cumulative Wins -->
    <div class="mt-6 grid gap-4 lg:grid-cols-2">
      <div class="card-surface p-4">
        <div class="text-sm text-muted mb-2">Win Margin Histogram (A − B, %)</div>
        <div class="hist">
          {#each histogram as count, i}
            <div
              class="bar"
              title={`${(histRangeMin + (i + 0.5) * histBinWidth).toFixed(2)}%`}
              style={`left:${(i / histogram.length) * 100}%; width:${(1 / histogram.length) * 100}%; height:${((count / histMaxCount) * 100).toFixed(1)}%; background:${(histRangeMin + (i + 0.5) * histBinWidth) >= 0 ? 'rgba(0,255,204,0.8)' : 'rgba(255,0,170,0.8)'};`}
            ></div>
          {/each}
          <div class="zero-line"></div>
        </div>
        <div class="mt-2 text-xs text-muted">Left (magenta) favors {idShortB}, Right (cyan) favors {idShortA}</div>
      </div>
      <div class="card-surface p-4">
        <div class="text-sm text-muted mb-2">Cumulative Wins Over Time</div>
        <svg viewBox="0 0 600 140" class="spark">
          <!-- Axes bg -->
          <rect x="0" y="0" width="600" height="140" fill="transparent" />
          {#if cumSeries.length > 1}
            {#key cumSeries.length}
              <!-- A line -->
              <polyline fill="none" stroke="rgba(0,255,204,0.9)" stroke-width="2" points={cumSeries.map(p => `${mapX(p.t, 600)},${mapY(p.a, 120)}`).join(' ')} />
              <!-- B line -->
              <polyline fill="none" stroke="rgba(255,0,170,0.9)" stroke-width="2" points={cumSeries.map(p => `${mapX(p.t, 600)},${mapY(p.b, 120)}`).join(' ')} />
            {/key}
          {/if}
        </svg>
      </div>
    </div>
  {/if}

  {#if items.length > 0}
    <div class="mt-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
      <div class="text-sm text-muted">
        {filtered.length} / {items.length} songs
      </div>
      <div class="flex items-center gap-4 text-sm text-muted flex-wrap justify-end">
        <label class="flex items-center gap-3">
          <span class="filter-label {filterWinMargin !== 'all' ? 'active' : ''}">Options:</span>
          <select class="neon-select {filterWinMargin !== 'all' ? 'active' : ''}" bind:value={filterWinMargin}>
            <option value="all">All Songs</option>
            <option value="1">{idShortA} wins by &gt;1%</option>
            <option value="2">{idShortA} wins by &gt;2%</option>
          </select>
        </label>
        <label class="flex items-center gap-2">Dir
          <select class="neon-select" bind:value={sortDir}>
            <option value="desc">Desc</option>
            <option value="asc">Asc</option>
          </select>
        </label>
        <label class="flex items-center gap-2">Page size
          <select class="neon-select" bind:value={pageSize}>
            <option value={12}>12</option>
            <option value={24}>24</option>
            <option value={36}>36</option>
            <option value={48}>48</option>
          </select>
        </label>
      </div>
    </div>

    {#if loadingMeta}
      <div class="mt-2 text-xs text-muted">Loading covers…</div>
    {/if}

    <div class="mt-4 grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
      {#each pageItems as item}
        <article class="card-surface overflow-hidden">
          <MapCard
            hash={item.hash}
            coverURL={metaByHash[item.hash]?.coverURL}
            songName={metaByHash[item.hash]?.songName}
            mapper={metaByHash[item.hash]?.mapper}
            timeset={item.timeset}
            diffName={item.diffName}
            modeName={item.modeName}
            leaderboardId={item.leaderboardId}
            beatsaverKey={metaByHash[item.hash]?.key}
            playerWithDifficulty={false}
          >
            <div slot="content">
              <div class="mt-3 grid grid-cols-2 gap-3 neon-surface">
                <div class="player-card playerA {item.accA != null && item.accB != null && item.accA > item.accB ? 'winner' : ''}">
                  <div class="label {item.accA != null && item.accB != null && item.accA > item.accB ? 'winner-label' : ''}">{idShortA}</div>
                  <div class="value {item.accA != null && item.accB != null && item.accA > item.accB ? 'winner-value' : ''}">{item.accA != null ? item.accA.toFixed(2) + '%' : '—'}</div>
                  <div class="sub">{item.rankA ? `Rank #${item.rankA}` : ''}</div>
                </div>
                <div class="player-card playerB {item.accA != null && item.accB != null && item.accB > item.accA ? 'winner' : ''}">
                  <div class="label {item.accA != null && item.accB != null && item.accB > item.accA ? 'winner-label' : ''}">{idShortB}</div>
                  <div class="value {item.accA != null && item.accB != null && item.accB > item.accA ? 'winner-value' : ''}">{item.accB != null ? item.accB.toFixed(2) + '%' : '—'}</div>
                  <div class="sub">{item.rankB ? `Rank #${item.rankB}` : ''}</div>
                </div>
              </div>
              <div class="mt-2 text-center text-sm">
                {#if item.accA != null && item.accB != null}
                  {#if item.accA === item.accB}
                    <span class="chip chip-draw">TIE?!</span>
                  {:else if item.accA > item.accB}
                    <span class="chip chip-win-a">Winner: {idShortA}</span>
                    <span class="ml-2 text-muted margin-text {(item.accA - item.accB) > 1 ? 'margin-bold' : ''} {(item.accA - item.accB) > 2 ? 'margin-bright' : ''}">by {(item.accA - item.accB).toFixed(2)}%</span>
                  {:else}
                    <span class="chip chip-win-b">Winner: {idShortB}</span>
                    <span class="ml-2 text-muted margin-text {(item.accB - item.accA) > 1 ? 'margin-bold' : ''} {(item.accB - item.accA) > 2 ? 'margin-bright' : ''}">by {(item.accB - item.accA).toFixed(2)}%</span>
                  {/if}
                {:else}
                  <span class="chip">Incomplete</span>
                {/if}
              </div>
            </div>
          </MapCard>
        </article>
      {/each}
    </div>

    {#if totalPages > 1}
      <div class="mt-6 flex items-center justify-center gap-2">
        <button class="rounded-md border border-white/10 px-3 py-1 text-sm disabled:opacity-50" on:click={() => (page = Math.max(1, page - 1))} disabled={page === 1}>Prev</button>
        <span class="text-sm text-muted">Page {page} / {totalPages}</span>
        <button class="rounded-md border border-white/10 px-3 py-1 text-sm disabled:opacity-50" on:click={() => (page = Math.min(totalPages, page + 1))} disabled={page === totalPages}>Next</button>
      </div>
    {/if}
  {/if}
</section>

<style>
  .text-danger { color: #dc2626; }

  /* Cyberpunk neon aesthetics */
  .neon-surface {
    padding: 16px;
    border-radius: 12px;
    background: radial-gradient(1200px 400px at -10% -10%, rgba(0, 255, 204, 0.08), transparent),
                radial-gradient(1200px 400px at 110% 110%, rgba(255, 0, 170, 0.08), transparent),
                rgba(12,12,18,0.6);
    border: 1px solid rgba(255,255,255,0.06);
    box-shadow: 0 0 20px rgba(0, 255, 204, 0.08), 0 0 30px rgba(255, 0, 170, 0.06) inset;
  }
  .player-card {
    padding: 16px;
    border-radius: 10px;
    background: linear-gradient(180deg, rgba(0,0,0,0.4), rgba(10,10,18,0.6));
    border: 1px solid rgba(255,255,255,0.06);
    transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
  }
  .player-card.playerA { border-color: rgba(0, 255, 204, 0.25); box-shadow: 0 0 12px rgba(0, 255, 204, 0.08) inset; }
  .player-card.playerB { border-color: rgba(255, 0, 170, 0.5); box-shadow: 0 0 12px rgba(255, 0, 170, 0.15) inset; }
  .player-card:hover {
    transform: translateY(-2px);
  }
  .player-card .label { color: #9ca3af; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; transition: all 0.2s ease; }
  .player-card .label.winner-label { color: #ff8800; font-weight: 700; text-shadow: 0 0 8px rgba(255, 136, 0, 0.5); }
  .player-card .value { font-size: 26px; line-height: 1; font-weight: 800; letter-spacing: 0.02em; margin-top: 4px; white-space: nowrap; color: #9ca3af; transition: all 0.2s ease; }
  .player-card .value.winner-value { color: #ffffff; }
  .player-card .sub { margin-top: 6px; font-size: 12px; color: #9ca3af; }
  .player-card.playerA.winner {
    box-shadow: 0 0 0 1px rgba(0, 255, 204, 0.2) inset, 0 0 18px rgba(0, 255, 204, 0.14);
    border-color: rgba(0, 255, 204, 0.35);
  }
  .player-card.playerB.winner {
    box-shadow: 0 0 0 1px rgba(255, 0, 170, 0.3) inset, 0 0 18px rgba(255, 0, 170, 0.25);
    border-color: rgba(255, 0, 170, 0.7);
  }
  .chip {
    display: inline-block;
    border-radius: 9999px;
    border: 1px solid rgba(255,255,255,0.12);
    padding: 4px 10px;
    font-size: 12px;
    background: rgba(255,255,255,0.04);
  }
  .chip-win-a {
    border-color: rgba(0, 255, 204, 0.5);
    background: linear-gradient(90deg, rgba(0, 255, 204, 0.18), rgba(0, 255, 170, 0.10));
  }
  .chip-win-b {
    border-color: rgba(255, 0, 170, 0.5);
    background: linear-gradient(90deg, rgba(255, 0, 170, 0.18), rgba(255, 102, 204, 0.10));
  }
  .chip-draw {
    border-color: rgba(255, 255, 0, 0.5);
    background: linear-gradient(90deg, rgba(255, 255, 0, 0.14), rgba(255, 215, 0, 0.08));
  }

  /* Margin text styling */
  .margin-text.margin-bold { font-weight: 700; }
  .margin-text.margin-bright { color: #ffffff; }

  /* KPI tiles */
  .kpi-tile {
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 12px;
    padding: 12px 14px;
    background: linear-gradient(180deg, rgba(0,0,0,0.35), rgba(15,15,25,0.6));
  }
  .kpi-tile.a { box-shadow: 0 0 0 1px rgba(0,255,204,0.14) inset; }
  .kpi-tile.b { box-shadow: 0 0 0 1px rgba(255,0,170,0.14) inset; }
  .kpi-label { font-size: 12px; color: #9ca3af; letter-spacing: 0.06em; text-transform: uppercase; }
  .kpi-value { font-size: 28px; font-weight: 800; margin-top: 2px; }
  .kpi-sublabel { font-size: 11px; color: #9ca3af; }
  .kpi-subvalue { font-size: 20px; font-weight: 700; margin-top: 2px; }

  /* Win share bar */
  .winshare-bar {
    height: 16px;
    border-radius: 9999px;
    overflow: hidden;
    display: flex;
    background: rgba(255,255,255,0.08);
  }
  .winshare-bar .seg { height: 100%; }
  .winshare-bar .seg.a { background: rgba(0,255,204,0.9); }
  .winshare-bar .seg.t { background: rgba(200,200,200,0.7); }
  .winshare-bar .seg.b { background: rgba(255,0,170,0.9); }
  .badge { display: inline-block; width: 10px; height: 10px; border-radius: 9999px; margin-right: 6px; }
  .badge.a { background: rgba(0,255,204,0.9); }
  .badge.t { background: rgba(200,200,200,0.7); }
  .badge.b { background: rgba(255,0,170,0.9); }

  /* Histogram */
  .hist { position: relative; height: 120px; width: 100%; background: linear-gradient(180deg, rgba(255,255,255,0.04), transparent); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; }
  .hist .bar { position: absolute; bottom: 0; }
  .hist .zero-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: rgba(255,255,255,0.15); }

  /* Sparkline */
  .spark { width: 100%; height: 140px; }

  /* Filter label styling */
  .filter-label {
    font-size: 1em;
    letter-spacing: 0.05em;
    font-weight: 700;
    color: rgba(255, 0, 170, 0.95);
    text-shadow: 0 0 8px rgba(255, 0, 170, 0.3);
    transition: all 0.3s ease;
    display: inline-flex;
    align-items: center;
    font-family: var(--font-display);
    line-height: 1;
  }
  .filter-label.active {
    color: rgba(255, 0, 170, 1);
    text-shadow: 0 0 12px rgba(255, 0, 170, 0.5);
  }

  /* Neon select dropdown */
  .neon-select {
    border-radius: 0.375rem;
    border: 1px solid rgba(34, 211, 238, 0.3);
    background: linear-gradient(180deg, rgba(15,23,42,0.9), rgba(11,15,23,0.95));
    padding: 0.25rem 0.5rem;
    font-size: 0.875rem;
    color: rgba(148, 163, 184, 1);
    transition: all 0.2s ease;
    box-shadow: 0 0 8px rgba(34, 211, 238, 0.15);
    cursor: pointer;
  }
  .neon-select.active {
    border-color: rgba(34, 211, 238, 0.6);
    color: rgba(255, 255, 255, 1);
    box-shadow: 0 0 18px rgba(34, 211, 238, 0.35);
  }
  .neon-select:hover {
    border-color: rgba(34, 211, 238, 0.5);
    color: rgba(255, 255, 255, 0.9);
    box-shadow: 0 0 16px rgba(34, 211, 238, 0.25);
  }
  .neon-select:focus {
    outline: none;
    border-color: rgba(34, 211, 238, 0.7);
    box-shadow: 0 0 20px rgba(34, 211, 238, 0.35), 0 0 0 2px rgba(34, 211, 238, 0.1);
  }
  .neon-select option {
    background: #0f172a;
    color: #fff;
  }
</style>


