import { getConfig } from "../config.js";
import { createLogger } from "../logger.js";
import { contentAppearsEmpty } from "./content-check.js";
import { getAuthOptions } from "./auth.js";
import { fetchWithPlaywright, shouldEscalate } from "./playwright-tier.js";
import { describeFetchError } from "./error-describe.js";
import {
  tlsFetch,
  isAntiBotSignal,
  isAntiBotStatus,
  isRateLimit,
  hasChallengeBody,
  looksJsRequired,
  describeAntiBot,
  TlsTierUnavailableError
} from "./tls-tier.js";
import {
  getDomainRouting,
  recordTlsImpersonationSuccess
} from "../cache/store.js";
const KNOWN_SPA_DOMAINS = /* @__PURE__ */ new Set([
  "react.dev",
  "nextjs.org",
  "vuejs.org",
  "svelte.dev",
  "angular.io",
  "angular.dev",
  "preactjs.com",
  "solidjs.com",
  "remix.run",
  "astro.build",
  "nuxt.com"
]);
function isKnownSpaDomain(host) {
  const lower = host.toLowerCase();
  if (KNOWN_SPA_DOMAINS.has(lower)) return true;
  for (const d of KNOWN_SPA_DOMAINS) {
    if (lower.endsWith(`.${d}`)) return true;
  }
  return false;
}
class SmartRouter {
  domainMap = /* @__PURE__ */ new Map();
  httpClient;
  browserPool;
  httpFetcher;
  playwrightFetcher;
  tlsFetcher;
  tlsPersistence;
  constructor(httpClientOrOptions, browserPool) {
    if (browserPool !== void 0) {
      this.httpClient = httpClientOrOptions;
      this.browserPool = browserPool;
    } else if (httpClientOrOptions && typeof httpClientOrOptions === "object" && ("httpClient" in httpClientOrOptions || "browserPool" in httpClientOrOptions || "httpFetcher" in httpClientOrOptions || "playwrightFetcher" in httpClientOrOptions || "tlsFetcher" in httpClientOrOptions || "tlsPersistence" in httpClientOrOptions)) {
      const opts = httpClientOrOptions;
      if (!opts.httpFetcher && !opts.httpClient) {
        throw new Error("SmartRouter: must provide either httpFetcher or httpClient in options");
      }
      this.httpClient = opts.httpClient;
      this.browserPool = opts.browserPool;
      this.httpFetcher = opts.httpFetcher ?? this.makeDefaultHttpFetcher();
      this.playwrightFetcher = opts.playwrightFetcher ?? fetchWithPlaywright;
      this.tlsFetcher = opts.tlsFetcher ?? tlsFetch;
      this.tlsPersistence = opts.tlsPersistence ?? defaultTlsPersistence();
      return;
    } else {
      this.httpClient = httpClientOrOptions;
    }
    this.httpFetcher = this.makeDefaultHttpFetcher();
    this.playwrightFetcher = fetchWithPlaywright;
    this.tlsFetcher = tlsFetch;
    this.tlsPersistence = defaultTlsPersistence();
  }
  makeDefaultHttpFetcher() {
    return async (url, opts) => {
      if (!this.httpClient) {
        throw new Error("SmartRouter: httpClient not configured");
      }
      const r = await this.httpClient.fetch(url, opts);
      return { url: r.url, html: r.html, text: "" };
    };
  }
  async fetch(url, options = {}) {
    const { renderJs = "auto", useAuth = false, headers, screenshot, actions, mode, conditionalHeaders } = options;
    const config = getConfig();
    const logger = createLogger("fetch");
    const threshold = config.browserFallbackThreshold;
    const domain = new URL(url).hostname;
    if (mode === "stealth") {
      logger.debug("routing to stealth (static then escalate)", { url });
      const staticResult = await this.httpFetcher(url, { headers });
      this.ensureStats(domain);
      if (!shouldEscalate(staticResult.text)) {
        return {
          url: staticResult.url,
          finalUrl: staticResult.url,
          html: staticResult.html,
          contentType: "text/html",
          statusCode: 200,
          method: "http",
          headers: {}
        };
      }
      try {
        const pw = await this.playwrightFetcher(url);
        return {
          url: staticResult.url,
          finalUrl: staticResult.url,
          html: pw.html,
          contentType: "text/html",
          statusCode: 200,
          method: "playwright",
          headers: {},
          escalated: true
        };
      } catch (err) {
        if (err instanceof Error && err.message === "playwright_not_installed") {
          const hint = err.hint ?? "npx playwright install chromium";
          return {
            error: "playwright_not_installed",
            error_reason: "Stealth mode requested but Playwright chromium is not installed",
            stage: "fetch",
            hint
          };
        }
        const described = describeFetchError(err);
        return {
          error: "playwright_fetch_failed",
          error_reason: described.reason,
          stage: "fetch",
          hint: described.hint ?? "Stealth fetch failed; check network or retry"
        };
      }
    }
    if (mode === "cache") {
      if (actions && actions.length > 0) {
        logger.warn("mode=cache ignores browser actions; switch to default/stealth to execute them", {
          url,
          actionCount: actions.length
        });
      }
      logger.debug("routing to http (cache)", { url });
      if (!this.httpClient) throw new Error("SmartRouter: httpClient not configured");
      const result = await this.httpClient.fetch(url, {
        headers,
        timeoutMs: config.fastTimeoutMs,
        conditionalHeaders
      });
      this.ensureStats(domain);
      const raw = this.toRawFetchResult(result);
      raw.jsRequired = result.statusCode === 304 ? false : contentAppearsEmpty(result.html);
      return raw;
    }
    if (actions && actions.length > 0) {
      if (!this.browserPool) throw new Error("SmartRouter: browserPool not configured");
      const authOptions = useAuth ? await getAuthOptions() ?? {} : {};
      logger.debug("routing to playwright", { url, reason: "actions present" });
      return this.browserPool.fetchWithBrowser(url, { headers, screenshot, actions, ...authOptions });
    }
    if (renderJs === "always" || useAuth) {
      if (!this.browserPool) throw new Error("SmartRouter: browserPool not configured");
      const authOptions = useAuth ? await getAuthOptions() ?? {} : {};
      logger.debug("routing to playwright", { url, reason: useAuth ? "auth" : "render_js=always" });
      return this.browserPool.fetchWithBrowser(url, { headers, screenshot, ...authOptions });
    }
    if (renderJs === "never") {
      if (!this.httpClient) throw new Error("SmartRouter: httpClient not configured");
      logger.debug("routing to http (never)", { url });
      const result = await this.httpClient.fetch(url, { headers, conditionalHeaders });
      const neverStats = this.ensureStats(domain);
      const neverStatus = result.statusCode;
      const neverOk = neverStatus >= 200 && neverStatus < 300;
      if (neverOk && neverStats.preferPlaywright && isKnownSpaDomain(domain) && !contentAppearsEmpty(result.html)) {
        logger.info("known-SPA domain served substantive HTTP via render_js:never \u2014 downgrading prefer-chromium", { url, domain });
        neverStats.preferPlaywright = false;
      }
      return this.toRawFetchResult(result);
    }
    const stats = this.ensureStats(domain);
    if (stats.preferPlaywright) {
      if (!this.browserPool) throw new Error("SmartRouter: browserPool not configured");
      logger.debug("routing to playwright (domain marked)", { url, domain });
      return this.browserPool.fetchWithBrowser(url, { headers, screenshot });
    }
    const tlsMode = config.tlsTier;
    const tlsTierEnabled = tlsMode !== "off";
    const tlsDomainPreferred = tlsTierEnabled && this.tlsPersistence.getPreferTls(domain);
    const tryTlsFirst = tlsMode === "on" || tlsDomainPreferred;
    if (tryTlsFirst) {
      const tlsTry = await this.tryTlsTier(url, domain, headers);
      if (tlsTry.ok) {
        return tlsTry.result;
      }
      logger.debug("tls-first miss, falling back to http", { url, domain, reason: tlsTry.reason });
    }
    try {
      if (!this.httpClient) throw new Error("SmartRouter: httpClient not configured");
      const result = await this.httpClient.fetch(url, { headers, conditionalHeaders });
      if (result.statusCode === 304) {
        return this.toRawFetchResult(result);
      }
      if (isRateLimit(result.statusCode, result.html)) {
        logger.debug("rate-limit (429) without challenge body \u2014 passing through, NOT escalating", { url, domain });
        return this.toRawFetchResult(result);
      }
      if (tlsTierEnabled && !tryTlsFirst && isAntiBotSignal(result.statusCode, result.html)) {
        const tlsTry = await this.tryTlsTier(url, domain, headers);
        if (tlsTry.ok) {
          return tlsTry.result;
        }
        if (!this.browserPool) throw new Error("SmartRouter: browserPool not configured");
        logger.info("anti-bot signal: tls tier failed, escalating to playwright", {
          url,
          domain,
          httpStatus: result.statusCode,
          tlsReason: tlsTry.reason
        });
        stats.preferPlaywright = true;
        return this.browserPool.fetchWithBrowser(url, { headers, screenshot });
      }
      if (!tlsTierEnabled && hasChallengeBody(result.html) && isAntiBotStatus(result.statusCode)) {
        if (!this.browserPool) throw new Error("SmartRouter: browserPool not configured");
        logger.info("anti-bot challenge body: escalating to playwright (tls tier disabled)", {
          url,
          domain,
          httpStatus: result.statusCode,
          signal: describeAntiBot(result.statusCode, result.html)
        });
        stats.preferPlaywright = true;
        return this.browserPool.fetchWithBrowser(url, { headers, screenshot });
      }
      const status = result.statusCode;
      const isSuccessful = status >= 200 && status < 300;
      if (isSuccessful && contentAppearsEmpty(result.html)) {
        if (!this.browserPool) throw new Error("SmartRouter: browserPool not configured");
        logger.info("SPA shell detected, marking domain for playwright", { url, domain });
        stats.preferPlaywright = true;
        return this.browserPool.fetchWithBrowser(url, { headers, screenshot });
      }
      if (isSuccessful && stats.preferPlaywright && isKnownSpaDomain(domain)) {
        logger.info("known-SPA domain returned substantive HTTP \u2014 downgrading prefer-chromium flag", { url, domain });
        stats.preferPlaywright = false;
      }
      return this.toRawFetchResult(result);
    } catch (err) {
      stats.failureCount++;
      logger.warn("http fetch failed", {
        url,
        domain,
        failureCount: stats.failureCount,
        error: err instanceof Error ? err.message : String(err)
      });
      if (stats.failureCount >= threshold) {
        if (!this.browserPool) throw new Error("SmartRouter: browserPool not configured");
        logger.info("failure threshold reached, marking domain for playwright", { url, domain, threshold });
        stats.preferPlaywright = true;
        return this.browserPool.fetchWithBrowser(url, { headers, screenshot });
      }
      throw err;
    }
  }
  getDomainStats(domain) {
    return this.domainMap.get(domain);
  }
  ensureStats(domain) {
    let stats = this.domainMap.get(domain);
    if (!stats) {
      stats = {
        failureCount: 0,
        preferPlaywright: isKnownSpaDomain(domain)
      };
      this.domainMap.set(domain, stats);
    }
    return stats;
  }
  // Exposed for testing — callers should not branch on this.
  /* istanbul ignore next */
  static isKnownSpaDomain(host) {
    return isKnownSpaDomain(host);
  }
  toRawFetchResult(result) {
    return {
      url: result.url,
      finalUrl: result.finalUrl,
      html: result.html,
      contentType: result.contentType,
      statusCode: result.statusCode,
      method: "http",
      headers: result.headers,
      rawBuffer: result.rawBuffer
    };
  }
  // --- Slice D2: TLS-impersonation tier helpers ---
  /**
   * Attempt the TLS-impersonation tier for `url`. Returns:
   *  - { ok: true, result } when the tier completed AND the response does
   *    not look like a still-blocking challenge / JS-required page
   *  - { ok: false, reason } when the tier is unavailable (missing native
   *    binary, network error) OR the response still looks anti-bot / JS-
   *    required so the router should escalate to Playwright.
   *
   * Records success against the domain on every healthy response. The
   * `prefer_tls_impersonation` flip is performed by the persistence layer
   * once the success threshold is reached.
   */
  async tryTlsTier(url, domain, headers) {
    const logger = createLogger("fetch");
    let r;
    try {
      r = await this.tlsFetcher(url, { headers });
    } catch (err) {
      if (err instanceof TlsTierUnavailableError) {
        logger.debug("tls tier unavailable, escalating", { url, domain });
        return { ok: false, reason: "unavailable", error: err };
      }
      logger.warn("tls tier error, escalating", {
        url,
        domain,
        error: err instanceof Error ? err.message : String(err)
      });
      return { ok: false, reason: "error", error: err };
    }
    if (isAntiBotSignal(r.statusCode, r.html)) {
      logger.info("tls tier returned anti-bot signal, escalating to playwright", {
        url,
        domain,
        statusCode: r.statusCode,
        signal: describeAntiBot(r.statusCode, r.html)
      });
      return { ok: false, reason: "still_blocked" };
    }
    if (looksJsRequired(r.html) && r.html.length < 2e3) {
      logger.info("tls tier landed on JS-required page, escalating", { url, domain });
      return { ok: false, reason: "js_required" };
    }
    try {
      this.tlsPersistence.recordSuccess(domain);
    } catch {
    }
    return {
      ok: true,
      result: {
        url: r.url,
        finalUrl: r.finalUrl,
        html: r.html,
        contentType: r.contentType,
        statusCode: r.statusCode,
        method: "tls-impersonation",
        headers: r.headers,
        rawBuffer: r.rawBuffer
      }
    };
  }
}
function defaultTlsPersistence() {
  return {
    getPreferTls(domain) {
      try {
        const row = getDomainRouting(domain);
        return row?.preferTlsImpersonation ?? false;
      } catch {
        return false;
      }
    },
    recordSuccess(domain) {
      try {
        recordTlsImpersonationSuccess(domain, getConfig().tlsSuccessThreshold);
      } catch {
      }
    }
  };
}
export {
  SmartRouter
};
//# sourceMappingURL=router.js.map