// Spotify PKCE auth + Web API helpers (client-side, no server proxy needed).
// Auth flow:
//   1. host taps Connect Spotify
//   2. we generate code_verifier + code_challenge, redirect to spotify
//   3. spotify redirects back to /host?code=...
//   4. we exchange the code (PKCE: no client secret) for tokens
//   5. tokens persisted to localStorage
//
// Notes:
// - Redirect URI must EXACTLY match what's registered in the Spotify app dashboard.
// - PKCE is the recommended flow for browser apps (no secret).

const SCOPES = [
  'user-read-playback-state',
  'user-modify-playback-state',
  'playlist-read-private',
  'playlist-read-collaborative',
  'streaming',
].join(' ');

function b64url(buf) {
  return btoa(String.fromCharCode(...new Uint8Array(buf)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

async function pkceChallenge() {
  const arr = new Uint8Array(64);
  crypto.getRandomValues(arr);
  const verifier = b64url(arr);
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
  return { verifier, challenge: b64url(digest) };
}

async function startAuth(clientId, redirectUri) {
  const { verifier, challenge } = await pkceChallenge();
  sessionStorage.setItem('hf_pkce', verifier);
  const url = new URL('https://accounts.spotify.com/authorize');
  url.searchParams.set('client_id', clientId);
  url.searchParams.set('response_type', 'code');
  url.searchParams.set('redirect_uri', redirectUri);
  url.searchParams.set('code_challenge_method', 'S256');
  url.searchParams.set('code_challenge', challenge);
  url.searchParams.set('scope', SCOPES);
  url.searchParams.set('state', Math.random().toString(36).slice(2));
  window.location.assign(url.toString());
}

async function exchangeCode(clientId, redirectUri, code) {
  const verifier = sessionStorage.getItem('hf_pkce');
  if (!verifier) throw new Error('Missing PKCE verifier');
  const body = new URLSearchParams();
  body.set('grant_type', 'authorization_code');
  body.set('code', code);
  body.set('redirect_uri', redirectUri);
  body.set('client_id', clientId);
  body.set('code_verifier', verifier);
  const resp = await fetch('https://accounts.spotify.com/api/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });
  const json = await resp.json();
  if (!resp.ok) throw new Error(json.error_description || json.error || 'auth_failed');
  sessionStorage.removeItem('hf_pkce');
  return {
    access: json.access_token,
    refresh: json.refresh_token,
    expiresAt: Date.now() + (json.expires_in - 60) * 1000,
  };
}

async function refreshToken(clientId, refresh) {
  const body = new URLSearchParams();
  body.set('grant_type', 'refresh_token');
  body.set('refresh_token', refresh);
  body.set('client_id', clientId);
  const resp = await fetch('https://accounts.spotify.com/api/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });
  const json = await resp.json();
  if (!resp.ok) throw new Error(json.error_description || 'refresh_failed');
  return {
    access: json.access_token,
    refresh: json.refresh_token || refresh,
    expiresAt: Date.now() + (json.expires_in - 60) * 1000,
  };
}

function loadStoredToken() {
  try {
    const raw = localStorage.getItem('hf_spotify_token');
    if (!raw) return null;
    return JSON.parse(raw);
  } catch (_) { return null; }
}

function storeToken(t) {
  if (t) localStorage.setItem('hf_spotify_token', JSON.stringify(t));
  else localStorage.removeItem('hf_spotify_token');
}

// Wrap fetch with auto-refresh
async function spApi(token, refreshIfNeeded, path, opts = {}) {
  let access = token.access;
  if (Date.now() >= token.expiresAt - 5000) {
    const fresh = await refreshIfNeeded();
    access = fresh.access;
  }
  const url = path.startsWith('http') ? path : `https://api.spotify.com/v1${path}`;
  const resp = await fetch(url, {
    ...opts,
    headers: {
      ...(opts.headers || {}),
      'Authorization': `Bearer ${access}`,
      ...(opts.body && !opts.noJsonContentType ? { 'Content-Type': 'application/json' } : {}),
    },
  });
  if (resp.status === 204) return null;
  // Spotify returns empty body on some endpoints
  const text = await resp.text();
  let json = null;
  if (text) {
    try { json = JSON.parse(text); } catch (_) {}
  }
  if (!resp.ok) {
    const err = new Error(json?.error?.message || resp.statusText || 'spotify_error');
    err.status = resp.status;
    err.body = json;
    throw err;
  }
  return json;
}

function parsePlaylistId(input) {
  if (!input) return null;
  const trimmed = input.trim();
  // Already an ID?
  if (/^[a-zA-Z0-9]{22}$/.test(trimmed)) return trimmed;
  // Spotify URI form
  const uriMatch = trimmed.match(/spotify:playlist:([a-zA-Z0-9]+)/);
  if (uriMatch) return uriMatch[1];
  // URL form
  try {
    const u = new URL(trimmed);
    const m = u.pathname.match(/\/playlist\/([a-zA-Z0-9]+)/);
    if (m) return m[1];
  } catch (_) {}
  return null;
}

function yearFromDate(d) {
  if (!d) return null;
  const y = parseInt(d.slice(0, 4), 10);
  return Number.isFinite(y) ? y : null;
}

async function getSelf(token, refresh) {
  return await spApi(token, refresh, '/me');
}

// Get the host's currently playing Spotify track. Returns null if nothing playing.
async function getCurrentlyPlaying(token, refresh) {
  const json = await spApi(token, refresh, '/me/player/currently-playing');
  if (!json || !json.item) return null;
  const t = json.item;
  return {
    id: t.id,
    uri: t.uri,
    title: t.name,
    artist: (t.artists || []).map(a => a.name).join(', '),
    year: yearFromDate(t.album?.release_date),
    isPlaying: !!json.is_playing,
    durationMs: t.duration_ms,
    progressMs: json.progress_ms,
  };
}

// Batch-fetch full track metadata (incl. album.release_date) by IDs.
// Spotify caps at 50 IDs per request; we page in 50s.
async function getTracks(token, refresh, ids) {
  const out = [];
  for (let i = 0; i < ids.length; i += 50) {
    const chunk = ids.slice(i, i + 50);
    const json = await spApi(token, refresh, `/tracks?ids=${chunk.join(',')}`);
    for (const t of json.tracks || []) {
      if (!t) continue;
      out.push(t);
    }
  }
  return out;
}

async function playTrack(token, refresh, uri, deviceId) {
  const path = deviceId ? `/me/player/play?device_id=${encodeURIComponent(deviceId)}` : '/me/player/play';
  await spApi(token, refresh, path, {
    method: 'PUT',
    body: JSON.stringify({ uris: [uri] }),
  });
}

async function transferPlayback(token, refresh, deviceId, play = false) {
  await spApi(token, refresh, '/me/player', {
    method: 'PUT',
    body: JSON.stringify({ device_ids: [deviceId], play }),
  });
}

async function skipToNext(token, refresh, deviceId) {
  const path = deviceId ? `/me/player/next?device_id=${encodeURIComponent(deviceId)}` : '/me/player/next';
  await spApi(token, refresh, path, { method: 'POST' });
}

async function pausePlayback(token, refresh, deviceId) {
  const path = deviceId ? `/me/player/pause?device_id=${encodeURIComponent(deviceId)}` : '/me/player/pause';
  await spApi(token, refresh, path, { method: 'PUT' });
}

async function resumePlayback(token, refresh, deviceId) {
  const path = deviceId ? `/me/player/play?device_id=${encodeURIComponent(deviceId)}` : '/me/player/play';
  await spApi(token, refresh, path, { method: 'PUT' });
}

// Page through /me/playlists. Returns the simplified playlist objects.
async function getMyPlaylists(token, refresh) {
  const all = [];
  let next = '/me/playlists?limit=50';
  while (next) {
    const page = await spApi(token, refresh, next);
    all.push(...(page.items || []));
    next = page.next ? page.next.replace('https://api.spotify.com/v1', '') : null;
    if (all.length >= 200) break; // sanity cap
  }
  return all;
}

// Translate any Spotify error into a human-readable string with a hint when useful.
function explainError(e, ctx = '') {
  const status = e.status;
  const msg = e.message || 'unknown';
  if (status === 401) return `Spotify session expired. Reconnect Spotify and try again.`;
  if (status === 403) {
    const lower = msg.toLowerCase();
    if (lower.includes('premium')) return `Premium required: ${msg}.`;
    return `Spotify says forbidden (403): "${msg}". Spotify only lets us read playlists you actually own or collaborate on — saved/followed/editorial ones are blocked. Pick from "Your Playlists" or duplicate the playlist in the Spotify app first (open the playlist → ⋯ → Add to Other Playlist → New Playlist).`;
  }
  if (status === 404) return `Spotify says not found (404): "${msg}". Editorial/algorithmic playlists return 404 since Nov 2024 — copy the playlist into your account first.`;
  if (status === 429) return `Spotify rate limit hit. Wait a moment and try again.`;
  return ctx ? `${ctx}: ${msg}${status ? ` (${status})` : ''}` : `${msg}${status ? ` (${status})` : ''}`;
}

window.Spotify = {
  SCOPES, startAuth, exchangeCode, refreshToken,
  loadStoredToken, storeToken,
  spApi, parsePlaylistId, yearFromDate,
  getSelf, getMyPlaylists, explainError,
  getCurrentlyPlaying, skipToNext, pausePlayback, resumePlayback,
  getTracks, playTrack, transferPlayback,
};
