// Deezer API client (read-only).
//
// Deezer provides a free, no-auth search API with high-quality cover art
// (up to 1000×1000 via the `cover_xl` field). Coverage is very good for
// European and mainstream releases.
//
// Rate limit: Deezer doesn't document a hard limit for the public search API,
// but ~50 req/min is a safe polite default, enforced via the http base.
//
// Best-effort: on any error, methods log a warning and return null rather than
// throwing, so enrichment never breaks the worker.

import { buildUserAgent, requestJson } from './http.js';

/** A normalised Deezer album search result (only the fields we read). */
export interface DeezerAlbum {
  artistName: string;
  title: string;
  /** 1000×1000 cover URL. */
  coverXl: string;
  /** 500×500 cover URL (fallback). */
  coverBig: string;
}

/** An album in an artist's discography (only the fields we read). */
export interface DeezerAlbumRelease {
  id: number;
  title: string;
  /** ISO date, YYYY-MM-DD. */
  releaseDate: string;
  /** 'album' | 'single' | 'ep' | 'compilation' as reported by Deezer. */
  recordType: string;
}

interface DeezerSearchResult {
  artist?: { name?: string };
  title?: string;
  cover_xl?: string;
  cover_big?: string;
}

interface DeezerSearchResponse {
  data?: DeezerSearchResult[];
}

/** Lowercase and strip diacritics, so a query for "Bjork" matches "Björk". */
const foldName = (name: string) =>
  name.trim().toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '');

export class DeezerClient {
  private readonly baseUrl = 'https://api.deezer.com';
  private readonly userAgent: string;
  private readonly minIntervalMs: number;

  constructor(contact = '', minIntervalMs = 1200) {
    this.userAgent = buildUserAgent(contact);
    this.minIntervalMs = minIntervalMs;
  }

  /**
   * Search the Deezer catalog for an album by artist + title and return the
   * best match, or null if no match / error.
   *
   * Deezer's search `q` supports the fielded syntax `artist:"…" album:"…"`
   * which gives precise matches.
   */
  async searchAlbum(artist: string, album: string): Promise<DeezerAlbum | null> {
    if (artist.trim() === '' || album.trim() === '') return null;

    const q = `artist:"${artist}" album:"${album}"`;
    const qs = new URLSearchParams({ q });
    const url = `${this.baseUrl}/search/album?${qs.toString()}`;

    try {
      const data = await requestJson<DeezerSearchResponse>(url, {
        userAgent: this.userAgent,
        minIntervalMs: this.minIntervalMs,
      });
      const results = data.data ?? [];
      if (results.length === 0) return null;

      const best = results[0];
      const coverXl = best.cover_xl ?? best.cover_big ?? null;
      const coverBig = best.cover_big ?? best.cover_xl ?? null;
      if (!coverXl) return null;

      return {
        artistName: best.artist?.name ?? artist,
        title: best.title ?? album,
        coverXl,
        coverBig: coverBig ?? coverXl,
      };
    } catch (err) {
      console.warn('[Deezer] searchAlbum failed:', (err as Error).message);
      return null;
    }
  }

  /**
   * Artist photo (1000×1000 `picture_xl`) by name, or null.
   *
   * ponytail: exact case-insensitive name match only. Deezer's artist search is
   * fuzzy and happily returns a tribute band for a near miss, so a wrong photo
   * is worse than the placeholder. Loosen it only if real artists get skipped.
   */
  async searchArtistImage(artist: string): Promise<string | null> {
    if (artist.trim() === '') return null;
    const qs = new URLSearchParams({ q: artist });
    try {
      const data = await requestJson<{ data?: { name?: string; picture_xl?: string; picture_big?: string }[] }>(
        `${this.baseUrl}/search/artist?${qs.toString()}`,
        { userAgent: this.userAgent, minIntervalMs: this.minIntervalMs }
      );
      const want = foldName(artist);
      for (const a of data.data ?? []) {
        if (foldName(a.name ?? '') !== want) continue;
        // Deezer serves a blank grey tile for artists it has no photo for: the
        // path is either empty or the md5 of the empty string. Both are useless,
        // and the same name can appear twice with only the second one real.
        const url = a.picture_xl ?? a.picture_big ?? '';
        if (url && !url.includes('d41d8cd98f00b204e9800998ecf8427e') && !url.includes('/artist//')) return url;
      }
      return null;
    } catch (err) {
      console.warn('[Deezer] searchArtistImage failed:', (err as Error).message);
      return null;
    }
  }

  /** Resolve an artist name to a Deezer artist id via exact fold-matched search. */
  private async resolveArtistId(artist: string): Promise<number | null> {
    if (artist.trim() === '') return null;
    const qs = new URLSearchParams({ q: artist });
    try {
      const data = await requestJson<{ data?: { id?: number; name?: string }[] }>(
        `${this.baseUrl}/search/artist?${qs.toString()}`,
        { userAgent: this.userAgent, minIntervalMs: this.minIntervalMs }
      );
      const want = foldName(artist);
      for (const a of data.data ?? []) {
        if (foldName(a.name ?? '') === want && typeof a.id === 'number') return a.id;
      }
      return null;
    } catch (err) {
      console.warn('[Deezer] resolveArtistId failed:', (err as Error).message);
      return null;
    }
  }

  /**
   * Albums by `artist` released on/after `sinceIso` (YYYY-MM-DD), newest first.
   *
   * ponytail: reads only the first page (25 albums). Deezer returns albums
   * newest-first, so a release-date cutoff never needs page two unless an
   * artist dropped 25 albums inside the window. Paginate if that ever happens.
   */
  async getArtistAlbumsSince(artist: string, sinceIso: string): Promise<DeezerAlbumRelease[]> {
    const artistId = await this.resolveArtistId(artist);
    if (artistId === null) return [];
    try {
      const data = await requestJson<{
        data?: { id?: number; title?: string; release_date?: string; record_type?: string }[];
      }>(`${this.baseUrl}/artist/${artistId}/albums?limit=25`, {
        userAgent: this.userAgent,
        minIntervalMs: this.minIntervalMs,
      });
      return (data.data ?? [])
        .filter((a) => typeof a.id === 'number' && a.title && a.release_date && a.release_date >= sinceIso)
        .map((a) => ({
          id: a.id as number,
          title: a.title as string,
          releaseDate: a.release_date as string,
          recordType: a.record_type ?? 'album',
        }));
    } catch (err) {
      console.warn('[Deezer] getArtistAlbumsSince failed:', (err as Error).message);
      return [];
    }
  }

  /** Track titles on a Deezer album, in tracklist order. */
  async getAlbumTracks(albumId: number): Promise<string[]> {
    try {
      const data = await requestJson<{ data?: { title?: string }[] }>(
        `${this.baseUrl}/album/${albumId}/tracks?limit=50`,
        { userAgent: this.userAgent, minIntervalMs: this.minIntervalMs }
      );
      return (data.data ?? []).map((t) => t.title ?? '').filter((t) => t !== '');
    } catch (err) {
      console.warn('[Deezer] getAlbumTracks failed:', (err as Error).message);
      return [];
    }
  }
}
