import crypto from 'crypto';

export interface NavidromeConfig {
  serverUrl: string;
  username: string;
  password: string;
}

export interface SubsonicResponse<T = Record<string, unknown>> {
  'subsonic-response': {
    status: string;
    version: string;
    type: string;
    serverVersion?: string;
    openSubsonic?: boolean;
    error?: {
      code: number;
      message: string;
    };
  } & T;
}

export interface Album {
  id: string;
  name: string;
  artist: string;
  artistId: string;
  coverArt?: string;
  songCount: number;
  duration: number;
  playCount?: number;
  created: string;
  starred?: string;
  year?: number;
  genre?: string;
}

export interface Artist {
  id: string;
  name: string;
  albumCount: number;
  starred?: string;
  coverArt?: string;
}

export interface Song {
  id: string;
  parent: string;
  isDir: boolean;
  title: string;
  album: string;
  artist: string;
  track?: number;
  year?: number;
  genre?: string;
  coverArt?: string;
  size: number;
  contentType: string;
  suffix: string;
  duration: number;
  bitRate?: number;
  path: string;
  playCount?: number;
  discNumber?: number;
  created: string;
  albumId: string;
  artistId: string;
  type: string;
  starred?: string;
  replayGain?: number;
}

export interface Playlist {
  id: string;
  name: string;
  comment?: string;
  owner: string;
  public: boolean;
  songCount: number;
  duration: number;
  created: string;
  changed: string;
  coverArt?: string;
}

export interface RadioStation {
  id: string;
  streamUrl: string;
  name: string;
  homePageUrl?: string;
}

export interface AlbumInfo {
  notes?: string;
  musicBrainzId?: string;
  lastFmUrl?: string;
  smallImageUrl?: string;
  mediumImageUrl?: string;
  largeImageUrl?: string;
  biography?: string;
}

export interface ArtistInfo {
  biography?: string;
  musicBrainzId?: string;
  lastFmUrl?: string;
  smallImageUrl?: string;
  mediumImageUrl?: string;
  largeImageUrl?: string;
  similarArtist?: Artist[];
}

export interface User {
  username: string;
  email?: string;
  scrobblingEnabled: boolean;
  maxBitRate?: number;
  adminRole: boolean;
  settingsRole: boolean;
  downloadRole: boolean;
  uploadRole: boolean;
  playlistRole: boolean;
  coverArtRole: boolean;
  commentRole: boolean;
  podcastRole: boolean;
  streamRole: boolean;
  jukeboxRole: boolean;
  shareRole: boolean;
  videoConversionRole: boolean;
  avatarLastChanged?: string;
}

class NavidromeAPI {
  private config: NavidromeConfig;
  private clientName = 'miceclient';
  private version = '1.16.0';

  constructor(config: NavidromeConfig) {
    this.config = config;
  }

  private generateSalt(): string {
    return crypto.randomBytes(8).toString('hex');
  }

  private generateToken(password: string, salt: string): string {
    return crypto.createHash('md5').update(password + salt).digest('hex');
  }

  async makeRequest(endpoint: string, params: Record<string, string | number> = {}): Promise<Record<string, unknown>> {
    const salt = this.generateSalt();
    const token = this.generateToken(this.config.password, salt);

    const queryParams = new URLSearchParams({
      u: this.config.username,
      t: token,
      s: salt,
      v: this.version,
      c: this.clientName,
      f: 'json',
      ...params
    });

    const url = `${this.config.serverUrl}/rest/${endpoint}?${queryParams.toString()}`;

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const data: SubsonicResponse = await response.json();
      
      if (data['subsonic-response'].status === 'failed') {
        throw new Error(data['subsonic-response'].error?.message || 'Unknown error');
      }
      
      return data['subsonic-response'];
    } catch (error) {
      console.error('Navidrome API request failed:', error);
      throw error;
    }
  }

  async ping(): Promise<boolean> {
    try {
      await this.makeRequest('ping');
      return true;
    } catch {
      return false;
    }
  }

  async getUserInfo(): Promise<User> {
    const response = await this.makeRequest('getUser', { username: this.config.username });
    const userData = response.user as User;
    return userData;
  }

  async getArtists(): Promise<Artist[]> {
    const response = await this.makeRequest('getArtists');
    const artists: Artist[] = [];
    
    const artistsData = response.artists as { index?: Array<{ artist?: Artist[] }> };
    if (artistsData?.index) {
      for (const index of artistsData.index) {
        if (index.artist) {
          artists.push(...index.artist);
        }
      }
    }
    
    return artists;
  }

  async getArtist(artistId: string): Promise<{ artist: Artist; albums: Album[] }> {
    try {
      const response = await this.makeRequest('getArtist', { id: artistId });
      // Check if artist data exists
      if (!response.artist) {
        throw new Error('Artist not found in response');
      }
      const artistData = response.artist as Artist & { album?: Album[] };
      return {
        artist: artistData,
        albums: artistData.album || []
      };
    } catch (error) {
      console.error('Navidrome API request failed:', error);
      throw new Error('Artist not found');
    }
  }

  async getAlbums(type?: 'newest' | 'recent' | 'frequent' | 'random' | 'alphabeticalByName' | 'alphabeticalByArtist' | 'starred' | 'highest', size: number = 500, offset: number = 0): Promise<Album[]> {
    const response = await this.makeRequest('getAlbumList2', { 
      type: type || 'newest',
      size,
      offset
    });
    const albumListData = response.albumList2 as { album?: Album[] };
    return albumListData?.album || [];
  }

  async getAlbum(albumId: string): Promise<{ album: Album; songs: Song[] }> {
    const response = await this.makeRequest('getAlbum', { id: albumId });
    const albumData = response.album as Album & { song?: Song[] };
    return {
      album: albumData,
      songs: albumData.song || []
    };
  }

  async search(query: string, artistCount = 20, albumCount = 20, songCount = 20): Promise<{
    artists: Artist[];
    albums: Album[];
    songs: Song[];
  }> {
    const response = await this.makeRequest('search3', {
      query,
      artistCount,
      albumCount,
      songCount
    });

    const searchData = response.searchResult3 as {
      artist?: Artist[];
      album?: Album[];
      song?: Song[];
    };

    return {
      artists: searchData?.artist || [],
      albums: searchData?.album || [],
      songs: searchData?.song || []
    };
  }

  async getPlaylists(): Promise<Playlist[]> {
    const response = await this.makeRequest('getPlaylists');
    const playlistsData = response.playlists as { playlist?: Playlist[] };
    return playlistsData?.playlist || [];
  }

  async getPlaylist(playlistId: string): Promise<{ playlist: Playlist; songs: Song[] }> {
    const response = await this.makeRequest('getPlaylist', { id: playlistId });
    const playlistData = response.playlist as Playlist & { entry?: Song[] };
    return {
      playlist: playlistData,
      songs: playlistData.entry || []
    };
  }

  async createPlaylist(name: string, songIds?: string[]): Promise<Playlist> {
    const params: Record<string, string | number> = { name };
    if (songIds && songIds.length > 0) {
      songIds.forEach((id, index) => {
        params[`songId[${index}]`] = id;
      });
    }
    
    const response = await this.makeRequest('createPlaylist', params);
    return response.playlist as Playlist;
  }

  async updatePlaylist(playlistId: string, name?: string, comment?: string, songIds?: string[]): Promise<void> {
    const params: Record<string, string | number> = { playlistId };
    if (name) params.name = name;
    if (comment) params.comment = comment;
    if (songIds) {
      songIds.forEach((id, index) => {
        params[`songId[${index}]`] = id;
      });
    }
    
    await this.makeRequest('updatePlaylist', params);
  }

  async deletePlaylist(playlistId: string): Promise<void> {
    await this.makeRequest('deletePlaylist', { id: playlistId });
  }

  getStreamUrl(songId: string, maxBitRate?: number): string {
    const salt = this.generateSalt();
    const token = this.generateToken(this.config.password, salt);
    
    const params = new URLSearchParams({
      u: this.config.username,
      t: token,
      s: salt,
      v: this.version,
      c: this.clientName,
      id: songId
    });

    if (maxBitRate) {
      params.append('maxBitRate', maxBitRate.toString());
    }

    return `${this.config.serverUrl}/rest/stream?${params.toString()}`;
  }

  // Direct download URL (original file). Useful for offline caching where the browser can handle transcoding.
  getDownloadUrl(songId: string): string {
    const salt = this.generateSalt();
    const token = this.generateToken(this.config.password, salt);

    const params = new URLSearchParams({
      u: this.config.username,
      t: token,
      s: salt,
      v: this.version,
      c: this.clientName,
      id: songId
    });

    return `${this.config.serverUrl}/rest/download?${params.toString()}`;
  }

  getCoverArtUrl(coverArtId: string, size?: number): string {
    const salt = this.generateSalt();
    const token = this.generateToken(this.config.password, salt);
    
    const params = new URLSearchParams({
      u: this.config.username,
      t: token,
      s: salt,
      v: this.version,
      c: this.clientName,
      id: coverArtId
    });

    if (size) {
      params.append('size', size.toString());
    }

    return `${this.config.serverUrl}/rest/getCoverArt?${params.toString()}`;
  }

  async star(id: string, type: 'song' | 'album' | 'artist'): Promise<void> {
    const paramName = type === 'song' ? 'id' : type === 'album' ? 'albumId' : 'artistId';
    await this.makeRequest('star', { [paramName]: id });
  }

  async unstar(id: string, type: 'song' | 'album' | 'artist'): Promise<void> {
    const paramName = type === 'song' ? 'id' : type === 'album' ? 'albumId' : 'artistId';
    await this.makeRequest('unstar', { [paramName]: id });
  }

  async scrobble(songId: string, submission: boolean = true): Promise<void> {
    await this.makeRequest('scrobble', { 
      id: songId, 
      submission: submission.toString(),
      time: Date.now()
    });
  }

  // Enhanced scrobbling functionality for Last.fm integration
  async updateNowPlaying(songId: string): Promise<void> {
    try {
      await this.makeRequest('scrobble', { 
        id: songId, 
        submission: 'false',
        time: Date.now()
      });
    } catch (error) {
      console.error('Failed to update now playing:', error);
      // Don't throw - this is not critical
    }
  }

  async scrobbleTrack(songId: string, timestamp?: number): Promise<void> {
    try {
      await this.makeRequest('scrobble', { 
        id: songId, 
        submission: 'true',
        time: timestamp || Date.now()
      });
    } catch (error) {
      console.error('Failed to scrobble track:', error);
      // Don't throw - this is not critical
    }
  }

  // Helper method to determine if a track should be scrobbled
  // According to Last.fm guidelines: track should be scrobbled if played for at least 
  // 30 seconds OR half the track duration, whichever comes first
  shouldScrobble(playedDuration: number, totalDuration: number): boolean {
    const minimumTime = 30; // 30 seconds minimum
    const halfTrackTime = totalDuration / 2;
    return playedDuration >= Math.min(minimumTime, halfTrackTime);
  }

  async getAllSongs(size = 500, offset = 0): Promise<Song[]> {
    const response = await this.makeRequest('search3', {
      query: '',
      songCount: size,
      songOffset: offset,
      artistCount: 0,
      albumCount: 0
    });
    
    const searchData = response.searchResult3 as { song?: Song[] };
    return searchData?.song || [];
  }

  async getRadioStations(): Promise<RadioStation[]> {
    const response = await this.makeRequest('getRadioStations');
    const radioStationsData = response.radioStations as { radioStation?: RadioStation[] };
    return radioStationsData?.radioStation || [];
  }

  async getRadioStation(stationId: string): Promise<RadioStation> {
    const response = await this.makeRequest('getRadioStation', { id: stationId });
    return response.radioStation as RadioStation;
  }

  async getInternetRadioStations(): Promise<RadioStation[]> {
    try {
      const response = await this.makeRequest('getInternetRadioStations');
      const radioData = response.internetRadioStations as { internetRadioStation?: RadioStation[] };
      return radioData?.internetRadioStation || [];
    } catch (error) {
      console.error('Failed to get internet radio stations:', error);
      return [];
    }
  }

  async createInternetRadioStation(name: string, streamUrl: string, homePageUrl?: string): Promise<void> {
    const params: Record<string, string> = { name, streamUrl };
    if (homePageUrl) params.homePageUrl = homePageUrl;
    await this.makeRequest('createInternetRadioStation', params);
  }

  async deleteInternetRadioStation(id: string): Promise<void> {
    await this.makeRequest('deleteInternetRadioStation', { id });
  }

  async getArtistInfo(artistId: string): Promise<{ artist: Artist; info: ArtistInfo }> {
    const response = await this.makeRequest('getArtistInfo2', { id: artistId });
    const artistData = response.artist as Artist;
    const artistInfo = response.info as ArtistInfo;
    return {
      artist: artistData,
      info: artistInfo
    };
  }

  async getAlbumInfo(albumId: string): Promise<{ album: Album; info: AlbumInfo }> {
    const response = await this.makeRequest('getAlbumInfo2', { id: albumId });
    const albumData = response.album as Album;
    const albumInfo = response.info as AlbumInfo;
    return {
      album: albumData,
      info: albumInfo
    };
  }

  async search2(query: string, artistCount = 20, albumCount = 20, songCount = 20): Promise<{
    artists: Artist[];
    albums: Album[];
    songs: Song[];
  }> {
    const response = await this.makeRequest('search2', {
      query,
      artistCount,
      albumCount,
      songCount
    });

    const searchData = response.searchResult2 as {
      artist?: Artist[];
      album?: Album[];
      song?: Song[];
    };

    return {
      artists: searchData?.artist || [],
      albums: searchData?.album || [],
      songs: searchData?.song || []
    };
  }

  async getArtistInfo2(artistId: string, count = 20, includeNotPresent = false): Promise<ArtistInfo> {
    const response = await this.makeRequest('getArtistInfo2', {
      id: artistId,
      count,
      includeNotPresent: includeNotPresent.toString()
    });
    return response.artistInfo2 as ArtistInfo;
  }

  async getAlbumInfo2(albumId: string): Promise<AlbumInfo> {
    const response = await this.makeRequest('getAlbumInfo2', {
      id: albumId
    });
    return response.albumInfo2 as AlbumInfo;
  }

  async getStarred2(): Promise<{ starred2: { song?: Song[]; album?: Album[]; artist?: Artist[] } }> {
    try {
      const response = await this.makeRequest('getStarred2');
      return response as { starred2: { song?: Song[]; album?: Album[]; artist?: Artist[] } };
    } catch (error) {
      console.error('Failed to get starred items:', error);
      return { starred2: {} };
    }
  }

  async getAlbumSongs(albumId: string): Promise<Song[]> {
    try {
      const response = await this.makeRequest('getAlbum', { id: albumId });
      const albumData = response.album as { song?: Song[] };
      return albumData?.song || [];
    } catch (error) {
      console.error('Failed to get album songs:', error);
      return [];
    }
  }

  async getArtistTopSongs(artistName: string, limit: number = 10): Promise<Song[]> {
    try {
      // Search for songs by the artist and return them sorted by play count
      const searchResult = await this.search2(artistName, 0, 0, limit * 3);
      
      // Filter songs that are actually by this artist (exact match)
      const artistSongs = searchResult.songs.filter(song => 
        song.artist.toLowerCase() === artistName.toLowerCase()
      );
      
      // Sort by play count (descending) and limit results
      return artistSongs
        .sort((a, b) => (b.playCount || 0) - (a.playCount || 0))
        .slice(0, limit);
    } catch (error) {
      console.error('Failed to get artist top songs:', error);
      return [];
    }
  }
}

// Singleton instance management
let navidromeInstance: NavidromeAPI | null = null;

export function getNavidromeAPI(customConfig?: NavidromeConfig): NavidromeAPI | null {
  let config: NavidromeConfig;
  
  if (customConfig) {
    config = customConfig;
  } else {
    // Try to get config from localStorage first (client-side)
    if (typeof window !== 'undefined') {
      const savedConfig = localStorage.getItem('navidrome-config');
      if (savedConfig) {
        try {
          config = JSON.parse(savedConfig);
        } catch (error) {
          console.error('Failed to parse saved Navidrome config:', error);
          config = getEnvConfig();
        }
      } else {
        config = getEnvConfig();
      }
    } else {
      // Server-side: use environment variables
      config = getEnvConfig();
    }
  }
  
  if (!config.serverUrl || !config.username || !config.password) {
    // Return null instead of throwing an error when configuration is incomplete
    // console.log('Navidrome configuration is incomplete. Please configure in settings.');
    return null;
  }
  
  // Always create a new instance if config is provided or if no instance exists
  if (customConfig || !navidromeInstance) {
    navidromeInstance = new NavidromeAPI(config);
  }
  
  return navidromeInstance;
}

function getEnvConfig(): NavidromeConfig {
  return {
    serverUrl: process.env.NEXT_PUBLIC_NAVIDROME_URL || '',
    username: process.env.NEXT_PUBLIC_NAVIDROME_USERNAME || '',
    password: process.env.NEXT_PUBLIC_NAVIDROME_PASSWORD || ''
  };
}

export function resetNavidromeAPI(): void {
  navidromeInstance = null;
}

export default NavidromeAPI;
