(function attachTMDBWebsiteClient(root) {
  "use strict";

  var Common = root.SafeSceneCommon || require("./common.js");

  function TMDBWebsiteClient(fetchImpl, baseUrl) {
    this.fetchImpl = fetchImpl;
    this.baseUrl = baseUrl || "https://www.themoviedb.org";
    this.timeoutMs = 5000;
    this.cacheTtlMs = 12 * 60 * 60 * 1000;
    this.cache = new Map();
  }

  TMDBWebsiteClient.prototype.getCached = async function getCached(key, loader) {
    var existing = this.cache.get(key);
    var now = Date.now();

    if (existing && existing.expiresAt > now) {
      return existing.value;
    }

    var value = await loader();
    this.cache.set(key, {
      value: value,
      expiresAt: now + this.cacheTtlMs
    });
    return value;
  };

  function decodeHtmlEntities(value) {
    return String(value || "")
      .replace(/&amp;/g, "&")
      .replace(/&quot;/g, "\"")
      .replace(/&#39;/g, "'")
      .replace(/&apos;/g, "'")
      .replace(/&lt;/g, "<")
      .replace(/&gt;/g, ">")
      .replace(/&#(\d+);/g, function decodeNumeric(match, codePoint) {
        var numericCode = Number.parseInt(codePoint, 10);
        return Number.isFinite(numericCode) ? String.fromCharCode(numericCode) : match;
      });
  }

  function stripTags(value) {
    return decodeHtmlEntities(String(value || "").replace(/<[^>]+>/g, " "))
      .replace(/\s+/g, " ")
      .trim();
  }

  function extractYear(rawValue) {
    var match = String(rawValue || "").match(/\b(19|20)\d{2}\b/);
    return match ? Number.parseInt(match[0], 10) : null;
  }

  function parseSearchResults(html, mediaType) {
    var results = [];
    var expression = new RegExp(
      "data-media-type=\"" + mediaType + "\"[\\s\\S]*?href=\"\\/" + mediaType + "\\/([0-9]+)-[^\"]+\"[\\s\\S]*?<h2[^>]*>([\\s\\S]*?)<\\/h2>[\\s\\S]*?<span class=\"release_date[^\"]*\"[^>]*>([\\s\\S]*?)<\\/span>",
      "g"
    );
    var seen = new Set();
    var match = expression.exec(html);

    while (match) {
      var id = Number.parseInt(match[1], 10);
      var headingMarkup = match[2];
      var releaseMarkup = match[3];
      var spanMatches = headingMarkup.match(/<span[^>]*>([\s\S]*?)<\/span>/g) || [];
      var primaryTitle = spanMatches[0] ? stripTags(spanMatches[0]) : stripTags(headingMarkup);
      var alternateTitle = spanMatches[1] ? stripTags(spanMatches[1]).replace(/^\(|\)$/g, "").trim() : null;

      if (!seen.has(id) && primaryTitle) {
        seen.add(id);
        results.push({
          id: id,
          mediaType: mediaType,
          title: primaryTitle,
          alternateTitle: alternateTitle,
          year: extractYear(releaseMarkup)
        });
      }

      match = expression.exec(html);
    }

    return results;
  }

  TMDBWebsiteClient.prototype.requestText = async function requestText(pathname) {
    var controller = new AbortController();
    var timeoutId = setTimeout(function onTimeout() {
      controller.abort();
    }, this.timeoutMs);
    var response = await this.fetchImpl(this.baseUrl + pathname, {
      headers: {
        "accept": "text/html,application/xhtml+xml",
        "accept-language": "en-US,en;q=0.9"
      },
      signal: controller.signal
    });
    clearTimeout(timeoutId);

    if (!response.ok) {
      throw new Error("TMDB website request failed: " + response.status + " " + pathname);
    }

    return response.text();
  };

  TMDBWebsiteClient.prototype.searchTvShows = async function searchTvShows(query) {
    var pathname = "/search/tv?language=en-US&query=" + encodeURIComponent(query);
    var cacheKey = "tv|" + String(query || "").toLowerCase();
    var self = this;
    return this.getCached(cacheKey, async function loadTvSearch() {
      return parseSearchResults(await self.requestText(pathname), "tv");
    });
  };

  TMDBWebsiteClient.prototype.searchMovies = async function searchMovies(query) {
    var pathname = "/search/movie?language=en-US&query=" + encodeURIComponent(query);
    var cacheKey = "movie|" + String(query || "").toLowerCase();
    var self = this;
    return this.getCached(cacheKey, async function loadMovieSearch() {
      return parseSearchResults(await self.requestText(pathname), "movie");
    });
  };

  TMDBWebsiteClient.prototype.findBestTvShow = async function findBestTvShow(query, year) {
    var candidates = await this.searchTvShows(query);

    if (!candidates.length) {
      return null;
    }

    candidates = candidates
      .map(function scoreCandidate(candidate) {
        var similarity = Math.max(
          Common.scoreTextSimilarity(candidate.title, query),
          candidate.alternateTitle ? Common.scoreTextSimilarity(candidate.alternateTitle, query) : 0
        );
        var yearPenalty = 0;

        if (year && candidate.year) {
          yearPenalty = Math.min(15, Math.abs(candidate.year - year) * 4);
        }

        return {
          candidate: candidate,
          score: similarity - yearPenalty
        };
      })
      .sort(function sortByScore(left, right) {
        return right.score - left.score;
      });

    return candidates[0].candidate;
  };

  var api = {
    TMDBWebsiteClient: TMDBWebsiteClient,
    parseSearchResults: parseSearchResults
  };

  root.SafeSceneTMDBWebsite = api;

  if (typeof module !== "undefined" && module.exports) {
    module.exports = api;
  }
}(typeof globalThis !== "undefined" ? globalThis : this));
