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

  function CommonSenseMediaClient(fetchImpl, options) {
    this.fetchImpl = fetchImpl;
    this.baseUrl = options && options.baseUrl ? options.baseUrl : "https://www.commonsensemedia.org";
    this.timeoutMs = options && options.timeoutMs ? options.timeoutMs : 5000;
    this.cacheTtlMs = 12 * 60 * 60 * 1000;
    this.cache = new Map();
  }

  CommonSenseMediaClient.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;
  };

  CommonSenseMediaClient.prototype.requestText = async function requestText(pathname) {
    var controller = new AbortController();
    var timeoutId = setTimeout(function onTimeout() {
      controller.abort();
    }, this.timeoutMs);

    try {
      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
      });

      if (response.status === 404) {
        return null;
      }

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

      return response.text();
    } finally {
      clearTimeout(timeoutId);
    }
  };

  function unique(values) {
    var seen = new Set();
    return values.filter(function filterValue(value) {
      if (!value || seen.has(value)) {
        return false;
      }

      seen.add(value);
      return true;
    });
  }

  function slugifyTitle(value) {
    return String(value || "")
      .toLowerCase()
      .replace(/&/g, " and ")
      .replace(/['’]/g, "")
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/^-+|-+$/g, "")
      .replace(/-{2,}/g, "-");
  }

  function buildSlugCandidates(showTitle, aliases) {
    var rawCandidates = [showTitle].concat(Array.isArray(aliases) ? aliases : []).filter(Boolean);
    var slugCandidates = [];

    rawCandidates.forEach(function addCandidate(title) {
      var trimmed = String(title || "").trim();
      if (!trimmed) {
        return;
      }

      var colonTrimmed = trimmed.split(/\s*:\s*/)[0];
      var dashTrimmed = colonTrimmed.split(/\s+-\s+/)[0];
      var deParenTrimmed = dashTrimmed.replace(/\s*\([^)]*\)\s*/g, " ").replace(/\s+/g, " ").trim();
      var titleWithoutArticle = deParenTrimmed.replace(/^(the|a|an)\s+/i, "").trim();
      var possessiveTailMatch = trimmed.match(/^[^:]+['’]s\s+(.+)$/i);

      slugCandidates.push(slugifyTitle(trimmed));
      slugCandidates.push(slugifyTitle(deParenTrimmed));
      slugCandidates.push(slugifyTitle(titleWithoutArticle));

      if (possessiveTailMatch && possessiveTailMatch[1]) {
        slugCandidates.push(slugifyTitle(possessiveTailMatch[1]));
      }
    });

    return unique(slugCandidates.filter(Boolean));
  }

  function decodeHtmlEntities(value) {
    return String(value || "")
      .replace(/&lt;/g, "<")
      .replace(/&gt;/g, ">")
      .replace(/&quot;/g, "\"")
      .replace(/&#39;/g, "'")
      .replace(/&amp;/g, "&")
      .replace(/&nbsp;/g, " ");
  }

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

  function getSeverityLabel(score) {
    var numeric = Number(score) || 0;
    if (numeric >= 5) {
      return "severe";
    }
    if (numeric >= 4) {
      return "strong";
    }
    if (numeric >= 3) {
      return "moderate";
    }
    if (numeric >= 2) {
      return "mild";
    }
    if (numeric >= 1) {
      return "light";
    }
    return "none";
  }

  function parseReviewDetails(html, pathname) {
    var normalizedHtml = String(html || "");
    var ratingMatch = normalizedHtml.match(/"csm_review_rating_details_sex":(\d+)/i);
    var ratingScore = ratingMatch ? Number.parseInt(ratingMatch[1], 10) : 0;

    if (!Number.isFinite(ratingScore) || ratingScore <= 0) {
      return null;
    }

    var summaryMatch = normalizedHtml.match(/content-grid-content"[^>]*data-text="([^"]*)"[\s\S]{0,1200}id="content-grid-item-sex"/i);
    var ageMatch = normalizedHtml.match(/"csm_review_rating_age":(\d+)/i);
    var summary = summaryMatch ? stripHtml(summaryMatch[1]) : "";
    var recommendedAge = ageMatch ? Number.parseInt(ageMatch[1], 10) : null;
    var severityLabel = getSeverityLabel(ratingScore);

    if (!summary) {
      summary = "Common Sense Media rates this show " + severityLabel + " for Sex, Romance & Nudity.";
    } else {
      summary = "Common Sense Media rates this show " + severityLabel + " for Sex, Romance & Nudity. " + summary;
    }

    if (recommendedAge) {
      summary += " Recommended age: " + recommendedAge + "+.";
    }

    return {
      sourceName: "Common Sense Media",
      sourceUrl: "https://www.commonsensemedia.org" + pathname,
      ratingScore: ratingScore,
      summary: summary
    };
  }

  CommonSenseMediaClient.prototype.findTvReview = async function findTvReview(showTitle, aliases) {
    var slugCandidates = buildSlugCandidates(showTitle, aliases);
    var self = this;

    for (var index = 0; index < slugCandidates.length; index += 1) {
      var slug = slugCandidates[index];
      var pathname = "/tv-reviews/" + slug;
      var cacheKey = "tv-review|" + slug;
      var review = await this.getCached(cacheKey, async function loadReview() {
        var html = await self.requestText(pathname);
        if (!html) {
          return null;
        }

        return parseReviewDetails(html, pathname);
      });

      if (review) {
        return review;
      }
    }

    return null;
  };

  var api = {
    CommonSenseMediaClient: CommonSenseMediaClient,
    buildSlugCandidates: buildSlugCandidates,
    parseReviewDetails: parseReviewDetails
  };

  root.SafeSceneCommonSenseMedia = api;

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