// Thin TypeScript wrapper for Rust repository implementation
// All API calls go through Tauri commands in src-tauri/src/commands/repository.rs
// NO direct HTTP calls - everything routes through Rust backend

import { commands } from "./bindings";
import type { JRayActor, DownloadDiskUsage, SearchScope } from "./bindings";
import type { QualityPreset } from "./quality-presets";
import type {
  Library,
  MediaItem,
  SearchResult,
  GetItemsOptions,
  SearchOptions,
  PlaybackInfo,
  LiveStreamInfo,
  ImageType,
  ImageOptions,
  Genre,
  PlaylistEntry,
  PlaylistCreatedResult,
} from "./types";

/**
 * Repository client - thin wrapper over Rust HybridRepository
 * Uses handle-based system: create() returns a UUID handle for all operations
 */
export class RepositoryClient {
  private handle: string | null = null;
  private _serverUrl: string | null = null;
  private _accessToken: string | null = null;

  /**
   * Create a new repository instance in Rust
   * Returns the repository handle for subsequent operations
   */
  async create(
    serverUrl: string,
    userId: string,
    accessToken: string,
    serverId: string
  ): Promise<string> {
    console.log("[RepositoryClient] Creating Rust repository...");
    this.handle = await commands.repositoryCreate(serverUrl, userId, accessToken, serverId);

    // Store for URL construction
    this._serverUrl = serverUrl;
    this._accessToken = accessToken;

    console.log("[RepositoryClient] Repository created with handle:", this.handle);
    return this.handle;
  }

  /**
   * Destroy the repository instance in Rust
   * Call this on logout or when switching servers
   */
  async destroy(): Promise<void> {
    if (this.handle) {
      await commands.repositoryDestroy(this.handle);
      this.handle = null;
      this._serverUrl = null;
      this._accessToken = null;
    }
  }

  private ensureHandle(): string {
    if (!this.handle) {
      throw new Error("Repository not initialized - call create() first");
    }
    return this.handle;
  }

  /**
   * Get the repository handle for passing to backend commands
   */
  getHandle(): string {
    return this.ensureHandle();
  }

  // ===== Library Methods (all via Rust) =====

  async getLibraries(): Promise<Library[]> {
    return commands.repositoryGetLibraries(this.ensureHandle());
  }

  async getItems(parentId: string, options?: GetItemsOptions): Promise<SearchResult> {
    return commands.repositoryGetItems(this.ensureHandle(), parentId, options ?? null);
  }

  async getItem(itemId: string): Promise<MediaItem> {
    return commands.repositoryGetItem(this.ensureHandle(), itemId);
  }

  /**
   * Downloaded-only browse: libraries that contain downloaded content.
   * Never merges server results; an empty list is authoritative.
   * TRACES: UR-055 | DR-082
   */
  async getDownloadedLibraries(): Promise<Library[]> {
    return commands.repositoryGetDownloadedLibraries(this.ensureHandle());
  }

  /**
   * Downloaded-only browse: items under a container that are on the device.
   * TRACES: UR-055 | DR-082, DR-083
   */
  async getDownloadedItems(parentId: string, options?: GetItemsOptions): Promise<SearchResult> {
    return commands.repositoryGetDownloadedItems(this.ensureHandle(), parentId, options ?? null);
  }

  /**
   * On-disk usage of downloaded content (device total + per-item/container bytes).
   * TRACES: UR-056 | DR-085
   */
  async getDownloadDiskUsage(): Promise<DownloadDiskUsage> {
    return commands.repositoryGetDownloadDiskUsage(this.ensureHandle());
  }

  /**
   * Query the optional JRay plugin for the actors on screen at time `t`
   * (seconds) in an item. Resolves to an empty array when JRay isn't installed
   * or has no data for the item.
   */
  async jrayActorsAt(itemId: string, t: number): Promise<JRayActor[]> {
    return commands.repositoryJrayActorsAt(this.ensureHandle(), itemId, t);
  }

  async getLatestItems(parentId: string, limit?: number): Promise<MediaItem[]> {
    return commands.repositoryGetLatestItems(this.ensureHandle(), parentId, limit ?? null);
  }

  async getResumeItems(parentId?: string, limit?: number): Promise<MediaItem[]> {
    return commands.repositoryGetResumeItems(this.ensureHandle(), parentId ?? null, limit ?? null);
  }

  async getNextUpEpisodes(seriesId?: string, limit?: number): Promise<MediaItem[]> {
    return commands.repositoryGetNextUpEpisodes(this.ensureHandle(), seriesId ?? null, limit ?? null);
  }

  /**
   * Every episode of a series, across all seasons, already in series order.
   * The backend owns the season fan-out and the flat-series fallback.
   *
   * TRACES: UR-062 | DR-101
   */
  async getSeriesEpisodes(seriesId: string): Promise<MediaItem[]> {
    return commands.repositoryGetSeriesEpisodes(this.ensureHandle(), seriesId);
  }

  /**
   * The episode the viewer should land on when opening this series. `null` only
   * when the series has no episodes.
   *
   * TRACES: UR-062 | DR-101
   */
  async getSeriesCurrentEpisode(seriesId: string): Promise<MediaItem | null> {
    return commands.repositoryGetSeriesCurrentEpisode(this.ensureHandle(), seriesId);
  }

  /**
   * Erase watch history for an item. On a series or season the server applies
   * it to everything inside, so the container returns to "never watched".
   * Requires the server — this fails offline rather than diverging local state.
   *
   * TRACES: UR-064 | DR-106
   */
  async clearWatchHistory(itemId: string): Promise<void> {
    await commands.repositoryClearWatchHistory(this.ensureHandle(), itemId);
  }

  async getRecentlyPlayedAudio(limit?: number): Promise<MediaItem[]> {
    return commands.repositoryGetRecentlyPlayedAudio(this.ensureHandle(), limit ?? null);
  }

  async getResumeMovies(limit?: number): Promise<MediaItem[]> {
    return commands.repositoryGetResumeMovies(this.ensureHandle(), limit ?? null);
  }

  /** Albums the user has played but not listened to recently ("rediscover"). */
  async getRediscoverAlbums(parentId?: string, limit?: number): Promise<MediaItem[]> {
    return commands.repositoryGetRediscoverAlbums(this.ensureHandle(), parentId ?? null, limit ?? null);
  }

  async getGenres(parentId?: string): Promise<Genre[]> {
    return commands.repositoryGetGenres(this.ensureHandle(), parentId ?? null);
  }

  async search(query: string, options?: SearchOptions, requestId = 0): Promise<SearchResult> {
    return commands.repositorySearch(this.ensureHandle(), query, options ?? null, requestId);
  }

  // ===== Playback Methods (all via Rust) =====

  async getPlaybackInfo(itemId: string): Promise<PlaybackInfo> {
    return commands.repositoryGetPlaybackInfo(this.ensureHandle(), itemId);
  }

  async reportPlaybackStart(itemId: string, positionMs: number): Promise<void> {
    await commands.repositoryReportPlaybackStart(this.ensureHandle(), itemId, positionMs);
  }

  async reportPlaybackProgress(itemId: string, positionMs: number): Promise<void> {
    await commands.repositoryReportPlaybackProgress(this.ensureHandle(), itemId, positionMs);
  }

  async reportPlaybackStopped(itemId: string, positionMs: number): Promise<void> {
    await commands.repositoryReportPlaybackStopped(this.ensureHandle(), itemId, positionMs);
  }

  // ===== Stream URL Methods (via Rust) =====

  async getAudioStreamUrl(itemId: string): Promise<string> {
    return commands.repositoryGetAudioStreamUrl(this.ensureHandle(), itemId);
  }

  async getVideoStreamUrl(
    itemId: string,
    mediaSourceId?: string,
    startTimeSeconds?: number,
    audioStreamIndex?: number
  ): Promise<string> {
    return commands.repositoryGetVideoStreamUrl(
      this.ensureHandle(),
      itemId,
      mediaSourceId ?? null,
      startTimeSeconds ?? null,
      audioStreamIndex ?? null
    );
  }

  /**
   * Audio-only stream URL for a video item, for the background-audio handoff.
   * The server extracts just the audio track — no video is decoded on-device.
   * TRACES: UR-040 | JA-032
   */
  async getAudioOnlyStreamUrlForVideo(
    itemId: string,
    mediaSourceId?: string,
    startTimeSeconds?: number,
    audioStreamIndex?: number
  ): Promise<string> {
    return commands.repositoryGetAudioOnlyStreamUrlForVideo(
      this.ensureHandle(),
      itemId,
      mediaSourceId ?? null,
      startTimeSeconds ?? null,
      audioStreamIndex ?? null
    );
  }

  // ===== Live TV / Channels =====

  /** Browse Live TV channels (broadcast / IPTV). */
  async getLiveTvChannels(): Promise<MediaItem[]> {
    return commands.repositoryGetLiveTvChannels(this.ensureHandle());
  }

  /** Browse the root list of plugin "Channels". Drill-down uses getItems(channelId). */
  async getChannels(): Promise<SearchResult> {
    return commands.repositoryGetChannels(this.ensureHandle());
  }

  /** Open a live stream for a Live TV channel / live item before HLS playback. */
  async openLiveStream(itemId: string): Promise<LiveStreamInfo> {
    return commands.repositoryOpenLiveStream(this.ensureHandle(), itemId);
  }

  // ===== URL Construction Methods (sync, no server call) =====

  /**
   * Get image URL from backend
   * The Rust backend constructs and returns the URL with proper credentials handling
   */
  async getImageUrl(itemId: string, imageType: ImageType = "Primary", options?: ImageOptions): Promise<string> {
    return commands.repositoryGetImageUrl(this.ensureHandle(), itemId, imageType, options ?? null);
  }

  /**
   * Get subtitle URL from backend
   * The Rust backend constructs and returns the URL with proper credentials handling
   */
  async getSubtitleUrl(
    itemId: string,
    mediaSourceId: string,
    streamIndex: number,
    format: string = "vtt"
  ): Promise<string> {
    return commands.repositoryGetSubtitleUrl(this.ensureHandle(), itemId, mediaSourceId, streamIndex, format);
  }

  /**
   * Get video download URL with quality preset from backend
   * The Rust backend constructs and returns the URL with proper credentials handling
   * Used for offline downloads and transcoding
   */
  async getVideoDownloadUrl(
    itemId: string,
    quality: QualityPreset = "original",
    mediaSourceId?: string
  ): Promise<string> {
    return commands.repositoryGetVideoDownloadUrl(this.ensureHandle(), itemId, quality, mediaSourceId ?? null);
  }

  // ===== Favorite Methods (via Rust) =====

  async markFavorite(itemId: string): Promise<void> {
    await commands.repositoryMarkFavorite(this.ensureHandle(), itemId);
  }

  async unmarkFavorite(itemId: string): Promise<void> {
    await commands.repositoryUnmarkFavorite(this.ensureHandle(), itemId);
  }

  /**
   * Everything favourited, across libraries, narrowed by an opaque scope the
   * backend expands into item types. The frontend never names a Jellyfin type
   * here — see docs/specs/scoped-search-boundary.md.
   *
   * Resolves with the local answer; a later `favorites-changed` event reports
   * ids the server disagreed with.
   *
   * TRACES: UR-067 | DR-115
   */
  async getFavorites(scope: SearchScope, options?: GetItemsOptions): Promise<SearchResult> {
    return commands.repositoryGetFavorites(this.ensureHandle(), scope, options ?? null);
  }

  // ===== Person Methods (via Rust) =====

  async getPerson(personId: string): Promise<MediaItem> {
    return commands.repositoryGetPerson(this.ensureHandle(), personId);
  }

  async getItemsByPerson(personId: string, options?: GetItemsOptions): Promise<SearchResult> {
    return commands.repositoryGetItemsByPerson(this.ensureHandle(), personId, options ?? null);
  }

  async getSimilarItems(itemId: string, limit?: number): Promise<SearchResult> {
    return commands.repositoryGetSimilarItems(this.ensureHandle(), itemId, limit ?? null);
  }

  // ===== Playlist Methods (via Rust) =====

  async createPlaylist(name: string, itemIds?: string[]): Promise<PlaylistCreatedResult> {
    return commands.playlistCreate(this.ensureHandle(), name, itemIds ?? null);
  }

  async deletePlaylist(playlistId: string): Promise<void> {
    await commands.playlistDelete(this.ensureHandle(), playlistId);
  }

  async renamePlaylist(playlistId: string, name: string): Promise<void> {
    await commands.playlistRename(this.ensureHandle(), playlistId, name);
  }

  async getPlaylistItems(playlistId: string): Promise<PlaylistEntry[]> {
    return commands.playlistGetItems(this.ensureHandle(), playlistId);
  }

  async addToPlaylist(playlistId: string, itemIds: string[]): Promise<void> {
    await commands.playlistAddItems(this.ensureHandle(), playlistId, itemIds);
  }

  async removeFromPlaylist(playlistId: string, entryIds: string[]): Promise<void> {
    await commands.playlistRemoveItems(this.ensureHandle(), playlistId, entryIds);
  }

  async movePlaylistItem(playlistId: string, itemId: string, newIndex: number): Promise<void> {
    await commands.playlistMoveItem(this.ensureHandle(), playlistId, itemId, newIndex);
  }

  // ===== Getters =====

  get serverUrl(): string {
    if (!this._serverUrl) {
      throw new Error("Repository not initialized - call create() first");
    }
    return this._serverUrl;
  }
}
