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

  type BeatLeaderScore = {
    timeset?: string | number;
    leaderboard?: {
      // BeatLeader tends to expose a short id for the leaderboard route
      id?: string | number | null;
      leaderboardId?: string | number | null;
      song?: { hash?: string | null };
      difficulty?: { value?: number | string | null; modeName?: string | null };
    };
  };

  type BeatLeaderScoresResponse = {
    data?: BeatLeaderScore[];
    metadata?: { page?: number; itemsPerPage?: number; total?: number };
  };

  type Difficulty = {
    name: string;
    characteristic: string;
  };

  type SongItem = {
    hash: string;
    difficulties: Difficulty[];
    timeset: number;
    leaderboardId?: string;
  };

  const ONE_YEAR_SECONDS = 365 * 24 * 60 * 60;

  let playerA = '';
  let playerB = '';
  let loading = false;
  let errorMsg: string | null = null;
  let results: SongItem[] = [];
  let loadingMeta = false;

  // Sorting and pagination state
  let sortBy: 'date' | 'difficulty' = 'date';
  let sortDir: 'asc' | 'desc' = 'desc';
  let page = 1;
  let pageSize: number | string = 24;
  $: pageSizeNum = Number(pageSize) || 24;

  // Configurable lookback windows (months)
  let monthsA: number | string = 6; // default 6 months
  let monthsB: number | string = 24; // default 24 months

  // Derived lists
  $: sortedResults = [...results].sort((a, b) => {
    let cmp = 0;
    if (sortBy === 'date') {
      cmp = a.timeset - b.timeset;
    } else {
      const an = a.difficulties[0]?.name ?? '';
      const bn = b.difficulties[0]?.name ?? '';
      cmp = an.localeCompare(bn);
    }
    return sortDir === 'asc' ? cmp : -cmp;
  });
  $: totalPages = Math.max(1, Math.ceil(sortedResults.length / pageSizeNum));
  $: page = Math.min(page, totalPages);
  $: pageItems = sortedResults.slice((page - 1) * pageSizeNum, (page - 1) * pageSizeNum + pageSizeNum);

  type MapMeta = {
    songName?: string;
    key?: string;
    coverURL?: string;
    mapper?: string;
  };
  let metaByHash: Record<string, MapMeta> = {};
  
  type StarInfo = {
    stars?: number;
    accRating?: number;
    passRating?: number;
    techRating?: number;
    status?: number;
  };
  // Keyed by `${hash}|${difficultyName}|${modeName}` for precise lookup
  let starsByKey: Record<string, StarInfo> = {};
  let loadingStars = false;

  async function fetchBeatSaverMeta(hash: string): Promise<MapMeta | null> {
    try {
      const res = await fetch(`https://api.beatsaver.com/maps/hash/${encodeURIComponent(hash)}`);
      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/${hash.toLowerCase()}.jpg`;
      return {
        songName: data?.metadata?.songName ?? data?.name ?? undefined,
        key: data?.id ?? undefined,
        coverURL: cover,
        mapper: data?.uploader?.name ?? undefined
      };
    } catch {
      // Fallback to CDN cover only
      return { coverURL: `https://cdn.beatsaver.com/${hash.toLowerCase()}.jpg` };
    }
  }

  async function loadMetaForResults(items: SongItem[]): Promise<void> {
    const needed = Array.from(new Set(items.map((i) => i.hash))).filter((h) => !metaByHash[h]);
    if (needed.length === 0) return;
    loadingMeta = true;
    for (const h of needed) {
      const meta = await fetchBeatSaverMeta(h);
      if (meta) metaByHash = { ...metaByHash, [h]: meta };
    }
    loadingMeta = false;
  }
  
  async function fetchBeatLeaderStarsByHash(hash: string): Promise<void> {
    try {
      const res = await fetch(`/api/beatleader?path=/leaderboards/hash/${encodeURIComponent(hash)}`);
      if (!res.ok) return;
      const data: any = await res.json();
      const leaderboards: any[] = Array.isArray(data?.leaderboards) ? data.leaderboards : Array.isArray(data) ? data : [];
      for (const lb of leaderboards) {
        const diffName: string | undefined = lb?.difficulty?.difficultyName ?? lb?.difficulty?.name ?? undefined;
        const modeName: string | undefined = lb?.difficulty?.modeName ?? lb?.modeName ?? 'Standard';
        if (!diffName || !modeName) continue;
        const normalized = normalizeDifficultyName(diffName);
        const key = `${hash}|${normalized}|${modeName}`;
        const info: StarInfo = {
          stars: lb?.difficulty?.stars ?? lb?.stars,
          accRating: lb?.difficulty?.accRating,
          passRating: lb?.difficulty?.passRating,
          techRating: lb?.difficulty?.techRating,
          status: lb?.difficulty?.status
        };
        starsByKey = { ...starsByKey, [key]: info };
      }
    } catch {
      // ignore
    }
  }
  
  async function loadStarsForResults(items: SongItem[]): Promise<void> {
    const neededHashes = Array.from(new Set(items.map((i) => i.hash)));
    if (neededHashes.length === 0) return;
    loadingStars = true;
    for (const h of neededHashes) {
      await fetchBeatLeaderStarsByHash(h);
    }
    loadingStars = false;
  }


  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(months: number | string): number {
    const m = Number(months) || 0;
    const seconds = Math.max(0, m) * 30 * 24 * 60 * 60; // approx 30 days per month
    return Math.floor(Date.now() / 1000) - seconds;
  }

  async function fetchAllRecentScores(playerId: string, cutoffEpoch: number, maxPages = 15): Promise<BeatLeaderScore[]> {
    const qs = new URLSearchParams({ diff: 'ExpertPlus', 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 ?? [];
  }


  

  function incrementPlaylistCount(): number {
    try {
      const raw = localStorage.getItem('playlist_counts');
      const obj = raw ? (JSON.parse(raw) as Record<string, number>) : {};
      const key = 'beatleader_compare_players';
      const next = (obj[key] ?? 0) + 1;
      obj[key] = next;
      localStorage.setItem('playlist_counts', JSON.stringify(obj));
      return next;
    } catch {
      return 1;
    }
  }

  function toPlaylistJson(songs: SongItem[]): unknown {
    const count = incrementPlaylistCount();
    const playlistTitle = `beatleader_compare_players-${String(count).padStart(2, '0')}`;
    return {
      playlistTitle,
      playlistAuthor: 'SaberList Tool',
      songs: songs.map((s) => ({
        hash: s.hash,
        difficulties: s.difficulties,
      })),
      description: `A's recent songs not played by B. Generated ${new Date().toISOString()}`,
      allowDuplicates: false,
      customData: {}
    };
  }

  function downloadPlaylist(): void {
    const payload = toPlaylistJson(results);
    const title = (payload as any).playlistTitle ?? 'playlist';
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${title}.bplist`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
  }

  async function onCompare(ev: SubmitEvent) {
    ev.preventDefault();
    errorMsg = null;
    results = [];
    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(monthsA);
      const cutoffB = getCutoffEpochFromMonths(monthsB);
      const [aScores, bScores] = await Promise.all([
        fetchAllRecentScores(a, cutoff),
        fetchAllRecentScores(b, cutoffB, 100)
      ]);

      const bLeaderboardIds = new Set<string>();
      const bExpertPlusKeys = new Set<string>(); // `${hashLower}|ExpertPlus|${modeName}`
      for (const s of bScores) {
        const rawHash = s.leaderboard?.song?.hash ?? undefined;
        const hashLower = rawHash ? String(rawHash).toLowerCase() : undefined;
        const lbIdRaw = (s.leaderboard as any)?.id ?? (s.leaderboard as any)?.leaderboardId;
        const lbId = lbIdRaw != null ? String(lbIdRaw) : undefined;
        const bDiffValue = s.leaderboard?.difficulty?.value ?? undefined;
        const bModeName = s.leaderboard?.difficulty?.modeName ?? 'Standard';
        const bDiffName = normalizeDifficultyName(bDiffValue);
        if (bDiffName !== 'ExpertPlus') continue; // ignore non-ExpertPlus for B
        if (lbId) bLeaderboardIds.add(lbId);
        if (hashLower) bExpertPlusKeys.add(`${hashLower}|ExpertPlus|${bModeName}`);
      }

      
      const runSeen = new Set<string>(); // avoid duplicates within this run

      const candidates: SongItem[] = [];
      for (const entry of aScores) {
        const t = parseTimeset(entry.timeset);
        if (!t || t < cutoff) continue;

        const rawHash = entry.leaderboard?.song?.hash ?? undefined;
        const diffValue = entry.leaderboard?.difficulty?.value ?? undefined;
        const modeName = entry.leaderboard?.difficulty?.modeName ?? 'Standard';
        const leaderboardIdRaw = (entry.leaderboard as any)?.id ?? (entry.leaderboard as any)?.leaderboardId;
        const leaderboardId = leaderboardIdRaw != null ? String(leaderboardIdRaw) : undefined;
        if (!rawHash) continue;
        const hashLower = String(rawHash).toLowerCase();
        const diffName = normalizeDifficultyName(diffValue);
        if (diffName !== 'ExpertPlus') continue; // Only compare ExpertPlus for A
        // B has played ExpertPlus of same map+mode if matching leaderboard or key exists
        if ((leaderboardId && bLeaderboardIds.has(leaderboardId)) || bExpertPlusKeys.has(`${hashLower}|${diffName}|${modeName}`)) continue;
        

        const key = `${rawHash}|${diffName}|${modeName}`;
        if (runSeen.has(key)) continue;
        runSeen.add(key);

        candidates.push({
          hash: rawHash,
          difficulties: [{ name: diffName, characteristic: modeName ?? 'Standard' }],
          timeset: t,
          leaderboardId
        });
      }

      candidates.sort((x, y) => y.timeset - x.timeset);
      const limited = candidates; // return all; pagination handled client-side

      

      results = limited;
      page = 1;
      // Load BeatSaver metadata (covers, titles) for tiles
      loadMetaForResults(limited);
      // Load BeatLeader star ratings per hash/diff
      loadStarsForResults(limited);
    } catch (err) {
      errorMsg = err instanceof Error ? err.message : 'Unknown error';
    } finally {
      loading = false;
    }
  }

  onMount(() => {
    // Try prefill from URL params if present
    const sp = new URLSearchParams(location.search);
    playerA = sp.get('a') ?? '';
    playerB = sp.get('b') ?? '';
  });
</script>

<section class="py-8">
  <h1 class="font-display text-3xl sm:text-4xl">BeatLeader: Compare Players</h1>
  <p class="mt-2 text-muted">Maps Player A has played that Player B hasn't — configurable lookback.</p>
  

  <form class="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3 items-end" on:submit|preventDefault={onCompare}>
    <div>
      <label class="block text-sm text-muted">Player A ID (source)
        <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 (target)
        <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">Player A 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={monthsA} />
      </label>
    </div>
    <div>
      <label class="block text-sm text-muted">Player B 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={monthsB} />
      </label>
    </div>
    <div class="flex items-center gap-3">
      <button class="btn-neon" disabled={loading}>
        {#if loading}
          Loading...
        {:else}
          Compare
        {/if}
      </button>
      {#if results.length > 0}
        <button type="button" class="rounded-md border border-white/10 px-3 py-2 text-sm" on:click={downloadPlaylist}>Download .bplist</button>
      {/if}
    </div>
  </form>

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

  {#if results.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">
        {results.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">Options:</span>
          <select class="neon-select" bind:value={sortBy}>
            <option value="date">Date</option>
            <option value="difficulty">Difficulty</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}
    {#if loadingStars}
      <div class="mt-2 text-xs text-muted">Loading star ratings…</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}
            stars={starsByKey[`${item.hash}|${item.difficulties[0]?.name ?? 'ExpertPlus'}|${item.difficulties[0]?.characteristic ?? 'Standard'}`]?.stars}
            timeset={item.timeset}
            diffName={item.difficulties[0]?.name ?? 'ExpertPlus'}
            modeName={item.difficulties[0]?.characteristic ?? 'Standard'}
            leaderboardId={item.leaderboardId}
            beatsaverKey={metaByHash[item.hash]?.key}
            playerWithDifficulty={true}
          />
        </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; }

  /* 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;
  }

  /* 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: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>


