import { BrowserContext, Page, chromium } from 'playwright';
import { EventEmitter } from 'events';
import {
  IBrowserSession,
  SessionState,
  SessionConfig,
  InteractionEvent,
  PreviewInteractionEvent,
  DOMUpdate,
} from '../types';
import { createLogger } from '../utils/logger';
import { config } from '../config';
import {
  getRandomUserAgent,
  getRandomTimezone,
  getRandomLocale,
  getViewport,
} from '../utils/user-agents';

const logger = createLogger('BrowserSession');

/**
 * BrowserSession manages a single browser context and page
 * Provides streaming capabilities for DOM updates and user interactions
 */
export class BrowserSession extends EventEmitter implements IBrowserSession {
  public readonly sessionId: string;
  public readonly context: BrowserContext;
  public page: Page;
  public state: SessionState;
  public currency?: string;

  private updateQueue: DOMUpdate[] = [];
  private observers: Set<(update: DOMUpdate) => void> = new Set();
  private htmlDebounceTimer: NodeJS.Timeout | null = null;
  private lastHtmlSnapshot: string = '';

  constructor(
    sessionId: string,
    context: BrowserContext,
    page: Page,
    sessionConfig: SessionConfig
  ) {
    super();
    this.sessionId = sessionId;
    this.context = context;
    this.page = page;
    this.state = {
      sessionId,
      userId: sessionConfig.userId,
      deviceMode: sessionConfig.deviceMode,
      createdAt: new Date(),
      lastActivity: new Date(),
      currentUrl: '',
      isActive: true,
    };

    this.currency = sessionConfig.currency;
    this.setupPageListeners();
    logger.info(`Browser session created: ${sessionId}`, {
      userId: sessionConfig.userId,
      deviceMode: sessionConfig.deviceMode,
    });
  }

  /**
   * Static factory method to create a new browser session
   */
  static async create(sessionConfig: SessionConfig): Promise<BrowserSession> {
    // Create browser context with anti-bot measures
    const contextOptions: any = {
      viewport: sessionConfig.viewport || getViewport(sessionConfig.deviceMode),
      userAgent: sessionConfig.userAgent ||
        (config.antiBot.enableUARandomization
          ? getRandomUserAgent(sessionConfig.deviceMode)
          : undefined),
      locale: sessionConfig.locale ||
        (config.antiBot.enableTimezoneSpoofing
          ? getRandomLocale()
          : 'en-US'),
      timezoneId: sessionConfig.timezone ||
        (config.antiBot.enableTimezoneSpoofing
          ? getRandomTimezone()
          : undefined),
      geolocation: sessionConfig.geolocation,
      permissions: ['geolocation'],
      ignoreHTTPSErrors: true,
      // Enable device emulation for mobile
      isMobile: sessionConfig.deviceMode === 'mobile',
      hasTouch: sessionConfig.deviceMode === 'mobile',
    };

    // Add upstream proxy if configured
    if (sessionConfig.upstreamProxy) {
      contextOptions.proxy = sessionConfig.upstreamProxy;
    } else if (config.upstreamProxy.enabled && config.upstreamProxy.server) {
      contextOptions.proxy = {
        server: config.upstreamProxy.server,
        username: config.upstreamProxy.username,
        password: config.upstreamProxy.password,
      };
    }

    // Launch a persistent context with anti-detection args
    const browser = await chromium.launch({
      headless: true,
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage',
        '--disable-accelerated-2d-canvas',
        '--no-first-run',
        '--no-zygote',
        '--disable-gpu',
        // Force HTTP/1.1 — avoids ERR_HTTP2_PROTOCOL_ERROR from Akamai/Cloudflare anti-bot on hotels.com etc.
        '--disable-http2',
        // Critical anti-detection flags
        '--disable-blink-features=AutomationControlled',
        '--disable-features=IsolateOrigins,site-per-process',
        '--disable-site-isolation-trials',
        // Prevent detection via missing media codecs
        '--autoplay-policy=user-gesture-required',
        '--disable-background-networking',
        '--disable-background-timer-throttling',
        '--disable-backgrounding-occluded-windows',
        '--disable-breakpad',
        '--disable-component-extensions-with-background-pages',
        '--disable-component-update',
        '--disable-default-apps',
        '--disable-extensions',
        '--disable-hang-monitor',
        '--disable-ipc-flooding-protection',
        '--disable-popup-blocking',
        '--disable-prompt-on-repost',
        '--disable-renderer-backgrounding',
        '--disable-sync',
        '--enable-features=NetworkService,NetworkServiceInProcess',
        '--force-color-profile=srgb',
        '--metrics-recording-only',
        '--password-store=basic',
        '--use-mock-keychain',
        '--export-tagged-pdf',
      ],
    });

    const context = await browser.newContext(contextOptions);

    // Apply comprehensive anti-detection measures
    await context.addInitScript(`
      // Override navigator.webdriver - the most common detection
      Object.defineProperty(navigator, 'webdriver', {
        get: () => undefined,
        configurable: true,
      });

      // Delete webdriver property entirely if possible
      delete Object.getPrototypeOf(navigator).webdriver;

      // Create realistic plugin array
      const mockPlugins = [
        { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
        { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
        { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
      ];

      const pluginArray = Object.create(PluginArray.prototype);
      mockPlugins.forEach((p, i) => {
        const plugin = Object.create(Plugin.prototype);
        Object.defineProperties(plugin, {
          name: { value: p.name, enumerable: true },
          filename: { value: p.filename, enumerable: true },
          description: { value: p.description, enumerable: true },
          length: { value: 0, enumerable: true },
        });
        pluginArray[i] = plugin;
      });
      Object.defineProperty(pluginArray, 'length', { value: mockPlugins.length });

      Object.defineProperty(navigator, 'plugins', {
        get: () => pluginArray,
        configurable: true,
      });

      // Override languages to match realistic browser
      Object.defineProperty(navigator, 'languages', {
        get: () => ['en-US', 'en'],
        configurable: true,
      });

      Object.defineProperty(navigator, 'language', {
        get: () => 'en-US',
        configurable: true,
      });

      // Add chrome object with realistic properties
      if (!window.chrome) {
        window.chrome = {};
      }
      window.chrome.runtime = {
        connect: () => {},
        sendMessage: () => {},
        onMessage: { addListener: () => {} },
        PlatformOs: { MAC: 'mac', WIN: 'win', ANDROID: 'android', CROS: 'cros', LINUX: 'linux', OPENBSD: 'openbsd' },
        PlatformArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
        PlatformNaclArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
        RequestUpdateCheckStatus: { THROTTLED: 'throttled', NO_UPDATE: 'no_update', UPDATE_AVAILABLE: 'update_available' },
      };
      window.chrome.csi = () => ({});
      window.chrome.loadTimes = () => ({});

      // Override permissions query
      const originalQuery = window.navigator.permissions.query;
      window.navigator.permissions.query = (parameters) => {
        if (parameters.name === 'notifications') {
          return Promise.resolve({ state: Notification.permission, onchange: null });
        }
        return originalQuery.call(navigator.permissions, parameters);
      };

      // Override connection info to look realistic
      Object.defineProperty(navigator, 'connection', {
        get: () => ({
          effectiveType: '4g',
          rtt: 50,
          downlink: 10,
          saveData: false,
        }),
        configurable: true,
      });

      // Hardware concurrency (number of CPU cores)
      Object.defineProperty(navigator, 'hardwareConcurrency', {
        get: () => 8,
        configurable: true,
      });

      // Device memory
      Object.defineProperty(navigator, 'deviceMemory', {
        get: () => 8,
        configurable: true,
      });

      // Override WebGL vendor and renderer to match real hardware
      const getParameterProxyHandler = {
        apply: function(target, thisArg, args) {
          const param = args[0];
          const gl = thisArg;

          // UNMASKED_VENDOR_WEBGL
          if (param === 37445) {
            return 'Google Inc. (NVIDIA)';
          }
          // UNMASKED_RENDERER_WEBGL
          if (param === 37446) {
            return 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Direct3D11 vs_5_0 ps_5_0, D3D11)';
          }
          return Reflect.apply(target, thisArg, args);
        }
      };

      // Proxy WebGL getParameter
      const originalGetParameter = WebGLRenderingContext.prototype.getParameter;
      WebGLRenderingContext.prototype.getParameter = new Proxy(originalGetParameter, getParameterProxyHandler);

      const originalGetParameter2 = WebGL2RenderingContext.prototype.getParameter;
      WebGL2RenderingContext.prototype.getParameter = new Proxy(originalGetParameter2, getParameterProxyHandler);

      // Prevent iframe detection
      Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth });
      Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight + 85 });

      // Override toString to hide proxy modifications
      const nativeToString = Function.prototype.toString;
      Function.prototype.toString = function() {
        if (this === window.navigator.permissions.query) {
          return 'function query() { [native code] }';
        }
        return nativeToString.call(this);
      };

      // Console.debug used by some detection scripts
      console.debug = console.log;
    `);

    const page = await context.newPage();

    // Set extra HTTP headers for anti-bot (mimic real Chrome browser)
    if (config.antiBot.enableHeaderSpoofing) {
      await page.setExtraHTTPHeaders({
        'Accept-Language': 'en-US,en;q=0.9',
        'Accept-Encoding': 'gzip, deflate, br, zstd',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'Cache-Control': 'max-age=0',
        'Sec-Ch-Ua': '"Chromium";v="131", "Google Chrome";v="131", "Not-A.Brand";v="24"',
        'Sec-Ch-Ua-Mobile': sessionConfig.deviceMode === 'mobile' ? '?1' : '?0',
        'Sec-Ch-Ua-Platform': sessionConfig.deviceMode === 'mobile' ? '"Android"' : '"Windows"',
        'Sec-Fetch-Dest': 'document',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-Site': 'none',
        'Sec-Fetch-User': '?1',
        'Upgrade-Insecure-Requests': '1',
      });
    }

    return new BrowserSession(sessionConfig.sessionId, context, page, sessionConfig);
  }

  /**
   * Setup page event listeners for streaming
   */
  private setupPageListeners(): void {
    // Listen for navigation events
    this.page.on('load', () => {
      this.updateLastActivity();
      this.emitUpdate({
        type: 'navigation',
        timestamp: Date.now(),
        data: { url: this.page.url(), status: 'load' },
      });

      // Emit HTML snapshot on page load
      this.emitHtmlSnapshot();
    });

    this.page.on('domcontentloaded', () => {
      this.updateLastActivity();
      this.emitUpdate({
        type: 'navigation',
        timestamp: Date.now(),
        data: { url: this.page.url(), status: 'domcontentloaded' },
      });
    });

    // Listen for console messages
    this.page.on('console', (msg) => {
      this.emitUpdate({
        type: 'console',
        timestamp: Date.now(),
        data: {
          type: msg.type(),
          text: msg.text(),
        },
      });
    });

    // Listen for page errors
    this.page.on('pageerror', (error) => {
      this.emitUpdate({
        type: 'error',
        timestamp: Date.now(),
        data: {
          message: error.message,
          stack: error.stack,
        },
      });
    });

    // Listen for network responses (for resource tracking)
    this.page.on('response', (response) => {
      this.emitUpdate({
        type: 'resource',
        timestamp: Date.now(),
        data: {
          url: response.url(),
          status: response.status(),
          contentType: response.headers()['content-type'],
        },
      });
    });

    // Setup mutation observer for DOM changes
    this.page.exposeFunction('__domMutation', (mutation: any) => {
      this.emitUpdate({
        type: 'mutation',
        timestamp: Date.now(),
        data: mutation,
      });

      // Debounce HTML snapshot emission
      this.emitHtmlSnapshot();
    });

    // Inject mutation observer on every page load
    this.page.on('load', async () => {
      await this.page.evaluate(`
        const observer = new MutationObserver((mutations) => {
          const changes = mutations.map((m) => ({
            type: m.type,
            target: m.target.nodeName,
            addedNodes: m.addedNodes.length,
            removedNodes: m.removedNodes.length,
          }));
          window.__domMutation(changes);
        });

        observer.observe(document.body, {
          childList: true,
          subtree: true,
          attributes: true,
          characterData: true,
        });
      `).catch(() => {
        // Ignore errors if body is not ready
      });
    });
  }

  /**
   * Emit a DOM update to all observers
   */
  private emitUpdate(update: DOMUpdate): void {
    this.updateQueue.push(update);
    this.observers.forEach((observer) => observer(update));
    this.emit('update', update);
  }

  /**
   * Subscribe to DOM updates
   */
  public subscribe(observer: (update: DOMUpdate) => void): () => void {
    this.observers.add(observer);
    return () => this.observers.delete(observer);
  }

  /**
   * Force a specific currency on supported booking site URLs
   */
  private static forceCurrency(url: string, currency: string): string {
    try {
      const u = new URL(url);
      const host = u.hostname.toLowerCase();

      if (host.includes('booking.com')) {
        u.searchParams.set('selected_currency', currency);
      } else if (host.includes('vrbo.com')) {
        u.searchParams.set('top_cur', currency);
      } else if (host.includes('hotels.com') || host.includes('expedia.')) {
        u.searchParams.set('currency', currency);
      } else if (host.includes('airbnb.')) {
        u.searchParams.set('currency', currency);
      } else if (host.includes('agoda.com')) {
        u.searchParams.set('currencyCode', currency);
      } else if (host.includes('trip.com')) {
        u.searchParams.set('curr', currency);
        u.searchParams.set('barcurr', currency);
      } else if (host.includes('hostelworld.com')) {
        u.searchParams.set('currency', currency);
      } else if (host.includes('travelocity.com')) {
        u.searchParams.set('top_cur', currency);
      } else if (host.includes('fliggytravel.com') || host.includes('fliggy.com')) {
        u.searchParams.set('__currency__', currency);
      } else if (host.includes('makemytrip.') || host.includes('goibibo.com')) {
        u.searchParams.set('_uCurrency', currency);
      }

      return u.toString();
    } catch {
      return url;
    }
  }

  /**
   * Navigate to a URL
   */
  async navigate(url: string): Promise<void> {
    this.updateLastActivity();

    // Force currency via URL parameters if set
    if (this.currency) {
      url = BrowserSession.forceCurrency(url, this.currency);
    }

    const isVrbo = /vrbo\.com/i.test(url);
    // Most providers are SPAs.  Only booking.com, casasrurales.net, and homestay.com
    // are traditional server-rendered sites that benefit from the retry-based navigation path.
    const isServerRendered = /booking\.com|casasrurales\.net|homestay\.com/i.test(url);
    const isSpaProvider = !isServerRendered && !isVrbo;

    // VRBO has aggressive bot detection (PerimeterX).
    // Strategy: launch a stealth browser using system Chrome (much harder to fingerprint
    // than Playwright's bundled Chromium), warm up on the homepage to establish trust
    // cookies, then navigate to the actual listing.
    if (isVrbo) {
      await this.navigateVrboStealth(url);
      return;
    }

    // For SPA providers: try page.goto() once with a short timeout, fall back to fetch quickly
    // For others: try page.goto() with normal timeout and retry
    if (isSpaProvider) {
      try {
        logger.info(`Navigating to ${url} (SPA provider, trying goto first)`, { sessionId: this.sessionId });
        await this.page.goto(url, {
          waitUntil: 'domcontentloaded',
          timeout: 15000, // short timeout — fail fast if HTTP2 blocked
        });
        this.state.currentUrl = this.page.url();
        await this.rewriteLinks();
        logger.info(`Navigation completed via goto: ${url}`, { sessionId: this.sessionId });
        return;
      } catch (error) {
        const msg = error instanceof Error ? error.message : String(error);
        logger.warn(`goto failed for SPA provider, falling back to fetch: ${msg}`, { sessionId: this.sessionId });
        await this.navigateViaFetch(url);
        return;
      }
    }

    const maxAttempts = 2;
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        logger.info(`Navigating to ${url} (attempt ${attempt})`, { sessionId: this.sessionId });

        await this.page.goto(url, {
          waitUntil: 'domcontentloaded',
          timeout: config.browserPool.browserTimeout,
        });

        this.state.currentUrl = this.page.url();
        await this.rewriteLinks();

        logger.info(`Navigation completed: ${url}`, { sessionId: this.sessionId });
        return;
      } catch (error) {
        const msg = error instanceof Error ? error.message : String(error);
        const isRetryable = msg.includes('ERR_HTTP2_PROTOCOL_ERROR') ||
                            msg.includes('ERR_CONNECTION_CLOSED') ||
                            msg.includes('ERR_EMPTY_RESPONSE');

        if (isRetryable && attempt < maxAttempts) {
          logger.warn(`Retryable navigation error (attempt ${attempt}), retrying: ${msg}`, { sessionId: this.sessionId });
          await new Promise(r => setTimeout(r, 2000));
          continue;
        }

        logger.error(`Navigation failed: ${url}`, {
          sessionId: this.sessionId,
          error: msg,
        });
        throw error;
      }
    }
  }

  /**
   * Navigate by fetching the page HTML via Node.js (bypasses Chromium's TLS/HTTP2 fingerprint)
   * then setting the page content directly.
   */
  /**
   * Navigate to VRBO using a stealth approach:
   * 1. Launch a clean browser with system Chrome and minimal args
   * 2. Warm up on vrbo.com homepage to establish PerimeterX trust cookies
   * 3. Navigate to the actual listing
   */
  private async navigateVrboStealth(url: string): Promise<void> {
    logger.info(`VRBO stealth navigation to ${url}`, { sessionId: this.sessionId });

    // Close the current page's browser and relaunch with stealth settings
    const oldContext = this.context;

    try {
      // Launch a new browser with system Chrome (much harder to detect)
      // and minimal args — no automation-revealing flags
      const stealthBrowser = await chromium.launch({
        channel: 'chrome',  // Use system-installed Chrome instead of bundled Chromium
        headless: false,    // Use headed mode — PerimeterX detects all headless variants
        args: [
          '--headless=new',  // Chrome's new headless — full browser engine, not the detectable shell
          '--disable-blink-features=AutomationControlled',
          '--disable-features=IsolateOrigins,site-per-process',
          '--no-first-run',
          '--no-default-browser-check',
          '--disable-popup-blocking',
          '--disable-gpu',
          '--window-size=1280,900',
        ],
      });

      // Create context — let system Chrome provide its own headers and fingerprint
      const stealthContext = await stealthBrowser.newContext({
        viewport: this.page.viewportSize() || { width: 1280, height: 900 },
        // No custom userAgent — let Chrome use its real one
        locale: 'en-US',
        timezoneId: 'America/New_York',
        ignoreHTTPSErrors: true,
        // Add proxy if configured
        ...(config.upstreamProxy.enabled && config.upstreamProxy.server ? {
          proxy: {
            server: config.upstreamProxy.server,
            username: config.upstreamProxy.username,
            password: config.upstreamProxy.password,
          },
        } : {}),
      });

      // Minimal anti-detection: only hide webdriver flag
      await stealthContext.addInitScript(`
        Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: true });
      `);

      const stealthPage = await stealthContext.newPage();

      // Log blocked requests to help debug PerimeterX issues
      stealthPage.on('response', (response) => {
        const status = response.status();
        const url = response.url();
        if (status === 403 || status === 429) {
          logger.warn(`VRBO blocked resource: ${status} ${url}`, { sessionId: this.sessionId });
        }
      });

      // Helper: random delay between min and max ms
      const randomDelay = (min: number, max: number) =>
        Math.floor(Math.random() * (max - min + 1)) + min;

      // Step 1: Warm up on the VRBO homepage to get PerimeterX cookies
      logger.info('VRBO: warming up on homepage...', { sessionId: this.sessionId });
      await stealthPage.goto('https://www.vrbo.com/', {
        waitUntil: 'load',
        timeout: 20000,
      }).catch(() => {
        // If load times out, homepage may still have set some cookies — continue anyway
        logger.warn('VRBO: homepage load timed out, continuing...', { sessionId: this.sessionId });
      });
      // Wait for PerimeterX to load and start evaluating
      await stealthPage.waitForTimeout(randomDelay(2000, 4000));

      // Simulate human-like behavior on homepage: move mouse, scroll
      try {
        await stealthPage.mouse.move(randomDelay(200, 600), randomDelay(200, 500));
        await stealthPage.waitForTimeout(randomDelay(300, 800));
        await stealthPage.mouse.move(randomDelay(400, 800), randomDelay(300, 600));
        await stealthPage.waitForTimeout(randomDelay(300, 800));
        await stealthPage.evaluate(`window.scrollBy(0, ${randomDelay(150, 400)})`);
        await stealthPage.waitForTimeout(randomDelay(500, 1500));
        await stealthPage.evaluate(`window.scrollBy(0, -${randomDelay(50, 150)})`);
      } catch { /* ignore interaction errors */ }

      // Give PerimeterX enough time to set trust cookies (random 5-10s)
      await stealthPage.waitForTimeout(randomDelay(5000, 10000));

      // Step 2: Navigate to the actual listing
      logger.info('VRBO: navigating to listing...', { sessionId: this.sessionId });
      await stealthPage.goto(url, {
        waitUntil: 'domcontentloaded',
        timeout: 20000,
      });
      await stealthPage.waitForTimeout(randomDelay(4000, 8000));

      // Check for bot challenge
      const challenged = await stealthPage.evaluate(`(function() {
        var t = (document.title || '').toLowerCase();
        var b = (document.body ? document.body.innerText : '').toLowerCase();
        return t.includes('human') ||
          b.includes('show us your human side') ||
          b.includes("can't tell if you") ||
          !!document.querySelector('[id*="px-captcha"], [id*="perimeterx"]');
      })()`).catch(() => false);

      if (challenged) {
        logger.warn('VRBO: still challenged after stealth approach, waiting longer...', { sessionId: this.sessionId });
        // Wait longer — sometimes PerimeterX clears after its scripts finish
        await stealthPage.waitForTimeout(10000);
      }

      // Swap the page and context to the stealth ones
      this.page = stealthPage;
      // Close old context (don't await — fire and forget)
      oldContext.close().catch(() => {});

      this.state.currentUrl = stealthPage.url();
      await this.rewriteLinks();
      logger.info(`VRBO stealth navigation completed: ${url}`, { sessionId: this.sessionId });
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error);
      logger.error(`VRBO stealth navigation failed: ${msg}`, { sessionId: this.sessionId });

      // Fall back to regular navigation on the original page
      try {
        await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
        this.state.currentUrl = this.page.url();
        await this.rewriteLinks();
      } catch {
        await this.navigateViaFetch(url);
      }
    }
  }

  private async navigateViaFetch(url: string): Promise<void> {
    logger.info(`Fetch-based navigation to ${url}`, { sessionId: this.sessionId });

    try {
      // Recover page if prior goto destroyed the context
      try {
        await this.page.evaluate('1');
      } catch {
        logger.info('Page context was destroyed, creating fresh page', { sessionId: this.sessionId });
        this.page = await this.context.newPage();
      }

      // Fetch the HTML via Node.js fetch (bypasses Chromium TLS/HTTP2 fingerprint)
      const ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
      const response = await fetch(url, {
        headers: {
          'User-Agent': ua,
          'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
          'Accept-Language': 'en-US,en;q=0.9',
          'Accept-Encoding': 'gzip, deflate, br',
          'Cache-Control': 'no-cache',
          'Sec-Fetch-Dest': 'document',
          'Sec-Fetch-Mode': 'navigate',
          'Sec-Fetch-Site': 'none',
          'Sec-Fetch-User': '?1',
          'Upgrade-Insecure-Requests': '1',
        },
        redirect: 'follow',
      });

      if (!response.ok) {
        throw new Error(`Fetch failed with status ${response.status}: ${response.statusText}`);
      }

      const html = await response.text();
      const finalUrl = response.url;

      logger.info(`Fetched ${html.length} bytes from ${finalUrl}`, { sessionId: this.sessionId });

      // Set content directly — Hotels.com is SSR so prices are already in the HTML.
      // No SPA hydration needed for price extraction.
      await this.page.setContent(html, { waitUntil: 'domcontentloaded', timeout: 15000 });

      this.state.currentUrl = finalUrl;

      logger.info(`Fetch-based navigation completed: ${finalUrl}`, { sessionId: this.sessionId });
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error);
      logger.error(`Fetch-based navigation failed: ${url}`, {
        sessionId: this.sessionId,
        error: msg,
      });
      throw error;
    }
  }

  /**
   * Rewrite links and prevent default navigation
   * This is mainly to prevent the page from navigating away in the browser context
   */
  private async rewriteLinks(): Promise<void> {
    await this.page.evaluate(`
      const baseUrl = window.location.origin;

      // Prevent all anchor tags from navigating
      document.querySelectorAll('a[href]').forEach((link) => {
        const href = link.getAttribute('href');
        if (href && !href.startsWith('javascript:') && !href.startsWith('#')) {
          const absoluteUrl = new URL(href, baseUrl).href;
          link.setAttribute('data-original-href', absoluteUrl);

          // Prevent default navigation - the user should navigate through the API
          link.addEventListener('click', (e) => {
            e.preventDefault();
            console.log('Link clicked (navigation blocked):', absoluteUrl);
          }, { capture: true });
        }
      });

      // Prevent form submissions from navigating
      document.querySelectorAll('form').forEach((form) => {
        form.addEventListener('submit', (e) => {
          e.preventDefault();
          console.log('Form submission blocked');
        }, { capture: true });
      });
    `).catch(() => {
      // Ignore errors if DOM is not ready
    });
  }

  /**
   * Handle user interactions
   */
  async interact(event: InteractionEvent): Promise<void> {
    this.updateLastActivity();

    try {
      switch (event.type) {
        case 'click':
          if (event.selector) {
            await this.page.click(event.selector);
          }
          break;

        case 'type':
          if (event.selector && event.data?.text) {
            await this.page.fill(event.selector, event.data.text);
          }
          break;

        case 'scroll':
          if (event.data?.x !== undefined && event.data?.y !== undefined) {
            await this.page.evaluate(
              `window.scrollTo(${event.data.x}, ${event.data.y})`
            );
          }
          break;

        case 'keypress':
          if (event.data?.key) {
            await this.page.keyboard.press(event.data.key);
          }
          break;
      }
    } catch (error) {
      logger.error(`Interaction failed`, {
        sessionId: this.sessionId,
        event,
        error: error instanceof Error ? error.message : String(error),
      });
      throw error;
    }
  }

  /**
   * Emit HTML snapshot with debouncing
   */
  private emitHtmlSnapshot(): void {
    // Clear existing timer
    if (this.htmlDebounceTimer) {
      clearTimeout(this.htmlDebounceTimer);
    }

    // Set new timer (500ms debounce - increased to give page more time to stabilize)
    this.htmlDebounceTimer = setTimeout(async () => {
      try {
        // Wait for the page to be in a stable state before getting HTML
        // This prevents "page is navigating" errors
        await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {
          // Ignore timeout - try to get HTML anyway
        });

        const html = await this.page.content();

        // Only emit if HTML has changed
        if (html !== this.lastHtmlSnapshot) {
          this.lastHtmlSnapshot = html;
          this.emitUpdate({
            type: 'html_snapshot',
            timestamp: Date.now(),
            data: { html },
          });
        }
      } catch (error) {
        const errorMsg = error instanceof Error ? error.message : String(error);
        logger.error('Failed to emit HTML snapshot', {
          sessionId: this.sessionId,
          error: errorMsg,
        });

        // If the error is due to navigation, retry once after a delay
        if (errorMsg.includes('navigating') || errorMsg.includes('changing the content')) {
          logger.info('Retrying HTML snapshot after navigation error', { sessionId: this.sessionId });
          setTimeout(() => this.emitHtmlSnapshot(), 1000);
        }
      }
    }, 500);
  }

  /**
   * Handle preview interactions from the live HTML preview
   */
  async handlePreviewInteraction(event: PreviewInteractionEvent): Promise<void> {
    this.updateLastActivity();

    try {
      switch (event.type) {
        case 'preview_click':
          if (event.x !== undefined && event.y !== undefined &&
              event.viewportWidth && event.viewportHeight) {
            // Scale coordinates from preview to actual viewport
            const viewport = this.page.viewportSize();
            if (viewport) {
              const scaleX = viewport.width / event.viewportWidth;
              const scaleY = viewport.height / event.viewportHeight;
              const actualX = event.x * scaleX;
              const actualY = event.y * scaleY;

              await this.page.mouse.click(actualX, actualY);
            }
          }
          break;

        case 'preview_scroll':
          if (event.scrollX !== undefined && event.scrollY !== undefined) {
            await this.page.evaluate(
              `window.scrollTo(${event.scrollX}, ${event.scrollY})`
            );
          }
          break;

        case 'preview_keyboard':
          if (event.key) {
            await this.page.keyboard.press(event.key);
          }
          break;
      }
    } catch (error) {
      logger.error('Preview interaction failed', {
        sessionId: this.sessionId,
        event,
        error: error instanceof Error ? error.message : String(error),
      });
      throw error;
    }
  }

  /**
   * Get current page HTML
   */
  async getHTML(): Promise<string> {
    this.updateLastActivity();

    // Wait for the page to be stable before getting HTML
    // This prevents "page is navigating" errors
    try {
      await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 });
    } catch (error) {
      // Log but continue - try to get HTML anyway
      logger.warn('Timeout waiting for page load state', {
        sessionId: this.sessionId,
        error: error instanceof Error ? error.message : String(error),
      });
    }

    return await this.page.content();
  }

  /**
   * Get current page screenshot
   */
  async getScreenshot(): Promise<Buffer> {
    this.updateLastActivity();
    return await this.page.screenshot({ fullPage: false });
  }

  /**
   * Update last activity timestamp
   */
  private updateLastActivity(): void {
    this.state.lastActivity = new Date();
  }

  /**
   * Check if session is idle
   */
  public isIdle(): boolean {
    const idleTime = Date.now() - this.state.lastActivity.getTime();
    return idleTime > config.browserPool.idleTimeout;
  }

  /**
   * Close the browser session
   */
  async close(): Promise<void> {
    try {
      this.state.isActive = false;
      this.observers.clear();

      await this.page.close();
      await this.context.close();

      // Close the browser instance
      const browser = this.context.browser();
      if (browser) {
        await browser.close();
      }

      logger.info(`Browser session closed: ${this.sessionId}`);
    } catch (error) {
      logger.error(`Error closing session: ${this.sessionId}`, {
        error: error instanceof Error ? error.message : String(error),
      });
      throw error;
    }
  }
}
