'use strict';

const settingsService = require('./settingsService');
const logger = require('./logger').child('TMDB');

const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
const TMDB_IMAGE_BASE_URL = 'https://image.tmdb.org/t/p';
const TMDB_TIMEOUT_MS = 10000;

class TmdbService {
  normalizeNameList(values = [], options = {}) {
    const maxItems = Math.max(1, Number(options.maxItems || 10));
    const source = Array.isArray(values) ? values : [];
    const output = [];
    const seen = new Set();
    for (const item of source) {
      const name = String(item?.name || item || '').trim();
      if (!name) {
        continue;
      }
      const key = name.toLowerCase();
      if (seen.has(key)) {
        continue;
      }
      seen.add(key);
      output.push(name);
      if (output.length >= maxItems) {
        break;
      }
    }
    return output;
  }

  formatRuntimeLabel(value) {
    if (Array.isArray(value)) {
      const values = value
        .map((entry) => Number(entry))
        .filter((entry) => Number.isFinite(entry) && entry > 0)
        .map((entry) => Math.trunc(entry));
      if (values.length === 0) {
        return null;
      }
      const min = Math.min(...values);
      const max = Math.max(...values);
      return min === max ? `${min} min` : `${min}-${max} min`;
    }
    const numeric = Number(value);
    if (Number.isFinite(numeric) && numeric > 0) {
      return `${Math.trunc(numeric)} min`;
    }
    const text = String(value || '').trim();
    return text || null;
  }

  async getConfig() {
    const settings = await settingsService.getSettingsMap();
    return {
      readAccessToken: String(settings.tmdb_api_read_access_token || '').trim() || null,
      language: String(settings.dvd_series_language || 'de-DE').trim() || 'de-DE'
    };
  }

  async isConfigured() {
    const config = await this.getConfig();
    return Boolean(config.readAccessToken);
  }

  async resolveLanguage(explicitLanguage = null) {
    const config = await this.getConfig();
    return String(explicitLanguage || config.language || 'de-DE').trim() || 'de-DE';
  }

  async request(pathName, options = {}) {
    const config = await this.getConfig();
    if (!config.readAccessToken) {
      return null;
    }

    const timeoutMs = Math.max(1000, Number(options.timeoutMs || TMDB_TIMEOUT_MS));
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);

    try {
      const normalizedPath = String(pathName || '').replace(/^\/+/, '');
      const url = new URL(normalizedPath, `${TMDB_BASE_URL}/`);
      if (options.query && typeof options.query === 'object') {
        for (const [key, value] of Object.entries(options.query)) {
          if (value === undefined || value === null || value === '') {
            continue;
          }
          url.searchParams.set(key, String(value));
        }
      }

      const response = await fetch(url, {
        method: options.method || 'GET',
        headers: {
          'accept': 'application/json',
          'Authorization': `Bearer ${config.readAccessToken}`,
          'User-Agent': 'Ripster/1.0'
        },
        signal: controller.signal
      });

      if (!response.ok) {
        const error = new Error(`TMDb request failed (${response.status})`);
        error.statusCode = response.status;
        error.url = url.toString();
        throw error;
      }

      return response.json();
    } finally {
      clearTimeout(timer);
    }
  }

  buildImageUrl(imagePath, size = 'w342') {
    const normalizedPath = String(imagePath || '').trim();
    if (!normalizedPath) {
      return null;
    }
    return `${TMDB_IMAGE_BASE_URL}/${String(size || 'w342').trim()}/${normalizedPath.replace(/^\/+/, '')}`;
  }

  async searchSeries(query, options = {}) {
    const normalizedQuery = String(query || '').trim();
    if (!normalizedQuery || !(await this.isConfigured())) {
      return [];
    }
    const language = await this.resolveLanguage(options.language);

    const data = await this.request('/search/tv', {
      query: {
        query: normalizedQuery,
        first_air_date_year: options.year || undefined,
        language,
        page: options.page || 1,
        include_adult: false
      }
    }).catch((error) => {
      logger.warn('search:failed', {
        query: normalizedQuery,
        error: error?.message || String(error)
      });
      return null;
    });

    const rows = Array.isArray(data?.results) ? data.results : [];
    return rows
      .map((row) => ({
        id: Number(row?.id || 0) || null,
        title: String(row?.name || row?.original_name || '').trim() || null,
        originalTitle: String(row?.original_name || '').trim() || null,
        year: Number(String(row?.first_air_date || '').slice(0, 4)) || null,
        overview: String(row?.overview || '').trim() || null,
        posterPath: String(row?.poster_path || '').trim() || null,
        poster: this.buildImageUrl(row?.poster_path, 'w342'),
        backdropPath: String(row?.backdrop_path || '').trim() || null,
        backdrop: this.buildImageUrl(row?.backdrop_path, 'w780'),
        originalLanguage: String(row?.original_language || '').trim() || null,
        popularity: Number(row?.popularity || 0) || 0
      }))
      .filter((row) => row.id && row.title);
  }

  async getSeriesDetails(seriesId, options = {}) {
    const id = Number(seriesId);
    if (!Number.isFinite(id) || id <= 0 || !(await this.isConfigured())) {
      return null;
    }
    const language = await this.resolveLanguage(options.language);
    const appendToResponse = Array.isArray(options.appendToResponse)
      ? options.appendToResponse.map((item) => String(item || '').trim()).filter(Boolean)
      : [];
    return this.request(`/tv/${Math.trunc(id)}`, {
      query: {
        language,
        append_to_response: appendToResponse.length > 0 ? appendToResponse.join(',') : undefined
      }
    }).catch((error) => {
      logger.warn('series:details:failed', {
        seriesId: Math.trunc(id),
        error: error?.message || String(error)
      });
      return null;
    });
  }

  async getEpisodeGroups(seriesId) {
    const id = Number(seriesId);
    if (!Number.isFinite(id) || id <= 0 || !(await this.isConfigured())) {
      return [];
    }
    const response = await this.request(`/tv/${Math.trunc(id)}/episode_groups`).catch((error) => {
      logger.warn('series:episode-groups:failed', {
        seriesId: Math.trunc(id),
        error: error?.message || String(error)
      });
      return null;
    });
    return Array.isArray(response?.results) ? response.results : [];
  }

  async getEpisodeGroupDetails(groupId, options = {}) {
    const normalizedId = String(groupId || '').trim();
    if (!normalizedId || !(await this.isConfigured())) {
      return null;
    }
    const language = await this.resolveLanguage(options.language);
    return this.request(`/tv/episode_group/${normalizedId}`, {
      query: {
        language
      }
    }).catch((error) => {
      logger.warn('episode-group:details:failed', {
        groupId: normalizedId,
        error: error?.message || String(error)
      });
      return null;
    });
  }

  async getSeasonDetails(seriesId, seasonNumber, options = {}) {
    const id = Number(seriesId);
    const season = Number(seasonNumber);
    if (!Number.isFinite(id) || id <= 0 || !Number.isFinite(season) || season < 0 || !(await this.isConfigured())) {
      return null;
    }
    const language = await this.resolveLanguage(options.language);
    return this.request(`/tv/${Math.trunc(id)}/season/${Math.trunc(season)}`, {
      query: {
        language
      }
    }).catch(() => null);
  }

  async getSeasonCredits(seriesId, seasonNumber, options = {}) {
    const id = Number(seriesId);
    const season = Number(seasonNumber);
    if (!Number.isFinite(id) || id <= 0 || !Number.isFinite(season) || season < 0 || !(await this.isConfigured())) {
      return null;
    }
    const language = await this.resolveLanguage(options.language);
    return this.request(`/tv/${Math.trunc(id)}/season/${Math.trunc(season)}/credits`, {
      query: {
        language
      }
    }).catch((error) => {
      logger.warn('season:credits:failed', {
        seriesId: Math.trunc(id),
        seasonNumber: Math.trunc(season),
        error: error?.message || String(error)
      });
      return null;
    });
  }

  buildSeasonSummary(seasonDetails = null) {
    const details = seasonDetails && typeof seasonDetails === 'object' ? seasonDetails : {};
    const episodes = Array.isArray(details.episodes) ? details.episodes : [];
    return {
      seasonNumber: Number(details.season_number || details.seasonNumber || 0) || null,
      name: String(details.name || '').trim() || null,
      overview: String(details.overview || '').trim() || null,
      posterPath: String(details.poster_path || '').trim() || null,
      poster: this.buildImageUrl(details.poster_path, 'w342'),
      episodeCount: episodes.length,
      episodes: episodes.map((episode) => ({
        id: Number(episode?.id || 0) || null,
        number: Number(episode?.episode_number || episode?.episodeNumber || 0) || null,
        seasonNumber: Number(episode?.season_number || details.season_number || 0) || null,
        name: String(episode?.name || '').trim() || null,
        overview: String(episode?.overview || '').trim() || null,
        runtime: Number(episode?.runtime || 0) || null,
        airDate: String(episode?.air_date || '').trim() || null,
        stillPath: String(episode?.still_path || '').trim() || null,
        still: this.buildImageUrl(episode?.still_path, 'w300')
      })).filter((episode) => episode.id && episode.number)
    };
  }

  buildSeriesDetailsSummary(seriesDetails = null) {
    const details = seriesDetails && typeof seriesDetails === 'object' ? seriesDetails : {};
    const crew = Array.isArray(details?.credits?.crew) ? details.credits.crew : [];
    const cast = Array.isArray(details?.credits?.cast) ? details.credits.cast : [];
    const creators = Array.isArray(details?.created_by) ? details.created_by : [];
    const genres = Array.isArray(details?.genres) ? details.genres : [];
    const runtimeLabel = this.formatRuntimeLabel(details?.episode_run_time);
    const directorNames = this.normalizeNameList(
      crew.filter((member) => String(member?.job || '').trim().toLowerCase() === 'director'),
      { maxItems: 5 }
    );
    const creatorNames = this.normalizeNameList(creators, { maxItems: 5 });
    const actorNames = this.normalizeNameList(cast, { maxItems: 10 });
    const genreNames = this.normalizeNameList(genres, { maxItems: 10 });
    const voteAverageRaw = Number(details?.vote_average || 0);
    const voteAverage = Number.isFinite(voteAverageRaw) && voteAverageRaw > 0
      ? Number(voteAverageRaw.toFixed(1))
      : null;
    const voteCount = Number(details?.vote_count || 0);
    const imdbId = String(details?.external_ids?.imdb_id || '').trim() || null;

    return {
      director: directorNames.length > 0
        ? directorNames.join(', ')
        : (creatorNames.length > 0 ? creatorNames.join(', ') : null),
      actors: actorNames.length > 0 ? actorNames.join(', ') : null,
      runtime: runtimeLabel,
      runtimeLabel,
      genre: genreNames.length > 0 ? genreNames.join(', ') : null,
      imdbRating: voteAverage !== null ? voteAverage.toFixed(1) : null,
      voteAverage,
      voteCount: Number.isFinite(voteCount) && voteCount > 0 ? Math.trunc(voteCount) : null,
      rottenTomatoes: null,
      imdbId,
      tmdbId: Number(details?.id || 0) || null,
      firstAirDate: String(details?.first_air_date || '').trim() || null
    };
  }

  buildSeriesMetadataCandidate(series = {}, options = {}) {
    const season = series?.season && typeof series.season === 'object'
      ? series.season
      : null;
    const seasonNumber = Number(options.seasonNumber || season?.seasonNumber || 0) || null;
    const providerId = seasonNumber
      ? `tmdb:${series.id}:season:${seasonNumber}`
      : `tmdb:${series.id}`;

    return {
      provider: 'tmdb',
      providerId,
      metadataKind: seasonNumber ? 'season' : 'series',
      tmdbId: Number(series?.id || 0) || null,
      title: String(series?.title || '').trim() || null,
      originalTitle: String(series?.originalTitle || '').trim() || null,
      year: Number(series?.year || 0) || null,
      overview: String(series?.overview || '').trim() || null,
      poster: series?.poster || this.buildImageUrl(series?.posterPath, 'w342'),
      backdrop: series?.backdrop || this.buildImageUrl(series?.backdropPath, 'w780'),
      seasonNumber,
      seasonName: String(season?.name || '').trim() || null,
      seasonOverview: String(season?.overview || '').trim() || null,
      seasonPoster: season?.poster || this.buildImageUrl(season?.posterPath, 'w342'),
      episodeCount: Number(season?.episodeCount || 0) || 0,
      episodes: Array.isArray(season?.episodes) ? season.episodes : []
    };
  }

  buildSeasonSummariesFromSeriesDetails(seriesDetails = null) {
    const details = seriesDetails && typeof seriesDetails === 'object' ? seriesDetails : {};
    const seasons = Array.isArray(details.seasons) ? details.seasons : [];
    return seasons
      .map((season) => ({
        seasonNumber: Number(season?.season_number || season?.seasonNumber || 0) || null,
        name: String(season?.name || '').trim() || null,
        overview: String(season?.overview || '').trim() || null,
        posterPath: String(season?.poster_path || '').trim() || null,
        poster: this.buildImageUrl(season?.poster_path, 'w342'),
        episodeCount: Number(season?.episode_count || season?.episodeCount || 0) || 0,
        episodes: []
      }))
      .filter((season) => season.seasonNumber !== null && season.episodeCount > 0);
  }

  async searchSeriesWithSeasons(query, options = {}) {
    const candidates = await this.searchSeries(query, options);
    if (candidates.length === 0) {
      return [];
    }

    const limit = Math.max(1, Math.min(10, Number(options.limit || 5)));
    const selectedCandidates = candidates.slice(0, limit);

    const expanded = await Promise.all(selectedCandidates.map(async (candidate) => {
      const details = await this.getSeriesDetails(candidate.id, options);
      const seasons = this.buildSeasonSummariesFromSeriesDetails(details);
      if (seasons.length === 0) {
        return [this.buildSeriesMetadataCandidate(candidate)];
      }
      return seasons.map((season) => this.buildSeriesMetadataCandidate({
        ...candidate,
        season
      }, {
        seasonNumber: season.seasonNumber
      }));
    }));

    return expanded.flat().filter((item) => item?.tmdbId && item?.title);
  }

  async searchSeriesWithSeason(query, seasonNumber, options = {}) {
    const normalizedSeason = Number(seasonNumber);
    if (!Number.isFinite(normalizedSeason) || normalizedSeason < 0) {
      return [];
    }

    const candidates = await this.searchSeries(query, options);
    if (candidates.length === 0) {
      return [];
    }

    const limit = Math.max(1, Math.min(10, Number(options.limit || 5)));
    const selectedCandidates = candidates.slice(0, limit);
    const withSeasons = await Promise.all(selectedCandidates.map(async (candidate) => {
      const seasonDetails = await this.getSeasonDetails(candidate.id, normalizedSeason, options);
      if (!seasonDetails) {
        return {
          ...candidate,
          season: null
        };
      }
      return {
        ...candidate,
        season: this.buildSeasonSummary(seasonDetails)
      };
    }));

    return withSeasons
      .filter((candidate) => candidate.season && candidate.season.episodeCount > 0)
      .map((candidate) => this.buildSeriesMetadataCandidate(candidate, {
        seasonNumber: normalizedSeason
      }));
  }
}

module.exports = new TmdbService();
