/* Nutri Movie Night — TMDb client (window.TMDB).
   Free, personal-use movie discovery powered by TMDb ONLY (no OMDb / IMDb scraping / paid API).

   Credentials are NEVER hardcoded — the user pastes them in the in-app Movie Night
   Settings screen and they are stored (user-specific) in Store.movieCredentials():
     { apiKey, readToken }
   Placeholders only, for reference:
     TMDB_API_KEY=your_tmdb_api_key_here
     TMDB_READ_ACCESS_TOKEN=your_tmdb_read_access_token_here

   Auth: we prefer the v4 Read Access Token as a Bearer header (so the secret never
   lands in a URL and can't be cached/persisted). We fall back to ?api_key= when only
   the v3 key is present. All requests use cache:'no-store' for defense in depth — the
   SW also never caches cross-origin (cors) responses, so the key is never persisted.

   Attribution (shown in Settings/About/Import):
     "This product uses the TMDb API but is not endorsed or certified by TMDb." */
(function () {
  const API_BASE = 'https://api.themoviedb.org/3';
  const IMG_BASE = 'https://image.tmdb.org/t/p/';

  // ── Backend proxy (keys live on the server) + legacy local-key fallback ──
  // Keys now live in the secure Cloud Function `movieApi`. The legacy "enter your
  // key in Settings" path is kept only as a fallback so nothing breaks before the
  // functions are deployed (see DEPLOY_FUNCTIONS.md).
  function _creds() {
    try {
      const c = (window.Store && window.Store.movieCredentials && window.Store.movieCredentials()) || {};
      return { apiKey: (c.apiKey || '').trim(), readToken: (c.readToken || '').trim() };
    } catch (_) { return { apiKey: '', readToken: '' }; }
  }
  function _hasLocalKeys() { const c = _creds(); return !!(c.readToken || c.apiKey); }

  let _proxyState = null; // null=unknown · true=works · false=unavailable this session
  function _proxyFn() {
    try {
      if (_proxyState === false) return null;
      if (window.firebase && firebase.app && firebase.functions) return firebase.app().functions().httpsCallable('movieApi');
    } catch (_) {}
    return null;
  }
  function proxyAvailable() { return _proxyState !== false && !!(window.firebase && firebase.functions); }
  // The app is "connected" if the backend proxy is reachable OR a legacy key exists.
  function hasCredentials() { return proxyAvailable() || _hasLocalKeys(); }

  // Map a Firebase callable error → the legacy { code, message } the UI expects.
  function _mapProxyErr(e) {
    const st = e && e.details && e.details.tmdbStatus;
    const err = new Error((e && e.message) || 'Movie service error.');
    if (st === 401) err.code = 'auth';
    else if (st === 404) err.code = 'not-found';
    else if (st === 429 || (e && e.code === 'functions/resource-exhausted')) err.code = 'rate-limit';
    else if (e && /functions\/(not-found|unimplemented|failed-precondition|internal|unavailable)/.test(e.code || '')) { err.code = 'no-creds'; err.message = 'Movie service not connected. Deploy the Movie Night functions (DEPLOY_FUNCTIONS.md), or add a key in Settings → Developer.'; }
    else err.code = 'http';
    return err;
  }
  // Lightweight backend status (used by Settings to show "Connected").
  async function proxyHealth() {
    const fn = _proxyFn();
    if (!fn) return { ok: false, reason: 'no-sdk' };
    try { const r = await fn({ service: 'health' }); _proxyState = true; return Object.assign({ ok: true }, r.data || {}); }
    catch (e) {
      if (e && /functions\/(not-found|unimplemented|failed-precondition|internal|unavailable)/.test(e.code || '')) _proxyState = false;
      return { ok: false, reason: (e && e.code) || 'error', message: e && e.message };
    }
  }

  // ── Core request — backend proxy first, legacy direct key as fallback ──
  // path: e.g. '/search/movie'. params: query object (strings/numbers).
  async function _req(path, params) {
    const fn = _proxyFn();
    if (fn) {
      try {
        const res = await fn({ service: 'tmdb', path, params: params || {} });
        _proxyState = true;
        return res.data;
      } catch (e) {
        if (e && e.details && e.details.tmdbStatus != null) throw _mapProxyErr(e); // upstream error → surface
        // not-found/unavailable/internal (no upstream status) ⇒ function not deployed/reachable
        const permanent = e && /functions\/(not-found|unimplemented|failed-precondition|internal|unavailable)/.test(e.code || '');
        if (permanent) _proxyState = false;
        if (!_hasLocalKeys()) throw _mapProxyErr(e); // can't fall back → clear error + retry
        // else fall through to the legacy direct request below
      }
    }
    return _directReq(path, params);
  }

  // Legacy: call TMDb directly with the user's saved key (pre-deploy fallback only).
  async function _directReq(path, params) {
    const c = _creds();
    if (!c.readToken && !c.apiKey) {
      const err = new Error('Movie service not connected. Deploy the Movie Night functions (or add a TMDb key in Settings → Developer).');
      err.code = 'no-creds';
      throw err;
    }
    const url = new URL(API_BASE + path);
    const p = params || {};
    Object.keys(p).forEach(k => {
      if (p[k] !== undefined && p[k] !== null && p[k] !== '') url.searchParams.set(k, p[k]);
    });
    const headers = { 'Accept': 'application/json' };
    if (c.readToken) headers['Authorization'] = 'Bearer ' + c.readToken;
    else url.searchParams.set('api_key', c.apiKey);

    let res;
    try {
      res = await fetch(url.toString(), { method: 'GET', headers, cache: 'no-store' });
    } catch (netErr) {
      const err = new Error('Network error reaching TMDb. Check your connection and try again.');
      err.code = 'network';
      throw err;
    }
    if (res.status === 401) {
      const err = new Error('TMDb rejected your credentials (401). Re-check the key / token in Settings.');
      err.code = 'auth';
      throw err;
    }
    if (res.status === 404) {
      const err = new Error('Not found on TMDb (404).');
      err.code = 'not-found';
      throw err;
    }
    if (res.status === 429) {
      const err = new Error('TMDb rate limit hit (429). Wait a moment and retry.');
      err.code = 'rate-limit';
      throw err;
    }
    if (!res.ok) {
      const err = new Error('TMDb error (HTTP ' + res.status + ').');
      err.code = 'http';
      throw err;
    }
    return res.json();
  }

  // ── Image URL builders ────────────────────────────────────────────────
  function posterUrl(path, size) {
    if (!path) return null;
    return IMG_BASE + (size || 'w500') + path;
  }
  function backdropUrl(path, size) {
    if (!path) return null;
    return IMG_BASE + (size || 'w780') + path;
  }

  // ── Genre map (cached for the session) ───────────────────────────────
  let _genreCache = null;
  let _genrePromise = null;
  async function genres() {
    if (_genreCache) return _genreCache;
    if (_genrePromise) return _genrePromise;
    _genrePromise = _req('/genre/movie/list', { language: 'en-US' })
      .then(data => {
        const map = {};
        (data.genres || []).forEach(g => { map[g.id] = g.name; });
        _genreCache = map;
        _genrePromise = null;
        return map;
      })
      .catch(e => { _genrePromise = null; throw e; });
    return _genrePromise;
  }
  function genreMapSync() { return _genreCache || {}; }

  // ── Normalize a TMDb movie (list result OR full detail) → metadata only.
  // Produces NO user fields (status/dates/notes/lists) — those belong to the Store.
  function normalize(raw, genreMap) {
    if (!raw) return null;
    const gm = genreMap || genreMapSync();
    let genreNames = [];
    if (Array.isArray(raw.genres) && raw.genres.length) {
      genreNames = raw.genres.map(g => g.name).filter(Boolean);
    } else if (Array.isArray(raw.genre_ids)) {
      genreNames = raw.genre_ids.map(id => gm[id]).filter(Boolean);
    }
    const releaseDate = raw.release_date || '';
    const year = releaseDate ? +releaseDate.slice(0, 4) : (raw.year || null);
    const imdbId = raw.imdb_id || (raw.external_ids && raw.external_ids.imdb_id) || '';
    return {
      tmdbId: raw.id,
      imdbId: imdbId || '',
      title: raw.title || raw.name || raw.original_title || 'Untitled',
      originalTitle: raw.original_title || raw.original_name || '',
      year: year || null,
      releaseDate,
      posterPath: raw.poster_path || '',
      posterUrl: posterUrl(raw.poster_path),
      backdropPath: raw.backdrop_path || '',
      backdropUrl: backdropUrl(raw.backdrop_path),
      overview: raw.overview || '',
      genres: genreNames,
      genreIds: Array.isArray(raw.genre_ids) ? raw.genre_ids.slice()
                : (Array.isArray(raw.genres) ? raw.genres.map(g => g.id) : []),
      runtime: raw.runtime != null ? raw.runtime : null,
      originalLanguage: raw.original_language || '',
      tmdbRating: raw.vote_average != null ? +(+raw.vote_average).toFixed(1) : null,
      voteCount: raw.vote_count != null ? raw.vote_count : null,
      popularity: raw.popularity != null ? +raw.popularity : null,
    };
  }

  // ── Endpoints ─────────────────────────────────────────────────────────
  async function testConnection() {
    try {
      // /configuration is the cheapest authenticated endpoint.
      await _req('/configuration', {});
      // Warm the genre cache while we're here (best-effort).
      genres().catch(() => {});
      return { ok: true, message: 'Connected to TMDb ✓' };
    } catch (e) {
      return { ok: false, code: e.code || 'error', message: e.message || 'Connection failed.' };
    }
  }

  async function search(query, page) {
    const data = await _req('/search/movie', {
      query: query || '', page: page || 1, include_adult: 'false', language: 'en-US',
    });
    const gm = await genres().catch(() => ({}));
    return {
      page: data.page, totalPages: data.total_pages, totalResults: data.total_results,
      results: (data.results || []).map(r => normalize(r, gm)),
    };
  }

  async function details(tmdbId) {
    const data = await _req('/movie/' + tmdbId, {
      language: 'en-US', append_to_response: 'external_ids,credits',
    });
    const gm = await genres().catch(() => ({}));
    const base = normalize(data, gm);
    // Enrich with director + top cast (handy for details view).
    const credits = data.credits || {};
    const director = (credits.crew || []).find(c => c.job === 'Director');
    base.director = director ? director.name : '';
    base.cast = (credits.cast || []).slice(0, 8).map(c => c.name).filter(Boolean);
    base.tagline = data.tagline || '';
    base.status = data.status || '';
    return base;
  }

  async function externalIds(tmdbId) {
    const data = await _req('/movie/' + tmdbId + '/external_ids', {});
    return { imdbId: data.imdb_id || '' };
  }

  async function similar(tmdbId, page) {
    const data = await _req('/movie/' + tmdbId + '/similar', { page: page || 1, language: 'en-US' });
    const gm = await genres().catch(() => ({}));
    return (data.results || []).map(r => normalize(r, gm));
  }

  async function recommendations(tmdbId, page) {
    const data = await _req('/movie/' + tmdbId + '/recommendations', { page: page || 1, language: 'en-US' });
    const gm = await genres().catch(() => ({}));
    return (data.results || []).map(r => normalize(r, gm));
  }

  async function trending(timeWindow, page) {
    const data = await _req('/trending/movie/' + (timeWindow || 'week'), { page: page || 1, language: 'en-US' });
    const gm = await genres().catch(() => ({}));
    return (data.results || []).map(r => normalize(r, gm));
  }

  async function popular(page) {
    const data = await _req('/movie/popular', { page: page || 1, language: 'en-US' });
    const gm = await genres().catch(() => ({}));
    return (data.results || []).map(r => normalize(r, gm));
  }

  async function topRated(page) {
    const data = await _req('/movie/top_rated', { page: page || 1, language: 'en-US' });
    const gm = await genres().catch(() => ({}));
    return (data.results || []).map(r => normalize(r, gm));
  }

  // Resolve an IMDb id → TMDb movie metadata (used to seed recs from CSV-imported movies).
  async function findByImdb(imdbId) {
    if (!imdbId) return null;
    const data = await _req('/find/' + imdbId, { external_source: 'imdb_id' });
    const m = (data.movie_results || [])[0];
    if (!m) return null;
    const gm = await genres().catch(() => ({}));
    return normalize(m, gm);
  }

  // opts: { genreIds:[int], year, voteGte, runtimeLte, lang, sortBy, page }
  async function discover(opts) {
    const o = opts || {};
    const params = {
      include_adult: 'false', include_video: 'false', language: 'en-US',
      page: o.page || 1,
      sort_by: o.sortBy || 'popularity.desc',
    };
    if (o.genreIds && o.genreIds.length) params.with_genres = o.genreIds.join(',');
    if (o.year) params.primary_release_year = o.year;
    if (o.yearGte) params['primary_release_date.gte'] = o.yearGte + '-01-01';
    if (o.yearLte) params['primary_release_date.lte'] = o.yearLte + '-12-31';
    if (o.voteGte) params['vote_average.gte'] = o.voteGte;
    // vote-count floor: explicit minVotes, else a small floor when filtering by rating
    if (o.minVotes != null) params['vote_count.gte'] = o.minVotes;
    else if (o.voteGte) params['vote_count.gte'] = 50;
    if (o.runtimeLte) params['with_runtime.lte'] = o.runtimeLte;
    if (o.runtimeGte) params['with_runtime.gte'] = o.runtimeGte;
    if (o.lang) params.with_original_language = o.lang;
    const data = await _req('/discover/movie', params);
    const gm = await genres().catch(() => ({}));
    return {
      page: data.page, totalPages: data.total_pages, totalResults: data.total_results,
      results: (data.results || []).map(r => normalize(r, gm)),
    };
  }

  window.TMDB = {
    hasCredentials, testConnection, proxyAvailable, proxyHealth,
    search, details, externalIds, similar, recommendations, trending, popular, discover,
    topRated, findByImdb,
    genres, genreMapSync, normalize, posterUrl, backdropUrl,
    API_BASE, IMG_BASE,
    ATTRIBUTION: 'This product uses the TMDb API but is not endorsed or certified by TMDb.',
  };
})();
