<script lang="ts">
  import { onMount } from 'svelte';
  import SongPlayer from '$lib/components/SongPlayer.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;

  // Toast notification state
  let toastMessage = '';
  let showToast = false;
  let toastTimeout: number | null = null;

  // Button feedback state - track which buttons are currently "lit up"
  let litButtons = new Set<string>();

  function showToastMessage(message: string) {
    // Clear any existing toast
    if (toastTimeout) {
      clearTimeout(toastTimeout);
    }
    
    toastMessage = message;
    showToast = true;
    
    // Auto-hide toast after 3 seconds
    toastTimeout = setTimeout(() => {
      showToast = false;
      toastMessage = '';
    }, 3000);
  }

  function lightUpButton(buttonId: string) {
    litButtons.add(buttonId);
    // Reassign here too to trigger reactivity immediately
    litButtons = litButtons;
    // Remove the lighting effect after 1 second
    setTimeout(() => {
      litButtons.delete(buttonId);
      litButtons = litButtons; // Trigger reactivity
    }, 1000);
  }

  async function copyBsrCommand(key: string, hash: string) {
    try {
      const bsrCommand = `!bsr ${key}`;
      await navigator.clipboard.writeText(bsrCommand);
      
      // Show success feedback with the actual command
      showToastMessage(`Copied "${bsrCommand}" to clipboard`);
      lightUpButton(`bsr-${hash}`);
    } catch (err) {
      console.error('Failed to copy to clipboard:', err);
      showToastMessage('Failed to copy to clipboard');
    }
  }

  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 difficultyToColor(name: string | undefined): string {
    const n = (name ?? 'ExpertPlus').toLowerCase();
    if (n === 'easy') return 'MediumSeaGreen';
    if (n === 'normal') return '#59b0f4';
    if (n === 'hard') return 'tomato';
    if (n === 'expert') return '#bf2a42';
    if (n === 'expertplus' || n === 'expert+' || n === 'ex+' ) return '#8f48db';
    return '#8f48db';
  }

  

  

  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>
      <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 results.length > 0}
    <div class="mt-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
      <div class="flex items-center gap-3 text-sm text-muted">
        <span>{results.length} songs</span>
        <span>·</span>
        <label class="flex items-center gap-2">Sort
          <select class="rounded-md border border-white/10 bg-transparent px-2 py-1 text-sm" 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="rounded-md border border-white/10 bg-transparent px-2 py-1 text-sm" 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="rounded-md border border-white/10 bg-transparent px-2 py-1 text-sm" 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 class="flex items-center gap-3">
        <button class="rounded-md border border-white/10 px-3 py-2 text-sm" on:click={downloadPlaylist}>Download .bplist</button>
      </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">
          <div class="aspect-square bg-black/30">
            {#if metaByHash[item.hash]?.coverURL}
              <img
                src={metaByHash[item.hash].coverURL}
                alt={metaByHash[item.hash]?.songName ?? item.hash}
                loading="lazy"
                class="h-full w-full object-cover"
              />
            {:else}
              <div class="h-full w-full flex items-center justify-center text-xs text-muted">No cover</div>
            {/if}
          </div>
          <div class="p-3">
            <div class="font-semibold truncate" title={metaByHash[item.hash]?.songName ?? item.hash}>
              {metaByHash[item.hash]?.songName ?? item.hash}
            </div>
            {#if metaByHash[item.hash]?.mapper}
              <div class="mt-0.5 text-xs text-muted truncate">{metaByHash[item.hash]?.mapper}</div>
            {/if}
            <div class="mt-2 flex items-center justify-between text-[11px]">
              <span class="rounded bg-white/10 px-2 py-0.5">
                {item.difficulties[0]?.characteristic ?? 'Standard'} ·
                <span class="rounded px-1 ml-1" style="background-color: {difficultyToColor(item.difficulties[0]?.name)}; color: #fff;">
                  {item.difficulties[0]?.name}
                </span>
              </span>
              <span class="text-muted">{new Date(item.timeset * 1000).toLocaleDateString()}</span>
            </div>
            {#if starsByKey[`${item.hash}|${item.difficulties[0]?.name ?? 'ExpertPlus'}|${item.difficulties[0]?.characteristic ?? 'Standard'}`]?.stars}
              <div class="mt-1 text-xs">
                {#key `${item.hash}|${item.difficulties[0]?.name}|${item.difficulties[0]?.characteristic}`}
                  <span title="BeatLeader star rating">★ {starsByKey[`${item.hash}|${item.difficulties[0]?.name ?? 'ExpertPlus'}|${item.difficulties[0]?.characteristic ?? 'Standard'}`]?.stars?.toFixed(2)}</span>
                {/key}
              </div>
            {/if}
            <div class="mt-3">
              <SongPlayer hash={item.hash} preferBeatLeader={true} />
            </div>
            <div class="mt-3 flex flex-wrap gap-2">
              <a
                class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20"
                href={item.leaderboardId
                  ? `https://beatleader.com/leaderboard/global/${item.leaderboardId}`
                  : `https://beatleader.com/leaderboard/global/${item.hash}?diff=${encodeURIComponent(item.difficulties[0]?.name ?? 'ExpertPlus')}&mode=${encodeURIComponent(item.difficulties[0]?.characteristic ?? 'Standard')}`}
                target="_blank"
                rel="noopener"
                title="Open in BeatLeader"
                >BL</a
              >
              <a
                class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20"
                href={metaByHash[item.hash]?.key ? `https://beatsaver.com/maps/${metaByHash[item.hash]?.key}` : `https://beatsaver.com/search/hash/${item.hash}`}
                target="_blank"
                rel="noopener"
                title="Open in BeatSaver"
                >BSR</a
              >
              <button
                class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20 disabled:opacity-50"
                class:lit-up={litButtons.has(`bsr-${item.hash}`)}
                on:click={() => { const key = metaByHash[item.hash]?.key; if (key) copyBsrCommand(key, item.hash); }}
                disabled={!metaByHash[item.hash]?.key}
                title="Copy !bsr"
              >Copy !bsr</button>
            </div>
          </div>
        </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>

<!-- Toast Notification -->
{#if showToast}
  <div class="toast-notification" role="status" aria-live="polite">
    {toastMessage}
  </div>
{/if}

<style>
  .text-danger { color: #dc2626; }
  
  /* Toast notification styles */
  .toast-notification {
    position: fixed;
    top: 20px;
    right: 20px;
    background: #10b981;
    color: white;
    padding: 12px 16px;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    z-index: 1000;
    font-size: 14px;
    font-weight: 500;
    animation: slideIn 0.3s ease-out;
  }
  
  @keyframes slideIn {
    from {
      transform: translateX(100%);
      opacity: 0;
    }
    to {
      transform: translateX(0);
      opacity: 1;
    }
  }
  
  /* Button lighting effect */
  .lit-up {
    background: linear-gradient(45deg, #10b981, #059669);
    border-color: #10b981;
    color: white;
    box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
    animation: pulse 0.6s ease-out;
  }
  
  @keyframes pulse {
    0% {
      box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
    }
    70% {
      box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
    }
    100% {
      box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
    }
  }
</style>


