import { chromium, firefox, webkit } from "playwright";
import { getConfig } from "../config.js";
import { createLogger } from "../logger.js";
import { BrowserSelector } from "./browser-selector.js";
import { executeActions } from "./action-executor.js";
import { HYDRATION_PROBE_SOURCE } from "./hydration-probe.js";
const log = createLogger("fetch");
const NAV_RACE_PATTERN = /execution context (?:was )?destroyed|page is navigating|frame.*detached|target closed/i;
async function readContentWithRetry(page, url) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      return await page.content();
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      if (!NAV_RACE_PATTERN.test(msg) || attempt === 2) throw err;
      log.debug("page.content hit navigation race, retrying", { url, attempt, msg });
      await page.waitForLoadState("domcontentloaded").catch(() => void 0);
      await new Promise((r) => setTimeout(r, 300));
    }
  }
  return await page.content();
}
function getLauncher(type) {
  switch (type) {
    case "firefox":
      return firefox;
    case "webkit":
      return webkit;
    default:
      return chromium;
  }
}
class MultiBrowserPool {
  pools = /* @__PURE__ */ new Map();
  selector;
  configuredTypes;
  shutdownCalled = false;
  constructor(options) {
    let types = options?.browserTypes ?? ["chromium"];
    if (types.length === 0) {
      log.warn("empty browserTypes, defaulting to chromium");
      types = ["chromium"];
    }
    this.configuredTypes = [...types];
    this.selector = new BrowserSelector(types, options?.selectionStrategy ?? "round-robin");
    for (const type of types) {
      this.pools.set(type, {
        browser: null,
        pool: [],
        activeCount: 0,
        waitQueue: [],
        idleTimers: /* @__PURE__ */ new Map()
      });
    }
    log.info("multi-browser pool initialized", {
      types: this.configuredTypes,
      strategy: options?.selectionStrategy ?? "round-robin"
    });
  }
  getConfiguredTypes() {
    return [...this.configuredTypes];
  }
  getStats() {
    return this.configuredTypes.map((type) => {
      const p = this.pools.get(type);
      return {
        type,
        activeCount: p.activeCount,
        pooledCount: p.pool.length
      };
    });
  }
  resolveType(requested, url) {
    if (requested && this.pools.has(requested)) {
      return requested;
    }
    if (requested && !this.pools.has(requested)) {
      log.warn("requested browser type not configured, falling back", {
        requested,
        available: this.configuredTypes
      });
      return this.configuredTypes[0];
    }
    if (url && this.selector.getStrategy() === "hostname-hash") {
      try {
        const hostname = new URL(url).hostname;
        return this.selector.selectForHostname(hostname);
      } catch {
        return this.selector.select();
      }
    }
    return this.selector.select();
  }
  async launchBrowser(type) {
    const typePool = this.pools.get(type);
    if (!typePool.browser) {
      const launcher = getLauncher(type);
      log.debug("launching browser", { type });
      typePool.browser = await launcher.launch({ headless: true });
    }
    return typePool.browser;
  }
  async acquireForType(type) {
    const config = getConfig();
    const maxBrowsers = config.maxBrowsers;
    const typePool = this.pools.get(type);
    if (typePool.pool.length > 0) {
      const ctx = typePool.pool.pop();
      const timer = typePool.idleTimers.get(ctx);
      if (timer !== void 0) {
        clearTimeout(timer);
        typePool.idleTimers.delete(ctx);
      }
      return ctx;
    }
    if (typePool.activeCount < maxBrowsers) {
      typePool.activeCount++;
      const browser = await this.launchBrowser(type);
      return browser.newContext();
    }
    return new Promise((resolve) => {
      typePool.waitQueue.push(resolve);
    });
  }
  releaseForType(type, ctx) {
    const config = getConfig();
    const idleTimeoutMs = config.browserIdleTimeoutMs;
    const typePool = this.pools.get(type);
    if (typePool.waitQueue.length > 0) {
      const resolve = typePool.waitQueue.shift();
      resolve(ctx);
      return;
    }
    typePool.pool.push(ctx);
    const timer = setTimeout(() => {
      const idx = typePool.pool.indexOf(ctx);
      if (idx !== -1) {
        typePool.pool.splice(idx, 1);
        typePool.idleTimers.delete(ctx);
        typePool.activeCount = Math.max(0, typePool.activeCount - 1);
        ctx.close().catch(() => {
        });
      }
    }, idleTimeoutMs);
    typePool.idleTimers.set(ctx, timer);
  }
  async fetchWithBrowser(url, options = {}) {
    const config = getConfig();
    const navTimeoutMs = options.timeoutMs ?? config.playwrightNavTimeoutMs;
    const loadTimeoutMs = config.playwrightLoadTimeoutMs;
    let ctx;
    let cdpBrowser = null;
    let resolvedType;
    if (options.cdpUrl) {
      resolvedType = "chromium";
      try {
        log.info("connecting via CDP", { cdpUrl: options.cdpUrl });
        cdpBrowser = await chromium.connectOverCDP(options.cdpUrl);
        const contexts = cdpBrowser.contexts();
        ctx = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
      } catch (err) {
        log.warn("CDP connection failed, falling back to launch", {
          cdpUrl: options.cdpUrl,
          error: err instanceof Error ? err.message : String(err)
        });
        ctx = await this.acquireForType(resolvedType);
      }
    } else {
      resolvedType = this.resolveType(options.browserType, url);
      log.debug("fetching with browser", { url, type: resolvedType });
      ctx = await this.acquireForType(resolvedType);
    }
    const page = await ctx.newPage();
    if (options.headers) {
      await page.setExtraHTTPHeaders(options.headers);
    }
    let statusCode = 200;
    let contentType = "";
    let responseHeaders = {};
    let finalUrl = url;
    let gotoTimedOut = false;
    try {
      try {
        const response = await page.goto(url, {
          timeout: navTimeoutMs,
          waitUntil: "domcontentloaded"
        });
        if (response) {
          statusCode = response.status();
          finalUrl = response.url();
          const rawHeaders = response.headers();
          responseHeaders = rawHeaders;
          contentType = rawHeaders["content-type"] ?? "";
        }
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        const isTimeout = err instanceof Error && err.name === "TimeoutError" || /Timeout\s+\d+ms\s+exceeded/i.test(msg);
        if (!isTimeout) throw err;
        gotoTimedOut = true;
        log.warn("page.goto timed out, returning partial content", { url, navTimeoutMs });
      }
      try {
        await page.waitForLoadState("networkidle", { timeout: loadTimeoutMs });
      } catch {
        log.debug("networkidle timeout, using page content as-is", { url, type: resolvedType });
      }
      if (typeof page.waitForFunction === "function") {
        const hydrationBudget = Math.min(8e3, Math.max(1500, Math.floor(navTimeoutMs / 4)));
        await page.waitForFunction(HYDRATION_PROBE_SOURCE, void 0, {
          timeout: hydrationBudget
        }).catch(() => void 0);
      }
      let actionResults;
      if (options.actions && options.actions.length > 0) {
        actionResults = await executeActions(page, options.actions);
      }
      const html = await readContentWithRetry(page, url);
      let screenshotBase64;
      if (options.screenshot) {
        const buf = await page.screenshot({ fullPage: true });
        screenshotBase64 = buf.toString("base64");
      }
      return {
        url,
        finalUrl,
        html,
        contentType,
        statusCode,
        method: "playwright",
        headers: responseHeaders,
        screenshot: screenshotBase64,
        actionResults,
        ...gotoTimedOut ? { warning: "goto_timeout_partial_content" } : {}
      };
    } finally {
      await page.close();
      if (cdpBrowser) {
        await cdpBrowser.close().catch(() => {
        });
      } else {
        this.releaseForType(resolvedType, ctx);
      }
    }
  }
  async shutdown() {
    if (this.shutdownCalled) return;
    this.shutdownCalled = true;
    for (const [type, typePool] of this.pools) {
      for (const [, timer] of typePool.idleTimers) {
        clearTimeout(timer);
      }
      typePool.idleTimers.clear();
      const closePromises = typePool.pool.map((ctx) => ctx.close().catch(() => {
      }));
      typePool.pool = [];
      await Promise.all(closePromises);
      if (typePool.browser) {
        await typePool.browser.close().catch(() => {
        });
        typePool.browser = null;
      }
      typePool.activeCount = 0;
      log.debug("browser pool shut down", { type });
    }
  }
}
class BrowserPool extends MultiBrowserPool {
  singleType;
  constructor(options) {
    const type = options?.browserType ?? "chromium";
    super({
      browserTypes: [type]
    });
    this.singleType = type;
  }
  async acquire() {
    return this.acquireForType(this.singleType);
  }
  release(ctx) {
    this.releaseForType(this.singleType, ctx);
  }
}
export {
  BrowserPool,
  MultiBrowserPool
};
//# sourceMappingURL=browser-pool.js.map