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

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

  function getReleaseYear(show, episode) {
    var episodeDate = episode && episode.airdate ? String(episode.airdate) : "";
    var showDate = show && show.premiered ? String(show.premiered) : "";
    var match = episodeDate.match(/\b(19|20)\d{2}\b/) || showDate.match(/\b(19|20)\d{2}\b/);
    return match ? Number.parseInt(match[0], 10) : null;
  }

  function getCategorySelection(skipCategories) {
    var normalized = Array.isArray(skipCategories) ? skipCategories.map(Common.normalizeText) : [];
    return {
      skipNudity: normalized.includes("nudity"),
      skipSex: normalized.includes("sex"),
      skipGore: normalized.includes("gore")
    };
  }

  function getMatchingCountForCategories(counts, categorySelection) {
    if (!counts) {
      return 0;
    }

    var total = 0;
    if (categorySelection.skipNudity) {
      total += Number(counts.nudity || 0);
    }
    if (categorySelection.skipSex) {
      total += Number(counts.sex || 0);
    }
    if (categorySelection.skipGore) {
      total += Number(counts.gore || 0);
    }

    return total;
  }

  function buildCountWarning(sourceName, sourceUrl, matchingCount, counts, requiresConnection) {
    if (!matchingCount) {
      return [];
    }

    return [{
      sourceName: sourceName,
      sourceUrl: sourceUrl,
      sceneCount: matchingCount,
      summary: requiresConnection
        ? matchingCount + " matching scene(s) are recorded, but exact auto-skip ranges require a connected Skipit account."
        : matchingCount + " matching scene(s) are recorded for this episode.",
      counts: counts,
      requiresConnection: Boolean(requiresConnection)
    }];
  }

  function normalizeProviderName(rawValue) {
    return Common.normalizeText(rawValue).replace(/\s+/g, "-");
  }

  async function withTimeout(promise, timeoutMs) {
    return new Promise(function resolveWithTimeout(resolve, reject) {
      var settled = false;
      var timeoutId = setTimeout(function onTimeout() {
        if (settled) {
          return;
        }

        settled = true;
        reject(new Error("Timed out after " + timeoutMs + "ms"));
      }, timeoutMs);

      promise.then(function onResolve(value) {
        if (settled) {
          return;
        }

        settled = true;
        clearTimeout(timeoutId);
        resolve(value);
      }).catch(function onReject(error) {
        if (settled) {
          return;
        }

        settled = true;
        clearTimeout(timeoutId);
        reject(error);
      });
    });
  }

  function scoreShowResult(result, candidate, pageContext) {
    var show = result.show || {};
    var score = Number(result.score || 0) * 10;
    var candidateName = candidate.showTitle;
    var providerHint = Common.getProviderHint(pageContext.hostname);
    var aliases = [];

    if (Array.isArray(show.akas)) {
      aliases = aliases.concat(show.akas.map(function mapAka(aka) { return aka.name; }));
    }

    if (show.externals && show.externals.imdb && pageContext.url && pageContext.url.includes(show.externals.imdb)) {
      score += 50;
    }

    score += Common.scoreTextSimilarity(show.name, candidateName);

    aliases.forEach(function scoreAlias(alias) {
      score = Math.max(score, Common.scoreTextSimilarity(alias, candidateName) + 10);
    });

    if (providerHint) {
      var networkName = show.network && show.network.name ? normalizeProviderName(show.network.name) : "";
      var webChannelName = show.webChannel && show.webChannel.name ? normalizeProviderName(show.webChannel.name) : "";
      if (networkName.includes(providerHint) || webChannelName.includes(providerHint)) {
        score += 12;
      }
    }

    return score;
  }

  async function resolveEpisode(options) {
    var pageContext = options.pageContext;
    var tvMazeClient = options.tvMazeClient;
    var sceneDatabase = options.sceneDatabase;
    var tmdbWebsiteClient = options.tmdbWebsiteClient;
    var commonSenseMediaClient = options.commonSenseMediaClient;
    var doesTheDogDieClient = options.doesTheDogDieClient;
    var videoSkipClient = options.videoSkipClient;
    var filmiwayClient = options.filmiwayClient;
    var cleanStreamClient = options.cleanStreamClient;
    var skipitClient = options.skipitClient;
    var settings = Object.assign({}, Common.DEFAULT_SETTINGS, options.settings || {});
    var skipitAuthToken = options.skipitAuthToken || null;
    var candidates = Common.parseCandidatesFromContext(pageContext);
    var categorySelection = getCategorySelection(settings.skipCategories);

    if (!candidates.length) {
      return {
        status: "unmatched",
        reason: "No season/episode pattern was found in the page title or metadata.",
        parseCandidates: [],
        sceneMatch: {
          status: "none",
          exactRanges: [],
          warningOnly: [],
          matchingSceneCount: 0,
          providerConnectionState: {
            skipitConnected: Boolean(skipitAuthToken)
          }
        }
      };
    }

    var searchCache = new Map();

    for (var candidateIndex = 0; candidateIndex < candidates.length; candidateIndex += 1) {
      var candidate = candidates[candidateIndex];

      if (!searchCache.has(candidate.showTitle)) {
        searchCache.set(candidate.showTitle, await tvMazeClient.searchShows(candidate.showTitle));
      }

      var ranked = searchCache.get(candidate.showTitle)
        .map(function mapResult(result) {
          return {
            result: result,
            score: scoreShowResult(result, candidate, pageContext)
          };
        })
        .sort(function sortByScore(left, right) {
          return right.score - left.score;
        });

      if (!ranked.length || ranked[0].score < 40) {
        continue;
      }

      try {
        var selected = ranked[0].result.show;
        var episode = await tvMazeClient.getEpisodeByNumber(selected.id, candidate.seasonNumber, candidate.episodeNumber);
        var localSceneMatch = sceneDatabase.lookup({
          showTitle: selected.name,
          seasonNumber: candidate.seasonNumber,
          episodeNumber: candidate.episodeNumber
        });
        var detected = {
          showTitle: selected.name,
          tvmazeId: selected.id,
          imdbId: selected.externals ? selected.externals.imdb || null : null,
          seasonNumber: candidate.seasonNumber,
          episodeNumber: candidate.episodeNumber,
          episodeTitle: episode && episode.name ? episode.name : candidate.episodeTitle || null,
          providerHint: Common.getProviderHint(pageContext.hostname),
          parseSource: candidate.parseSource,
          releaseYear: getReleaseYear(selected, episode),
          tmdbId: null,
          skipitUrl: null
        };
        var sceneMatch = {
          status: "none",
          exactRanges: [],
          warningOnly: [],
          matchingSceneCount: 0,
          providerConnectionState: {
            skipitConnected: Boolean(skipitAuthToken)
          },
          sources: []
        };

        if (tmdbWebsiteClient && skipitClient) {
          var tmdbMatch = await tmdbWebsiteClient.findBestTvShow(selected.name, detected.releaseYear);

          if (tmdbMatch) {
            detected.tmdbId = tmdbMatch.id;
            detected.skipitUrl = "https://getskipit.com/tvshow/" +
              tmdbMatch.id +
              "/" +
              candidate.seasonNumber +
              "/" +
              candidate.episodeNumber;

            try {
              var publicSkipitData = await skipitClient.getPublicEpisodeData(
                tmdbMatch.id,
                candidate.seasonNumber,
                candidate.episodeNumber
              );
              var skipitCount = getMatchingCountForCategories(publicSkipitData.counts, categorySelection);
              var publicWarnings = buildCountWarning(
                publicSkipitData.sourceName,
                publicSkipitData.sourceUrl,
                skipitCount,
                publicSkipitData.counts,
                !skipitAuthToken
              );

              sceneMatch.warningOnly = sceneMatch.warningOnly.concat(publicWarnings);
              sceneMatch.matchingSceneCount = Math.max(sceneMatch.matchingSceneCount, skipitCount);
              sceneMatch.sources.push({
                name: publicSkipitData.sourceName,
                kind: skipitAuthToken ? "skipit-public+auth" : "skipit-public-count",
                sceneCount: skipitCount,
                counts: publicSkipitData.counts,
                sourceUrl: publicSkipitData.sourceUrl
              });

              if (skipitAuthToken && skipitCount > 0) {
                var exactSkipitData = await skipitClient.getAuthenticatedEpisodeRanges({
                  tmdbId: tmdbMatch.id,
                  seasonNumber: candidate.seasonNumber,
                  episodeNumber: candidate.episodeNumber,
                  authToken: skipitAuthToken,
                  skipNudity: categorySelection.skipNudity,
                  skipSex: categorySelection.skipSex,
                  skipGore: categorySelection.skipGore
                });

                if (exactSkipitData.authRequired) {
                  sceneMatch.providerConnectionState.skipitConnected = false;
                } else if (exactSkipitData.exactRanges.length > 0) {
                  sceneMatch.status = "exact";
                  sceneMatch.exactRanges = exactSkipitData.exactRanges;
                  sceneMatch.matchingSceneCount = exactSkipitData.exactRanges.length;
                }
              }
            } catch (providerError) {
              // Keep resolving with fallback sources.
            }
          }
        }

        if (sceneMatch.status !== "exact" && videoSkipClient && (categorySelection.skipNudity || categorySelection.skipSex || categorySelection.skipGore)) {
          try {
            var videoSkipEpisode = await withTimeout(videoSkipClient.lookupEpisode({
              showTitle: selected.name,
              aliases: [candidate.showTitle],
              seasonNumber: candidate.seasonNumber,
              episodeNumber: candidate.episodeNumber,
              categorySelection: categorySelection
            }), 8000);

            if (videoSkipEpisode && videoSkipEpisode.exactRanges.length > 0) {
              sceneMatch.status = "exact";
              sceneMatch.exactRanges = videoSkipEpisode.exactRanges;
              sceneMatch.matchingSceneCount = videoSkipEpisode.exactRanges.length;
              sceneMatch.warningOnly = [];
              sceneMatch.sources.push({
                name: videoSkipEpisode.sourceName,
                kind: "videoskip-exact",
                sceneCount: videoSkipEpisode.exactRanges.length,
                counts: null,
                sourceUrl: videoSkipEpisode.sourceUrl
              });
            }
          } catch (videoSkipError) {
            // Keep resolving with remaining fallbacks.
          }
        }

        if (sceneMatch.status !== "exact" && filmiwayClient && (categorySelection.skipNudity || categorySelection.skipSex || categorySelection.skipGore)) {
          try {
            var filmiwayMatch = await withTimeout(filmiwayClient.lookupTitle({
              title: selected.name,
              aliases: [candidate.showTitle],
              imdbId: detected.imdbId,
              releaseYear: detected.releaseYear,
              categorySelection: categorySelection
            }), 8000);

            if (filmiwayMatch && filmiwayMatch.exactRanges.length > 0) {
              sceneMatch.status = "exact";
              sceneMatch.exactRanges = filmiwayMatch.exactRanges;
              sceneMatch.matchingSceneCount = filmiwayMatch.exactRanges.length;
              sceneMatch.warningOnly = [];
              sceneMatch.sources.push({
                name: filmiwayMatch.sourceName,
                kind: "filmiway-exact",
                sceneCount: filmiwayMatch.exactRanges.length,
                counts: null,
                sourceUrl: filmiwayMatch.sourceUrl
              });
            }
          } catch (filmiwayError) {
            // Keep resolving with remaining fallbacks.
          }
        }

        if (sceneMatch.status !== "exact" && cleanStreamClient && detected.imdbId && (categorySelection.skipNudity || categorySelection.skipSex || categorySelection.skipGore)) {
          try {
            var cleanStreamMatch = await withTimeout(cleanStreamClient.lookupTitle({
              imdbId: detected.imdbId,
              categorySelection: categorySelection
            }), 8000);

            if (cleanStreamMatch && cleanStreamMatch.exactRanges.length > 0) {
              sceneMatch.status = "exact";
              sceneMatch.exactRanges = cleanStreamMatch.exactRanges;
              sceneMatch.matchingSceneCount = cleanStreamMatch.exactRanges.length;
              sceneMatch.warningOnly = [];
              sceneMatch.sources.push({
                name: cleanStreamMatch.sourceName,
                kind: "cleanstream-exact",
                sceneCount: cleanStreamMatch.exactRanges.length,
                counts: null,
                sourceUrl: cleanStreamMatch.sourceUrl
              });
            }
          } catch (cleanStreamError) {
            // Keep resolving with remaining fallbacks.
          }
        }

        if (sceneMatch.status !== "exact" && doesTheDogDieClient && (categorySelection.skipNudity || categorySelection.skipSex)) {
          try {
            var dddEpisode = await withTimeout(doesTheDogDieClient.lookupEpisode({
              showTitle: selected.name,
              aliases: [candidate.showTitle],
              releaseYear: detected.releaseYear,
              seasonNumber: candidate.seasonNumber,
              episodeNumber: candidate.episodeNumber
            }), 8000);

            if (dddEpisode) {
              if (dddEpisode.exactRanges.length > 0) {
                sceneMatch.status = "exact";
                sceneMatch.exactRanges = dddEpisode.exactRanges;
                sceneMatch.matchingSceneCount = dddEpisode.exactRanges.length;
                sceneMatch.warningOnly = [];
              } else if (dddEpisode.warningOnly.length > 0) {
                sceneMatch.warningOnly = sceneMatch.warningOnly.concat(dddEpisode.warningOnly);
                sceneMatch.matchingSceneCount = Math.max(
                  sceneMatch.matchingSceneCount,
                  Number(dddEpisode.matchingSceneCount || dddEpisode.warningOnly.length || 0)
                );
              }

              sceneMatch.sources.push({
                name: dddEpisode.sourceName,
                kind: dddEpisode.exactRanges.length > 0 ? "ddd-exact" : "ddd-warning",
                sceneCount: dddEpisode.exactRanges.length > 0
                  ? dddEpisode.exactRanges.length
                  : Number(dddEpisode.matchingSceneCount || dddEpisode.warningOnly.length || 0),
                counts: null,
                sourceUrl: dddEpisode.sourceUrl
              });
            }
          } catch (dddError) {
            // Keep resolving with remaining fallbacks.
          }
        }

        if (sceneMatch.status !== "exact" && sceneMatch.matchingSceneCount === 0 && commonSenseMediaClient) {
          try {
            var commonSenseReview = await commonSenseMediaClient.findTvReview(
              selected.name,
              [candidate.showTitle]
            );

            if (commonSenseReview && commonSenseReview.ratingScore > 0) {
              sceneMatch.warningOnly.push({
                sourceName: commonSenseReview.sourceName,
                sourceUrl: commonSenseReview.sourceUrl,
                sceneCount: null,
                summary: commonSenseReview.summary,
                counts: null,
                requiresConnection: false
              });
              sceneMatch.matchingSceneCount = 1;
              sceneMatch.sources.push({
                name: commonSenseReview.sourceName,
                kind: "commonsense-review",
                sceneCount: null,
                counts: null,
                sourceUrl: commonSenseReview.sourceUrl
              });
            }
          } catch (fallbackError) {
            // Keep resolving with remaining fallbacks.
          }
        }

        if (sceneMatch.status !== "exact" && localSceneMatch.status === "exact") {
          sceneMatch.status = "exact";
          sceneMatch.exactRanges = localSceneMatch.exactRanges;
          sceneMatch.matchingSceneCount = localSceneMatch.exactRanges.length;
          sceneMatch.warningOnly = [];
          sceneMatch.sources.push({
            name: "Local fallback",
            kind: "local-exact",
            sceneCount: localSceneMatch.exactRanges.length,
            counts: null,
            sourceUrl: null
          });
        } else if (sceneMatch.warningOnly.length === 0 && localSceneMatch.status === "warning_only") {
          sceneMatch.warningOnly = localSceneMatch.warningOnly;
          sceneMatch.status = "warning_only";
          sceneMatch.matchingSceneCount = Math.max(sceneMatch.matchingSceneCount, 1);
          sceneMatch.sources.push({
            name: "Local fallback",
            kind: "local-warning",
            sceneCount: null,
            counts: null,
            sourceUrl: null
          });
        } else if (sceneMatch.status !== "exact" && sceneMatch.warningOnly.length > 0) {
          sceneMatch.status = "warning_only";
        }

        if (sceneMatch.status !== "exact" && sceneMatch.status !== "warning_only") {
          sceneMatch.status = "none";
        }

        return {
          status: sceneMatch.status === "exact"
            ? "ready"
            : sceneMatch.status === "warning_only"
              ? "warning_only"
              : "clear",
          reason: sceneMatch.status === "exact"
            ? "Exact timestamp data found for this episode."
            : sceneMatch.matchingSceneCount > 0
              ? "Scene counts were found, but exact auto-skip ranges are not currently available."
              : "The episode was detected, but no explicit scene data was found.",
          parseCandidates: candidates,
          detected: detected,
          sceneMatch: sceneMatch
        };
      } catch (error) {
        continue;
      }
    }

    return {
      status: "unmatched",
      reason: "A show candidate was found, but the season/episode could not be resolved against TVMaze.",
      parseCandidates: candidates,
      sceneMatch: {
        status: "none",
        exactRanges: [],
        warningOnly: []
      }
    };
  }

  var api = {
    resolveEpisode: resolveEpisode
  };

  root.SafeSceneResolver = api;

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