// Cienki klient REST do backendu goon. Używa fetch + headerów X-API-Key.
import { getAppSignatureHash } from './native/antiTamper';
import { getDeviceId } from './storage';
import type {
  BlacklistKind,
  BlacklistOut,
  FavoriteAddOut,
  FavoriteListOut,
  FavoriteMovieAddOut,
  FavoriteMovieListOut,
  FavoriteStudioAddOut,
  FavoriteStudioListOut,
  KeepSide,
  MergeCandidateDetail,
  MergeCandidateListOut,
  MovieListOut,
  MovieOut,
  MoviesListParams,
  PerformerListOut,
  ProgressOut,
  ResolveAction,
  ResolveOut,
  ResolveResult,
  SavedSearchOut,
  ScenesListParams,
  SceneListOut,
  SceneOut,
  SourceListOut,
  StudioListOut,
  TagListOut,
  WatchListOut,
} from './types';

export class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = 'ApiError';
  }
}

export class GoonClient {
  constructor(private baseUrl: string, private apiKey: string) {
    this.baseUrl = baseUrl.replace(/\/+$/, '');
  }

  // Auth headers shared between request() i raw fetch wrappers (DELETE/204
  // responses). Każdy hit do backendu MUSI zawierać X-App-Signature inaczej
  // backend zwraca 403 (anti-tamper sig check, patrz auth.py).
  private async _authHeaders(): Promise<Record<string, string>> {
    const sig = await getAppSignatureHash();
    const deviceId = await getDeviceId();
    return {
      'X-API-Key': this.apiKey,
      'X-Device-Id': deviceId,
      ...(sig ? { 'X-App-Signature': sig } : {}),
    };
  }

  // Jednorazowe przejęcie legacy stanu (sprzed device-scopingu) na to urządzenie.
  async adoptLegacy(): Promise<{ device_id: string; moved: Record<string, number> }> {
    return this.request('/me/adopt-legacy', { method: 'POST' });
  }

  private async request<T>(path: string, init?: RequestInit): Promise<T> {
    const auth = await this._authHeaders();
    const res = await fetch(`${this.baseUrl}${path}`, {
      ...init,
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        ...auth,
        ...(init?.headers || {}),
      },
    });
    if (!res.ok) {
      const text = await res.text().catch(() => res.statusText);
      throw new ApiError(res.status, `${res.status}: ${text}`);
    }
    // 204 No Content (np. mark-dead endpoints) — puste body, `res.json()` rzuciłby
    // "JSON Parse error". To powodowało fałszywy "Failed" alert przy "Mark as invalid"
    // mimo że backend POPRAWNIE oznaczał link dead (bug-report 2026-05-28 Voe + 06-03).
    if (res.status === 204) {
      return undefined as T;
    }
    const data = (await res.json()) as T;
    // Backend zwraca path-only `/proxy/...` dla CDN-ów wymagających Referera
    // (thumbnails, stream URLs). Klient prefixuje baseUrl-em rekursywnie tak żeby
    // konsumenci dostali gotowe absolute URL-e bez per-screen logiki.
    return this._absolutizeProxyUrls(data);
  }

  private _absolutizeProxyUrls<T>(data: T): T {
    if (data === null || data === undefined) return data;
    if (typeof data === 'string') {
      return (data.startsWith('/proxy/') ? `${this.baseUrl}${data}` : data) as unknown as T;
    }
    if (Array.isArray(data)) {
      return data.map((x) => this._absolutizeProxyUrls(x)) as unknown as T;
    }
    if (typeof data === 'object') {
      const out: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(data)) {
        out[k] = this._absolutizeProxyUrls(v);
      }
      return out as T;
    }
    return data;
  }

  async healthz(): Promise<{ status: string }> {
    return this.request('/healthz');
  }

  async listScenes(params: ScenesListParams = {}): Promise<SceneListOut> {
    const qs = new URLSearchParams();
    if (params.q) qs.set('q', params.q);
    if (params.studio_slugs && params.studio_slugs.length) {
      qs.set('studio_slugs', params.studio_slugs.join(','));
    }
    if (params.tags && params.tags.length) qs.set('tags', params.tags.join(','));
    if (params.performer_ids && params.performer_ids.length) {
      qs.set('performer_ids', params.performer_ids.join(','));
    }
    if (params.has_playback !== undefined) qs.set('has_playback', String(params.has_playback));
    // Default: filtrujemy sceny <60s — bug-report 2026-05-23 (40cd28aa):
    // "Takie sceny po 1 min to można wywalić". Pornapp/freshporno czasem
    // zassuje teasery/trailery 30-50s, które są bezużyteczne na listach.
    // Caller może override przez explicit 0 (lub null) — np. admin browse.
    const minDur = params.min_duration_sec ?? 60;
    if (minDur > 0) qs.set('min_duration_sec', String(minDur));
    if (params.max_duration_sec !== undefined)
      qs.set('max_duration_sec', String(params.max_duration_sec));
    if (params.released_within_days !== undefined)
      qs.set('released_within_days', String(params.released_within_days));
    if (params.min_quality_p !== undefined) qs.set('min_quality_p', String(params.min_quality_p));
    if (params.include_stubs !== undefined) qs.set('include_stubs', String(params.include_stubs));
    if (params.origin) qs.set('origin', params.origin);
    if (params.sort) qs.set('sort', params.sort);
    qs.set('page', String(params.page ?? 1));
    qs.set('per_page', String(params.per_page ?? 50));
    return this.request(`/scenes?${qs.toString()}`);
  }

  async listMovies(params: MoviesListParams = {}): Promise<MovieListOut> {
    const qs = new URLSearchParams();
    if (params.q) qs.set('q', params.q);
    if (params.studio_slugs && params.studio_slugs.length) {
      qs.set('studio_slugs', params.studio_slugs.join(','));
    }
    if (params.tags && params.tags.length) qs.set('tags', params.tags.join(','));
    if (params.performer_ids && params.performer_ids.length) {
      qs.set('performer_ids', params.performer_ids.join(','));
    }
    if (params.year_from !== undefined) qs.set('year_from', String(params.year_from));
    if (params.year_to !== undefined) qs.set('year_to', String(params.year_to));
    if (params.has_playback !== undefined) qs.set('has_playback', String(params.has_playback));
    if (params.sort) qs.set('sort', params.sort);
    qs.set('page', String(params.page ?? 1));
    qs.set('per_page', String(params.per_page ?? 50));
    return this.request(`/movies?${qs.toString()}`);
  }

  async getMovie(movieId: string): Promise<MovieOut> {
    return this.request(`/movies/${movieId}`);
  }

  async resolveMoviePlayback(movieId: string, playbackId: string): Promise<ResolveOut> {
    return this.request(`/movies/${movieId}/playback/${playbackId}/resolve`, {
      method: 'POST',
    });
  }

  async addSceneFavorite(sceneId: string): Promise<void> {
    await this.request(`/scene-favorites/${sceneId}`, { method: 'POST' });
  }

  async listSceneFavorites(): Promise<{ items: SceneOut[]; total: number }> {
    return this.request('/scene-favorites');
  }

  async removeSceneFavorite(sceneId: string): Promise<void> {
    const res = await fetch(`${this.baseUrl}/scene-favorites/${sceneId}`, {
      method: 'DELETE',
      headers: await this._authHeaders(),
    });
    if (!res.ok && res.status !== 204) {
      throw new ApiError(res.status, `${res.status}: ${await res.text()}`);
    }
  }

  // Saved searches — zapisane słowa kluczowe (user-report mobilism).
  async listSavedSearches(): Promise<SavedSearchOut[]> {
    return this.request('/saved-searches');
  }

  async addSavedSearch(query: string): Promise<SavedSearchOut> {
    return this.request('/saved-searches', {
      method: 'POST',
      body: JSON.stringify({ query }),
    });
  }

  async removeSavedSearch(id: string): Promise<void> {
    await this.request(`/saved-searches/${id}`, { method: 'DELETE' });
  }

  async listTags(params: {
    q?: string;
    order?: 'popular' | 'name';
    page?: number;
    per_page?: number;
    for_movies?: boolean;
    only_with_content?: boolean;
  } = {}): Promise<TagListOut> {
    const qs = new URLSearchParams();
    if (params.q) qs.set('q', params.q);
    qs.set('order', params.order ?? 'popular');
    qs.set('page', String(params.page ?? 1));
    qs.set('per_page', String(params.per_page ?? 200));
    if (params.for_movies) qs.set('for_movies', 'true');
    if (params.only_with_content) qs.set('only_with_content', 'true');
    return this.request(`/tags?${qs.toString()}`);
  }

  // Top tagi/kategorie performera (PerformerScenes chips). Agregat ze scen z żywym playbackiem.
  async performerTags(performerId: string, limit = 20): Promise<TagListOut> {
    return this.request(`/performers/${performerId}/tags?limit=${limit}`);
  }

  async listPerformers(params: { q?: string; order?: 'scene_count' | 'name'; page?: number; per_page?: number } = {}): Promise<PerformerListOut> {
    const qs = new URLSearchParams();
    if (params.q) qs.set('q', params.q);
    qs.set('order', params.order ?? 'scene_count');
    qs.set('page', String(params.page ?? 1));
    qs.set('per_page', String(params.per_page ?? 100));
    return this.request(`/performers?${qs.toString()}`);
  }

  async refreshPerformer(performerId: string, pages = 2): Promise<{
    performer_id: string;
    canonical_name: string;
    counters: Record<string, Record<string, number>>;
    new_scenes: number;
    last_searched_at: string | null;
  }> {
    return this.request(`/performers/${performerId}/refresh?pages=${pages}`, {
      method: 'POST',
    });
  }

  async rescrapePerformer(performerId: string): Promise<{
    performer_id: string;
    canonical_name: string;
    scenes_total: number;
    scenes_processed: number;
    thumbs_added: number;
    tags_added: number;
    failures: number;
    capped: boolean;
    cap_reason: string | null;
  }> {
    return this.request(`/performers/${performerId}/rescrape`, { method: 'POST' });
  }

  async markPlaybackDead(sceneId: string, playbackId: string): Promise<void> {
    await this.request(`/scenes/${sceneId}/playback/${playbackId}/mark-dead`, {
      method: 'POST',
    });
  }

  // Long-press tile akcje. hideScene → wszystkie playback dead (scena znika z list).
  async hideScene(sceneId: string): Promise<{ scene_id: string; playback_marked_dead: number }> {
    return this.request(`/scenes/${sceneId}/hide`, { method: 'POST' });
  }

  // Scal `dropId` w `keepId` (mark-duplicate flow: long-press dup → wybór oryginału).
  async mergeDuplicateScene(keepId: string, dropId: string): Promise<{ keep_id: string; dropped_id: string }> {
    return this.request(`/scenes/${keepId}/merge/${dropId}`, { method: 'POST' });
  }

  async markMoviePlaybackDead(movieId: string, playbackId: string): Promise<void> {
    await this.request(`/movies/${movieId}/playback/${playbackId}/mark-dead`, {
      method: 'POST',
    });
  }

  async listSources(): Promise<SourceListOut> {
    return this.request('/sources');
  }

  /** Telemetria odtwarzania (fire-and-forget) — zasila health/szybkość w rankingu
   * źródeł. Best-effort: błąd jest połykany, nie może psuć playbacku. */
  async reportPlaybackEvent(ev: {
    origin: string;
    status: 'success' | 'error';
    scene_id?: string;
    error_kind?: string;
    ttff_ms?: number;
  }): Promise<void> {
    try {
      await this.request('/playback-events', {
        method: 'POST',
        body: JSON.stringify(ev),
      });
    } catch {
      // świadomie ignorujemy — telemetria nie jest krytyczna
    }
  }

  async listStudios(params: {
    q?: string;
    order?: 'name' | 'scene_count';
    page?: number;
    per_page?: number;
    for_movies?: boolean;
    only_with_content?: boolean;
  } = {}): Promise<StudioListOut> {
    const qs = new URLSearchParams();
    if (params.q) qs.set('q', params.q);
    qs.set('order', params.order ?? 'name');
    qs.set('page', String(params.page ?? 1));
    qs.set('per_page', String(params.per_page ?? 200));
    if (params.for_movies) qs.set('for_movies', 'true');
    if (params.only_with_content) qs.set('only_with_content', 'true');
    return this.request(`/studios?${qs.toString()}`);
  }

  async getScene(id: string): Promise<SceneOut> {
    return this.request(`/scenes/${id}`);
  }

  async resolvePlayback(sceneId: string, playbackId: string): Promise<ResolveOut> {
    return this.request<ResolveOut>(`/scenes/${sceneId}/playback/${playbackId}/resolve`, {
      method: 'POST',
    });
    // Note: `/proxy/...` URLs są absolutyzowane w `request()` rekursywnie.
  }

  // expose baseUrl for player headers etc.
  get apiBaseUrl(): string {
    return this.baseUrl;
  }

  async listMergeCandidates(params: { status?: string; page?: number; per_page?: number } = {}): Promise<MergeCandidateListOut> {
    const qs = new URLSearchParams();
    qs.set('status', params.status ?? 'pending');
    qs.set('kind', 'scene');
    qs.set('page', String(params.page ?? 1));
    qs.set('per_page', String(params.per_page ?? 50));
    return this.request(`/admin/merge-candidates?${qs.toString()}`);
  }

  async getMergeCandidate(id: string): Promise<MergeCandidateDetail> {
    return this.request(`/admin/merge-candidates/${id}`);
  }

  async resolveMergeCandidate(
    id: string,
    body: { action: ResolveAction; keep?: KeepSide; resolved_by?: string }
  ): Promise<ResolveResult> {
    return this.request(`/admin/merge-candidates/${id}/resolve`, {
      method: 'POST',
      body: JSON.stringify({ keep: 'left', resolved_by: 'mobile', ...body }),
    });
  }

  async listFavorites(): Promise<FavoriteListOut> {
    return this.request('/favorites');
  }

  async addFavorite(performerId: string): Promise<FavoriteAddOut> {
    return this.request(`/favorites/${performerId}`, { method: 'POST' });
  }

  async removeFavorite(performerId: string): Promise<void> {
    const res = await fetch(`${this.baseUrl}/favorites/${performerId}`, {
      method: 'DELETE',
      headers: await this._authHeaders(),
    });
    if (!res.ok && res.status !== 204) {
      const text = await res.text().catch(() => res.statusText);
      throw new ApiError(res.status, `${res.status}: ${text}`);
    }
  }

  async markFavoriteSeen(performerId: string): Promise<void> {
    await this.request(`/favorites/${performerId}/seen`, { method: 'POST' });
  }

  async listFavoriteStudios(): Promise<FavoriteStudioListOut> {
    return this.request('/favorites/studios');
  }

  async addFavoriteStudio(studioId: string): Promise<FavoriteStudioAddOut> {
    return this.request(`/favorites/studios/${studioId}`, { method: 'POST' });
  }

  async removeFavoriteStudio(studioId: string): Promise<void> {
    const res = await fetch(`${this.baseUrl}/favorites/studios/${studioId}`, {
      method: 'DELETE',
      headers: await this._authHeaders(),
    });
    if (!res.ok && res.status !== 204) {
      const text = await res.text().catch(() => res.statusText);
      throw new ApiError(res.status, `${res.status}: ${text}`);
    }
  }

  async markFavoriteStudioSeen(studioId: string): Promise<void> {
    await this.request(`/favorites/studios/${studioId}/seen`, { method: 'POST' });
  }

  async listFavoriteMovies(): Promise<FavoriteMovieListOut> {
    return this.request('/favorites/movies');
  }

  async addFavoriteMovie(movieId: string): Promise<FavoriteMovieAddOut> {
    return this.request(`/favorites/movies/${movieId}`, { method: 'POST' });
  }

  async removeFavoriteMovie(movieId: string): Promise<void> {
    const res = await fetch(`${this.baseUrl}/favorites/movies/${movieId}`, {
      method: 'DELETE',
      headers: await this._authHeaders(),
    });
    if (!res.ok && res.status !== 204) {
      const text = await res.text().catch(() => res.statusText);
      throw new ApiError(res.status, `${res.status}: ${text}`);
    }
  }

  async markFavoriteMovieSeen(movieId: string): Promise<void> {
    await this.request(`/favorites/movies/${movieId}/seen`, { method: 'POST' });
  }

  async removeTagFromScene(sceneId: string, tagId: string): Promise<void> {
    const res = await fetch(`${this.baseUrl}/scenes/${sceneId}/tags/${tagId}`, {
      method: 'DELETE',
      headers: await this._authHeaders(),
    });
    if (!res.ok && res.status !== 204) {
      const text = await res.text().catch(() => res.statusText);
      throw new ApiError(res.status, `${res.status}: ${text}`);
    }
  }

  async removePerformerFromScene(sceneId: string, performerId: string): Promise<void> {
    const res = await fetch(
      `${this.baseUrl}/scenes/${sceneId}/performers/${performerId}`,
      { method: 'DELETE', headers: await this._authHeaders() },
    );
    if (!res.ok && res.status !== 204) {
      const text = await res.text().catch(() => res.statusText);
      throw new ApiError(res.status, `${res.status}: ${text}`);
    }
  }

  async enrichSceneTags(sceneId: string): Promise<{
    scene_id: string;
    added: number;
    tube_used: string | null;
    tags: string[];
  }> {
    return this.request(`/scenes/${sceneId}/enrich-tags`, { method: 'POST' });
  }

  async enrichSceneDuration(sceneId: string): Promise<{
    scene_id: string;
    duration_sec: number | null;
    tube_used: string | null;
  }> {
    return this.request(`/scenes/${sceneId}/enrich-duration`, { method: 'POST' });
  }

  async enrichSceneThumbnail(sceneId: string, force = false): Promise<{
    scene_id: string;
    thumbnail_url: string | null;
    tube_used: string | null;
    sources_updated: number;
  }> {
    const qs = force ? '?force=true' : '';
    return this.request(`/scenes/${sceneId}/enrich-thumbnail${qs}`, { method: 'POST' });
  }

  async enrichSceneStudio(sceneId: string): Promise<{
    scene_id: string;
    studio_id: string | null;
    studio_name: string | null;
    tube_used: string | null;
  }> {
    return this.request(`/scenes/${sceneId}/enrich-studio`, { method: 'POST' });
  }

  async submitBugReport(payload: {
    message: string;
    screen_name?: string | null;
    app_version?: string | null;
    scene_id?: string | null;
    screenshot_b64?: string | null;
  }): Promise<{ id: string }> {
    return this.request('/bug-reports', {
      method: 'POST',
      body: JSON.stringify(payload),
    });
  }

  async listMyBugReports(): Promise<{
    items: {
      id: string;
      created_at: string;
      screen_name: string | null;
      message: string;
      response: string | null;
      responded_at: string | null;
      response_seen: boolean;
    }[];
    unseen: number;
  }> {
    return this.request('/bug-reports/mine');
  }

  async markBugRepliesSeen(): Promise<{ marked: number }> {
    return this.request('/bug-reports/mine/seen', { method: 'POST' });
  }

  async getServerVersion(): Promise<{ version: string; apk_url: string | null }> {
    // /version nie wymaga API key (publiczny endpoint do upgrade discovery).
    // Używamy this.request bo czysty fetch bez API key też jest OK, ale endpoint jest auth-free.
    const res = await fetch(`${this.baseUrl}/version`);
    if (!res.ok) {
      throw new ApiError(res.status, `version: HTTP ${res.status}`);
    }
    return res.json();
  }

  // Blacklist
  async listBlacklist(): Promise<BlacklistOut> {
    return this.request('/blacklist');
  }

  async addBlacklist(kind: BlacklistKind, entityId: string): Promise<{ created: boolean }> {
    return this.request(`/blacklist/${kind}/${entityId}`, { method: 'POST' });
  }

  async removeBlacklist(kind: BlacklistKind, entityId: string): Promise<void> {
    const res = await fetch(`${this.baseUrl}/blacklist/${kind}/${entityId}`, {
      method: 'DELETE',
      headers: await this._authHeaders(),
    });
    if (!res.ok && res.status !== 204) {
      const text = await res.text().catch(() => res.statusText);
      throw new ApiError(res.status, `${res.status}: ${text}`);
    }
  }

  // Watch history
  async upsertProgress(
    sceneId: string,
    body: { position_sec: number; duration_sec?: number; finished?: boolean },
  ): Promise<ProgressOut> {
    return this.request(`/scenes/${sceneId}/progress`, {
      method: 'POST',
      body: JSON.stringify(body),
    });
  }

  // Movie progress — mirror upsertProgress dla movies. Backend dodał 2026-05-28.
  async upsertMovieProgress(
    movieId: string,
    body: { position_sec: number; duration_sec?: number; finished?: boolean },
  ): Promise<ProgressOut> {
    return this.request(`/movies/${movieId}/progress`, {
      method: 'POST',
      body: JSON.stringify(body),
    });
  }

  async listRecentWatch(limit = 10): Promise<WatchListOut> {
    return this.request(`/watch/recent?limit=${limit}`);
  }
}
