import axios from 'axios';
import { db } from './db.js';

type SpotifyTokenSet = {
  access_token: string;
  refresh_token?: string;
  token_expires_at?: number;
};

export function getUserTokens(uid: string): SpotifyTokenSet | null {
  const row = db.db
    .prepare(
      'SELECT access_token, refresh_token, token_expires_at FROM users WHERE id = ?'
    )
    .get(uid) as any;
  if (!row) return null;
  return {
    access_token: row.access_token,
    refresh_token: row.refresh_token,
    token_expires_at: row.token_expires_at,
  };
}

export async function ensureValidAccessToken(uid: string): Promise<string> {
  const tokens = getUserTokens(uid);
  if (!tokens?.access_token) {
    throw new Error('User not authenticated');
  }
  const now = Date.now();
  if (tokens.token_expires_at && tokens.token_expires_at > now + 30_000) {
    return tokens.access_token;
  }
  if (!tokens.refresh_token) {
    return tokens.access_token;
  }

  // Refresh token via Spotify
  const clientId = process.env.SPOTIFY_CLIENT_ID || '';
  const clientSecret = process.env.SPOTIFY_CLIENT_SECRET || '';
  const resp = await axios.post(
    'https://accounts.spotify.com/api/token',
    new URLSearchParams({ grant_type: 'refresh_token', refresh_token: tokens.refresh_token }),
    {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
      },
    }
  );

  const { access_token, expires_in } = resp.data as { access_token: string; expires_in: number };
  const token_expires_at = Date.now() + expires_in * 1000 - 60_000;
  db.db
    .prepare('UPDATE users SET access_token = ?, token_expires_at = ?, updated_at=? WHERE id = ?')
    .run(access_token, token_expires_at, Date.now(), uid);
  return access_token;
}

export type Vibe = 'energetic' | 'chill' | 'happy' | 'sad' | 'party' | 'focus';

export function vibeToTargets(vibe: Vibe | undefined): Record<string, number> {
  switch (vibe) {
    case 'energetic':
      return { target_energy: 0.85, target_danceability: 0.7, target_tempo: 130 };
    case 'chill':
      return { target_energy: 0.3, target_danceability: 0.4, target_tempo: 90 };
    case 'happy':
      return { target_valence: 0.85, target_danceability: 0.6 };
    case 'sad':
      return { target_valence: 0.2 };
    case 'party':
      return { target_danceability: 0.85, target_energy: 0.8, target_tempo: 125 };
    case 'focus':
      return { target_energy: 0.4, target_instrumentalness: 0.5 };
    default:
      return {};
  }
}

export async function fetchRecommendations(
  accessToken: string,
  seeds: { seed_tracks?: string[]; seed_artists?: string[]; seed_genres?: string[] },
  targets: Record<string, number>,
  limit = 25
): Promise<string[]> {
  // Legacy kept for compatibility; prefer fetchSimilarTrackUris below
  return fetchSimilarTrackUris(accessToken, seeds.seed_tracks, seeds.seed_artists, seeds.seed_genres, targets, limit);
}

export async function createSpotifyPlaylist(
  accessToken: string,
  userId: string,
  name: string,
  description: string
): Promise<{ id: string; url: string }> {
  const resp = await axios.post(
    `https://api.spotify.com/v1/users/${encodeURIComponent(userId)}/playlists`,
    { name, description, public: false },
    { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }
  );
  const pl = resp.data as any;
  return { id: pl.id, url: pl.external_urls?.spotify };
}

export async function addTracks(
  accessToken: string,
  playlistId: string,
  trackUris: string[]
): Promise<void> {
  if (!trackUris.length) return;
  await axios.post(
    `https://api.spotify.com/v1/playlists/${encodeURIComponent(playlistId)}/tracks`,
    { uris: trackUris },
    { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }
  );
}

// ----- New: Similar tracks without recommendations endpoint -----

async function getRelatedArtists(accessToken: string, artistId: string): Promise<string[]> {
  const resp = await axios.get(`https://api.spotify.com/v1/artists/${encodeURIComponent(artistId)}/related-artists`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const artists = (resp.data?.artists || []) as Array<{ id?: string }>;
  return artists.map(a => a.id!).filter(Boolean);
}

async function getArtistTopTracks(accessToken: string, artistId: string): Promise<string[]> {
  const resp = await axios.get(`https://api.spotify.com/v1/artists/${encodeURIComponent(artistId)}/top-tracks?market=from_token`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const tracks = (resp.data?.tracks || []) as Array<{ id?: string; uri?: string }>;
  return tracks.map(t => t.uri || (t.id ? `spotify:track:${t.id}` : null)).filter((u): u is string => !!u);
}

async function searchTracksByGenre(accessToken: string, genre: string, limit: number): Promise<string[]> {
  const resp = await axios.get('https://api.spotify.com/v1/search', {
    headers: { Authorization: `Bearer ${accessToken}` },
    params: { q: `genre:"${genre}"`, type: 'track', limit: Math.min(limit, 50) },
  });
  const tracks = (resp.data?.tracks?.items || []) as Array<{ id?: string; uri?: string }>;
  return tracks.map(t => t.uri || (t.id ? `spotify:track:${t.id}` : null)).filter((u): u is string => !!u);
}

async function filterByTargets(accessToken: string, trackUris: string[], targets: Record<string, number>): Promise<string[]> {
  if (!trackUris.length || Object.keys(targets).length === 0) return trackUris;
  const ids = trackUris.map(u => u.split(':').pop()).filter(Boolean) as string[];
  const resp = await axios.get('https://api.spotify.com/v1/audio-features', {
    headers: { Authorization: `Bearer ${accessToken}` },
    params: { ids: ids.slice(0, 100).join(',') },
  });
  const features = (resp.data?.audio_features || []) as Array<Record<string, any>>;
  const pass = new Set(
    features
      .filter(f => !!f && (!('target_energy' in targets) || Math.abs((targets as any).target_energy - f.energy) <= 0.4))
      .filter(f => !('target_danceability' in targets) || Math.abs((targets as any).target_danceability - f.danceability) <= 0.4)
      .filter(f => !('target_valence' in targets) || Math.abs((targets as any).target_valence - f.valence) <= 0.5)
      .filter(f => !('target_tempo' in targets) || Math.abs((targets as any).target_tempo - f.tempo) <= 30)
      .filter(f => !('target_instrumentalness' in targets) || Math.abs((targets as any).target_instrumentalness - f.instrumentalness) <= 0.5)
      .map(f => f.id)
  );
  return trackUris.filter(u => pass.has(u.split(':').pop() as string));
}

export async function fetchSimilarTrackUris(
  accessToken: string,
  seedTrackUris?: string[],
  seedArtistIds?: string[],
  seedGenres?: string[],
  targets: Record<string, number> = {},
  limit = 25
): Promise<string[]> {
  const candidateUris = new Set<string>();

  // From seed artists and from artists of seed tracks
  const trackIds = (seedTrackUris || []).map(u => u?.split(':').pop()).filter(Boolean) as string[];
  const seedArtistSet = new Set<string>(seedArtistIds || []);

  // If we got tracks, fetch their artists to expand
  if (trackIds.length) {
    const tracksResp = await axios.get('https://api.spotify.com/v1/tracks', {
      headers: { Authorization: `Bearer ${accessToken}` },
      params: { ids: trackIds.slice(0, 50).join(',') },
    });
    const tracks = (tracksResp.data?.tracks || []) as Array<{ artists?: Array<{ id?: string }> }>;
    tracks.forEach(t => (t.artists || []).forEach(a => a?.id && seedArtistSet.add(a.id)));
  }

  // Expand via related artists and take top tracks
  const seedArtists = Array.from(seedArtistSet).slice(0, 10);
  for (const artistId of seedArtists) {
    try {
      const related = (await getRelatedArtists(accessToken, artistId)).slice(0, 10);
      const pool = [artistId, ...related];
      for (const a of pool) {
        const tops = await getArtistTopTracks(accessToken, a);
        tops.forEach(u => candidateUris.add(u));
        if (candidateUris.size >= limit * 4) break;
      }
      if (candidateUris.size >= limit * 4) break;
    } catch {}
  }

  // Genre-based search fallback
  for (const g of (seedGenres || []).slice(0, 3)) {
    try {
      const found = await searchTracksByGenre(accessToken, g, 50);
      found.forEach(u => candidateUris.add(u));
      if (candidateUris.size >= limit * 4) break;
    } catch {}
  }

  let uris = Array.from(candidateUris);
  uris = await filterByTargets(accessToken, uris, targets);
  // Shuffle and take limit
  uris.sort(() => Math.random() - 0.5);
  return uris.slice(0, limit);
}


