import 'package:chopper/chopper.dart';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';

import 'finamp_user_helper.dart';
import 'jellyfin_api.dart';
import '../models/finamp_models.dart';
import '../models/jellyfin_models.dart';

class JellyfinApiHelper {
  final jellyfinApi = JellyfinApi.create();
  final _jellyfinApiHelperLogger = Logger("JellyfinApiHelper");

  // Stores the ids of the artists that the user selected to mix
  List<String> selectedMixArtistsIds = [];

  // Stores the ids of albums that the user selected to mix
  List<String> selectedMixAlbumIds = [];

  Uri? baseUrlTemp;

  final _finampUserHelper = GetIt.instance<FinampUserHelper>();

  Future<List<BaseItemDto>?> getItems({
    BaseItemDto? parentItem,
    String? includeItemTypes,
    String? sortBy,
    String? sortOrder,
    String? searchTerm,
    required bool isGenres,
    String? filters,

    /// The record index to start at. All items with a lower index will be
    /// dropped from the results.
    int? startIndex,

    /// The maximum number of records to return.
    int? limit,
  }) async {
    final currentUser = _finampUserHelper.currentUser;
    if (currentUser == null) {
      // When logging out, this request causes errors since currentUser is
      // required sometimes. We just return an empty list since this error
      // usually happens becuase the listeners on MusicScreenTabView update
      // milliseconds before the page is popped. This shouldn't happen in normal
      // use.
      return [];
    }

    var response;

    // We send a different request for playlists so that we get them back in the
    // right order. Doing this in the same function makes sense since they both
    // return the same thing. It also means we can easily share album widgets
    // with playlists.
    if (parentItem?.type == "Playlist") {
      response = await jellyfinApi.getPlaylistItems(
        playlistId: parentItem!.id,
        userId: currentUser.id,
        parentId: parentItem.id,
        includeItemTypes: includeItemTypes,
        recursive: true,
      );
    } else if (includeItemTypes == "MusicArtist") {
      // For artists, we need to use a different endpoint
      response = await jellyfinApi.getAlbumArtists(
        parentId: parentItem?.id,
        recursive: true,
        sortBy: sortBy,
        sortOrder: sortOrder,
        searchTerm: searchTerm,
        filters: filters,
        startIndex: startIndex,
        limit: limit,
        userId: currentUser.id,
      );
    } else if (parentItem?.type == "MusicArtist") {
      // For getting the children of artists, we need to use albumArtistIds
      // instead of parentId
      response = await jellyfinApi.getItems(
        userId: currentUser.id,
        albumArtistIds: parentItem?.id,
        includeItemTypes: includeItemTypes,
        recursive: true,
        sortBy: sortBy,
        sortOrder: sortOrder,
        searchTerm: searchTerm,
        filters: filters,
        startIndex: startIndex,
        limit: limit,
      );
    } else if (includeItemTypes == "MusicGenre") {
      response = await jellyfinApi.getGenres(
        parentId: parentItem?.id,
        // includeItemTypes: includeItemTypes,
        searchTerm: searchTerm,
        startIndex: startIndex,
        limit: limit,
      );
    } else if (parentItem?.type == "MusicGenre") {
      response = await jellyfinApi.getItems(
        userId: currentUser.id,
        genreIds: parentItem?.id,
        includeItemTypes: includeItemTypes,
        recursive: true,
        sortBy: sortBy,
        sortOrder: sortOrder,
        searchTerm: searchTerm,
        filters: filters,
        startIndex: startIndex,
        limit: limit,
      );
    } else {
      // This will be run when getting albums, songs in albums, and stuff like
      // that.
      response = await jellyfinApi.getItems(
        userId: currentUser.id,
        parentId: parentItem?.id,
        includeItemTypes: includeItemTypes,
        recursive: true,
        sortBy: sortBy,
        sortOrder: sortOrder,
        searchTerm: searchTerm,
        filters: filters,
        startIndex: startIndex,
        limit: limit,
      );
    }

    return (QueryResult_BaseItemDto.fromJson(response).items);
  }

  /// Authenticates a user and saves the login details
  Future<void> authenticateViaName({
    required String username,
    String? password,
  }) async {
    var response;

    // Some users won't have a password.
    if (password == null) {
      response = await jellyfinApi.authenticateViaName({"Username": username});
    } else {
      response = await jellyfinApi
          .authenticateViaName({"Username": username, "Pw": password});
    }

    AuthenticationResult newUserAuthenticationResult =
        AuthenticationResult.fromJson(response);

    FinampUser newUser = FinampUser(
      id: newUserAuthenticationResult.user!.id,
      baseUrl: baseUrlTemp!.toString(),
      accessToken: newUserAuthenticationResult.accessToken!,
      serverId: newUserAuthenticationResult.serverId!,
      views: {},
    );

    await _finampUserHelper.saveUser(newUser);
  }

  /// Gets all the user's views.
  Future<List<BaseItemDto>> getViews() async {
    var response =
        await jellyfinApi.getViews(_finampUserHelper.currentUser!.id);

    return QueryResult_BaseItemDto.fromJson(response).items!;
  }

  /// Gets the playback info for an item, such as format and bitrate. Usually, I'd require a BaseItemDto as an argument
  /// but since this will be run inside of [MusicPlayerBackgroundTask], I've just set the raw id as an argument.
  Future<List<MediaSourceInfo>?> getPlaybackInfo(String itemId) async {
    var response = await jellyfinApi.getPlaybackInfo(
      id: itemId,
      userId: _finampUserHelper.currentUser!.id,
    );

    // getPlaybackInfo returns a PlaybackInfoResponse. We only need the List<MediaSourceInfo> in it so we convert it here and
    // return that List<MediaSourceInfo>.
    final PlaybackInfoResponse decodedResponse =
        PlaybackInfoResponse.fromJson(response);
    return decodedResponse.mediaSources;
  }

  /// Starts an instant mix using the data from the item provided.
  Future<List<BaseItemDto>?> getInstantMix(BaseItemDto? parentItem) async {
    var response = await jellyfinApi.getInstantMix(
        id: parentItem!.id,
        userId: _finampUserHelper.currentUser!.id,
        limit: 200);

    return (QueryResult_BaseItemDto.fromJson(response).items);
  }

  /// Tells the Jellyfin server that playback has started
  Future<void> reportPlaybackStart(
      PlaybackProgressInfo playbackProgressInfo) async {
    await jellyfinApi.startPlayback(playbackProgressInfo);
  }

  /// Updates player progress so that Jellyfin can track what we're listening to
  Future<void> updatePlaybackProgress(
      PlaybackProgressInfo playbackProgressInfo) async {
      await jellyfinApi.playbackStatusUpdate(playbackProgressInfo);
  }

  /// Tells Jellyfin that we've stopped listening to music (called when the audio service is stopped)
  Future<void> stopPlaybackProgress(
      PlaybackProgressInfo playbackProgressInfo) async {
      await jellyfinApi.playbackStatusStopped(playbackProgressInfo);
  }

  /// Gets an item from a user's library.
  Future<BaseItemDto> getItemById(String itemId) async {
    var response = await jellyfinApi.getItemById(
      userId: _finampUserHelper.currentUser!.id,
      itemId: itemId,
    );

    return (BaseItemDto.fromJson(response));
  }

  /// Creates a new playlist.
  Future<NewPlaylistResponse> createNewPlaylist(NewPlaylist newPlaylist) async {
    var response = await jellyfinApi.createNewPlaylist(
      newPlaylist: newPlaylist,
    );

    return NewPlaylistResponse.fromJson(response);
  }

  /// Adds items to a playlist.
  Future<void> addItemstoPlaylist({
    /// The playlist id.
    required String playlistId,

    /// Item ids to add.
    List<String>? ids,
  }) async {
    await jellyfinApi.addItemsToPlaylist(
      playlistId: playlistId,
      ids: ids?.join(","),
    );
  }

  /// Remove items to a playlist.
  Future<void> removeItemsFromPlaylist({
    /// The playlist id.
    required String playlistId,

    /// Item ids to add.
    List<String>? entryIds,
  }) async {
    await jellyfinApi.removeItemsFromPlaylist(
      playlistId: playlistId,
      entryIds: entryIds?.join(","),
    );
  }

  /// Updates an item.
  Future<void> updateItem({
    /// The item id.
    required String itemId,

    /// What to update the item with. You should give a BaseItemDto with only
    /// changed values.
    required BaseItemDto newItem,
  }) async {
    await jellyfinApi.updateItem(itemId: itemId, newItem: newItem);
  }

  /// Marks an item as a favorite.
  Future<UserItemDataDto> addFavourite(String itemId) async {
    var response = await jellyfinApi.addFavourite(
        userId: _finampUserHelper.currentUser!.id, itemId: itemId);

    return UserItemDataDto.fromJson(response);
  }

  /// Unmarks item as a favorite.
  Future<UserItemDataDto> removeFavourite(String itemId) async {
    var response = await jellyfinApi.removeFavourite(
        userId: _finampUserHelper.currentUser!.id, itemId: itemId);

    return UserItemDataDto.fromJson(response);
  }

  void addArtistToMixBuilderList(BaseItemDto item) {
    selectedMixArtistsIds.add(item.id);
  }

  void removeArtistFromBuilderList(BaseItemDto item) {
    selectedMixArtistsIds.remove(item.id);
  }

  void addAlbumToMixBuilderList(BaseItemDto item) {
    selectedMixAlbumIds.add(item.id);
  }

  void removeAlbumFromBuilderList(BaseItemDto item) {
    selectedMixAlbumIds.remove(item.id);
  }

  Future<List<BaseItemDto>?> getArtistMix(List<String> artistIds) async {
    var response = await jellyfinApi.getItems(
        userId: _finampUserHelper.currentUser!.id,
        artistIds: artistIds.join(","),
        filters: "IsNotFolder",
        recursive: true,
        sortBy: "Random",
        limit: 300,
        fields: "Chapters");

    return (QueryResult_BaseItemDto.fromJson(response).items);
  }

  Future<List<BaseItemDto>?> getAlbumMix(List<String> albumIds) async {
    var response = await jellyfinApi.getItems(
        userId: _finampUserHelper.currentUser!.id,
        albumIds: albumIds.join(","),
        filters: "IsNotFolder",
        recursive: true,
        sortBy: "Random",
        limit: 300,
        fields: "Chapters");

    return (QueryResult_BaseItemDto.fromJson(response).items);
  }

  /// Removes the current user from the DB and revokes the token on Jellyfin
  Future<void> logoutCurrentUser() async {
    // We put this in a try-catch loop that basically ignores errors so that the
    // user can still log out during scenarios like wrong IP, no internet etc.

    try {
      await jellyfinApi.logout().timeout(
            const Duration(seconds: 3),
            onTimeout: () => _jellyfinApiHelperLogger.warning(
                "Logout request timed out. Logging out anyway, but be aware that Jellyfin may have not got the signal."),
          );
    } catch (e) {
      _jellyfinApiHelperLogger.warning(
          "Jellyfin logout failed. Logging out anyway, but be aware that Jellyfin may have not got the signal.",
          e);
    } finally {
      // If we're unauthorised, the logout command will fail but we're already
      // basically logged out so we shouldn't fail.
      _finampUserHelper.removeUser(_finampUserHelper.currentUser!.id);
    }
  }

  /// Returns the correct image URL for the given item, or null if there is no
  /// image. Uses [getImageId] to get the actual id. [maxWidth] and [maxHeight]
  /// can be specified to return a smaller image. [quality] can be modified to
  /// get a higher/lower quality image.
  Uri? getImageUrl({
    required BaseItemDto item,
    int? maxWidth,
    int? maxHeight,
    int? quality = 90,
    String? format = "jpg",
  }) {
    if (item.imageId == null) {
      return null;
    }

    final parsedBaseUrl = Uri.parse(_finampUserHelper.currentUser!.baseUrl);
    List<String> builtPath = List<String>.from(parsedBaseUrl.pathSegments);
    builtPath.addAll([
      "Items",
      item.imageId!,
      "Images",
      "Primary",
    ]);
    return Uri(
        host: parsedBaseUrl.host,
        port: parsedBaseUrl.port,
        scheme: parsedBaseUrl.scheme,
        userInfo: parsedBaseUrl.userInfo,
        pathSegments: builtPath,
        queryParameters: {
          if (format != null) "format": format,
          if (quality != null) "quality": quality.toString(),
          if (maxWidth != null) "MaxWidth": maxWidth.toString(),
          if (maxHeight != null) "MaxHeight": maxHeight.toString(),
        });
  }
}
