// ============================================================
// Spotify client — PKCE auth + Web API wrapper
// ============================================================
// Uses Authorization Code + PKCE flow (no client secret needed, safe for SPA).
// Token persists in localStorage; auto-refreshes when near expiry.
//
// Setup:
//   1. https://developer.spotify.com/dashboard → Create app
//   2. Add your redirect URIs (http://localhost, https://your-host/, etc.)
//   3. Paste the Client ID below (or set window.__SPOTIFY_CLIENT_ID before this script runs).
// ============================================================

const SPOTIFY_CLIENT_ID = window.__SPOTIFY_CLIENT_ID || /*EDITMODE-SPOTIFY-BEGIN*/'YOUR_SPOTIFY_CLIENT_ID'/*EDITMODE-SPOTIFY-END*/;

const SPOTIFY_SCOPES = [
  'user-read-private',
  'user-read-email',
  'user-library-read',
  'playlist-read-private',
  'playlist-read-collaborative',
  'user-read-recently-played',
  'user-top-read',
].join(' ');

const SP_STORAGE_KEY = 'djanything.spotify.tokens.v1';
const SP_LEGACY_STORAGE_KEY = 'mixfm.spotify.tokens.v1';
const SP_VERIFIER_KEY = 'djanything.spotify.pkce_verifier';
const SP_RETURN_KEY = 'djanything.spotify.return_path';

// ---------- PKCE helpers ----------
function b64urlEncode(bytes) {
  return btoa(String.fromCharCode.apply(null, bytes))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
async function sha256(str) {
  const data = new TextEncoder().encode(str);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return b64urlEncode(new Uint8Array(hash));
}
function randomString(len = 64) {
  const arr = new Uint8Array(len);
  crypto.getRandomValues(arr);
  return b64urlEncode(arr).slice(0, len);
}

// ---------- Token storage ----------
function loadTokens() {
  try {
    const raw = localStorage.getItem(SP_STORAGE_KEY);
    if (raw) return JSON.parse(raw);
    const legacy = localStorage.getItem(SP_LEGACY_STORAGE_KEY);
    if (!legacy) return null;
    localStorage.setItem(SP_STORAGE_KEY, legacy);
    return JSON.parse(legacy);
  } catch { return null; }
}
function saveTokens(tokens) {
  try { localStorage.setItem(SP_STORAGE_KEY, JSON.stringify(tokens)); } catch {}
}
function clearTokens() {
  try {
    localStorage.removeItem(SP_STORAGE_KEY);
    localStorage.removeItem(SP_LEGACY_STORAGE_KEY);
  } catch {}
}

// ---------- Auth flow ----------
async function beginLogin() {
  if (!SPOTIFY_CLIENT_ID || SPOTIFY_CLIENT_ID === 'YOUR_SPOTIFY_CLIENT_ID') {
    alert('Spotify Client ID not configured. See spotify-client.jsx for setup instructions.');
    throw new Error('Spotify Client ID not set');
  }
  const verifier = randomString(64);
  const challenge = await sha256(verifier);
  sessionStorage.setItem(SP_VERIFIER_KEY, verifier);
  // Remember the exact URL so we can return here after redirect
  sessionStorage.setItem(SP_RETURN_KEY, window.location.href);

  const redirectUri = window.location.origin + window.location.pathname;
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: SPOTIFY_CLIENT_ID,
    scope: SPOTIFY_SCOPES,
    code_challenge_method: 'S256',
    code_challenge: challenge,
    redirect_uri: redirectUri,
  });
  window.location.href = `https://accounts.spotify.com/authorize?${params}`;
}

async function exchangeCode(code) {
  const verifier = sessionStorage.getItem(SP_VERIFIER_KEY);
  if (!verifier) throw new Error('Missing PKCE verifier');
  const redirectUri = window.location.origin + window.location.pathname;
  const body = new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: redirectUri,
    client_id: SPOTIFY_CLIENT_ID,
    code_verifier: verifier,
  });
  const resp = await fetch('https://accounts.spotify.com/api/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });
  if (!resp.ok) throw new Error(`Token exchange failed: ${resp.status}`);
  const json = await resp.json();
  const tokens = {
    accessToken: json.access_token,
    refreshToken: json.refresh_token,
    expiresAt: Date.now() + json.expires_in * 1000,
  };
  saveTokens(tokens);
  sessionStorage.removeItem(SP_VERIFIER_KEY);
  return tokens;
}

async function refreshAccessToken(refreshToken) {
  const body = new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: refreshToken,
    client_id: SPOTIFY_CLIENT_ID,
  });
  const resp = await fetch('https://accounts.spotify.com/api/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });
  if (!resp.ok) { clearTokens(); throw new Error(`Refresh failed: ${resp.status}`); }
  const json = await resp.json();
  const tokens = {
    accessToken: json.access_token,
    refreshToken: json.refresh_token || refreshToken,
    expiresAt: Date.now() + json.expires_in * 1000,
  };
  saveTokens(tokens);
  return tokens;
}

async function getValidToken() {
  let t = loadTokens();
  if (!t) return null;
  if (Date.now() < t.expiresAt - 30_000) return t.accessToken;
  try { t = await refreshAccessToken(t.refreshToken); return t.accessToken; }
  catch { return null; }
}

// Call early at page load to pick up ?code= from redirect
async function handleRedirectCallback() {
  const url = new URL(window.location.href);
  const code = url.searchParams.get('code');
  const err = url.searchParams.get('error');
  if (!code && !err) return false;
  // Clean URL regardless
  url.searchParams.delete('code');
  url.searchParams.delete('error');
  url.searchParams.delete('state');
  window.history.replaceState({}, '', url.toString());
  if (err) { console.warn('Spotify auth error:', err); return false; }
  try { await exchangeCode(code); return true; }
  catch (e) { console.error(e); return false; }
}

function isConnected() { return !!loadTokens(); }
function logout() { clearTokens(); }

// ---------- API calls ----------
async function apiCall(path, opts = {}) {
  const token = await getValidToken();
  if (!token) throw new Error('Not connected to Spotify');
  const resp = await fetch(`https://api.spotify.com/v1${path}`, {
    ...opts,
    headers: { ...(opts.headers || {}), Authorization: `Bearer ${token}` },
  });
  if (resp.status === 401) { clearTokens(); throw new Error('Spotify session expired'); }
  if (resp.status === 429) {
    // Rate limited — basic retry after
    const retry = parseInt(resp.headers.get('Retry-After') || '1', 10);
    await new Promise(r => setTimeout(r, retry * 1000));
    return apiCall(path, opts);
  }
  if (!resp.ok) throw new Error(`Spotify API ${resp.status}: ${resp.statusText}`);
  return resp.json();
}

// Normalize a Spotify track object to our internal shape
function normalizeTrack(t) {
  if (!t || !t.id) return null;
  return {
    spotifyId: t.id,
    spotifyUri: t.uri,
    title: t.name,
    artist: (t.artists || []).map(a => a.name).join(', '),
    album: t.album?.name,
    thumb: t.album?.images?.[1]?.url || t.album?.images?.[0]?.url || null,
    duration: Math.round((t.duration_ms || 0) / 1000),
    explicit: !!t.explicit,
    previewUrl: t.preview_url,
    externalUrl: t.external_urls?.spotify,
  };
}

async function search(query, limit = 20) {
  if (!query?.trim()) return [];
  const params = new URLSearchParams({ q: query, type: 'track', limit: String(limit) });
  const json = await apiCall(`/search?${params}`);
  return (json.tracks?.items || []).map(normalizeTrack).filter(Boolean);
}

async function getMe() {
  return apiCall('/me');
}

async function getLikedSongs(limit = 50, offset = 0) {
  const json = await apiCall(`/me/tracks?limit=${limit}&offset=${offset}`);
  return {
    total: json.total,
    items: (json.items || []).map(it => normalizeTrack(it.track)).filter(Boolean),
  };
}

async function getPlaylists(limit = 50, offset = 0) {
  const json = await apiCall(`/me/playlists?limit=${limit}&offset=${offset}`);
  return {
    total: json.total,
    items: (json.items || []).map(p => ({
      id: p.id,
      name: p.name,
      trackCount: p.tracks?.total || 0,
      thumb: p.images?.[0]?.url || null,
      owner: p.owner?.display_name,
    })),
  };
}

async function getPlaylistTracks(playlistId, limit = 100, offset = 0) {
  const json = await apiCall(`/playlists/${playlistId}/tracks?limit=${limit}&offset=${offset}`);
  return {
    total: json.total,
    items: (json.items || []).map(it => normalizeTrack(it.track)).filter(Boolean),
  };
}

async function getRecentlyPlayed(limit = 50) {
  const json = await apiCall(`/me/player/recently-played?limit=${limit}`);
  // Dedupe — recently-played often repeats the same track
  const seen = new Set();
  const items = [];
  for (const it of json.items || []) {
    const t = normalizeTrack(it.track);
    if (t && !seen.has(t.spotifyId)) {
      seen.add(t.spotifyId);
      items.push(t);
    }
  }
  return { items };
}

// Batch BPM/key/energy for up to 100 track IDs
async function getAudioFeatures(spotifyIds) {
  if (!spotifyIds?.length) return {};
  const out = {};
  for (let i = 0; i < spotifyIds.length; i += 100) {
    const batch = spotifyIds.slice(i, i + 100);
    const json = await apiCall(`/audio-features?ids=${batch.join(',')}`);
    for (const f of json.audio_features || []) {
      if (!f) continue;
      out[f.id] = {
        bpm: f.tempo ? Math.round(f.tempo * 10) / 10 : null,
        key: spotifyKeyToCamelot(f.key, f.mode),
        energy: f.energy,
        danceability: f.danceability,
        loudness: f.loudness,
        duration: f.duration_ms ? Math.round(f.duration_ms / 1000) : null,
      };
    }
  }
  return out;
}

// Spotify key (0-11) + mode (0=minor, 1=major) → Camelot notation
function spotifyKeyToCamelot(key, mode) {
  if (key == null || key < 0) return null;
  // Circle of fifths mapping — Camelot wheel
  // Major keys (B suffix):  C=8B, G=9B, D=10B, A=11B, E=12B, B=1B, F#=2B, Db=3B, Ab=4B, Eb=5B, Bb=6B, F=7B
  // Minor keys (A suffix):  Am=8A, Em=9A, Bm=10A, F#m=11A, C#m=12A, G#m=1A, D#m=2A, Bbm=3A, Fm=4A, Cm=5A, Gm=6A, Dm=7A
  const majors = ['8B','3B','10B','5B','12B','7B','2B','9B','4B','11B','6B','1B'];
  const minors = ['5A','12A','7A','2A','9A','4A','11A','6A','1A','8A','3A','10A'];
  return mode === 1 ? majors[key] : minors[key];
}

// ---------- YouTube resolver ----------
// Given a Spotify track, find the best YouTube match.
// Strategy: build a query, call our backend's YouTube search, score results.
async function resolveToYouTube(spotifyTrack) {
  const query = `${spotifyTrack.artist} ${spotifyTrack.title}`;
  const candidates = await BackendClient.searchYouTube(query, 5);
  if (!candidates?.length) return null;
  // Score: prefer shorter duration-match, title contains both artist + track name,
  // penalize words like "cover", "karaoke", "reaction", "tutorial"
  const scored = candidates.map(c => {
    let score = 0;
    const title = (c.title || '').toLowerCase();
    const artist = spotifyTrack.artist.toLowerCase().split(',')[0].trim();
    const track = spotifyTrack.title.toLowerCase();
    if (title.includes(artist)) score += 3;
    if (title.includes(track)) score += 3;
    if (title.includes('official')) score += 2;
    if (title.includes('audio')) score += 1;
    ['cover', 'karaoke', 'reaction', 'tutorial', 'lyrics video', 'sped up', 'slowed'].forEach(bad => {
      if (title.includes(bad)) score -= 3;
    });
    // Duration match (within 10%)
    if (spotifyTrack.duration && c.duration) {
      const ratio = c.duration / spotifyTrack.duration;
      if (ratio > 0.9 && ratio < 1.1) score += 4;
      else if (ratio > 0.8 && ratio < 1.2) score += 1;
      else score -= 2;
    }
    return { ...c, score };
  });
  scored.sort((a, b) => b.score - a.score);
  return {
    best: scored[0],
    alternatives: scored.slice(1, 4),
  };
}

window.SpotifyClient = {
  CLIENT_ID: SPOTIFY_CLIENT_ID,
  beginLogin, handleRedirectCallback, isConnected, logout,
  search, getMe,
  getLikedSongs, getPlaylists, getPlaylistTracks, getRecentlyPlayed,
  getAudioFeatures, resolveToYouTube,
};
