import '../../models/jellyfin_session.dart';
import '../../models/lyrics.dart';
import 'jellyfin_api.dart';

/// The single seam through which Linthra talks HTTP to a Jellyfin server.
///
/// Every request to Jellyfin goes through this interface, so the rest of the
/// app (authenticator, source, settings) depends only on it — never on `http`,
/// URLs, headers, or JSON. That keeps networking swappable and, crucially,
/// lets tests drive the whole feature with a fake client and canned responses
/// (no real server, no Cloudflare).
///
/// Implementations throw a [JellyfinException] (with a friendly message and a
/// [JellyfinErrorKind]) for every failure, and must never put the password or
/// access token into an exception, log, or any other output.
abstract interface class JellyfinClient {
  /// Confirms an address is a reachable Jellyfin server and returns its public
  /// info. Backs the "Test connection" action; needs no credentials.
  Future<JellyfinServerInfo> fetchServerInfo(String baseUrl);

  /// Exchanges a username + password for an access token.
  ///
  /// [deviceId] is generated by the caller (the authenticator) so the same id
  /// can be reused for the resulting session's later requests. The password is
  /// used only for this call and is never persisted by anything downstream.
  Future<JellyfinAuthResult> authenticateByName({
    required String baseUrl,
    required String username,
    required String password,
    required String deviceId,
  });

  /// Lists library items of one [kind] for the signed-in user in [session].
  ///
  /// Pulls the whole library in bounded pages (so a large/slow server can't
  /// time out a single unbounded request) and is tolerant per item: an entry
  /// too malformed to use is skipped, never thrown, and counted in the returned
  /// [JellyfinItemListing.skippedCount]. A transient network/5xx failure is
  /// retried a bounded number of times before it surfaces as a
  /// [JellyfinException]; auth (401/403) and other client errors are not
  /// retried. A failure *between* pages throws (so the caller keeps the
  /// previous catalog) rather than returning a truncated listing.
  Future<JellyfinItemListing> fetchItems(
    JellyfinSession session, {
    required JellyfinItemKind kind,
  });

  /// Confirms [session] is still accepted by the server — the token is valid
  /// and the server is reachable — without fetching anything large.
  ///
  /// Backs the pre-stream check the playback resolver runs so it can report an
  /// expired session or an unreachable server precisely. Throws a
  /// [JellyfinException] ([JellyfinErrorKind.unauthorized] for an expired token,
  /// [JellyfinErrorKind.notReachable] when offline, …) on failure.
  Future<void> verifySession(JellyfinSession session);

  /// Probes a minted stream [url] with a tiny ranged request to confirm the
  /// server returns playable audio *before* the URL is handed to the audio
  /// engine, returning the observed status and content type. Redirects (e.g.
  /// from Cloudflare) are followed, so the result reflects the final response.
  ///
  /// Throws a [JellyfinException] only for transport failures
  /// ([JellyfinErrorKind.notReachable]); a non-2xx status is returned in the
  /// [JellyfinStreamProbe] for the caller to classify, not thrown. The [url]
  /// carries the session token, so it must never be logged or placed in a
  /// thrown error.
  Future<JellyfinStreamProbe> probeStream(Uri url);

  /// Fetches lyrics for the audio item [itemId], or `null` when the server has
  /// none (a 404 — a normal outcome, not an error). Throws a [JellyfinException]
  /// for transport or auth failures.
  Future<Lyrics?> fetchLyrics(JellyfinSession session, String itemId);

  /// Reports a playback lifecycle [event] for the audio item [itemId] at
  /// [position] to the server's play-session endpoints, so Jellyfin's
  /// dashboard shows this client as an active player and the item's play
  /// state stays in sync.
  ///
  /// The server keys the session on the client/device identity in the
  /// `Authorization` header — the same one used at sign-in — so a
  /// pause/resume updates one player entry instead of spawning new ones.
  /// Throws a [JellyfinException] on failure; the *caller* (the playback
  /// reporter) treats every failure as best-effort and swallows it.
  Future<void> reportPlayback(
    JellyfinSession session, {
    required String itemId,
    required JellyfinPlaybackEvent event,
    required Duration position,
  });

  /// Registers this client's session capabilities via
  /// `POST /Sessions/Capabilities/Full`, declaring audio playback and media
  /// control so other Jellyfin apps list it as a controllable player and the
  /// server delivers transport (Playstate) commands over the control socket.
  ///
  /// Throws a [JellyfinException] on failure; the caller (the remote-control
  /// receiver) treats it as best-effort, so a registration hiccup never stops
  /// it from listening for commands.
  Future<void> registerRemoteControlCapabilities(JellyfinSession session);

  /// The audio item ids the signed-in user has marked as favourites.
  Future<Set<String>> fetchFavoriteIds(JellyfinSession session);

  /// Marks (when [favorite]) or unmarks the audio item [itemId] as a favourite
  /// for the signed-in user. Throws a [JellyfinException] on failure.
  Future<void> setFavorite(
    JellyfinSession session,
    String itemId, {
    required bool favorite,
  });

  /// Lists the signed-in user's playlists (id + name only).
  Future<List<JellyfinPlaylistDto>> fetchPlaylists(JellyfinSession session);

  /// Lists the ordered entries of the playlist [playlistId]. Each entry carries
  /// the media item id and its playlist-scoped entry id.
  Future<List<JellyfinPlaylistEntry>> fetchPlaylistEntries(
    JellyfinSession session,
    String playlistId,
  );

  /// Creates a playlist named [name], optionally seeded with audio [itemIds],
  /// and returns the new server playlist id. Throws on failure.
  Future<String> createPlaylist(
    JellyfinSession session, {
    required String name,
    List<String> itemIds = const <String>[],
  });

  /// Appends audio [itemIds] to the playlist [playlistId]. Throws on failure.
  Future<void> addItemsToPlaylist(
    JellyfinSession session,
    String playlistId,
    List<String> itemIds,
  );

  /// Removes the entries holding [itemIds] from the playlist [playlistId]. The
  /// implementation resolves each media item id to its playlist entry id first,
  /// since Jellyfin removes by entry id. Throws on failure.
  Future<void> removeItemsFromPlaylist(
    JellyfinSession session,
    String playlistId,
    List<String> itemIds,
  );

  /// Deletes the playlist [playlistId] from the server. Requires the user to
  /// have delete permission; a permission failure surfaces as a friendly
  /// [JellyfinException]. Throws on failure.
  Future<void> deletePlaylist(JellyfinSession session, String playlistId);
}
