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

  function SkipitClient(fetchImpl, options) {
    this.fetchImpl = fetchImpl;
    this.baseUrl = options && options.baseUrl ? options.baseUrl : "https://getskipit.com";
    this.apiBaseUrl = options && options.apiBaseUrl ? options.apiBaseUrl : "https://getskipit.com/api";
    this.timeoutMs = options && options.timeoutMs ? options.timeoutMs : 5000;
    this.publicCacheTtlMs = 6 * 60 * 60 * 1000;
    this.exactCacheTtlMs = 10 * 60 * 1000;
    this.publicCache = new Map();
    this.exactCache = new Map();
  }

  SkipitClient.prototype.getCached = async function getCached(cacheStore, ttlMs, key, loader) {
    var existing = cacheStore.get(key);
    var now = Date.now();

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

    var value = await loader();
    cacheStore.set(key, {
      value: value,
      expiresAt: now + ttlMs
    });
    return value;
  };

  function extractJsonObjectAfterMarker(text, marker) {
    var normalizedText = String(text || "");
    var markerIndex = normalizedText.indexOf(marker);

    if (markerIndex === -1) {
      markerIndex = normalizedText.indexOf(marker.replace(/"/g, "\\\""));
    }

    if (markerIndex === -1) {
      return null;
    }

    var braceIndex = normalizedText.indexOf("{", markerIndex + marker.length);

    if (braceIndex === -1) {
      return null;
    }

    var depth = 0;
    var inString = false;
    var isEscaped = false;

    for (var cursor = braceIndex; cursor < normalizedText.length; cursor += 1) {
      var character = normalizedText.charAt(cursor);

      if (isEscaped) {
        isEscaped = false;
        continue;
      }

      if (character === "\\") {
        isEscaped = true;
        continue;
      }

      if (character === "\"") {
        inString = !inString;
        continue;
      }

      if (inString) {
        continue;
      }

      if (character === "{") {
        depth += 1;
      } else if (character === "}") {
        depth -= 1;
        if (depth === 0) {
          return normalizedText.slice(braceIndex, cursor + 1);
        }
      }
    }

    return null;
  }

  function normalizeCountObject(rawCounts) {
    var counts = rawCounts || {};
    return {
      nudity: Number(counts.Nudity || 0),
      sex: Number(counts.Sex || 0),
      gore: Number(counts.Gore || 0),
      total: Number(counts.total || 0)
    };
  }

  function parseEmbeddedJsonObject(rawValue) {
    if (!rawValue) {
      return null;
    }

    return JSON.parse(String(rawValue).replace(/\\"/g, "\""));
  }

  function normalizeCategory(category) {
    return String(category || "").toLowerCase();
  }

  function normalizeTimestampEntry(timestamp, tmdbId, seasonNumber, episodeNumber, index) {
    var category = normalizeCategory(timestamp.type);
    var startSeconds = Number(timestamp.start_time);
    var endSeconds = Number(timestamp.end_time);

    if (!Number.isFinite(startSeconds) || !Number.isFinite(endSeconds) || endSeconds <= startSeconds) {
      return null;
    }

    return {
      id: [
        "skipit",
        tmdbId,
        seasonNumber,
        episodeNumber,
        category || "unknown",
        index
      ].join("-"),
      category: category || "unknown",
      start: startSeconds,
      end: endSeconds,
      startSeconds: startSeconds,
      endSeconds: endSeconds,
      confidence: "provider-exact",
      notes: null,
      sourceName: "Skipit",
      sourceUrl: "https://getskipit.com/tvshow/" + tmdbId + "/" + seasonNumber + "/" + episodeNumber
    };
  }

  SkipitClient.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("Skipit page request failed: " + response.status + " " + pathname);
    }

    return response.text();
  };

  SkipitClient.prototype.getPublicEpisodeData = async function getPublicEpisodeData(tmdbId, seasonNumber, episodeNumber) {
    var pathname = "/tvshow/" + tmdbId + "/" + seasonNumber + "/" + episodeNumber;
    var cacheKey = ["public", tmdbId, seasonNumber, episodeNumber].join("|");
    var self = this;
    return this.getCached(this.publicCache, this.publicCacheTtlMs, cacheKey, async function loadPublicEpisode() {
      var html = await self.requestText(pathname);
      var countsJson = extractJsonObjectAfterMarker(html, "\"initialSkipCounts\":");
      var counts = normalizeCountObject(parseEmbeddedJsonObject(countsJson));

      return {
        sourceName: "Skipit",
        sourceUrl: self.baseUrl + pathname,
        counts: counts
      };
    });
  };

  SkipitClient.prototype.getAuthenticatedEpisodeRanges = async function getAuthenticatedEpisodeRanges(options) {
    var query = new URLSearchParams({
      source: "safescene",
      skip_nudity: String(Boolean(options.skipNudity)),
      skip_sex: String(Boolean(options.skipSex)),
      skip_gore: String(Boolean(options.skipGore)),
      seasonNumber: String(options.seasonNumber),
      episodeNumber: String(options.episodeNumber)
    });
    var cacheKey = [
      "exact",
      options.tmdbId,
      options.seasonNumber,
      options.episodeNumber,
      Boolean(options.skipNudity),
      Boolean(options.skipSex),
      Boolean(options.skipGore),
      options.authToken ? String(options.authToken).slice(-12) : "guest"
    ].join("|");
    var self = this;
    return this.getCached(this.exactCache, this.exactCacheTtlMs, cacheKey, async function loadExactRanges() {
      var controller = new AbortController();
      var timeoutId = setTimeout(function onTimeout() {
        controller.abort();
      }, self.timeoutMs);
      var response = await self.fetchImpl(
        self.apiBaseUrl + "/timestamps/episode/" + options.tmdbId + "?" + query.toString(),
        {
          headers: {
            "accept": "application/json",
            "authorization": "Bearer " + options.authToken
          },
          signal: controller.signal
        }
      );
      clearTimeout(timeoutId);

      if (response.status === 401) {
        return {
          authRequired: true,
          exactRanges: []
        };
      }

      if (!response.ok) {
        throw new Error("Skipit API request failed: " + response.status + " /timestamps/episode/" + options.tmdbId);
      }

      var payload = await response.json();
      var timestamps = Array.isArray(payload.timestamps) ? payload.timestamps : [];

      return {
        authRequired: false,
        exactRanges: timestamps.map(function mapTimestamp(timestamp, index) {
          return normalizeTimestampEntry(
            timestamp,
            options.tmdbId,
            options.seasonNumber,
            options.episodeNumber,
            index + 1
          );
        }).filter(Boolean)
      };
    });
  };

  var api = {
    SkipitClient: SkipitClient,
    extractJsonObjectAfterMarker: extractJsonObjectAfterMarker,
    parseEmbeddedJsonObject: parseEmbeddedJsonObject,
    normalizeCountObject: normalizeCountObject
  };

  root.SafeSceneSkipit = api;

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