// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';

import '../utils/media_server_http_client.dart' show AbortController;
import 'media_kind.dart';

part 'library_query.freezed.dart';

/// Sort order applied to a library query.
enum LibrarySortDirection { ascending, descending }

@freezed
sealed class LibrarySort with _$LibrarySort {
  /// Backend-neutral sort field. Common values: `addedAt`, `originallyAvailableAt`,
  /// `lastViewedAt`, `title`, `rating`, `viewCount`, `random`.
  const factory LibrarySort({
    required String field,
    @Default(LibrarySortDirection.descending) LibrarySortDirection direction,
  }) = _LibrarySort;
}

/// A single filter clause. The semantics of `field` and `value` are
/// backend-translated — the neutral query just carries the intent.
@freezed
sealed class LibraryFilter with _$LibraryFilter {
  const factory LibraryFilter({required String field, @Default('=') String op, required List<String> values}) =
      _LibraryFilter;
}

/// Backend-neutral library content query. Each backend's adapter translates
/// these into its own query DSL (Plex `/library/sections/{id}/all?type=...`
/// or Jellyfin `/Items?ParentId=...&Filters=...`).
@freezed
sealed class LibraryQuery with _$LibraryQuery {
  const factory LibraryQuery({
    /// Restrict to a single kind (e.g. `MediaKind.movie`). Null = library default.
    MediaKind? kind,

    /// Restrict to multiple kinds when no single [kind] represents the browse
    /// surface. When non-empty, translators prefer this over [kind].
    @Default(<MediaKind>[]) List<MediaKind> includeKinds,

    /// Pagination — zero-based offset.
    @Default(0) int offset,
    @Default(50) int limit,

    LibrarySort? sort,
    @Default(<LibraryFilter>[]) List<LibraryFilter> filters,

    /// Free-text search restricted to this library. Distinct from the global
    /// search endpoint.
    String? search,

    /// Whether to include items the active user has already watched.
    @Default(true) bool includeWatched,

    /// Restrict to items the user marked favorite (Jellyfin `Filters=IsFavorite`).
    /// Plex has no equivalent; its translator ignores the flag.
    @Default(false) bool favoritesOnly,

    /// Restrict the result to items whose sort name starts with this string —
    /// the alpha-jump bar's filter UX. The literal `#` is a sentinel for
    /// "non-alphabetic" and translates to a `NameLessThan=A` query for backends
    /// that support it.
    String? nameStartsWith,

    /// Genre filter — used by the per-library filter sheet. Backends that
    /// take multiple values (Jellyfin) AND/intersect; those that take one
    /// (Plex's existing flow) consult `filters` instead.
    List<String>? genres,
    List<String>? officialRatings,
    List<int>? years,
    List<String>? tags,
  }) = _LibraryQuery;
}

/// Page of items returned by [MediaServerClient.getLibraryContent].
/// Carries the total count so the UI can render correct pagination affordances.
@freezed
sealed class LibraryPage<T> with _$LibraryPage<T> {
  const factory LibraryPage({required List<T> items, required int totalCount, @Default(0) int offset}) =
      _LibraryPage<T>;
}

/// Conservative total for a page whose backend omitted an exact count. A full
/// page adds one sentinel item so callers keep pagination enabled without
/// claiming to know the real total.
int fallbackPageTotal({required int offset, required int itemCount, int? requestedSize}) {
  final fullPage = requestedSize != null && requestedSize > 0 && itemCount >= requestedSize;
  return offset + itemCount + (fullPage ? 1 : 0);
}

/// Walk every page of a paginated endpoint and concatenate the results.
///
/// [fetchPage] receives a zero-based offset and [pageSize] and is called until
/// a page comes back empty, the accumulated count reaches the page's
/// [LibraryPage.totalCount], or — when [stopOnShortPage] is set — a page comes
/// back shorter than [pageSize]. The short-page break is for backends whose
/// total is unreliable; leave it off when the total is authoritative.
///
/// [abort] is checked before and after every request. Errors propagate.
Future<List<T>> drainPages<T>(
  Future<LibraryPage<T>> Function(int start, int size) fetchPage, {
  required int pageSize,
  AbortController? abort,
  bool stopOnShortPage = false,
}) async {
  final all = <T>[];
  var start = 0;
  while (true) {
    abort?.throwIfAborted();
    final page = await fetchPage(start, pageSize);
    abort?.throwIfAborted();
    if (page.items.isEmpty) break;
    all.addAll(page.items);
    start += page.items.length;
    if (start >= page.totalCount) break;
    if (stopOnShortPage && page.items.length < pageSize) break;
  }
  return all;
}
