import { Injectable, Logger } from '@nestjs/common';
import { resolve, dirname } from 'path';
import * as fs from 'fs';
import { EnvironmentEnum } from '../environmentEnum';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fetch = require('isomorphic-unfetch');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getDetails } = require('spotify-url-info')(fetch);

interface StoredSpotifyUserToken {
  access_token: string;
  refresh_token: string;
  expires_at: number;
  scope?: string;
  token_type?: string;
}

@Injectable()
export class SpotifyApiService {
  private readonly logger = new Logger(SpotifyApiService.name);
  private accessToken: string | null = null;
  private tokenExpiry = 0;

  private getPlaylistId(url: string): string {
    try {
      const urlObj = new URL(url);
      const pathParts = urlObj.pathname.split('/');
      const playlistIndex = pathParts.findIndex((part) => part === 'playlist');
      if (playlistIndex >= 0 && pathParts.length > playlistIndex + 1) {
        return pathParts[playlistIndex + 1].split('?')[0];
      }
      throw new Error('Invalid Spotify playlist URL');
    } catch (error) {
      this.logger.error(`Failed to extract playlist ID: ${error.message}`);
      throw error;
    }
  }

  isTrackUrl(url: string): boolean {
    try {
      const urlObj = new URL(url);
      return urlObj.pathname.includes('/track/');
    } catch {
      return false;
    }
  }

  private getTrackId(url: string): string {
    try {
      const urlObj = new URL(url);
      const pathParts = urlObj.pathname.split('/');
      const trackIndex = pathParts.findIndex((part) => part === 'track');
      if (trackIndex >= 0 && pathParts.length > trackIndex + 1) {
        return pathParts[trackIndex + 1].split('?')[0];
      }
      throw new Error('Invalid Spotify track URL');
    } catch (error) {
      this.logger.error(`Failed to extract track ID: ${error.message}`);
      throw error;
    }
  }

  isArtistUrl(url: string): boolean {
    try {
      const urlObj = new URL(url);
      return urlObj.pathname.includes('/artist/');
    } catch {
      return false;
    }
  }

  private getArtistId(url: string): string {
    try {
      const urlObj = new URL(url);
      const pathParts = urlObj.pathname.split('/');
      const artistIndex = pathParts.findIndex((part) => part === 'artist');
      if (artistIndex >= 0 && pathParts.length > artistIndex + 1) {
        return pathParts[artistIndex + 1].split('?')[0];
      }
      throw new Error('Invalid Spotify artist URL');
    } catch (error) {
      this.logger.error(`Failed to extract artist ID: ${error.message}`);
      throw error;
    }
  }

  private getClientId(): string {
    const clientId = process.env.SPOTIFY_CLIENT_ID;
    if (!clientId) {
      throw new Error('Missing SPOTIFY_CLIENT_ID');
    }
    return clientId;
  }

  private getClientSecret(): string {
    const clientSecret = process.env.SPOTIFY_CLIENT_SECRET;
    if (!clientSecret) {
      throw new Error('Missing SPOTIFY_CLIENT_SECRET');
    }
    return clientSecret;
  }

  private getRedirectUri(): string {
    return (
      process.env[EnvironmentEnum.SPOTIFY_REDIRECT_URI] ||
      'http://127.0.0.1:3000/api/spotify/callback'
    );
  }

  private getTokenPath(): string {
    if (process.env[EnvironmentEnum.SPOTIFY_TOKEN_PATH]) {
      return process.env[EnvironmentEnum.SPOTIFY_TOKEN_PATH];
    }

    const dbPath = process.env[EnvironmentEnum.DB_PATH] || './config/db.sqlite';
    return resolve(dirname(resolve(__dirname, dbPath)), 'spotify-user-token.json');
  }

  private getBasicAuthHeader(): string {
    const credentials = Buffer.from(
      `${this.getClientId()}:${this.getClientSecret()}`,
    ).toString('base64');
    return `Basic ${credentials}`;
  }

  private readUserToken(): StoredSpotifyUserToken | null {
    const tokenPath = this.getTokenPath();

    if (!fs.existsSync(tokenPath)) {
      return null;
    }

    try {
      return JSON.parse(fs.readFileSync(tokenPath, 'utf-8'));
    } catch (error) {
      this.logger.error(`Failed to read Spotify user token: ${error.message}`);
      return null;
    }
  }

  private writeUserToken(token: StoredSpotifyUserToken): void {
    const tokenPath = this.getTokenPath();
    fs.mkdirSync(dirname(tokenPath), { recursive: true });
    fs.writeFileSync(tokenPath, JSON.stringify(token, null, 2), {
      encoding: 'utf-8',
      mode: 0o600,
    });
  }

  async hasUserToken(): Promise<boolean> {
    return !!this.readUserToken();
  }

  getAuthorizationUrl(state?: string): string {
    const url = new URL('https://accounts.spotify.com/authorize');

    url.searchParams.set('response_type', 'code');
    url.searchParams.set('client_id', this.getClientId());
    url.searchParams.set(
      'scope',
      [
        'playlist-read-private',
        'playlist-read-collaborative',
        'user-read-private',
      ].join(' '),
    );
    url.searchParams.set('redirect_uri', this.getRedirectUri());
    url.searchParams.set('show_dialog', 'true');

    if (state) {
      url.searchParams.set('state', state);
    }

    return url.toString();
  }

  async exchangeAuthorizationCode(code: string): Promise<void> {
    const response = await fetch('https://accounts.spotify.com/api/token', {
      method: 'POST',
      headers: {
        Authorization: this.getBasicAuthHeader(),
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code,
        redirect_uri: this.getRedirectUri(),
      }).toString(),
    });

    if (!response.ok) {
      const errorData = await response.text();
      throw new Error(`Failed to exchange Spotify authorization code: ${errorData}`);
    }

    const data = await response.json();

    this.writeUserToken({
      access_token: data.access_token,
      refresh_token: data.refresh_token,
      expires_at: Date.now() + data.expires_in * 1000 - 60_000,
      scope: data.scope,
      token_type: data.token_type,
    });

    this.logger.debug('Stored Spotify user access token');
  }

  private async refreshUserAccessToken(
    token: StoredSpotifyUserToken,
  ): Promise<StoredSpotifyUserToken> {
    const response = await fetch('https://accounts.spotify.com/api/token', {
      method: 'POST',
      headers: {
        Authorization: this.getBasicAuthHeader(),
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: token.refresh_token,
      }).toString(),
    });

    if (!response.ok) {
      const errorData = await response.text();
      throw new Error(`Failed to refresh Spotify user token: ${errorData}`);
    }

    const data = await response.json();
    const refreshed = {
      ...token,
      access_token: data.access_token,
      refresh_token: data.refresh_token || token.refresh_token,
      expires_at: Date.now() + data.expires_in * 1000 - 60_000,
      scope: data.scope || token.scope,
      token_type: data.token_type || token.token_type,
    };

    this.writeUserToken(refreshed);
    return refreshed;
  }

  private async getUserAccessToken(): Promise<string> {
    const token = this.readUserToken();

    if (!token) {
      throw new Error('Spotify user is not connected. Open /api/spotify/login first.');
    }

    if (Date.now() < token.expires_at) {
      return token.access_token;
    }

    return (await this.refreshUserAccessToken(token)).access_token;
  }

  async getTrackMetadata(
    spotifyUrl: string,
  ): Promise<{ name: string; artist: string; image: string; durationMs?: number }> {
    try {
      this.logger.debug(`Getting track metadata for ${spotifyUrl}`);
      const trackId = this.getTrackId(spotifyUrl);
      const accessToken = await this.getClientCredentialsAccessToken();

      const response = await fetch(
        `https://api.spotify.com/v1/tracks/${trackId}`,
        {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        },
      );

      if (!response.ok) {
        throw new Error(`Failed to fetch track: ${response.status}`);
      }

      const data = await response.json();

      return {
        name: data.name,
        artist: data.artists.map((a) => a.name).join(', '),
        image: data.album.images[0]?.url || '',
        durationMs: data.duration_ms,
      };
    } catch (error) {
      this.logger.error(`Failed to get track metadata: ${error.message}`);
      throw error;
    }
  }

  async getPlaylistMetadata(
    spotifyUrl: string,
  ): Promise<{ name: string; image: string }> {
    try {
      this.logger.debug(`Getting playlist metadata for ${spotifyUrl}`);
      const detail = await getDetails(spotifyUrl);

      return {
        name: detail.preview.title,
        image: detail.preview.image,
      };
    } catch (error) {
      this.logger.error(`Failed to get playlist metadata: ${error.message}`);
      throw error;
    }
  }

  private async getClientCredentialsAccessToken(): Promise<string> {
    if (this.accessToken && Date.now() < this.tokenExpiry) {
      return this.accessToken;
    }

    try {
      this.logger.debug('Getting new Spotify client credentials access token');

      const response = await fetch('https://accounts.spotify.com/api/token', {
        method: 'POST',
        headers: {
          Authorization: this.getBasicAuthHeader(),
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: 'grant_type=client_credentials',
      });

      if (!response.ok) {
        const errorData = await response.text();
        throw new Error(`Failed to get access token: ${errorData}`);
      }

      const data = await response.json();
      this.accessToken = data.access_token;
      this.tokenExpiry = Date.now() + data.expires_in * 1000 - 60000;

      this.logger.debug('Successfully obtained Spotify client credentials access token');
      return this.accessToken;
    } catch (error) {
      this.logger.error(`Error getting Spotify access token: ${error.message}`);
      throw error;
    }
  }


  async searchTrack(
    artist: string,
    title: string,
  ): Promise<{ spotifyUrl: string; name: string; artist: string; image: string; durationMs?: number }> {
    const accessToken = await this.getClientCredentialsAccessToken();
    const plainQuery = `${artist} ${title}`.trim();
    const structuredQuery = `track:"${title}" artist:"${artist}"`;

    const url = new URL('https://api.spotify.com/v1/search');
    url.searchParams.set('q', structuredQuery || plainQuery);
    url.searchParams.set('type', 'track');
    url.searchParams.set('limit', '10');

    const response = await fetch(url.toString(), {
      headers: { Authorization: `Bearer ${accessToken}` },
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Spotify track search failed: ${response.status} ${errorText}`);
    }

    const data = await response.json();
    const tracks = data?.tracks?.items || [];

    if (!tracks.length) {
      throw new Error(`No Spotify track found for ${artist} - ${title}`);
    }

    const normalize = (value: string) =>
      (value || '')
        .toLowerCase()
        .normalize('NFD')
        .replace(/[\u0300-\u036f]/g, '')
        .replace(/[^a-z0-9]+/g, ' ')
        .replace(/\s+/g, ' ')
        .trim();

    const wantedArtist = normalize(artist);
    const wantedTitle = normalize(title);

    const scoreTrack = (track: any): number => {
      const trackName = normalize(track.name);
      const artistNames = (track.artists || []).map((item: any) => normalize(item.name));
      const allArtists = artistNames.join(' ');

      let score = 0;
      if (trackName === wantedTitle) score += 70;
      else if (trackName.includes(wantedTitle) || wantedTitle.includes(trackName)) score += 40;

      if (artistNames.some((name: string) => name === wantedArtist)) score += 50;
      else if (allArtists.includes(wantedArtist) || wantedArtist.includes(allArtists)) score += 25;

      if (track.explicit !== undefined) score += 1;
      if (track.popularity) score += Math.min(20, Math.floor(track.popularity / 5));

      return score;
    };

    const ranked = [...tracks]
      .map((track: any) => ({ track, score: scoreTrack(track) }))
      .sort((a, b) => b.score - a.score);

    const best = ranked[0]?.track || tracks[0];

    this.logger.debug(
      `Spotify search "${plainQuery}" selected "${best?.artists?.map((item: any) => item.name).join(', ')} - ${best?.name}" score=${ranked[0]?.score ?? 0}`,
    );

    return {
      spotifyUrl: best.external_urls.spotify,
      name: best.name,
      artist: (best.artists || []).map((item: any) => item.name).join(', '),
      image: best.album?.images?.[0]?.url || '',
      durationMs: best.duration_ms,
    };
  }

  async getAllPlaylistTracks(spotifyUrl: string): Promise<any[]> {
    try {
      this.logger.debug(`Getting all tracks for playlist ${spotifyUrl}`);

      const playlistId = this.getPlaylistId(spotifyUrl);
      this.logger.debug(`Extracted playlist ID: ${playlistId}`);

      const accessToken = await this.getUserAccessToken();

      const allTracks = [];
      let offset = 0;
      let hasMoreTracks = true;

      while (hasMoreTracks) {
        this.logger.debug(
          `Fetching tracks from Spotify API with offset ${offset}`,
        );

        const response = await fetch(
          `https://api.spotify.com/v1/playlists/${playlistId}/items?offset=${offset}&limit=100`,
          {
            headers: {
              Authorization: `Bearer ${accessToken}`,
            },
          },
        );

        if (!response.ok) {
          const errorText = await response.text();
          this.logger.error(
            `Spotify API error: ${response.status} ${errorText}`,
          );

          if (response.status === 403) {
            throw new Error(
              'Spotify API refused playlist item access with 403 Forbidden. Connect Spotify through /api/spotify/login and ensure the authenticated user can access this playlist.',
            );
          }

          throw new Error(`Failed to fetch tracks: ${response.status}`);
        }

        const data = await response.json();

        if (!data.items || data.items.length === 0) {
          this.logger.debug('No more tracks to fetch from Spotify API');
          hasMoreTracks = false;
          continue;
        }

        const pageTracks = data.items
          .map(
            (item: {
              item: {
                id: string;
                name: any;
                artists: any[];
                preview_url: any;
                album: { images: any[] };
                duration_ms?: number;
              };
            }) => {
              if (!item.item) return null;

              return {
                id: item.item.id,
                name: item.item.name,
                artist: item.item.artists.map((a) => a.name).join(', '),
                previewUrl: item.item.preview_url,
                coverUrl: item.item.album?.images?.[0]?.url || null,
                durationMs: item.item.duration_ms,
              };
            },
          )
          .filter((track) => track !== null);

        this.logger.debug(
          `Retrieved ${pageTracks.length} tracks from Spotify API at offset ${offset}`,
        );

        if (pageTracks.length > 0) {
          allTracks.push(...pageTracks);
        }

        if (!data.next) {
          hasMoreTracks = false;
        } else {
          offset += 100;
        }
      }

      this.logger.debug(
        `Total tracks retrieved from Spotify API: ${allTracks.length}`,
      );
      return allTracks;
    } catch (error) {
      this.logger.error(`Failed to get all playlist tracks: ${error.message}`);
      throw error;
    }
  }

  async getArtistLibrary(
    spotifyUrl: string,
  ): Promise<{ name: string; image: string; tracks: any[] }> {
    try {
      this.logger.debug(`Getting artist library for ${spotifyUrl}`);
      const artistId = this.getArtistId(spotifyUrl);
      const accessToken = await this.getUserAccessToken();

      const artistResponse = await fetch(
        `https://api.spotify.com/v1/artists/${artistId}`,
        {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        },
      );

      if (!artistResponse.ok) {
        const errorText = await artistResponse.text();
        throw new Error(`Failed to fetch artist metadata: ${artistResponse.status} ${errorText}`);
      }

      const artist = await artistResponse.json();
      const albums = await this.getAllArtistAlbums(artistId, accessToken);
      const tracks = await this.getTracksForAlbums(albums, accessToken, artist.name);

      return {
        name: `Artist Library: ${artist.name}`,
        image: artist.images?.[0]?.url || '',
        tracks,
      };
    } catch (error) {
      this.logger.error(`Failed to get artist library: ${error.message}`);
      throw error;
    }
  }

  private async getAllArtistAlbums(artistId: string, accessToken: string): Promise<any[]> {
    const includeGroups = process.env.SPOTIFY_ARTIST_INCLUDE_GROUPS || 'album,single';
    const albumByKey = new Map<string, any>();
    const artistAlbumPageLimit = 20;
    let offset = 0;
    let hasMoreAlbums = true;

    while (hasMoreAlbums) {
      this.logger.debug(
        `Fetching artist albums from Spotify API with offset ${offset}`,
      );

      const albumUrl = new URL(
        `https://api.spotify.com/v1/artists/${artistId}/albums`,
      );
      albumUrl.searchParams.set('include_groups', includeGroups);
      albumUrl.searchParams.set('offset', String(offset));

      this.logger.debug(`Spotify artist albums URL: ${albumUrl.toString()}`);

      const response = await fetch(albumUrl.toString(), {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      });

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`Failed to fetch artist albums: ${response.status} ${errorText}`);
      }

      const data = await response.json();

      for (const album of data.items || []) {
        const key = this.normalizeLibraryKey(`${album.name}|${album.release_date}|${album.total_tracks}`);
        if (!albumByKey.has(key)) {
          albumByKey.set(key, album);
        }
      }

      if (!data.next) {
        hasMoreAlbums = false;
      } else {
        offset += artistAlbumPageLimit;
      }
    }

    const albums = [...albumByKey.values()];
    this.logger.debug(`Retrieved ${albums.length} deduplicated artist albums/singles`);
    return albums;
  }

  private async getTracksForAlbums(
    albums: any[],
    accessToken: string,
    artistName: string,
  ): Promise<any[]> {
    const trackByKey = new Map<string, any>();
    const maxTracks = Number(process.env.SPOTIFY_ARTIST_LIBRARY_MAX_TRACKS || 1000);

    for (const album of albums) {
      let offset = 0;
      let hasMoreTracks = true;

      while (hasMoreTracks) {
        const response = await fetch(
          `https://api.spotify.com/v1/albums/${album.id}/tracks?limit=50&offset=${offset}`,
          {
            headers: {
              Authorization: `Bearer ${accessToken}`,
            },
          },
        );

        if (!response.ok) {
          const errorText = await response.text();
          throw new Error(`Failed to fetch album tracks: ${response.status} ${errorText}`);
        }

        const data = await response.json();

        for (const item of data.items || []) {
          if (!item?.name || !item?.artists?.length) {
            continue;
          }

          const fullTrack = await this.getTrackById(item.id, accessToken);
          const isrc = fullTrack?.external_ids?.isrc;
          const durationMs = fullTrack?.duration_ms || item.duration_ms;
          const artist = item.artists.map((a) => a.name).join(', ');
          const key = isrc
            ? `isrc:${isrc}`
            : this.normalizeLibraryKey(`${artist}|${item.name}|${Math.round((durationMs || 0) / 2000)}`);

          if (!trackByKey.has(key)) {
            trackByKey.set(key, {
              id: item.id,
              name: item.name,
              artist,
              previewUrl: item.external_urls?.spotify || null,
              coverUrl: album.images?.[0]?.url || null,
              durationMs,
              albumName: album.name,
              primaryArtist: artistName,
            });
          }

          if (trackByKey.size >= maxTracks) {
            this.logger.warn(`Artist library max track limit reached: ${maxTracks}`);
            return [...trackByKey.values()];
          }
        }

        if (!data.next) {
          hasMoreTracks = false;
        } else {
          offset += 50;
        }
      }
    }

    const tracks = [...trackByKey.values()];
    this.logger.debug(`Retrieved ${tracks.length} deduplicated artist library tracks`);
    return tracks;
  }

  private async getTrackById(trackId: string, accessToken: string): Promise<any> {
    const response = await fetch(
      `https://api.spotify.com/v1/tracks/${trackId}`,
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      },
    );

    if (!response.ok) {
      return null;
    }

    return response.json();
  }

  private normalizeLibraryKey(value: string): string {
    return value
      .toLowerCase()
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .replace(/&/g, ' and ')
      .replace(/[^a-z0-9]+/g, ' ')
      .replace(/\s+/g, ' ')
      .trim();
  }

}
