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

const OMDB_BASE_URL = 'https://www.omdbapi.com/';
const OMDB_TIMEOUT_MS = 10000;
const OMDB_MAX_ATTEMPTS = 2;
const OMDB_RETRY_BASE_DELAY_MS = 300;

function normalizeOmdbTimeoutMs(value) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) {
    return OMDB_TIMEOUT_MS;
  }
  return Math.max(1000, Math.trunc(parsed));
}

function isRetryableOmdbError(error, aborted = false) {
  if (aborted) {
    return true;
  }
  const code = String(error?.code || '').trim().toUpperCase();
  if (code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'ECONNABORTED') {
    return true;
  }
  const causeCode = String(error?.cause?.code || '').trim().toUpperCase();
  if (causeCode === 'UND_ERR_CONNECT_TIMEOUT' || causeCode === 'UND_ERR_HEADERS_TIMEOUT' || causeCode === 'UND_ERR_SOCKET') {
    return true;
  }
  const message = String(error?.message || '').toLowerCase();
  return message.includes('timed out') || message.includes('timeout');
}

function sleep(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

class OmdbService {
  mapSearchResults(data) {
    if (!data || typeof data !== 'object') {
      return [];
    }
    if (data.Response === 'False' || !Array.isArray(data.Search)) {
      return [];
    }
    return data.Search.map((item) => ({
      title: item.Title,
      year: item.Year,
      imdbId: item.imdbID,
      type: item.Type,
      poster: item.Poster
    }));
  }

  async requestJson(url, meta = {}, options = {}) {
    const timeoutMs = normalizeOmdbTimeoutMs(options?.timeoutMs);
    const maxAttempts = Math.max(1, Math.trunc(Number(options?.maxAttempts || OMDB_MAX_ATTEMPTS)));

    for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), timeoutMs);
      try {
        const response = await fetch(url, {
          headers: {
            'Accept': 'application/json',
            'User-Agent': 'Ripster/1.0'
          },
          signal: controller.signal
        });
        if (!response.ok) {
          logger.warn('request:http-failed', {
            ...meta,
            status: response.status,
            attempt,
            maxAttempts
          });
          return null;
        }
        return await response.json();
      } catch (error) {
        const aborted = error?.name === 'AbortError';
        const retryable = isRetryableOmdbError(error, aborted);
        const willRetry = retryable && attempt < maxAttempts;
        logger[willRetry ? 'info' : 'warn']('request:failed', {
          ...meta,
          timeoutMs,
          aborted,
          retryable,
          attempt,
          maxAttempts,
          willRetry,
          message: error?.message || String(error)
        });
        if (willRetry) {
          await sleep(OMDB_RETRY_BASE_DELAY_MS * attempt);
          continue;
        }
        return null;
      } finally {
        clearTimeout(timer);
      }
    }
    return null;
  }

  async search(query) {
    const normalizedQuery = String(query || '').trim();
    if (!normalizedQuery) {
      return [];
    }
    logger.info('search:start', { query: normalizedQuery });

    // Allow direct IMDb-ID lookups in the search field (useful for History remapping).
    const imdbLike = normalizedQuery.toLowerCase();
    if (/^tt\d{6,12}$/.test(imdbLike)) {
      const byId = await this.fetchByImdbId(imdbLike);
      return byId
        ? [{
            title: byId.title,
            year: byId.year != null ? String(byId.year) : null,
            imdbId: byId.imdbId,
            type: byId.type,
            poster: byId.poster
          }]
        : [];
    }

    const settings = await settingsService.getSettingsMap();
    const apiKey = settings.omdb_api_key;
    if (!apiKey) {
      return [];
    }
    const timeoutMs = normalizeOmdbTimeoutMs(settings.omdb_timeout_ms);

    const configuredType = String(settings.omdb_default_type || 'movie').trim().toLowerCase() || 'movie';
    const requestSearch = async (type = null) => {
      const url = new URL(OMDB_BASE_URL);
      url.searchParams.set('apikey', apiKey);
      url.searchParams.set('s', normalizedQuery);
      if (type) {
        url.searchParams.set('type', type);
      }
      const data = await this.requestJson(
        url,
        { query: normalizedQuery, action: 'search', type: type || null },
        { timeoutMs }
      );
      return {
        data,
        results: this.mapSearchResults(data)
      };
    };

    let { data, results } = await requestSearch(configuredType);
    if (results.length === 0 && configuredType) {
      logger.info('search:fallback-no-type', {
        query: normalizedQuery,
        configuredType,
        response: data?.Response || null,
        error: data?.Error || null
      });
      ({ data, results } = await requestSearch(null));
    }

    if (results.length === 0) {
      logger.warn('search:no-results', {
        query: normalizedQuery,
        response: data?.Response || null,
        error: data?.Error || null,
        configuredType
      });
      return [];
    }

    logger.info('search:done', { query: normalizedQuery, count: results.length, configuredType });
    return results;
  }

  async fetchByImdbId(imdbId) {
    const normalizedId = String(imdbId || '').trim().toLowerCase();
    if (!/^tt\d{6,12}$/.test(normalizedId)) {
      return null;
    }

    logger.info('fetchByImdbId:start', { imdbId: normalizedId });
    const settings = await settingsService.getSettingsMap();
    const apiKey = settings.omdb_api_key;
    if (!apiKey) {
      return null;
    }
    const timeoutMs = normalizeOmdbTimeoutMs(settings.omdb_timeout_ms);

    const url = new URL(OMDB_BASE_URL);
    url.searchParams.set('apikey', apiKey);
    url.searchParams.set('i', normalizedId);
    url.searchParams.set('plot', 'full');

    const data = await this.requestJson(url, { imdbId: normalizedId, action: 'fetchByImdbId' }, { timeoutMs });
    if (!data || typeof data !== 'object') {
      return null;
    }
    if (data.Response === 'False') {
      logger.warn('fetchByImdbId:not-found', { imdbId: normalizedId, error: data.Error });
      return null;
    }

    const yearMatch = String(data.Year || '').match(/\b(19|20)\d{2}\b/);
    const year = yearMatch ? Number(yearMatch[0]) : null;
    const poster = data.Poster && data.Poster !== 'N/A' ? data.Poster : null;

    const result = {
      title: data.Title || null,
      year: Number.isFinite(year) ? year : null,
      imdbId: String(data.imdbID || normalizedId),
      type: data.Type || null,
      poster,
      raw: data
    };
    logger.info('fetchByImdbId:done', { imdbId: result.imdbId, title: result.title });
    return result;
  }
}

module.exports = new OmdbService();
