<script lang="ts">
  import SongPlayer from '$lib/components/SongPlayer.svelte';
  import HasToolAccess from '$lib/components/HasToolAccess.svelte';
  import PlayerCard from '$lib/components/PlayerCard.svelte';
  import { onMount } from 'svelte';
  import {
    TOOL_REQUIREMENTS,
    type BeatLeaderPlayerProfile,
    fetchAllPlayerScores,
    fetchBeatSaverMeta,
    type MapMeta
  } from '$lib/utils/plebsaber-utils';

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

  type PlaylistSong = {
    hash: string;
    difficulties?: Difficulty[];
    key?: string;
    levelId?: string;
    songName?: string;
  };

  type Playlist = {
    playlistTitle?: string;
    songs?: PlaylistSong[];
    playlistAuthor?: string;
    image?: string | null;
    coverImage?: string | null;
    description?: string;
    allowDuplicates?: boolean;
    customData?: Record<string, unknown> | null;
  };

  type BeatLeaderScore = {
    leaderboard?: {
      id?: string | number | null;
      leaderboardId?: string | number | null;
      song?: { hash?: string | null };
    };
  };

  export let data: { player: BeatLeaderPlayerProfile | null; adminRank: number | null; adminPlayer: BeatLeaderPlayerProfile | null };

  const requirement = TOOL_REQUIREMENTS['player-playlist-gaps'];

  $: playerProfile = data?.player ?? null;
  $: adminRank = data?.adminRank ?? null;
  $: adminPlayer = data?.adminPlayer ?? null;

  let playerId = '';
  let selectedFileName: string | null = null;
  let parsedTitle: string | null = null;
  let playlistSongs: PlaylistSong[] = [];

  let loading = false;
  let errorMsg: string | null = null;

  let results: PlaylistSong[] = [];
  let metaByHash: Record<string, MapMeta> = {};
  let loadingMeta = false;
  let blUrlByHash: Record<string, string> = {};
  
  let hasAnalyzed = false;
  let analyzedPlayerProfile: BeatLeaderPlayerProfile | null = null;
  let previewPlayerProfile: BeatLeaderPlayerProfile | null = null;
  let loadingPreview = false;

  // Persist playerId across refreshes (client-side only)
  let hasMounted = false;
  const PLAYER_ID_KEY = 'ps_bl_gap_playerId';
  const WEEK_MS = 7 * 24 * 60 * 60 * 1000;

  type PersistedPlayerId = {
    value: string;
    expiresAt: number;
  };

  function loadPlayerIdFromStorage(): void {
    if (typeof localStorage === 'undefined') return;
    const raw = localStorage.getItem(PLAYER_ID_KEY);
    if (!raw) return;
    try {
      const parsed = JSON.parse(raw) as Partial<PersistedPlayerId> | string;
      if (typeof parsed === 'string') {
        playerId = parsed;
        return;
      }
      const expiresAt = typeof parsed?.expiresAt === 'number' ? parsed.expiresAt : 0;
      if (expiresAt && Date.now() > expiresAt) {
        localStorage.removeItem(PLAYER_ID_KEY);
        return;
      }
      const value = typeof parsed?.value === 'string' ? parsed.value : '';
      if (value) playerId = value;
    } catch {
      // Backward-compat for plain string values
      playerId = raw;
    }
  }
  onMount(() => {
    try {
      // Prefill with logged-in player ID if available
      if (!playerId && playerProfile?.id) {
        playerId = playerProfile.id;
        // Load preview immediately for prefilled user
        void loadPreviewProfile(playerProfile.id);
      } else {
        loadPlayerIdFromStorage();
        // Load preview for stored player ID
        if (playerId && playerId.trim()) {
          void loadPreviewProfile(playerId.trim());
        }
      }
    } finally {
      hasMounted = true;
    }
  });
  $: if (hasMounted && typeof localStorage !== 'undefined') {
    if (playerId && playerId.trim().length > 0) {
      const record: PersistedPlayerId = { value: playerId, expiresAt: Date.now() + WEEK_MS };
      localStorage.setItem(PLAYER_ID_KEY, JSON.stringify(record));
    } else {
      localStorage.removeItem(PLAYER_ID_KEY);
    }
  }

  // Debounced preview loading
  let previewDebounceTimer: ReturnType<typeof setTimeout> | null = null;
  $: {
    if (hasMounted && playerId) {
      const trimmed = playerId.trim();
      if (previewDebounceTimer) clearTimeout(previewDebounceTimer);
      
      if (trimmed) {
        // Debounce for 800ms to avoid excessive requests
        previewDebounceTimer = setTimeout(() => {
          void loadPreviewProfile(trimmed);
        }, 800);
      } else {
        previewPlayerProfile = null;
      }
    }
  }

  async function loadPreviewProfile(id: string): Promise<void> {
    if (!id || id.trim().length < 3) {
      previewPlayerProfile = null;
      return;
    }
    
    loadingPreview = true;
    try {
      const profile = await fetchPlayerProfile(id);
      // Only update if this is still the current player ID
      if (playerId.trim() === id) {
        previewPlayerProfile = profile;
      }
    } catch {
      // Silently fail - don't show errors for preview loading
      if (playerId.trim() === id) {
        previewPlayerProfile = null;
      }
    } finally {
      loadingPreview = false;
    }
  }

  async function onFileChange(ev: Event) {
    const input = ev.target as HTMLInputElement;
    const file = input.files?.[0];
    if (!file) return;
    selectedFileName = file.name;
    errorMsg = null;
    results = [];
    playlistSongs = [];
    parsedTitle = null;
    try {
      const text = await file.text();
      const data = JSON.parse(text) as Playlist;
      const songs = Array.isArray(data?.songs) ? data.songs : [];
      const cleaned = songs
        .filter((s) => s && typeof s.hash === 'string' && s.hash.trim().length > 0)
        .map((s) => ({
          hash: s.hash.trim(),
          difficulties: Array.isArray(s.difficulties) ? s.difficulties : [],
          key: s.key,
          levelId: s.levelId,
          songName: s.songName
        }));
      if (cleaned.length === 0) {
        throw new Error('No valid songs found in playlist.');
      }
      playlistSongs = cleaned;
      parsedTitle = data?.playlistTitle ?? null;
    } catch (err) {
      errorMsg = err instanceof Error ? err.message : 'Failed to parse playlist file.';
    }
  }


  async function loadMetaForResults(items: PlaylistSong[]): Promise<void> {
    const needed = Array.from(new Set(items.map((i) => i.hash.toLowerCase()))).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;
  }

  type BLDifficulty = {
    modeName?: string;
    difficultyName?: string;
    value?: number;
    ModeName?: string;
    DifficultyName?: string;
    Value?: number;
  };
  type BLLeaderboardInfo = {
    id?: string | number | null;
    Id?: string | number | null;
    leaderboardId?: string | number | null;
    LeaderboardId?: string | number | null;
    difficulty?: BLDifficulty;
    Difficulty?: BLDifficulty;
  };
  type BLLeaderboardsByHashResponse = {
    leaderboards?: BLLeaderboardInfo[];
    Leaderboards?: BLLeaderboardInfo[];
  };

  function normalizeName(name?: string | null): string {
    return (name ?? '').toString().toLowerCase().replace(/\s+/g, '');
  }
  function difficultyRankIndex(diffName?: string | null): number {
    const order = ['easy', 'normal', 'hard', 'expert', 'expert+', 'expertplus'];
    const n = normalizeName(diffName).replace('expertplus', 'expert+');
    const idx = order.indexOf(n);
    return idx === -1 ? -1 : idx;
  }
  function getDifficulty(obj?: BLDifficulty | null): BLDifficulty {
    const o = obj ?? {} as BLDifficulty;
    return {
      modeName: (o.modeName ?? (o as any).ModeName) as string | undefined,
      difficultyName: (o.difficultyName ?? (o as any).DifficultyName) as string | undefined,
      value: (o.value ?? (o as any).Value) as number | undefined
    };
  }
  function pickPreferredLeaderboard(lbs: BLLeaderboardInfo[] | null | undefined): BLLeaderboardInfo | null {
    if (!Array.isArray(lbs) || lbs.length === 0) return null;
    const getDiff = (lb: BLLeaderboardInfo) => getDifficulty(lb.difficulty ?? (lb as any).Difficulty);
    const isStandard = (lb: BLLeaderboardInfo) => normalizeName(getDiff(lb)?.modeName) === 'standard';
    const inStandard = lbs.filter(isStandard);
    const pool = inStandard.length > 0 ? inStandard : lbs;
    const expertPlus = pool.find((lb) => {
      const n = normalizeName(getDiff(lb)?.difficultyName);
      return n === 'expertplus' || n === 'expert+';
    });
    if (expertPlus) return expertPlus;
    // Fallback to highest difficulty by known order, then by numeric value if available
    const byKnownOrder = [...pool].sort((a, b) => difficultyRankIndex(getDiff(a)?.difficultyName) - difficultyRankIndex(getDiff(b)?.difficultyName));
    const bestByOrder = byKnownOrder[byKnownOrder.length - 1];
    if (bestByOrder && difficultyRankIndex(getDiff(bestByOrder)?.difficultyName) >= 0) return bestByOrder;
    const byValue = [...pool].sort((a, b) => (getDiff(a)?.value ?? 0) - (getDiff(b)?.value ?? 0));
    return byValue[byValue.length - 1] ?? pool[0] ?? null;
  }
  async function fetchBLLeaderboardsByHash(hash: string): Promise<BLLeaderboardInfo[] | null> {
    try {
      const res = await fetch(`/api/beatleader?path=${encodeURIComponent('/leaderboards/hash/' + hash)}`);
      if (!res.ok) return null;
      const data = (await res.json()) as BLLeaderboardsByHashResponse | unknown;
      const leaderboards = (data as any)?.leaderboards ?? (data as any)?.Leaderboards;
      return Array.isArray(leaderboards) ? (leaderboards as BLLeaderboardInfo[]) : null;
    } catch {
      return null;
    }
  }
  function extractLeaderboardId(lb: BLLeaderboardInfo | null): string | null {
    if (!lb) return null;
    const anyLb: any = lb as any;
    const id = anyLb.id ?? anyLb.leaderboardId ?? anyLb.Id ?? anyLb.LeaderboardId;
    return id != null ? String(id) : null;
  }
  function buildBLUrlFromLeaderboard(lb: BLLeaderboardInfo | null, hash: string): string {
    const id = extractLeaderboardId(lb);
    if (id && id.length > 0) {
      return `https://beatleader.com/leaderboard/global/${id}`;
    }
    // Fallback: search by hash on the site
    return `https://beatleader.com/leaderboards?search=${encodeURIComponent(hash)}`;
  }

  async function openBeatLeader(hash: string): Promise<void> {
    const key = (hash ?? '').toLowerCase();
    let url = blUrlByHash[key];
    if (!url) {
      const lbs = await fetchBLLeaderboardsByHash(key);
      const picked = pickPreferredLeaderboard(lbs ?? undefined);
      url = buildBLUrlFromLeaderboard(picked, hash);
      blUrlByHash = { ...blUrlByHash, [key]: url };
    }
    if (typeof window !== 'undefined' && url) {
      window.open(url, '_blank', 'noopener');
    }
  }

  function downloadPlaylist(): void {
    const title = `playlist_gap_${playerId || 'player'}`;
    const payload = {
      playlistTitle: title,
      playlistAuthor: 'SaberList Tool',
      songs: results.map((s) => ({ hash: s.hash, difficulties: s.difficulties ?? [] })),
      description: `Subset of ${(parsedTitle ?? 'playlist')} that ${playerId} has not played. Generated ${new Date().toISOString()}`,
      allowDuplicates: false,
      customData: {}
    };
    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 fetchPlayerProfile(id: string): Promise<BeatLeaderPlayerProfile | null> {
    try {
      const res = await fetch(`https://api.beatleader.xyz/player/${encodeURIComponent(id)}`);
      if (!res.ok) return null;
      return (await res.json()) as BeatLeaderPlayerProfile;
    } catch {
      return null;
    }
  }

  async function onAnalyze(ev: SubmitEvent) {
    ev.preventDefault();
    errorMsg = null;
    results = [];
    metaByHash = {};
    analyzedPlayerProfile = null;
    hasAnalyzed = false;
    if (!playerId.trim()) {
      errorMsg = 'Please enter a BeatLeader player ID or SteamID64.';
      return;
    }
    if (playlistSongs.length === 0) {
      errorMsg = 'Please select a valid .bplist file first.';
      return;
    }
    loading = true;
    try {
      const [scores, profile] = await Promise.all([
        fetchAllPlayerScores(playerId.trim(), 150),
        fetchPlayerProfile(playerId.trim())
      ]);
      analyzedPlayerProfile = profile;
      hasAnalyzed = true;
      
      const playedHashes = new Set<string>();
      for (const s of scores) {
        const raw = s.leaderboard?.song?.hash ?? undefined;
        if (!raw) continue;
        playedHashes.add(String(raw).toLowerCase());
      }
      const unplayed: PlaylistSong[] = [];
      for (const song of playlistSongs) {
        const h = song.hash?.toLowerCase?.() ?? '';
        if (!h) continue;
        if (!playedHashes.has(h)) {
          unplayed.push(song);
        }
      }
      results = unplayed;
      loadMetaForResults(unplayed);
    } catch (err) {
      errorMsg = err instanceof Error ? err.message : 'Unknown error';
    } finally {
      loading = false;
    }
  }

  function resetForm(): void {
    hasAnalyzed = false;
    analyzedPlayerProfile = null;
    results = [];
    metaByHash = {};
    blUrlByHash = {};
    errorMsg = null;
    selectedFileName = null;
    parsedTitle = null;
    playlistSongs = [];
    // Don't reset playerId - let user keep it for next analysis
  }

  // Computed: which profile to display (analyzed takes priority over preview)
  $: displayProfile = hasAnalyzed ? analyzedPlayerProfile : previewPlayerProfile;
  $: showPlayerCard = !!(displayProfile || (loadingPreview && playerId.trim().length >= 3));
</script>

<section class="py-8">
  <h1 class="font-display text-3xl sm:text-4xl">Player Playlist Gaps</h1>
  <p class="mt-2 text-muted">Upload a .bplist and enter a player ID to find songs they have not played.</p>

  <HasToolAccess player={playerProfile} requirement={requirement} {adminRank} adminPlayer={adminPlayer}>
    {#if !hasAnalyzed}
      <form class="mt-6" on:submit|preventDefault={onAnalyze}>
        <div class="grid gap-4 sm:grid-cols-2 items-end">
          <div>
            <label class="block text-sm text-muted">Playlist file (.bplist)
              <input class="mt-1 rounded-md border border-white/10 bg-transparent px-3 py-2 text-sm outline-none file-input" type="file" accept=".bplist,application/json" on:change={onFileChange} />
            </label>
            {#if selectedFileName}
              <div class="mt-1 text-xs text-muted">{selectedFileName}{#if parsedTitle} · title: {parsedTitle}{/if} · {playlistSongs.length} songs</div>
            {/if}
          </div>
          <div class="input-tile">
            <label class="block text-sm text-muted">Player 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={playerId} placeholder="7656119... or BL ID" required />
            </label>
            {#if showPlayerCard}
              <div class="player-card-wrapper">
                {#if displayProfile}
                  <PlayerCard
                    name={displayProfile.name ?? 'Player'}
                    avatar={displayProfile.avatar ?? null}
                    country={displayProfile.country ?? null}
                    rank={displayProfile.rank ?? null}
                    showRank={typeof displayProfile.rank === 'number'}
                    width="100%"
                    avatarSize={56}
                    techPp={displayProfile.techPp}
                    accPp={displayProfile.accPp}
                    passPp={displayProfile.passPp}
                    playerId={displayProfile.id ?? null}
                    gradientId="playlist-gap-player-preview"
                  />
                {:else}
                  <div class="loading-placeholder">Loading player...</div>
                {/if}
              </div>
            {/if}
          </div>
        </div>
        <div class="mt-4">
          <button class="btn-neon" disabled={loading}>
            {#if loading}
              Loading...
            {:else}
              Analyze
            {/if}
          </button>
        </div>
      </form>
    {:else}
      <div class="mt-6 flex items-start gap-4">
        {#if displayProfile}
          <div class="analyzed-player-summary">
            <PlayerCard
              name={displayProfile.name ?? 'Player'}
              avatar={displayProfile.avatar ?? null}
              country={displayProfile.country ?? null}
              rank={displayProfile.rank ?? null}
              showRank={typeof displayProfile.rank === 'number'}
              width="100%"
              avatarSize={56}
              techPp={displayProfile.techPp}
              accPp={displayProfile.accPp}
              passPp={displayProfile.passPp}
              playerId={displayProfile.id ?? null}
              gradientId="playlist-gap-player"
            />
          </div>
        {/if}
        <button 
          class="rounded-md border border-white/10 px-3 py-2 text-sm hover:bg-white/10 transition-colors" 
          on:click={resetForm}
          title="Reset and analyze another playlist"
        >
          Reset
        </button>
      </div>
    {/if}

    {#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 not played</span>
        </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}

      <div class="mt-4 grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
        {#each results as item}
          <article class="card-surface overflow-hidden">
            <div class="aspect-square bg-black/30">
              {#if metaByHash[item.hash?.toLowerCase?.() || '']?.coverURL}
                <img
                  src={metaByHash[item.hash.toLowerCase()].coverURL}
                  alt={metaByHash[item.hash.toLowerCase()]?.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.toLowerCase()]?.songName ?? item.hash}>
                {metaByHash[item.hash.toLowerCase()]?.songName ?? item.hash}
              </div>
              {#if metaByHash[item.hash.toLowerCase()]?.mapper}
                <div class="mt-0.5 text-xs text-muted truncate">{metaByHash[item.hash.toLowerCase()]?.mapper}</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={blUrlByHash[item.hash.toLowerCase()] ?? undefined}
                  on:click|preventDefault={() => openBeatLeader(item.hash)}
                  on:auxclick|preventDefault={() => openBeatLeader(item.hash)}
                  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.toLowerCase()]?.key ? `https://beatsaver.com/maps/${metaByHash[item.hash.toLowerCase()]?.key}` : `https://beatsaver.com/search/hash/${item.hash}`}
                  target="_blank"
                  rel="noopener"
                  title="Open in BeatSaver"
                >BS</a>
                <button
                  class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20 disabled:opacity-50"
                  on:click={() => { const key = metaByHash[item.hash.toLowerCase()]?.key; if (key) navigator.clipboard.writeText(`!bsr ${key}`); }}
                  disabled={!metaByHash[item.hash.toLowerCase()]?.key}
                  title="Copy !bsr"
                >Copy !bsr</button>
              </div>
            </div>
          </article>
        {/each}
      </div>
    {/if}
  </HasToolAccess>
</section>

<style>
  .text-danger { color: #dc2626; }
  .btn-neon { cursor: pointer; }
  .card-surface { border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.03); }
  .text-muted { color: rgba(255,255,255,0.7); }
  
  .file-input {
    width: 15em;
  }
  
  .input-tile {
    border: 1px solid rgba(148, 163, 184, 0.18);
    border-radius: 0.75rem;
    padding: 1.25rem;
    background: linear-gradient(160deg, rgba(15, 23, 42, 0.6), rgba(8, 12, 24, 0.85));
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    transition: border-color 0.2s ease;
  }

  .input-tile:hover {
    border-color: rgba(148, 163, 184, 0.3);
  }

  .player-card-wrapper {
    margin-top: 1rem;
  }

  .loading-placeholder {
    padding: 2rem;
    text-align: center;
    color: rgba(148, 163, 184, 0.7);
    font-size: 0.9rem;
  }

  .analyzed-player-summary {
    border: 1px solid rgba(148, 163, 184, 0.18);
    border-radius: 0.75rem;
    padding: 1.25rem;
    background: linear-gradient(160deg, rgba(15, 23, 42, 0.6), rgba(8, 12, 24, 0.85));
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    max-width: 600px;
  }
</style>


