import { Router } from 'express';
import { v4 as uuidv4 } from 'uuid';
import jwt from 'jsonwebtoken';
import * as http from 'http';
import * as https from 'https';
import { BrowserPool } from '../core/BrowserPool';
import { verifyToken, verifyTokenWithQuery, optionalAuth } from '../middleware/auth';
import { apiLimiter, sessionLimiter } from '../middleware/rateLimit';
import { config } from '../config';
import { createLogger } from '../utils/logger';
import { rewriteResourceUrls, rewriteCssUrls } from '../utils/urlRewriter';
import {
  SessionConfig,
  NavigationRequest,
  InteractionEvent,
  DeviceMode,
  AuthPayload,
} from '../types';
import { navigationDurationHistogram, navigationsCounter } from '../utils/metrics';
import * as cheerio from 'cheerio';
import { SearchModel } from '../models/Search';

const router = Router();
const logger = createLogger('ProxyRoutes');

// Global browser pool instance
let browserPool: BrowserPool;

/**
 * Initialize the browser pool
 */
export function initializeBrowserPool() {
  browserPool = new BrowserPool();
  return browserPool;
}

/**
 * Follows HTTP redirects and returns the final URL (up to 10 hops).
 */
function resolveRedirects(urlStr: string, hops = 0): Promise<string> {
  return new Promise((resolve, reject) => {
    if (hops > 10) return reject(new Error('Too many redirects'));
    let parsed: URL;
    try {
      parsed = new URL(urlStr);
    } catch {
      return reject(new Error('Invalid URL'));
    }
    const lib = parsed.protocol === 'https:' ? https : http;
    const req = lib.get(urlStr, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
      if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
        const next = new URL(res.headers.location, urlStr).toString();
        res.resume();
        resolve(resolveRedirects(next, hops + 1));
      } else {
        res.resume();
        resolve(urlStr);
      }
    });
    req.on('error', reject);
    req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); });
  });
}

/**
 * GET /proxy/resolve-url?url=...
 * Follows redirects and returns the final URL. Used to expand share links.
 */
router.get('/resolve-url', verifyToken, apiLimiter, async (req, res) => {
  const { url } = req.query as { url?: string };
  if (!url) {
    res.status(400).json({ error: 'url query param required' });
    return;
  }
  try {
    const resolved = await resolveRedirects(url);
    res.json({ url: resolved });
  } catch (error) {
    logger.warn('resolve-url failed', { url, error: error instanceof Error ? error.message : String(error) });
    res.status(400).json({ error: error instanceof Error ? error.message : 'Failed to resolve URL' });
  }
});

/**
 * GET /proxy/pool/stats
 * Get browser pool statistics
 */
router.get('/pool/stats', verifyToken, apiLimiter, (_req, res) => {
  try {
    const stats = browserPool.getStats();
    res.json(stats);
  } catch (error) {
    logger.error('Failed to get pool stats', {
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({ error: 'Failed to get pool statistics' });
  }
});

/**
 * POST /proxy/session/create
 * Create a new browser session
 */
router.post('/session/create', verifyToken, sessionLimiter, async (req, res) => {
  try {
    const user = (req as any).user as AuthPayload;
    const { deviceMode, url, currency } = req.body as { deviceMode?: DeviceMode; url?: string; currency?: string };

    const sessionId = uuidv4();
    const sessionConfig: SessionConfig = {
      sessionId,
      userId: user.userId,
      deviceMode: deviceMode || 'desktop',
      currency: currency || undefined,
    };

    logger.info('Creating new session', {
      sessionId,
      userId: user.userId,
      deviceMode: sessionConfig.deviceMode,
    });

    const session = await browserPool.getSession(sessionConfig);

    // Navigate to initial URL if provided
    if (url) {
      const start = Date.now();
      try {
        await session.navigate(url);
        const duration = (Date.now() - start) / 1000;
        navigationDurationHistogram.observe({ status: 'success' }, duration);
        navigationsCounter.inc({ status: 'success' });
      } catch (error) {
        const duration = (Date.now() - start) / 1000;
        navigationDurationHistogram.observe({ status: 'error' }, duration);
        navigationsCounter.inc({ status: 'error' });
        throw error;
      }
    }

    res.status(201).json({
      sessionId,
      deviceMode: sessionConfig.deviceMode,
      currentUrl: session.state.currentUrl,
    });
  } catch (error) {
    logger.error('Failed to create session', {
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({
      error: error instanceof Error ? error.message : 'Failed to create session',
    });
  }
});

/**
 * POST /proxy/session/:sessionId/navigate
 * Navigate to a URL in an existing session
 */
router.post('/session/:sessionId/navigate', verifyToken, apiLimiter, async (req, res) => {
  try {
    const { sessionId } = req.params;
    const { url } = req.body as NavigationRequest;

    if (!url) {
      res.status(400).json({ error: 'URL is required' });
      return;
    }

    logger.info('Navigation request', { sessionId, url });

    const session = await browserPool.getSession({
      sessionId,
      userId: (req as any).user.userId,
      deviceMode: 'desktop', // Will reuse existing if available
    });

    const start = Date.now();
    try {
      await session.navigate(url);
      const duration = (Date.now() - start) / 1000;
      navigationDurationHistogram.observe({ status: 'success' }, duration);
      navigationsCounter.inc({ status: 'success' });

      res.json({
        sessionId,
        currentUrl: session.state.currentUrl,
        status: 'success',
      });
    } catch (error) {
      const duration = (Date.now() - start) / 1000;
      navigationDurationHistogram.observe({ status: 'error' }, duration);
      navigationsCounter.inc({ status: 'error' });
      throw error;
    }
  } catch (error) {
    logger.error('Navigation failed', {
      sessionId: req.params.sessionId,
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({
      error: error instanceof Error ? error.message : 'Navigation failed',
    });
  }
});

/**
 * POST /proxy/session/:sessionId/interact
 * Send user interaction to a session
 */
router.post('/session/:sessionId/interact', verifyToken, apiLimiter, async (req, res) => {
  try {
    const { sessionId } = req.params;
    const event: InteractionEvent = { ...req.body, sessionId };

    logger.info('Interaction request', { sessionId, type: event.type });

    const session = await browserPool.getSession({
      sessionId,
      userId: (req as any).user.userId,
      deviceMode: 'desktop',
    });

    await session.interact(event);

    res.json({
      sessionId,
      status: 'success',
    });
  } catch (error) {
    logger.error('Interaction failed', {
      sessionId: req.params.sessionId,
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({
      error: error instanceof Error ? error.message : 'Interaction failed',
    });
  }
});

/**
 * GET /proxy/session/:sessionId/html
 * Get current page HTML with rewritten resource URLs
 */
router.get('/session/:sessionId/html', verifyTokenWithQuery, apiLimiter, async (req, res) => {
  try {
    const { sessionId } = req.params;

    // Get token from either header or query parameter
    const authHeader = req.headers.authorization;
    let token: string;

    if (authHeader && authHeader.startsWith('Bearer ')) {
      token = authHeader.substring(7);
    } else {
      token = req.query.token as string;
    }

    const session = await browserPool.getSession({
      sessionId,
      userId: (req as any).user.userId,
      deviceMode: 'desktop',
    });

    const html = await session.getHTML();
    const currentUrl = session.state.currentUrl;

    // Rewrite all resource URLs to go through our proxy with the token
    const rewrittenHtml = rewriteResourceUrls(html, currentUrl, sessionId, token);

    // Set CORS headers
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');

    // Remove CSP headers that might block our inline intercept scripts
    res.removeHeader('Content-Security-Policy');
    res.removeHeader('Content-Security-Policy-Report-Only');
    res.removeHeader('X-Content-Security-Policy');

    // Remove X-Frame-Options to allow iframe embedding from different origins
    res.removeHeader('X-Frame-Options');

    // Set Content-Security-Policy frame-ancestors to allow embedding from any origin
    res.header('Content-Security-Policy', "frame-ancestors *");

    res.type('html').send(rewrittenHtml);
  } catch (error) {
    logger.error('Failed to get HTML', {
      sessionId: req.params.sessionId,
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({
      error: error instanceof Error ? error.message : 'Failed to get HTML',
    });
  }
});

/**
 * OPTIONS /proxy/session/:sessionId/:token/resource
 * Handle CORS preflight requests for resource endpoint (with token in path)
 */
router.options('/session/:sessionId/:token/resource', (_req, res) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Cross-Origin-Resource-Policy', 'cross-origin');
  res.header('Cross-Origin-Embedder-Policy', 'unsafe-none');
  res.sendStatus(204);
});

/**
 * GET /proxy/session/:sessionId/:token/resource
 * Proxy external resources with token in path (prevents URL corruption)
 */
router.get('/session/:sessionId/:token/resource', async (req, res) => {
  try {
    const { sessionId, token } = req.params;
    const { url } = req.query;

    logger.debug('Resource request (token in path)', {
      sessionId,
      url,
      hasToken: !!token,
    });

    if (!url || typeof url !== 'string') {
      logger.warn('Resource request missing URL parameter', { sessionId, query: req.query });
      res.status(400).json({ error: 'URL parameter is required' });
      return;
    }

    // Verify token manually (decode URI component in case it was encoded in the URL path)
    let decodedToken = token;
    try {
      decodedToken = decodeURIComponent(token);
    } catch {
      // If decode fails, use original
    }

    try {
      const decoded = jwt.verify(decodedToken, config.jwt.secret) as AuthPayload;
      (req as any).user = decoded;
    } catch (error) {
      logger.warn('Invalid token in path', {
        error: error instanceof Error ? error.message : String(error),
        sessionId,
        tokenLength: decodedToken.length,
      });
      res.status(401).json({ error: 'Invalid token' });
      return;
    }

    const session = await browserPool.getSession({
      sessionId,
      userId: (req as any).user.userId,
      deviceMode: 'desktop',
    });

    // Use page.request.fetch() to maintain session state
    const response = await session.page.request.fetch(url, {
      timeout: 10000,
    });

    if (!response) {
      logger.warn('Resource not found', { sessionId, url });
      res.status(404).json({ error: 'Resource not found' });
      return;
    }

    const contentType = response.headers()['content-type'] || '';
    const body = await response.body();

    logger.info('Resource fetched successfully (token in path)', {
      sessionId,
      url: url.substring(0, 100),
      contentType,
      bodyLength: body.length,
      status: response.status(),
    });

    // Set CORS headers to allow iframe access
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.header('Cross-Origin-Resource-Policy', 'cross-origin');
    res.header('Cross-Origin-Embedder-Policy', 'unsafe-none');

    // Remove CSP headers from proxied resources
    res.removeHeader('Content-Security-Policy');
    res.removeHeader('Content-Security-Policy-Report-Only');
    res.removeHeader('X-Content-Security-Policy');

    // Handle JavaScript file requests that returned HTML (usually 404 errors)
    // If the URL ends with .js but content-type is HTML, return empty JS instead
    if (url.endsWith('.js') && contentType.includes('text/html')) {
      logger.warn('JavaScript file returned HTML (likely 404), returning empty JS', {
        sessionId,
        url: url.substring(0, 100),
        status: response.status(),
      });

      // For module federation remoteEntry files, provide a proper empty container
      // This prevents "RemoteScriptNotLoadedError" in the browser console
      if (url.includes('remoteEntry')) {
        const emptyModuleFederation = `
// Empty module federation container for missing remote entry
(function() {
  try {
    if (typeof window !== 'undefined') {
      // Extract container name from URL if possible
      const match = /remoteEntry\\.([^.]+)\\.js/.exec('${url}');
      const containerName = match ? match[1] : 'unknownContainer';

      // Define an empty module federation container
      if (!window[containerName]) {
        window[containerName] = {
          get: function(module) {
            console.warn('[Proxy] Module not available:', containerName, module);
            return Promise.resolve(function() { return {}; });
          },
          init: function() {
            console.warn('[Proxy] Empty container initialized:', containerName);
            return Promise.resolve();
          }
        };
      }
    }
  } catch (e) {
    console.error('[Proxy] Error creating empty module container:', e);
  }
})();
`;
        res.type('application/javascript').send(emptyModuleFederation);
      } else {
        res.type('application/javascript').send('// Resource not found');
      }
      return;
    }

    // If it's CSS, rewrite URLs within it
    if (contentType.includes('text/css') || contentType.includes('css')) {
      const cssText = body.toString('utf-8');
      const rewrittenCss = rewriteCssUrls(cssText, url, sessionId, token);
      res.type('css').send(rewrittenCss);
    } else {
      // For other resources (JS, images, fonts), return as-is
      if (contentType) {
        res.type(contentType);
      }
      res.send(body);
    }
  } catch (error) {
    logger.error('Failed to proxy resource (token in path)', {
      sessionId: req.params.sessionId,
      url: req.query.url,
      error: error instanceof Error ? error.message : String(error),
    });

    // Return empty response instead of error for tracking pixels
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.header('Cross-Origin-Resource-Policy', 'cross-origin');
    res.header('Cross-Origin-Embedder-Policy', 'unsafe-none');

    // Remove CSP headers
    res.removeHeader('Content-Security-Policy');
    res.removeHeader('Content-Security-Policy-Report-Only');
    res.removeHeader('X-Content-Security-Policy');

    res.status(200).send('');
  }
});

/**
 * POST /proxy/session/:sessionId/:token/resource
 * Proxy POST requests with token in path (prevents URL corruption)
 */
router.post('/session/:sessionId/:token/resource', async (req, res) => {
  try {
    const { sessionId, token } = req.params;
    const { url } = req.query;

    if (!url || typeof url !== 'string') {
      res.status(400).json({ error: 'URL parameter is required' });
      return;
    }

    // Verify token (decode URI component in case it was encoded in the URL path)
    let decodedToken = token;
    try {
      decodedToken = decodeURIComponent(token);
    } catch {
      // If decode fails, use original
    }

    try {
      const decoded = jwt.verify(decodedToken, config.jwt.secret) as AuthPayload;
      (req as any).user = decoded;
    } catch (error) {
      logger.warn('Invalid token in path', {
        error: error instanceof Error ? error.message : String(error),
        sessionId,
        tokenLength: decodedToken.length,
      });
      res.status(401).json({ error: 'Invalid token' });
      return;
    }

    const session = await browserPool.getSession({
      sessionId,
      userId: (req as any).user.userId,
      deviceMode: 'desktop',
    });

    // Forward the POST request with headers and body
    const response = await session.page.request.fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': req.headers['content-type'] || 'application/json',
        'Accept': req.headers['accept'] || '*/*',
        'Origin': req.headers['origin'] || '',
        'Referer': req.headers['referer'] || '',
      },
      data: req.body,
      timeout: 10000,
    });

    if (!response) {
      res.status(404).json({ error: 'Resource not found' });
      return;
    }

    const contentType = response.headers()['content-type'] || '';
    const body = await response.body();

    // Set CORS headers to allow iframe access
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.header('Cross-Origin-Resource-Policy', 'cross-origin');
    res.header('Cross-Origin-Embedder-Policy', 'unsafe-none');

    // Remove CSP headers from proxied resources
    res.removeHeader('Content-Security-Policy');
    res.removeHeader('Content-Security-Policy-Report-Only');
    res.removeHeader('X-Content-Security-Policy');

    // Return response
    if (contentType) {
      res.type(contentType);
    }
    res.status(response.status()).send(body);
  } catch (error) {
    logger.error('Failed to proxy POST resource (token in path)', {
      sessionId: req.params.sessionId,
      url: req.query.url,
      error: error instanceof Error ? error.message : String(error),
    });

    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.header('Cross-Origin-Resource-Policy', 'cross-origin');
    res.header('Cross-Origin-Embedder-Policy', 'unsafe-none');

    // Remove CSP headers
    res.removeHeader('Content-Security-Policy');
    res.removeHeader('Content-Security-Policy-Report-Only');
    res.removeHeader('X-Content-Security-Policy');

    res.status(500).json({ error: 'Failed to proxy POST request' });
  }
});

/**
 * OPTIONS /proxy/session/:sessionId/resource
 * Handle CORS preflight requests for resource endpoint (legacy with token in query)
 */
router.options('/session/:sessionId/resource', (_req, res) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Cross-Origin-Resource-Policy', 'cross-origin');
  res.header('Cross-Origin-Embedder-Policy', 'unsafe-none');
  res.sendStatus(204);
});

/**
 * POST /proxy/session/:sessionId/resource
 * Proxy POST requests to external resources
 */
router.post('/session/:sessionId/resource', verifyTokenWithQuery, async (req, res) => {
  try {
    const { sessionId } = req.params;
    const { url } = req.query;

    if (!url || typeof url !== 'string') {
      res.status(400).json({ error: 'URL parameter is required' });
      return;
    }

    const session = await browserPool.getSession({
      sessionId,
      userId: (req as any).user.userId,
      deviceMode: 'desktop',
    });

    // Forward the POST request with headers and body
    const response = await session.page.request.fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': req.headers['content-type'] || 'application/json',
        'Accept': req.headers['accept'] || '*/*',
        'Origin': req.headers['origin'] || '',
        'Referer': req.headers['referer'] || '',
      },
      data: req.body,
      timeout: 10000,
    });

    if (!response) {
      res.status(404).json({ error: 'Resource not found' });
      return;
    }

    const contentType = response.headers()['content-type'] || '';
    const body = await response.body();

    // Set CORS headers to allow iframe access
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.header('Cross-Origin-Resource-Policy', 'cross-origin');
    res.header('Cross-Origin-Embedder-Policy', 'unsafe-none');

    // Remove CSP headers from proxied resources
    res.removeHeader('Content-Security-Policy');
    res.removeHeader('Content-Security-Policy-Report-Only');
    res.removeHeader('X-Content-Security-Policy');

    // Return response
    if (contentType) {
      res.type(contentType);
    }
    res.status(response.status()).send(body);
  } catch (error) {
    logger.error('Failed to proxy POST resource', {
      sessionId: req.params.sessionId,
      url: req.query.url,
      error: error instanceof Error ? error.message : String(error),
    });

    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.header('Cross-Origin-Resource-Policy', 'cross-origin');
    res.header('Cross-Origin-Embedder-Policy', 'unsafe-none');

    // Remove CSP headers
    res.removeHeader('Content-Security-Policy');
    res.removeHeader('Content-Security-Policy-Report-Only');
    res.removeHeader('X-Content-Security-Policy');

    res.status(500).json({ error: 'Failed to proxy POST request' });
  }
});

/**
 * GET /proxy/session/:sessionId/resource
 * Proxy external resources (JS, CSS, images, etc.)
 */
router.get('/session/:sessionId/resource', verifyTokenWithQuery, async (req, res) => {
  try {
    const { sessionId } = req.params;
    const { url } = req.query;

    logger.debug('Resource request', {
      sessionId,
      url,
      hasToken: !!req.query.token,
      hasAuthHeader: !!req.headers.authorization,
    });

    if (!url || typeof url !== 'string') {
      logger.warn('Resource request missing URL parameter', { sessionId, query: req.query });
      res.status(400).json({ error: 'URL parameter is required' });
      return;
    }

    // Get token from either header or query parameter
    const authHeader = req.headers.authorization;
    let token: string;

    if (authHeader && authHeader.startsWith('Bearer ')) {
      token = authHeader.substring(7);
    } else {
      token = req.query.token as string;
    }

    const session = await browserPool.getSession({
      sessionId,
      userId: (req as any).user.userId,
      deviceMode: 'desktop',
    });

    // Use page.request.fetch() instead of page.goto() to avoid navigating away
    // This maintains cookies and session state while making HTTP requests
    const response = await session.page.request.fetch(url, {
      timeout: 10000,
    });

    if (!response) {
      logger.warn('Resource not found', { sessionId, url });
      res.status(404).json({ error: 'Resource not found' });
      return;
    }

    const contentType = response.headers()['content-type'] || '';
    const body = await response.body();

    logger.info('Resource fetched successfully', {
      sessionId,
      url: url.substring(0, 100),
      contentType,
      bodyLength: body.length,
      status: response.status(),
    });

    // Set CORS headers to allow iframe access
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.header('Cross-Origin-Resource-Policy', 'cross-origin');
    res.header('Cross-Origin-Embedder-Policy', 'unsafe-none');

    // Remove CSP headers from proxied resources
    res.removeHeader('Content-Security-Policy');
    res.removeHeader('Content-Security-Policy-Report-Only');
    res.removeHeader('X-Content-Security-Policy');

    // Handle JavaScript file requests that returned HTML (usually 404 errors)
    // If the URL ends with .js but content-type is HTML, return empty JS instead
    if (url.endsWith('.js') && contentType.includes('text/html')) {
      logger.warn('JavaScript file returned HTML (likely 404), returning empty JS', {
        sessionId,
        url: url.substring(0, 100),
        status: response.status(),
      });

      // For module federation remoteEntry files, provide a proper empty container
      // This prevents "RemoteScriptNotLoadedError" in the browser console
      if (url.includes('remoteEntry')) {
        const emptyModuleFederation = `
// Empty module federation container for missing remote entry
(function() {
  try {
    if (typeof window !== 'undefined') {
      // Extract container name from URL if possible
      const match = /remoteEntry\\.([^.]+)\\.js/.exec('${url}');
      const containerName = match ? match[1] : 'unknownContainer';

      // Define an empty module federation container
      if (!window[containerName]) {
        window[containerName] = {
          get: function(module) {
            console.warn('[Proxy] Module not available:', containerName, module);
            return Promise.resolve(function() { return {}; });
          },
          init: function() {
            console.warn('[Proxy] Empty container initialized:', containerName);
            return Promise.resolve();
          }
        };
      }
    }
  } catch (e) {
    console.error('[Proxy] Error creating empty module container:', e);
  }
})();
`;
        res.type('application/javascript').send(emptyModuleFederation);
      } else {
        res.type('application/javascript').send('// Resource not found');
      }
      return;
    }

    // If it's CSS, rewrite URLs within it
    if (contentType.includes('text/css') || contentType.includes('css')) {
      const cssText = body.toString('utf-8');
      const rewrittenCss = rewriteCssUrls(cssText, url, sessionId, token);
      res.type('css').send(rewrittenCss);
    } else {
      // For other resources (JS, images, fonts), return as-is
      if (contentType) {
        res.type(contentType);
      }
      res.send(body);
    }
  } catch (error) {
    logger.error('Failed to proxy resource', {
      sessionId: req.params.sessionId,
      url: req.query.url,
      error: error instanceof Error ? error.message : String(error),
    });

    // For tracking pixels and failed resources, return empty response instead of error
    // This prevents breaking the page when analytics/tracking fails
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.header('Cross-Origin-Resource-Policy', 'cross-origin');
    res.header('Cross-Origin-Embedder-Policy', 'unsafe-none');

    // Remove CSP headers
    res.removeHeader('Content-Security-Policy');
    res.removeHeader('Content-Security-Policy-Report-Only');
    res.removeHeader('X-Content-Security-Policy');

    res.status(200).send('');
  }
});

/**
 * GET /proxy/session/:sessionId/resource/*
 * Catch-all for malformed resource requests with extra path segments
 */
router.get('/session/:sessionId/resource/*', (req, res) => {
  logger.error('Malformed resource request - extra path after resource endpoint', {
    sessionId: req.params.sessionId,
    path: req.path,
    originalUrl: req.originalUrl,
    query: req.query,
    params: req.params,
  });

  res.status(400).json({
    error: 'Malformed resource URL - path should not be appended after /resource',
    receivedPath: req.path,
    hint: 'URLs should be passed as query parameter: /resource?url=...',
    debug: {
      path: req.path,
      query: req.query,
    }
  });
});

/**
 * Detect which provider a URL belongs to
 */
type Provider = 'booking' | 'hotels' | 'expedia' | 'vrbo' | 'airbnb' | 'agoda' | 'trip'
  | 'hostelworld' | 'homestay' | 'travelocity' | 'casasrurales' | 'hometogo' | 'plumguide'
  | 'hipcamp' | 'misterbandb' | 'redawning' | 'ostrovok' | 'fliggy' | 'makemytrip'
  | 'oyo' | 'goibibo' | 'cleartrip' | 'yatra' | 'unknown';

function detectProvider(url: string): Provider {
  try {
    const hostname = new URL(url).hostname.toLowerCase();
    if (hostname.includes('booking.com')) return 'booking';
    if (hostname.includes('hotels.com')) return 'hotels';
    if (hostname.includes('vrbo.com')) return 'vrbo';
    if (hostname.includes('expedia.')) return 'expedia';
    if (hostname.includes('airbnb.')) return 'airbnb';
    if (hostname.includes('agoda.com')) return 'agoda';
    if (hostname.includes('trip.com')) return 'trip';
    if (hostname.includes('hostelworld.com')) return 'hostelworld';
    if (hostname.includes('homestay.com')) return 'homestay';
    if (hostname.includes('travelocity.com')) return 'travelocity';
    if (hostname.includes('casasrurales.net')) return 'casasrurales';
    if (hostname.includes('hometogo.com')) return 'hometogo';
    if (hostname.includes('plumguide.com')) return 'plumguide';
    if (hostname.includes('hipcamp.com')) return 'hipcamp';
    if (hostname.includes('misterbandb.com')) return 'misterbandb';
    if (hostname.includes('redawning.com')) return 'redawning';
    if (hostname.includes('ostrovok.ru')) return 'ostrovok';
    if (hostname.includes('fliggytravel.com') || hostname.includes('fliggy.com')) return 'fliggy';
    if (hostname.includes('makemytrip.')) return 'makemytrip';
    if (hostname.includes('oyorooms.com')) return 'oyo';
    if (hostname.includes('goibibo.com')) return 'goibibo';
    if (hostname.includes('cleartrip.com')) return 'cleartrip';
    if (hostname.includes('yatra.com')) return 'yatra';
    return 'unknown';
  } catch {
    return 'unknown';
  }
}

/**
 * Standalone helper: force currency in URL (mirrors BrowserSession.forceCurrency)
 */
function forceCurrencyUrl(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;
  }
}

// ── Cheerio-based HTML price extraction (mirrors browser extractors) ──

interface HtmlPriceResult { price: string; currency: string; description: string; reserveUrl: string }
interface ParsedPrice { value: number; currency: string }

const CURRENCY_RE = /([\u20AC$\u00A3\u00A5\u20B9\u20BA\u20A9\u20AA\u20BD]|[A-Z]{3})/;
const CURRENCY_SYMBOL_RE = /[\u20AC$\u00A3\u00A5\u20B9\u20BA\u20A9\u20AA\u20BD]/;
const PRICE_LIKE_RE = /[\u20AC$\u00A3\u00A5\u20B9\u20BA\u20A9\u20AA\u20BD]?\s*([0-9][0-9.,]*[0-9]|[0-9]+)/;

function htmlParsePrice(text: string): ParsedPrice | null {
  const cleaned = text.replace(/\s+/g, ' ').trim();
  const currencyMatch = cleaned.match(CURRENCY_RE);
  const currency = currencyMatch ? currencyMatch[1] : '';
  const priceMatch = cleaned.match(PRICE_LIKE_RE);
  const numStr = priceMatch ? priceMatch[0].replace(/[^0-9.,]/g, '') : cleaned.replace(/[^0-9.,]/g, '');
  if (!numStr) return null;
  let value: number;
  const commaThousands = /,\d{3}(?!\d)/.test(numStr);
  const dotThousands = /\.\d{3}(?!\d)/.test(numStr);
  if (commaThousands && !dotThousands) {
    value = parseFloat(numStr.replace(/,/g, ''));
  } else if (dotThousands && !commaThousands) {
    value = parseFloat(numStr.replace(/\./g, '').replace(',', '.'));
  } else {
    const lastComma = numStr.lastIndexOf(',');
    const lastDot = numStr.lastIndexOf('.');
    if (lastComma > lastDot) {
      value = parseFloat(numStr.replace(/\./g, '').replace(',', '.'));
    } else {
      value = parseFloat(numStr.replace(/,/g, ''));
    }
  }
  if (isNaN(value) || value <= 0) return null;
  return { value, currency };
}

function htmlIsStrikethrough($: cheerio.CheerioAPI, el: any): boolean {
  let current: any = el;
  while (current) {
    const tag = current.type === 'tag' ? current.name?.toLowerCase() : '';
    if (tag === 'del' || tag === 's' || tag === 'strike') return true;
    const cls = $(current).attr('class') || '';
    if (/uitk-text-strikethrough|CrossedOutPrice|OriginalPrice|line-through|was-price|origin-price|old-price/i.test(cls)) return true;
    const ariaLabel = $(current).attr('aria-label') || '';
    if (/\bwas\b|\boriginal\b/i.test(ariaLabel)) return true;
    const dataSel = $(current).attr('data-selenium') || '';
    if (/strikethrough|original/i.test(dataSel)) return true;
    const parent = $(current).parent();
    current = parent.length ? parent[0] : null;
  }
  return false;
}

function htmlIsInsideHiddenOrMeta($: cheerio.CheerioAPI, el: any): boolean {
  let current: any = el;
  while (current) {
    const tag = current.type === 'tag' ? current.name?.toLowerCase() : '';
    if (tag === 'script' || tag === 'style' || tag === 'noscript' || tag === 'template') return true;
    if ($(current).attr('type') === 'application/ld+json') return true;
    if ($(current).attr('hidden') !== undefined && $(current).attr('hidden') !== null) return true;
    const parent = $(current).parent();
    current = parent.length && parent[0] !== current ? parent[0] : null;
  }
  return false;
}

function htmlGetDescription($: cheerio.CheerioAPI): string {
  const h1 = $('h1').first();
  return h1.text()?.trim() || '';
}

interface CollectedPrice { value: number; currency: string; isStrikethrough: boolean; tier: number }

function htmlPickLowest(allPrices: CollectedPrice[]): { price: string; currency: string } {
  const nonStrikethrough = allPrices.filter(p => !p.isStrikethrough);
  const candidates = nonStrikethrough.length > 0 ? nonStrikethrough : allPrices;
  candidates.sort((a, b) => a.tier !== b.tier ? a.tier - b.tier : a.value - b.value);
  const seen = new Set<number>();
  const unique: CollectedPrice[] = [];
  for (const c of candidates) {
    if (!seen.has(c.value)) { seen.add(c.value); unique.push(c); }
  }
  const lowest = unique[0];
  return { price: lowest ? String(lowest.value) : '', currency: lowest ? lowest.currency : '' };
}

/**
 * Booking.com HTML extractor
 */
function extractBookingPriceFromHtml(html: string): HtmlPriceResult {
  const $ = cheerio.load(html);

  const priceSelectors = [
    '[data-testid="price-for-x-nights"]',
    '[data-testid="price-and-discounted-price"]',
    '.hprt-price-price',
    '.bui-price-display__value',
    '.prco-valign-middle-helper',
    '[class*="price"] [class*="amount"]',
    '.hprt-table .hprt-price',
  ];

  let priceText = '';
  for (const sel of priceSelectors) {
    const el = $(sel).first();
    const text = el.text()?.trim();
    if (text) { priceText = text; break; }
  }

  const currencyMatch = priceText.match(CURRENCY_RE);
  const currency = currencyMatch ? currencyMatch[1] : '';
  const numericMatch = priceText.match(/[\d,.\s]+/);
  const price = numericMatch ? numericMatch[0].trim().replace(/\s/g, '') : priceText;

  const descSelectors = [
    '.hprt-roomtype-icon-link',
    '[data-testid="property-section--content"] .room_link',
    '.hprt-table .hprt-roomtype-link',
    '.room_link strong',
  ];
  let description = '';
  for (const sel of descSelectors) {
    const el = $(sel).first();
    const text = el.text()?.trim();
    if (text) { description = text; break; }
  }

  let reserveUrl = '';
  const reserveLink = $('a[href*="/book."]').first();
  if (reserveLink.length) reserveUrl = reserveLink.attr('href') || '';

  return { price, currency, description, reserveUrl };
}

/**
 * Hotels.com / Expedia / VRBO / Travelocity HTML extractor (Expedia Group UITK)
 */
function extractExpediaGroupPriceFromHtml(html: string, provider: string): HtmlPriceResult {
  const $ = cheerio.load(html);
  const allPrices: CollectedPrice[] = [];

  const hasCurrency = (text: string) =>
    CURRENCY_SYMBOL_RE.test(text) || /(?:USD|EUR|GBP|CHF|RON|MKD)/.test(text);

  function collectFromContainers(selectors: string[], tier: number) {
    for (const sel of selectors) {
      $(sel).each((_i, container) => {
        $(container).find('span, div, strong, b').each((_j, el) => {
          if (htmlIsInsideHiddenOrMeta($, el)) return;
          const text = $(el).text()?.trim() || '';
          if (hasCurrency(text) && /\d/.test(text)) {
            const parsed = htmlParsePrice(text);
            if (parsed) {
              allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier });
            }
          }
        });
      });
    }
  }

  // Tier 1: UITK price containers
  const stidSelectors = [
    '[data-stid="price-summary"]',
    '[data-stid="content-hotel-lead-price"]',
    '[data-stid="price-lockup"]',
    '[data-stid="price-lockup-wrapper"]',
    '[data-stid="book-now-button"]',
  ];
  if (provider === 'vrbo') stidSelectors.push('[data-stid="section-room-price"]');
  collectFromContainers(stidSelectors, 1);

  // VRBO tier 1b: "per night" / "total" context
  if (allPrices.length === 0 && provider === 'vrbo') {
    $('span, div, h2, h3').each((_i, el) => {
      const text = $(el).text()?.trim() || '';
      if (text.length > 200) return;
      if (hasCurrency(text) && /\d/.test(text)) {
        const parentText = $(el).parent().text()?.toLowerCase() || '';
        if (/night|total|avg|nuit|nacht|notte|noche/i.test(parentText)) {
          const parsed = htmlParsePrice(text);
          if (parsed && parsed.value > 1) {
            allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier: 1 });
          }
        }
      }
    });
  }

  // Tier 2: Broader UITK selectors
  if (allPrices.length === 0) {
    collectFromContainers([
      '[data-stid*="price"]',
      '.uitk-type-500[aria-hidden="true"]', '.uitk-type-600[aria-hidden="true"]',
      '.uitk-type-300',
    ], 2);
  }

  // Tier 3: Generic class selectors
  if (allPrices.length === 0) {
    $('[class*="price"] span, [class*="price"] div').each((_i, el) => {
      if (htmlIsInsideHiddenOrMeta($, el)) return;
      const text = $(el).text()?.trim() || '';
      if (hasCurrency(text) && /\d/.test(text)) {
        const parsed = htmlParsePrice(text);
        if (parsed) allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier: 3 });
      }
    });
  }

  const { price, currency } = htmlPickLowest(allPrices);
  const description = htmlGetDescription($);
  return { price, currency, description, reserveUrl: '' };
}

/**
 * Airbnb HTML extractor
 */
function extractAirbnbPriceFromHtml(html: string): HtmlPriceResult {
  const $ = cheerio.load(html);
  const allPrices: CollectedPrice[] = [];

  const hasCurrency = (text: string) => CURRENCY_SYMBOL_RE.test(text);

  // Tier 1: Booking widget containers
  const bookingSelectors = [
    '[data-testid="book-it-default"]',
    '[data-testid="price-item-total"]',
    '[data-testid="price-item-price-per-night"]',
    '[data-section-id="BOOK_IT_SIDEBAR"]',
    '[aria-label*="price"]', '[aria-label*="Price"]',
    '[aria-label*="cost"]', '[aria-label*="Cost"]',
  ];
  for (const sel of bookingSelectors) {
    $(sel).find('span, div, strong, b').each((_i, el) => {
      const text = $(el).text()?.trim() || '';
      if (hasCurrency(text) && /\d/.test(text)) {
        const parsed = htmlParsePrice(text);
        if (parsed && parsed.value > 1) {
          allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier: 1 });
        }
      }
    });
  }

  // Tier 2: "night" / "total" context
  if (allPrices.length === 0) {
    $('span, div').each((_i, el) => {
      const text = $(el).text()?.trim() || '';
      if (text.length > 200) return;
      if (hasCurrency(text) && /\d/.test(text)) {
        const parentText = $(el).parent().text()?.toLowerCase() || '';
        if (/night|total|nuit|nacht|notte|noche|por noite/i.test(parentText)) {
          const parsed = htmlParsePrice(text);
          if (parsed && parsed.value > 1) {
            allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier: 2 });
          }
        }
      }
    });
  }

  // Tier 3: Broad — any price-like text
  if (allPrices.length === 0) {
    $('span, div, strong').each((_i, el) => {
      if (htmlIsInsideHiddenOrMeta($, el)) return;
      const text = $(el).text()?.trim() || '';
      if (text.length > 40 || text.length < 2) return;
      if (hasCurrency(text) && /\d/.test(text)) {
        const parsed = htmlParsePrice(text);
        if (parsed && parsed.value > 10) {
          allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier: 3 });
        }
      }
    });
  }

  const { price, currency } = htmlPickLowest(allPrices);
  const description = htmlGetDescription($);
  return { price, currency, description, reserveUrl: '' };
}

/**
 * Agoda HTML extractor
 */
function extractAgodaPriceFromHtml(html: string): HtmlPriceResult {
  const $ = cheerio.load(html);
  const allPrices: CollectedPrice[] = [];

  const hasCurrency = (text: string) =>
    CURRENCY_SYMBOL_RE.test(text) || /(?:EUR|USD|GBP|CHF)/.test(text);

  function collectFromContainers(selectors: string[], tier: number, minValue = 50) {
    for (const sel of selectors) {
      $(sel).each((_i, container) => {
        $(container).find('span, div, strong, b, p').each((_j, el) => {
          if (htmlIsInsideHiddenOrMeta($, el)) return;
          const text = $(el).text()?.trim() || '';
          if (text.length > 60 || text.length < 2) return;
          if (!hasCurrency(text) || !/\d/.test(text)) return;
          const parsed = htmlParsePrice(text);
          if (parsed && parsed.value >= minValue) {
            allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier });
          }
        });
      });
    }
  }

  // Tier 1: Room listing containers
  collectFromContainers([
    '#roomSection', '[data-selenium="RoomGridFilter"]',
    '[class*="ChildRoomsList"]', '[class*="MasterRoom"]',
    '[class*="RoomDetail"]', '[id*="roomSection"]',
  ], 1);

  // Tier 2: Agoda data-attribute price elements
  if (allPrices.length === 0) {
    collectFromContainers([
      '[data-selenium="PriceDisplay"]', '[data-selenium="PriceWidget"]',
      '[data-selenium="BookNowBox"]', '[data-element-name="final-price"]',
      '[data-element-name="price"]', '[data-ppapi="room-price"]',
    ], 2);
  }

  // Tier 3: Class-based selectors
  if (allPrices.length === 0) {
    collectFromContainers([
      '[class*="PriceDisplay"]', '[class*="pd-price"]', '[class*="RoomPrice"]',
      '[class*="FinalPrice"]', '[class*="price-box"]', '[class*="BookNow"]',
    ], 3);
  }

  const { price, currency } = htmlPickLowest(allPrices);
  const description = htmlGetDescription($);
  let reserveUrl = '';
  const bookLink = $('a[href*="book"], a[data-selenium="BookNowButton"]').first();
  if (bookLink.length) reserveUrl = bookLink.attr('href') || '';
  return { price, currency, description, reserveUrl };
}

/**
 * Trip.com HTML extractor
 */
function extractTripPriceFromHtml(html: string): HtmlPriceResult {
  const $ = cheerio.load(html);
  const allPrices: CollectedPrice[] = [];

  const hasCurrency = (text: string) =>
    CURRENCY_SYMBOL_RE.test(text) || /(?:EUR|USD|GBP|CHF)/.test(text);

  function collectFromContainers(selectors: string[], tier: number, minValue = 50) {
    for (const sel of selectors) {
      $(sel).each((_i, container) => {
        $(container).find('span, div, strong, b, p, em').each((_j, el) => {
          if (htmlIsInsideHiddenOrMeta($, el)) return;
          const text = $(el).text()?.trim() || '';
          if (text.length > 60 || text.length < 2) return;
          if (!hasCurrency(text) || !/\d/.test(text)) return;
          const parsed = htmlParsePrice(text);
          if (parsed && parsed.value >= minValue) {
            allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier });
          }
        });
      });
    }
  }

  // Tier 1: Room listing containers
  collectFromContainers([
    '[class*="room-list"]', '[class*="roomList"]', '[class*="RoomList"]',
    '[class*="room-item"]', '[class*="roomItem"]', '[class*="RoomItem"]',
    '[class*="room-card"]', '[class*="roomCard"]', '[class*="RoomCard"]',
  ], 1);

  // Tier 2: Price-specific containers
  if (allPrices.length === 0) {
    collectFromContainers([
      '[class*="price-box"]', '[class*="priceBox"]', '[class*="PriceBox"]',
      '[class*="price-num"]', '[class*="priceNum"]', '[class*="PriceNum"]',
      '[class*="hotel-price"]', '[class*="hotelPrice"]', '[class*="HotelPrice"]',
      '[class*="total-price"]', '[class*="totalPrice"]', '[class*="TotalPrice"]',
      '[data-testid*="price"]', '[data-testid*="Price"]',
    ], 2);
  }

  // Tier 3: Broader price-related class selectors
  if (allPrices.length === 0) {
    $('[class*="price"] span, [class*="price"] div, [class*="Price"] span, [class*="Price"] div').each((_i, el) => {
      if (htmlIsInsideHiddenOrMeta($, el)) return;
      const text = $(el).text()?.trim() || '';
      if (text.length > 60 || text.length < 2) return;
      if (!hasCurrency(text) || !/\d/.test(text)) return;
      const parsed = htmlParsePrice(text);
      if (parsed && parsed.value >= 50) {
        allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier: 3 });
      }
    });
  }

  const { price, currency } = htmlPickLowest(allPrices);
  const description = htmlGetDescription($);
  let reserveUrl = '';
  const bookLink = $('a[href*="book"], button[class*="book"], button[class*="Book"]').first();
  if (bookLink.length) reserveUrl = bookLink.attr('href') || '';
  return { price, currency, description, reserveUrl };
}

/**
 * MakeMyTrip / Goibibo HTML extractor.
 * MakeMyTrip: prices in .latoBlack.font28 (hero), .latoBlack.font26 (room list), strikethrough via .lineThrough.
 * Goibibo: prices in BookingWidget-styles__PriceValueStyled (hero), RoomFlavor-styles__ActualPriceTextStyled
 *          (room list). Currency shown via SVG RupeeIcon (no ₹ in text). Strikethrough via StrikeThrough class.
 */
function extractMakeMyTripPriceFromHtml(html: string): HtmlPriceResult {
  const $ = cheerio.load(html);
  const allPrices: CollectedPrice[] = [];

  // Detect currency from SVG RupeeIcon or from text content
  const hasRupeeIcon = $('[class*="RupeeIcon"]').length > 0;
  const inferredCurrency = hasRupeeIcon ? '\u20B9' : '';

  // Collect helper that allows numbers without currency symbols (for Goibibo pattern)
  function collectWithInfer(selector: string, tier: number, skipTaxes = false) {
    $(selector).each((_i, el) => {
      if (htmlIsInsideHiddenOrMeta($, el)) return;
      const cls = $(el).attr('class') || '';
      // Skip tax/fee elements
      if (skipTaxes && /TaxesText|PlusPriceValue/i.test(cls)) return;
      const text = $(el).text()?.trim() || '';
      if (text.length > 40 || text.length < 2) return;
      if (!/\d/.test(text)) return;
      // Skip "taxes & fees" text
      if (/tax|fee/i.test(text)) return;
      const parsed = htmlParsePrice(text);
      if (parsed && parsed.value >= 5) {
        const currency = parsed.currency || inferredCurrency;
        allPrices.push({ value: parsed.value, currency, isStrikethrough: htmlIsStrikethrough($, el), tier });
      }
    });
  }

  function collectStrict(selector: string, tier: number) {
    $(selector).each((_i, el) => {
      if (htmlIsInsideHiddenOrMeta($, el)) return;
      const text = $(el).text()?.trim() || '';
      if (text.length > 40 || text.length < 2) return;
      if (!CURRENCY_SYMBOL_RE.test(text) || !/\d/.test(text)) return;
      const parsed = htmlParsePrice(text);
      if (parsed && parsed.value >= 5) {
        allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier });
      }
    });
  }

  // ── MakeMyTrip pattern ──
  // Tier 1: Hero price (.latoBlack.font28)
  collectStrict('.latoBlack.font28', 1);
  // Tier 2: Room list prices (.latoBlack.font26)
  if (allPrices.length === 0) {
    collectStrict('.latoBlack.font26', 2);
  }

  // ── Goibibo pattern (styled-components) ──
  // Tier 1: Booking widget hero price
  if (allPrices.length === 0) {
    collectWithInfer('[class*="BookingWidget-styles__PriceValueStyled"]', 1, true);
  }
  // Tier 2: Room list actual prices
  if (allPrices.length === 0) {
    collectWithInfer('[class*="RoomFlavor-styles__ActualPriceTextStyled"]', 2);
  }

  // ── Shared fallback: "Per Night" context ──
  if (allPrices.length === 0) {
    $('div').each((_i, el) => {
      const text = $(el).text()?.trim() || '';
      if (!/per night/i.test(text) || text.length > 120) return;
      $(el).find('div, span, strong, b, p').each((_j, child) => {
        if (htmlIsInsideHiddenOrMeta($, child)) return;
        const childText = $(child).text()?.trim() || '';
        if (childText.length > 30 || childText.length < 2) return;
        if (!/\d/.test(childText)) return;
        if (/tax|fee/i.test(childText)) return;
        const parsed = htmlParsePrice(childText);
        if (parsed && parsed.value >= 10) {
          const currency = parsed.currency || inferredCurrency;
          allPrices.push({ value: parsed.value, currency, isStrikethrough: htmlIsStrikethrough($, child), tier: 3 });
        }
      });
    });
  }

  const { price, currency } = htmlPickLowest(allPrices);
  const description = htmlGetDescription($);
  return { price, currency, description, reserveUrl: '' };
}

/**
 * Cleartrip HTML extractor.
 * Prices in h2/h3 inside #selectRoomHighlights or room-card containers, with ₹ symbol.
 * Uses obfuscated styled-components (sc-*) classes — no "price" keyword in class names.
 */
function extractCleartripPriceFromHtml(html: string): HtmlPriceResult {
  const $ = cheerio.load(html);
  const allPrices: CollectedPrice[] = [];

  function collect(selector: string, tier: number) {
    $(selector).each((_i, el) => {
      if (htmlIsInsideHiddenOrMeta($, el)) return;
      const text = $(el).text()?.trim() || '';
      if (text.length > 40 || text.length < 2) return;
      if (!CURRENCY_SYMBOL_RE.test(text) || !/\d/.test(text)) return;
      // Skip taxes & fees
      if (/tax|fee/i.test(text)) return;
      const parsed = htmlParsePrice(text);
      if (parsed && parsed.value >= 50) {
        allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier });
      }
    });
  }

  // Tier 1: Price headings inside #selectRoomHighlights (booking widget hero)
  collect('#selectRoomHighlights h2, #selectRoomHighlights h3', 1);

  // Tier 2: Any h2/h3 with currency near "night" context
  if (allPrices.length === 0) {
    $('h2, h3').each((_i, el) => {
      if (htmlIsInsideHiddenOrMeta($, el)) return;
      const text = $(el).text()?.trim() || '';
      if (text.length > 40 || text.length < 3) return;
      if (!CURRENCY_SYMBOL_RE.test(text) || !/\d/.test(text)) return;
      if (/tax|fee/i.test(text)) return;
      const parentText = $(el).parent().text()?.toLowerCase() || '';
      if (/night|total/i.test(parentText)) {
        const parsed = htmlParsePrice(text);
        if (parsed && parsed.value >= 50) {
          allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier: 2 });
        }
      }
    });
  }

  // Tier 3: Fallback to generic-like broad scan (h2/h3/p/span/div with currency + night context)
  if (allPrices.length === 0) {
    $('h2, h3, span, div').each((_i, el) => {
      if (htmlIsInsideHiddenOrMeta($, el)) return;
      const text = $(el).text()?.trim() || '';
      if (text.length > 60 || text.length < 3) return;
      if (!CURRENCY_SYMBOL_RE.test(text) || !/\d/.test(text)) return;
      if (/tax|fee/i.test(text)) return;
      const parsed = htmlParsePrice(text);
      if (parsed && parsed.value >= 100) {
        allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier: 3 });
      }
    });
  }

  const { price, currency } = htmlPickLowest(allPrices);
  const description = htmlGetDescription($);
  return { price, currency, description, reserveUrl: '' };
}

/**
 * Generic HTML extractor for all other providers (Hostelworld, Homestay, CasasRurales, etc.)
 */
function extractGenericPriceFromHtml(html: string): HtmlPriceResult {
  const $ = cheerio.load(html);
  const allPrices: CollectedPrice[] = [];

  const hasCurrency = (text: string) =>
    CURRENCY_SYMBOL_RE.test(text) || /(?:EUR|USD|GBP|CHF)/.test(text);

  function collectVisible(selector: string, tier: number, minValue = 5) {
    $(selector).each((_i, el) => {
      if (htmlIsInsideHiddenOrMeta($, el)) return;
      const text = $(el).text()?.trim() || '';
      if (text.length > 60 || text.length < 2) return;
      if (!hasCurrency(text) || !/\d/.test(text)) return;
      const parsed = htmlParsePrice(text);
      if (parsed && parsed.value >= minValue) {
        allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier });
      }
    });
  }

  // Tier 1: Elements with price-related classes or data attributes (children)
  collectVisible('[class*="price"] span, [class*="price"] div, [class*="price"] strong, [class*="price"] b, [class*="price"] p, [class*="price"] em', 1);
  collectVisible('[class*="Price"] span, [class*="Price"] div, [class*="Price"] strong, [class*="Price"] b, [class*="Price"] p', 1);
  collectVisible('[data-testid*="price"] span, [data-testid*="price"] div, [data-testid*="price"] p, [data-testid*="Price"] span, [data-testid*="Price"] p', 1);
  // Also match the data-testid element itself (e.g. <p data-testid="price-label">$284</p>)
  collectVisible('[data-testid*="price"], [data-testid*="Price"]', 1);

  // Tier 2: Rate/cost/amount containers
  if (allPrices.length === 0) {
    collectVisible('[class*="rate"] span, [class*="Rate"] span, [class*="cost"] span, [class*="Cost"] span', 2);
    collectVisible('[class*="amount"] span, [class*="Amount"] span, [class*="total"] span, [class*="Total"] span', 2);
  }

  // Tier 3: Direct price-class elements
  if (allPrices.length === 0) {
    collectVisible('[class*="price"], [class*="Price"], [class*="rate"], [class*="Rate"]', 3);
  }

  // Tier 4: "/ night" or "total" context — find currency+number near booking keywords
  // Catches sites with obfuscated class names (e.g. Hipcamp, Cleartrip styled-components)
  if (allPrices.length === 0) {
    $('span, div, h2, h3, p, strong').each((_i, el) => {
      if (htmlIsInsideHiddenOrMeta($, el)) return;
      const text = $(el).text()?.trim() || '';
      if (text.length > 80 || text.length < 3) return;
      if (!hasCurrency(text) || !/\d/.test(text)) return;
      const parentText = $(el).parent().text()?.toLowerCase() || '';
      if (/night|total|nuit|nacht|notte|noche/i.test(parentText) || /night|total/i.test(text)) {
        const parsed = htmlParsePrice(text);
        if (parsed && parsed.value >= 5) {
          allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier: 4 });
        }
      }
    });
  }

  // Tier 5: Broadest fallback — any short text with currency symbol + digits
  if (allPrices.length === 0) {
    $('span, div, h2, h3, p, strong, b').each((_i, el) => {
      if (htmlIsInsideHiddenOrMeta($, el)) return;
      const text = $(el).text()?.trim() || '';
      if (text.length > 40 || text.length < 2) return;
      if (!CURRENCY_SYMBOL_RE.test(text) || !/\d/.test(text)) return;
      const parsed = htmlParsePrice(text);
      if (parsed && parsed.value >= 10) {
        allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: htmlIsStrikethrough($, el), tier: 5 });
      }
    });
  }

  const { price, currency } = htmlPickLowest(allPrices);
  const description = htmlGetDescription($);
  return { price, currency, description, reserveUrl: '' };
}

/**
 * Dispatch to the correct cheerio HTML extractor based on provider
 */
function extractPriceFromHtml(html: string, provider: Provider = 'unknown'): HtmlPriceResult {
  switch (provider) {
    case 'booking':
      return extractBookingPriceFromHtml(html);
    case 'hotels':
    case 'expedia':
    case 'vrbo':
    case 'travelocity':
      return extractExpediaGroupPriceFromHtml(html, provider);
    case 'airbnb':
      return extractAirbnbPriceFromHtml(html);
    case 'agoda':
      return extractAgodaPriceFromHtml(html);
    case 'trip':
      return extractTripPriceFromHtml(html);
    case 'makemytrip':
    case 'goibibo':
      return extractMakeMyTripPriceFromHtml(html);
    case 'cleartrip':
      return extractCleartripPriceFromHtml(html);
    default:
      return extractGenericPriceFromHtml(html);
  }
}

/**
 * Helper: extract the lowest non-strikethrough price from a Hotels.com property page (with retry)
 */
async function extractHotelsPriceFromPage(page: any, retries = 3): Promise<any> {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      await page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => {});
      // Wait for price-related selectors to appear (Hotels.com never reaches networkidle)
      await page.waitForSelector('[data-stid="price-summary"], [data-stid="content-hotel-lead-price"], [data-test-id="price-summary"], [class*="price"]', { timeout: 15000 }).catch(() => {});
      // Settle time for SPA hydration
      await page.waitForTimeout(3000);

      // Use string-based evaluate to bypass esbuild __name transformation
      return await page.evaluate(`(function() {
        var allPrices = [];

        function isStrikethrough(el) {
          var current = el;
          while (current) {
            var tag = current.tagName.toLowerCase();
            if (tag === 'del' || tag === 's' || tag === 'strike') return true;
            var style = window.getComputedStyle(current);
            if (style.textDecoration.includes('line-through') ||
                (style.textDecorationLine && style.textDecorationLine.includes('line-through'))) return true;
            if (current.classList.contains('uitk-text-strikethrough') ||
                (current.getAttribute('aria-label') || '').toLowerCase().includes('was ') ||
                (current.getAttribute('aria-label') || '').toLowerCase().includes('original')) return true;
            current = current.parentElement;
          }
          return false;
        }

        function parsePrice(text) {
          var cleaned = text.replace(/\\s+/g, ' ').trim();
          var currencyMatch = cleaned.match(/([\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]|[A-Z]{3})/);
          var currency = currencyMatch ? currencyMatch[1] : '';
          // Extract just the first price-like number (e.g. "1,354" from "$1,354 for 7 nights")
          var priceMatch = cleaned.match(/[\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]?\\s*([0-9][0-9.,]*[0-9]|[0-9]+)/);
          var numStr = priceMatch ? priceMatch[0].replace(/[^0-9.,]/g, '') : cleaned.replace(/[^0-9.,]/g, '');
          if (!numStr) return null;
          var value;
          var commaThousands = /,\\d{3}(?!\\d)/.test(numStr);
          var dotThousands = /\\.\\d{3}(?!\\d)/.test(numStr);
          if (commaThousands && !dotThousands) {
            value = parseFloat(numStr.replace(/,/g, ''));
          } else if (dotThousands && !commaThousands) {
            value = parseFloat(numStr.replace(/\\./g, '').replace(',', '.'));
          } else {
            var lastComma = numStr.lastIndexOf(',');
            var lastDot = numStr.lastIndexOf('.');
            if (lastComma > lastDot) {
              value = parseFloat(numStr.replace(/\\./g, '').replace(',', '.'));
            } else {
              value = parseFloat(numStr.replace(/,/g, ''));
            }
          }
          if (isNaN(value) || value <= 0) return null;
          return { value: value, currency: currency };
        }

        var priceSummaries = document.querySelectorAll(
          '[data-stid="price-summary"], [data-stid="content-hotel-lead-price"], [data-test-id="price-summary"]'
        );
        priceSummaries.forEach(function(container) {
          container.querySelectorAll('span, div, strong, b').forEach(function(el) {
            var text = (el.textContent || '').trim();
            if ((/[\\u20AC$\\u00A3\\u00A5\\u20B9]|(?:USD|EUR|GBP|CHF|RON|MKD)/).test(text) && /\\d/.test(text)) {
              var parsed = parsePrice(text);
              if (parsed) {
                allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(el) });
              }
            }
          });
        });

        if (allPrices.length === 0) {
          var priceSelectors = [
            '[data-stid*="price"] span', '[data-stid*="price"] div',
            '.uitk-type-500[aria-hidden="true"]', '.uitk-type-600[aria-hidden="true"]',
            '[class*="price"] span', '[class*="price"] div'
          ];
          for (var s = 0; s < priceSelectors.length; s++) {
            document.querySelectorAll(priceSelectors[s]).forEach(function(el) {
              var text = (el.textContent || '').trim();
              if ((/[\\u20AC$\\u00A3\\u00A5\\u20B9]|(?:USD|EUR|GBP|CHF|RON|MKD)/).test(text) && /\\d/.test(text)) {
                var parsed = parsePrice(text);
                if (parsed) {
                  allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(el) });
                }
              }
            });
          }
        }

        if (allPrices.length === 0) {
          var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
          var node;
          while ((node = walker.nextNode())) {
            var text = (node.textContent || '').trim();
            if (/[\\u20AC$\\u00A3\\u00A5\\u20B9]\\s*[\\d,.\\s]+/.test(text) || /[\\d,.\\s]+\\s*[\\u20AC$\\u00A3\\u00A5\\u20B9]/.test(text)) {
              var parsed = parsePrice(text);
              if (parsed && parsed.value > 10 && node.parentElement) {
                allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(node.parentElement) });
              }
            }
          }
        }

        var nonStrikethrough = allPrices.filter(function(p) { return !p.isStrikethrough; });
        var candidates = nonStrikethrough.length > 0 ? nonStrikethrough : allPrices;
        var seen = {};
        var unique = [];
        for (var i = 0; i < candidates.length; i++) {
          if (!seen[candidates[i].value]) { seen[candidates[i].value] = true; unique.push(candidates[i]); }
        }
        unique.sort(function(a, b) { return a.value - b.value; });
        var lowest = unique[0];

        var description = '';
        var h1 = document.querySelector('h1');
        if (h1 && h1.textContent && h1.textContent.trim()) {
          description = h1.textContent.trim();
        }

        return {
          price: lowest ? String(lowest.value) : '',
          currency: lowest ? lowest.currency : '',
          description: description,
          reserveUrl: '',
          allPricesFound: unique.length,
          hadStrikethroughPrices: allPrices.some(function(p) { return p.isStrikethrough; })
        };
      })()`);
    } catch (err: any) {
      if (attempt < retries - 1 && err.message?.includes('context was destroyed')) {
        await new Promise(r => setTimeout(r, 1000));
        continue;
      }
      throw err;
    }
  }
}

/**
 * Helper: extract the lowest non-strikethrough price from an Expedia property page (with retry).
 * Expedia shares the Expedia Group UITK framework with Hotels.com but uses slightly different
 * data-stid values and layout.  We target the booking sidebar / price summary first, then
 * fall back to broader selectors and a full TreeWalker scan.
 */
async function extractExpediaPriceFromPage(page: any, retries = 3): Promise<any> {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      await page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => {});
      // Wait for price elements to render (Expedia SPA hydration)
      await page.waitForSelector('[data-stid="price-summary"], [data-stid="content-hotel-lead-price"], [data-stid="price-lockup"], [data-stid="book-now-button"], [class*="price"]', { timeout: 15000 }).catch(() => {});
      await page.waitForTimeout(3000);

      return await page.evaluate(`(function() {
        var allPrices = [];

        function isStrikethrough(el) {
          var current = el;
          while (current) {
            var tag = current.tagName.toLowerCase();
            if (tag === 'del' || tag === 's' || tag === 'strike') return true;
            var style = window.getComputedStyle(current);
            if (style.textDecoration.includes('line-through') ||
                (style.textDecorationLine && style.textDecorationLine.includes('line-through'))) return true;
            if (current.classList.contains('uitk-text-strikethrough') ||
                (current.getAttribute('aria-label') || '').toLowerCase().includes('was ') ||
                (current.getAttribute('aria-label') || '').toLowerCase().includes('original')) return true;
            current = current.parentElement;
          }
          return false;
        }

        function parsePrice(text) {
          var cleaned = text.replace(/\\s+/g, ' ').trim();
          var currencyMatch = cleaned.match(/([\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]|[A-Z]{3})/);
          var currency = currencyMatch ? currencyMatch[1] : '';
          // Extract just the first price-like number (e.g. "1,354" from "$1,354 for 7 nights")
          var priceMatch = cleaned.match(/[\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]?\\s*([0-9][0-9.,]*[0-9]|[0-9]+)/);
          var numStr = priceMatch ? priceMatch[0].replace(/[^0-9.,]/g, '') : cleaned.replace(/[^0-9.,]/g, '');
          if (!numStr) return null;
          var value;
          var commaThousands = /,\\d{3}(?!\\d)/.test(numStr);
          var dotThousands = /\\.\\d{3}(?!\\d)/.test(numStr);
          if (commaThousands && !dotThousands) {
            value = parseFloat(numStr.replace(/,/g, ''));
          } else if (dotThousands && !commaThousands) {
            value = parseFloat(numStr.replace(/\\./g, '').replace(',', '.'));
          } else {
            var lastComma = numStr.lastIndexOf(',');
            var lastDot = numStr.lastIndexOf('.');
            if (lastComma > lastDot) {
              value = parseFloat(numStr.replace(/\\./g, '').replace(',', '.'));
            } else {
              value = parseFloat(numStr.replace(/,/g, ''));
            }
          }
          if (isNaN(value) || value <= 0) return null;
          return { value: value, currency: currency };
        }

        // 1) Expedia UITK price containers (data-stid based)
        var stidSelectors = [
          '[data-stid="price-summary"]',
          '[data-stid="content-hotel-lead-price"]',
          '[data-stid="price-lockup"]',
          '[data-stid="price-lockup-wrapper"]',
          '[data-stid="book-now-button"]'
        ];
        for (var si = 0; si < stidSelectors.length; si++) {
          var containers = document.querySelectorAll(stidSelectors[si]);
          containers.forEach(function(container) {
            container.querySelectorAll('span, div, strong, b').forEach(function(el) {
              var text = (el.textContent || '').trim();
              if ((/[\\u20AC$\\u00A3\\u00A5\\u20B9]|(?:USD|EUR|GBP|CHF|RON|MKD)/).test(text) && /\\d/.test(text)) {
                var parsed = parsePrice(text);
                if (parsed) {
                  allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(el) });
                }
              }
            });
          });
        }

        // 2) Broader UITK selectors
        if (allPrices.length === 0) {
          var broadSelectors = [
            '[data-stid*="price"] span', '[data-stid*="price"] div',
            '.uitk-type-500[aria-hidden="true"]', '.uitk-type-600[aria-hidden="true"]',
            '.uitk-type-300 span',
            '[class*="price"] span', '[class*="price"] div'
          ];
          for (var bs = 0; bs < broadSelectors.length; bs++) {
            document.querySelectorAll(broadSelectors[bs]).forEach(function(el) {
              var text = (el.textContent || '').trim();
              if ((/[\\u20AC$\\u00A3\\u00A5\\u20B9]|(?:USD|EUR|GBP|CHF|RON|MKD)/).test(text) && /\\d/.test(text)) {
                var parsed = parsePrice(text);
                if (parsed) {
                  allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(el) });
                }
              }
            });
          }
        }

        // 3) Full-page TreeWalker fallback
        if (allPrices.length === 0) {
          var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
          var node;
          while ((node = walker.nextNode())) {
            var text = (node.textContent || '').trim();
            if (/[\\u20AC$\\u00A3\\u00A5\\u20B9]\\s*[\\d,.\\s]+/.test(text) || /[\\d,.\\s]+\\s*[\\u20AC$\\u00A3\\u00A5\\u20B9]/.test(text)) {
              var parsed = parsePrice(text);
              if (parsed && parsed.value > 10 && node.parentElement) {
                allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(node.parentElement) });
              }
            }
          }
        }

        var nonStrikethrough = allPrices.filter(function(p) { return !p.isStrikethrough; });
        var candidates = nonStrikethrough.length > 0 ? nonStrikethrough : allPrices;
        var seen = {};
        var unique = [];
        for (var i = 0; i < candidates.length; i++) {
          if (!seen[candidates[i].value]) { seen[candidates[i].value] = true; unique.push(candidates[i]); }
        }
        unique.sort(function(a, b) { return a.value - b.value; });
        var lowest = unique[0];

        var description = '';
        var h1 = document.querySelector('h1');
        if (h1 && h1.textContent && h1.textContent.trim()) {
          description = h1.textContent.trim();
        }

        return {
          price: lowest ? String(lowest.value) : '',
          currency: lowest ? lowest.currency : '',
          description: description,
          reserveUrl: '',
          allPricesFound: unique.length,
          hadStrikethroughPrices: allPrices.some(function(p) { return p.isStrikethrough; })
        };
      })()`);
    } catch (err: any) {
      if (attempt < retries - 1 && err.message?.includes('context was destroyed')) {
        await new Promise(r => setTimeout(r, 1000));
        continue;
      }
      throw err;
    }
  }
}

/**
 * Helper: extract the lowest non-strikethrough price from a VRBO property page (with retry).
 * VRBO is part of Expedia Group and uses the UITK framework.  The booking sidebar typically
 * lives inside [data-stid="price-summary"] or the "book now" section.  We also look for
 * the per-night and total price patterns that VRBO renders.
 */
async function extractVrboPriceFromPage(page: any, retries = 3): Promise<any> {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      await page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => {});
      await page.waitForSelector('[data-stid="price-summary"], [data-stid="price-lockup"], [data-stid="book-now-button"], [data-stid="content-hotel-lead-price"], [class*="price"]', { timeout: 15000 }).catch(() => {});
      await page.waitForTimeout(3000);

      return await page.evaluate(`(function() {
        var allPrices = [];

        function isStrikethrough(el) {
          var current = el;
          while (current) {
            var tag = current.tagName.toLowerCase();
            if (tag === 'del' || tag === 's' || tag === 'strike') return true;
            var style = window.getComputedStyle(current);
            if (style.textDecoration.includes('line-through') ||
                (style.textDecorationLine && style.textDecorationLine.includes('line-through'))) return true;
            if (current.classList.contains('uitk-text-strikethrough') ||
                (current.getAttribute('aria-label') || '').toLowerCase().includes('was ') ||
                (current.getAttribute('aria-label') || '').toLowerCase().includes('original')) return true;
            current = current.parentElement;
          }
          return false;
        }

        function parsePrice(text) {
          var cleaned = text.replace(/\\s+/g, ' ').trim();
          var currencyMatch = cleaned.match(/([\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]|[A-Z]{3})/);
          var currency = currencyMatch ? currencyMatch[1] : '';
          // Extract just the first price-like number (e.g. "1,354" from "$1,354 for 7 nights")
          var priceMatch = cleaned.match(/[\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]?\\s*([0-9][0-9.,]*[0-9]|[0-9]+)/);
          var numStr = priceMatch ? priceMatch[0].replace(/[^0-9.,]/g, '') : cleaned.replace(/[^0-9.,]/g, '');
          if (!numStr) return null;
          var value;
          // Detect thousands vs decimal separators:
          // - Comma/dot followed by exactly 3 digits at end = thousands separator (e.g. 1,354 or 1.354)
          // - Comma/dot followed by 1-2 digits at end = decimal separator (e.g. 1,35 or 1.35)
          var commaThousands = /,\\d{3}(?!\\d)/.test(numStr);
          var dotThousands = /\\.\\d{3}(?!\\d)/.test(numStr);
          if (commaThousands && !dotThousands) {
            // e.g. $1,354 or $1,354.50 — comma is thousands separator
            value = parseFloat(numStr.replace(/,/g, ''));
          } else if (dotThousands && !commaThousands) {
            // e.g. €1.354 or €1.354,50 — dot is thousands separator
            value = parseFloat(numStr.replace(/\\./g, '').replace(',', '.'));
          } else {
            // Fallback: use last separator as decimal
            var lastComma = numStr.lastIndexOf(',');
            var lastDot = numStr.lastIndexOf('.');
            if (lastComma > lastDot) {
              value = parseFloat(numStr.replace(/\\./g, '').replace(',', '.'));
            } else {
              value = parseFloat(numStr.replace(/,/g, ''));
            }
          }
          if (isNaN(value) || value <= 0) return null;
          return { value: value, currency: currency };
        }

        // 1) UITK price containers used by VRBO
        var stidSelectors = [
          '[data-stid="price-summary"]',
          '[data-stid="content-hotel-lead-price"]',
          '[data-stid="price-lockup"]',
          '[data-stid="price-lockup-wrapper"]',
          '[data-stid="book-now-button"]',
          '[data-stid="section-room-price"]'
        ];
        for (var si = 0; si < stidSelectors.length; si++) {
          var containers = document.querySelectorAll(stidSelectors[si]);
          containers.forEach(function(container) {
            container.querySelectorAll('span, div, strong, b').forEach(function(el) {
              var text = (el.textContent || '').trim();
              if ((/[\\u20AC$\\u00A3\\u00A5\\u20B9]|(?:USD|EUR|GBP|CHF|RON|MKD)/).test(text) && /\\d/.test(text)) {
                var parsed = parsePrice(text);
                if (parsed) {
                  allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(el) });
                }
              }
            });
          });
        }

        // 2) VRBO-specific: look for "per night" / "total" price patterns near those keywords
        if (allPrices.length === 0) {
          var allElements = document.querySelectorAll('span, div, h2, h3');
          allElements.forEach(function(el) {
            var text = (el.textContent || '').trim();
            if (text.length > 200) return;
            if ((/[\\u20AC$\\u00A3\\u00A5\\u20B9]|(?:USD|EUR|GBP|CHF)/).test(text) && /\\d/.test(text)) {
              var parentText = (el.parentElement ? el.parentElement.textContent : '').toLowerCase();
              if (/night|total|avg|nuit|nacht|notte|noche/i.test(parentText)) {
                var parsed = parsePrice(text);
                if (parsed && parsed.value > 1) {
                  allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(el) });
                }
              }
            }
          });
        }

        // 3) Broader UITK selectors
        if (allPrices.length === 0) {
          var broadSelectors = [
            '[data-stid*="price"] span', '[data-stid*="price"] div',
            '.uitk-type-500[aria-hidden="true"]', '.uitk-type-600[aria-hidden="true"]',
            '.uitk-type-300 span',
            '[class*="price"] span', '[class*="price"] div'
          ];
          for (var bs = 0; bs < broadSelectors.length; bs++) {
            document.querySelectorAll(broadSelectors[bs]).forEach(function(el) {
              var text = (el.textContent || '').trim();
              if ((/[\\u20AC$\\u00A3\\u00A5\\u20B9]|(?:USD|EUR|GBP|CHF|RON|MKD)/).test(text) && /\\d/.test(text)) {
                var parsed = parsePrice(text);
                if (parsed) {
                  allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(el) });
                }
              }
            });
          }
        }

        // 4) Full-page TreeWalker fallback
        if (allPrices.length === 0) {
          var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
          var node;
          while ((node = walker.nextNode())) {
            var text = (node.textContent || '').trim();
            if (/[\\u20AC$\\u00A3\\u00A5\\u20B9]\\s*[\\d,.\\s]+/.test(text) || /[\\d,.\\s]+\\s*[\\u20AC$\\u00A3\\u00A5\\u20B9]/.test(text)) {
              var parsed = parsePrice(text);
              if (parsed && parsed.value > 10 && node.parentElement) {
                allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(node.parentElement) });
              }
            }
          }
        }

        var nonStrikethrough = allPrices.filter(function(p) { return !p.isStrikethrough; });
        var candidates = nonStrikethrough.length > 0 ? nonStrikethrough : allPrices;
        var seen = {};
        var unique = [];
        for (var i = 0; i < candidates.length; i++) {
          if (!seen[candidates[i].value]) { seen[candidates[i].value] = true; unique.push(candidates[i]); }
        }
        unique.sort(function(a, b) { return a.value - b.value; });
        var lowest = unique[0];

        var description = '';
        var h1 = document.querySelector('h1');
        if (h1 && h1.textContent && h1.textContent.trim()) {
          description = h1.textContent.trim();
        }

        return {
          price: lowest ? String(lowest.value) : '',
          currency: lowest ? lowest.currency : '',
          description: description,
          reserveUrl: '',
          allPricesFound: unique.length,
          hadStrikethroughPrices: allPrices.some(function(p) { return p.isStrikethrough; })
        };
      })()`);
    } catch (err: any) {
      if (attempt < retries - 1 && err.message?.includes('context was destroyed')) {
        await new Promise(r => setTimeout(r, 1000));
        continue;
      }
      throw err;
    }
  }
}

/**
 * Helper: extract price data from an Airbnb listing page.
 * Airbnb uses obfuscated class names that rotate, so we rely on:
 *  - Text content patterns (currency + numbers near "night"/"total")
 *  - data-testid attributes where available
 *  - Broad TreeWalker scan as fallback
 */
async function extractAirbnbPriceFromPage(page: any, retries = 3): Promise<any> {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      // Airbnb is heavily SPA — wait for network to settle and booking widget to render
      await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
      // Wait for Airbnb booking widget or price elements
      await page.waitForSelector('[data-testid="book-it-default"], [data-section-id="BOOK_IT_SIDEBAR"], [data-testid="price-item-total"]', { timeout: 15000 }).catch(() => {});
      // Extra settle time for SPA hydration
      await page.waitForTimeout(3000);

      // Use string-based evaluate to bypass esbuild __name transformation
      return await page.evaluate(`(function() {
        var allPrices = [];

        function parsePrice(text) {
          var currMatch = text.match(/([\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]|(?:USD|EUR|GBP|CHF|RON|MKD|AUD|CAD|NZD|SEK|NOK|DKK|CZK|PLN|HUF|BGN|HRK|RUB|TRY|BRL|MXN|ARS|CLP|COP|PEN|PHP|THB|MYR|SGD|IDR|INR|KRW|JPY|TWD|HKD|ZAR|ILS|AED|SAR|QAR|KWD|BHD|OMR|JOD|EGP|MAD|TND|DZD|LBP))/i);
          var numMatch = text.match(/[\\d,.\\s]+/);
          if (!numMatch) return null;
          var raw = numMatch[0].replace(/[\\s,]/g, '').replace(/\\.(?=\\d{3}$)/, '');
          var value = parseFloat(raw);
          if (isNaN(value) || value <= 0) return null;
          return { value: value, currency: currMatch ? currMatch[1] : '' };
        }

        function isStrikethrough(el) {
          var tag = (el.tagName || '').toLowerCase();
          if (tag === 'del' || tag === 's' || tag === 'strike') return true;
          try {
            var style = window.getComputedStyle(el);
            if (style.textDecoration.includes('line-through') ||
                (style.textDecorationLine && style.textDecorationLine.includes('line-through'))) return true;
          } catch(e) {}
          return false;
        }

        var bookingSelectors = [
          '[data-testid="book-it-default"]',
          '[data-testid="price-item-total"]',
          '[data-testid="price-item-price-per-night"]',
          '[data-section-id="BOOK_IT_SIDEBAR"]',
          '[aria-label*="price"]',
          '[aria-label*="Price"]',
          '[aria-label*="cost"]',
          '[aria-label*="Cost"]'
        ];

        for (var s = 0; s < bookingSelectors.length; s++) {
          var container = document.querySelector(bookingSelectors[s]);
          if (!container) continue;
          var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
          var node;
          while ((node = walker.nextNode())) {
            var text = (node.textContent || '').trim();
            if (/[\\u20AC$\\u00A3\\u00A5\\u20B9]\\s*[\\d,.\\s]+/.test(text) || /[\\d,.\\s]+\\s*[\\u20AC$\\u00A3\\u00A5\\u20B9]/.test(text)) {
              var parsed = parsePrice(text);
              if (parsed && parsed.value > 1 && node.parentElement) {
                allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(node.parentElement), context: 'booking-widget' });
              }
            }
          }
        }

        if (allPrices.length === 0) {
          var allElements = document.querySelectorAll('span, div');
          allElements.forEach(function(el) {
            var text = (el.textContent || '').trim();
            if (text.length > 200) return;
            if (/[\\u20AC$\\u00A3\\u00A5\\u20B9]\\s*[\\d,.\\s]+/.test(text) || /[\\d,.\\s]+\\s*[\\u20AC$\\u00A3\\u00A5\\u20B9]/.test(text)) {
              var parentText = (el.parentElement ? el.parentElement.textContent : '').toLowerCase();
              if (/night|total|nuit|nacht|notte|noche|por noite/i.test(parentText)) {
                var parsed = parsePrice(text);
                if (parsed && parsed.value > 1) {
                  allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(el), context: 'near-night' });
                }
              }
            }
          });
        }

        if (allPrices.length === 0) {
          var walker2 = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
          var node2;
          while ((node2 = walker2.nextNode())) {
            var text2 = (node2.textContent || '').trim();
            if (/[\\u20AC$\\u00A3\\u00A5\\u20B9]\\s*[\\d,.\\s]+/.test(text2) || /[\\d,.\\s]+\\s*[\\u20AC$\\u00A3\\u00A5\\u20B9]/.test(text2)) {
              var parsed2 = parsePrice(text2);
              if (parsed2 && parsed2.value > 10 && node2.parentElement) {
                allPrices.push({ value: parsed2.value, currency: parsed2.currency, isStrikethrough: isStrikethrough(node2.parentElement), context: 'broad' });
              }
            }
          }
        }

        var nonStrikethrough = allPrices.filter(function(p) { return !p.isStrikethrough; });
        var candidates = nonStrikethrough.length > 0 ? nonStrikethrough : allPrices;
        var seen = {};
        var unique = [];
        for (var i = 0; i < candidates.length; i++) {
          if (!seen[candidates[i].value]) { seen[candidates[i].value] = true; unique.push(candidates[i]); }
        }
        unique.sort(function(a, b) { return a.value - b.value; });
        var lowest = unique[0];

        var description = '';
        var h1 = document.querySelector('h1');
        if (h1 && h1.textContent && h1.textContent.trim()) {
          description = h1.textContent.trim();
        }

        return {
          price: lowest ? String(lowest.value) : '',
          currency: lowest ? lowest.currency : '',
          description: description,
          reserveUrl: '',
          allPricesFound: unique.length,
          hadStrikethroughPrices: allPrices.some(function(p) { return p.isStrikethrough; })
        };
      })()`);
    } catch (err: any) {
      if (attempt < retries - 1 && err.message?.includes('context was destroyed')) {
        await new Promise(r => setTimeout(r, 1000));
        continue;
      }
      throw err;
    }
  }
}

/**
 * Helper: extract the lowest non-strikethrough price from an Agoda property page (with retry).
 * Agoda is a React SPA that loads room prices via a secondary API call
 * (/api/cronos/property/BelowFoldParams/GetSecondaryData).  Prices render after hydration
 * inside the room-listing section (#roomSection, .ChildRoomsList, .MasterRoom).
 * Currency is forced via the `currencyCode` URL parameter.
 *
 * IMPORTANT: We must only consider *visible* elements inside known room/price containers
 * to avoid picking up metadata values (e.g. countryId=197) that can appear in hidden
 * config objects or JSON-LD scripts.
 */
async function extractAgodaPriceFromPage(page: any, retries = 3): Promise<any> {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      await page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => {});
      // Agoda loads room prices via a secondary API after initial page render.
      // Wait for the room section or any price element to appear.
      await page.waitForSelector(
        '#roomSection, [data-selenium="RoomGridFilter"], [data-selenium="PriceDisplay"], ' +
        '[data-element-name="final-price"], [data-ppapi="room-price"], ' +
        '[class*="ChildRoomsList"], [class*="MasterRoom"], [class*="RoomPrice"]',
        { timeout: 20000 }
      ).catch(() => {});
      // Extra settle time — Agoda's room prices load asynchronously
      await page.waitForTimeout(5000);

      return await page.evaluate(`(function() {
        var allPrices = [];

        function isVisible(el) {
          if (!el || !el.getBoundingClientRect) return false;
          var rect = el.getBoundingClientRect();
          if (rect.width === 0 && rect.height === 0) return false;
          try {
            var style = window.getComputedStyle(el);
            if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
          } catch(e) {}
          return true;
        }

        function isInsideHiddenOrMeta(el) {
          var current = el;
          while (current && current !== document.body) {
            var tag = (current.tagName || '').toLowerCase();
            if (tag === 'script' || tag === 'style' || tag === 'noscript' || tag === 'template') return true;
            if (current.getAttribute && current.getAttribute('type') === 'application/ld+json') return true;
            if (current.getAttribute && current.getAttribute('hidden') !== null) return true;
            current = current.parentElement;
          }
          return false;
        }

        function isStrikethrough(el) {
          var current = el;
          while (current) {
            var tag = (current.tagName || '').toLowerCase();
            if (tag === 'del' || tag === 's' || tag === 'strike') return true;
            try {
              var style = window.getComputedStyle(current);
              if (style.textDecoration.includes('line-through') ||
                  (style.textDecorationLine && style.textDecorationLine.includes('line-through'))) return true;
            } catch(e) {}
            if (current.classList && (
                current.classList.contains('CrossedOutPrice') ||
                current.classList.contains('OriginalPrice') ||
                (current.getAttribute('data-selenium') || '').toLowerCase().includes('strikethrough') ||
                (current.getAttribute('data-selenium') || '').toLowerCase().includes('original'))) return true;
            current = current.parentElement;
          }
          return false;
        }

        function parsePrice(text) {
          var cleaned = text.replace(/\\s+/g, ' ').trim();
          var currencyMatch = cleaned.match(/([\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]|[A-Z]{3})/);
          var currency = currencyMatch ? currencyMatch[1] : '';
          var priceMatch = cleaned.match(/[\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]?\\s*([0-9][0-9.,]*[0-9]|[0-9]+)/);
          var numStr = priceMatch ? priceMatch[0].replace(/[^0-9.,]/g, '') : cleaned.replace(/[^0-9.,]/g, '');
          if (!numStr) return null;
          var value;
          var commaThousands = /,\\d{3}(?!\\d)/.test(numStr);
          var dotThousands = /\\.\\d{3}(?!\\d)/.test(numStr);
          if (commaThousands && !dotThousands) {
            value = parseFloat(numStr.replace(/,/g, ''));
          } else if (dotThousands && !commaThousands) {
            value = parseFloat(numStr.replace(/\\./g, '').replace(',', '.'));
          } else {
            var lastComma = numStr.lastIndexOf(',');
            var lastDot = numStr.lastIndexOf('.');
            if (lastComma > lastDot) {
              value = parseFloat(numStr.replace(/\\./g, '').replace(',', '.'));
            } else {
              value = parseFloat(numStr.replace(/,/g, ''));
            }
          }
          if (isNaN(value) || value <= 0) return null;
          return { value: value, currency: currency };
        }

        function collectFromContainer(container, tier) {
          if (!container) return;
          container.querySelectorAll('span, div, strong, b, p').forEach(function(el) {
            if (!isVisible(el)) return;
            if (isInsideHiddenOrMeta(el)) return;
            var text = (el.textContent || '').trim();
            // Only consider short text nodes that look like standalone prices
            if (text.length > 60 || text.length < 2) return;
            if (!/\\d/.test(text)) return;
            if (!(/[\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]/).test(text) &&
                !(/(?:EUR|USD|GBP|CHF)/).test(text)) return;
            var parsed = parsePrice(text);
            if (parsed && parsed.value >= 50) {
              allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(el), tier: tier });
            }
          });
        }

        // Tier 1: Room listing containers (most reliable — actual room prices)
        var roomContainers = document.querySelectorAll(
          '#roomSection, [data-selenium="RoomGridFilter"], [class*="ChildRoomsList"], ' +
          '[class*="MasterRoom"], [class*="RoomDetail"], [id*="roomSection"]'
        );
        roomContainers.forEach(function(c) { collectFromContainer(c, 1); });

        // Tier 2: Agoda-specific data-attribute price elements
        if (allPrices.length === 0) {
          var priceContainers = document.querySelectorAll(
            '[data-selenium="PriceDisplay"], [data-selenium="PriceWidget"], ' +
            '[data-selenium="BookNowBox"], [data-element-name="final-price"], ' +
            '[data-element-name="price"], [data-ppapi="room-price"]'
          );
          priceContainers.forEach(function(c) { collectFromContainer(c, 2); });
        }

        // Tier 3: Class-based selectors
        if (allPrices.length === 0) {
          var classContainers = document.querySelectorAll(
            '[class*="PriceDisplay"], [class*="pd-price"], [class*="RoomPrice"], ' +
            '[class*="FinalPrice"], [class*="price-box"], [class*="BookNow"]'
          );
          classContainers.forEach(function(c) { collectFromContainer(c, 3); });
        }

        // Tier 4: TreeWalker fallback — only visible text nodes, high minimum
        if (allPrices.length === 0) {
          var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
          var node;
          while ((node = walker.nextNode())) {
            if (!node.parentElement || !isVisible(node.parentElement)) continue;
            if (isInsideHiddenOrMeta(node.parentElement)) continue;
            var text = (node.textContent || '').trim();
            if (text.length > 40 || text.length < 2) continue;
            if (/[\\u20AC$\\u00A3\\u00A5\\u20B9]\\s*[\\d,.]+/.test(text) || /[\\d,.]+\\s*[\\u20AC$\\u00A3\\u00A5\\u20B9]/.test(text)) {
              var parsed = parsePrice(text);
              if (parsed && parsed.value >= 100 && node.parentElement) {
                allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(node.parentElement), tier: 4 });
              }
            }
          }
        }

        // Pick the lowest non-strikethrough price, preferring earlier tiers
        var nonStrikethrough = allPrices.filter(function(p) { return !p.isStrikethrough; });
        var candidates = nonStrikethrough.length > 0 ? nonStrikethrough : allPrices;

        // Sort by tier first (lower tier = more reliable), then by value ascending
        candidates.sort(function(a, b) {
          if (a.tier !== b.tier) return a.tier - b.tier;
          return a.value - b.value;
        });

        var seen = {};
        var unique = [];
        for (var i = 0; i < candidates.length; i++) {
          if (!seen[candidates[i].value]) { seen[candidates[i].value] = true; unique.push(candidates[i]); }
        }
        var lowest = unique[0];

        var description = '';
        var h1 = document.querySelector('h1');
        if (h1 && h1.textContent && h1.textContent.trim()) {
          description = h1.textContent.trim();
        }

        var reserveUrl = '';
        var bookLink = document.querySelector('a[href*="book"], a[data-selenium="BookNowButton"], button[data-selenium="BookNowButton"]');
        if (bookLink && bookLink.href) {
          reserveUrl = bookLink.href;
        }

        return {
          price: lowest ? String(lowest.value) : '',
          currency: lowest ? lowest.currency : '',
          description: description,
          reserveUrl: reserveUrl,
          allPricesFound: unique.length,
          hadStrikethroughPrices: allPrices.some(function(p) { return p.isStrikethrough; })
        };
      })()`);
    } catch (err: any) {
      if (attempt < retries - 1 && err.message?.includes('context was destroyed')) {
        await new Promise(r => setTimeout(r, 1000));
        continue;
      }
      throw err;
    }
  }
}

/**
 * Helper: extract the lowest non-strikethrough price from a Trip.com hotel page (with retry).
 * Trip.com is a Next.js SSR + React SPA hybrid.  Room prices are loaded dynamically via
 * client-side API calls after initial hydration.  The room list typically renders inside
 * containers with classes like .room-list, .room-item, .price-box, .price-num, etc.
 * Currency is forced via the `curr` and `barcurr` URL parameters.
 */
async function extractTripPriceFromPage(page: any, retries = 3): Promise<any> {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      await page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => {});
      // Trip.com loads room prices dynamically — wait for room/price containers to render
      await page.waitForSelector(
        '[class*="room-list"], [class*="roomList"], [class*="price-box"], ' +
        '[class*="priceBox"], [class*="price-num"], [class*="priceNum"], ' +
        '[class*="hotel-price"], [class*="hotelPrice"], [data-testid*="price"], ' +
        '[class*="RoomItem"], [class*="room-item"]',
        { timeout: 20000 }
      ).catch(() => {});
      // Extra settle time for SPA hydration and room list API response
      await page.waitForTimeout(5000);

      return await page.evaluate(`(function() {
        var allPrices = [];

        function isVisible(el) {
          if (!el || !el.getBoundingClientRect) return false;
          var rect = el.getBoundingClientRect();
          if (rect.width === 0 && rect.height === 0) return false;
          try {
            var style = window.getComputedStyle(el);
            if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
          } catch(e) {}
          return true;
        }

        function isInsideHiddenOrMeta(el) {
          var current = el;
          while (current && current !== document.body) {
            var tag = (current.tagName || '').toLowerCase();
            if (tag === 'script' || tag === 'style' || tag === 'noscript' || tag === 'template') return true;
            if (current.getAttribute && current.getAttribute('type') === 'application/ld+json') return true;
            if (current.getAttribute && current.getAttribute('hidden') !== null) return true;
            current = current.parentElement;
          }
          return false;
        }

        function isStrikethrough(el) {
          var current = el;
          while (current) {
            var tag = (current.tagName || '').toLowerCase();
            if (tag === 'del' || tag === 's' || tag === 'strike') return true;
            try {
              var style = window.getComputedStyle(current);
              if (style.textDecoration.includes('line-through') ||
                  (style.textDecorationLine && style.textDecorationLine.includes('line-through'))) return true;
            } catch(e) {}
            if (current.classList && (
                current.classList.contains('origin-price') ||
                current.classList.contains('originPrice') ||
                current.classList.contains('was-price') ||
                current.classList.contains('line-through'))) return true;
            current = current.parentElement;
          }
          return false;
        }

        function parsePrice(text) {
          var cleaned = text.replace(/\\s+/g, ' ').trim();
          var currencyMatch = cleaned.match(/([\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]|[A-Z]{3})/);
          var currency = currencyMatch ? currencyMatch[1] : '';
          var priceMatch = cleaned.match(/[\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]?\\s*([0-9][0-9.,]*[0-9]|[0-9]+)/);
          var numStr = priceMatch ? priceMatch[0].replace(/[^0-9.,]/g, '') : cleaned.replace(/[^0-9.,]/g, '');
          if (!numStr) return null;
          var value;
          var commaThousands = /,\\d{3}(?!\\d)/.test(numStr);
          var dotThousands = /\\.\\d{3}(?!\\d)/.test(numStr);
          if (commaThousands && !dotThousands) {
            value = parseFloat(numStr.replace(/,/g, ''));
          } else if (dotThousands && !commaThousands) {
            value = parseFloat(numStr.replace(/\\./g, '').replace(',', '.'));
          } else {
            var lastComma = numStr.lastIndexOf(',');
            var lastDot = numStr.lastIndexOf('.');
            if (lastComma > lastDot) {
              value = parseFloat(numStr.replace(/\\./g, '').replace(',', '.'));
            } else {
              value = parseFloat(numStr.replace(/,/g, ''));
            }
          }
          if (isNaN(value) || value <= 0) return null;
          return { value: value, currency: currency };
        }

        function collectFromContainer(container, tier) {
          if (!container) return;
          container.querySelectorAll('span, div, strong, b, p, em').forEach(function(el) {
            if (!isVisible(el)) return;
            if (isInsideHiddenOrMeta(el)) return;
            var text = (el.textContent || '').trim();
            if (text.length > 60 || text.length < 2) return;
            if (!/\\d/.test(text)) return;
            if (!(/[\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]/).test(text) &&
                !(/(?:EUR|USD|GBP|CHF)/).test(text)) return;
            var parsed = parsePrice(text);
            if (parsed && parsed.value >= 50) {
              allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(el), tier: tier });
            }
          });
        }

        // Tier 1: Room listing containers (most reliable — actual room prices)
        var roomContainers = document.querySelectorAll(
          '[class*="room-list"], [class*="roomList"], [class*="RoomList"], ' +
          '[class*="room-item"], [class*="roomItem"], [class*="RoomItem"], ' +
          '[class*="room-card"], [class*="roomCard"], [class*="RoomCard"]'
        );
        roomContainers.forEach(function(c) { collectFromContainer(c, 1); });

        // Tier 2: Price-specific containers
        if (allPrices.length === 0) {
          var priceContainers = document.querySelectorAll(
            '[class*="price-box"], [class*="priceBox"], [class*="PriceBox"], ' +
            '[class*="price-num"], [class*="priceNum"], [class*="PriceNum"], ' +
            '[class*="hotel-price"], [class*="hotelPrice"], [class*="HotelPrice"], ' +
            '[class*="total-price"], [class*="totalPrice"], [class*="TotalPrice"], ' +
            '[data-testid*="price"], [data-testid*="Price"]'
          );
          priceContainers.forEach(function(c) { collectFromContainer(c, 2); });
        }

        // Tier 3: Broader price-related class selectors
        if (allPrices.length === 0) {
          var broadContainers = document.querySelectorAll(
            '[class*="price"] span, [class*="price"] div, [class*="Price"] span, [class*="Price"] div, ' +
            '[class*="amount"] span, [class*="Amount"] span, ' +
            '[class*="cost"] span, [class*="Cost"] span, ' +
            '[class*="rate"] span, [class*="Rate"] span'
          );
          broadContainers.forEach(function(el) {
            if (!isVisible(el)) return;
            if (isInsideHiddenOrMeta(el)) return;
            var text = (el.textContent || '').trim();
            if (text.length > 60 || text.length < 2) return;
            if (!/\\d/.test(text)) return;
            if (!(/[\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]/).test(text) &&
                !(/(?:EUR|USD|GBP|CHF)/).test(text)) return;
            var parsed = parsePrice(text);
            if (parsed && parsed.value >= 50) {
              allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(el), tier: 3 });
            }
          });
        }

        // Tier 4: TreeWalker fallback — only visible text nodes, high minimum
        if (allPrices.length === 0) {
          var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
          var node;
          while ((node = walker.nextNode())) {
            if (!node.parentElement || !isVisible(node.parentElement)) continue;
            if (isInsideHiddenOrMeta(node.parentElement)) continue;
            var text = (node.textContent || '').trim();
            if (text.length > 40 || text.length < 2) continue;
            if (/[\\u20AC$\\u00A3\\u00A5\\u20B9]\\s*[\\d,.]+/.test(text) || /[\\d,.]+\\s*[\\u20AC$\\u00A3\\u00A5\\u20B9]/.test(text)) {
              var parsed = parsePrice(text);
              if (parsed && parsed.value >= 100 && node.parentElement) {
                allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(node.parentElement), tier: 4 });
              }
            }
          }
        }

        // Pick the lowest non-strikethrough price, preferring earlier tiers
        var nonStrikethrough = allPrices.filter(function(p) { return !p.isStrikethrough; });
        var candidates = nonStrikethrough.length > 0 ? nonStrikethrough : allPrices;

        candidates.sort(function(a, b) {
          if (a.tier !== b.tier) return a.tier - b.tier;
          return a.value - b.value;
        });

        var seen = {};
        var unique = [];
        for (var i = 0; i < candidates.length; i++) {
          if (!seen[candidates[i].value]) { seen[candidates[i].value] = true; unique.push(candidates[i]); }
        }
        var lowest = unique[0];

        var description = '';
        var h1 = document.querySelector('h1');
        if (h1 && h1.textContent && h1.textContent.trim()) {
          description = h1.textContent.trim();
        }

        var reserveUrl = '';
        var bookLink = document.querySelector('a[href*="book"], button[class*="book"], button[class*="Book"], [class*="reserve"], [class*="Reserve"]');
        if (bookLink && bookLink.href) {
          reserveUrl = bookLink.href;
        }

        return {
          price: lowest ? String(lowest.value) : '',
          currency: lowest ? lowest.currency : '',
          description: description,
          reserveUrl: reserveUrl,
          allPricesFound: unique.length,
          hadStrikethroughPrices: allPrices.some(function(p) { return p.isStrikethrough; })
        };
      })()`);
    } catch (err: any) {
      if (attempt < retries - 1 && err.message?.includes('context was destroyed')) {
        await new Promise(r => setTimeout(r, 1000));
        continue;
      }
      throw err;
    }
  }
}

/**
 * Generic price extractor for providers without a dedicated extractor (Hostelworld,
 * Homestay, Casasrurales, HomeToGo, Plum Guide, etc.).
 * Uses visibility-checked tiered extraction with broad selectors that work across
 * different site frameworks.  Filters out hidden/metadata values to avoid false positives.
 */
async function extractGenericPriceFromPage(page: any, retries = 3): Promise<any> {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      await page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => {});
      // Wait for any price-like element to appear
      await page.waitForSelector(
        '[class*="price"], [class*="Price"], [class*="rate"], [class*="Rate"], ' +
        '[class*="cost"], [class*="Cost"], [class*="amount"], [class*="Amount"], ' +
        '[data-testid*="price"], [data-testid*="Price"]',
        { timeout: 15000 }
      ).catch(() => {});
      await page.waitForTimeout(4000);

      return await page.evaluate(`(function() {
        var allPrices = [];

        function isVisible(el) {
          if (!el || !el.getBoundingClientRect) return false;
          var rect = el.getBoundingClientRect();
          if (rect.width === 0 && rect.height === 0) return false;
          try {
            var style = window.getComputedStyle(el);
            if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
          } catch(e) {}
          return true;
        }

        function isInsideHiddenOrMeta(el) {
          var current = el;
          while (current && current !== document.body) {
            var tag = (current.tagName || '').toLowerCase();
            if (tag === 'script' || tag === 'style' || tag === 'noscript' || tag === 'template') return true;
            if (current.getAttribute && current.getAttribute('type') === 'application/ld+json') return true;
            if (current.getAttribute && current.getAttribute('hidden') !== null) return true;
            current = current.parentElement;
          }
          return false;
        }

        function isStrikethrough(el) {
          var current = el;
          while (current) {
            var tag = (current.tagName || '').toLowerCase();
            if (tag === 'del' || tag === 's' || tag === 'strike') return true;
            try {
              var style = window.getComputedStyle(current);
              if (style.textDecoration.includes('line-through') ||
                  (style.textDecorationLine && style.textDecorationLine.includes('line-through'))) return true;
            } catch(e) {}
            if (current.classList && (
                current.classList.contains('line-through') ||
                current.classList.contains('was-price') ||
                current.classList.contains('origin-price') ||
                current.classList.contains('old-price'))) return true;
            current = current.parentElement;
          }
          return false;
        }

        function parsePrice(text) {
          var cleaned = text.replace(/\\s+/g, ' ').trim();
          var currencyMatch = cleaned.match(/([\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]|[A-Z]{3})/);
          var currency = currencyMatch ? currencyMatch[1] : '';
          var priceMatch = cleaned.match(/[\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]?\\s*([0-9][0-9.,]*[0-9]|[0-9]+)/);
          var numStr = priceMatch ? priceMatch[0].replace(/[^0-9.,]/g, '') : cleaned.replace(/[^0-9.,]/g, '');
          if (!numStr) return null;
          var value;
          var commaThousands = /,\\d{3}(?!\\d)/.test(numStr);
          var dotThousands = /\\.\\d{3}(?!\\d)/.test(numStr);
          if (commaThousands && !dotThousands) {
            value = parseFloat(numStr.replace(/,/g, ''));
          } else if (dotThousands && !commaThousands) {
            value = parseFloat(numStr.replace(/\\./g, '').replace(',', '.'));
          } else {
            var lastComma = numStr.lastIndexOf(',');
            var lastDot = numStr.lastIndexOf('.');
            if (lastComma > lastDot) {
              value = parseFloat(numStr.replace(/\\./g, '').replace(',', '.'));
            } else {
              value = parseFloat(numStr.replace(/,/g, ''));
            }
          }
          if (isNaN(value) || value <= 0) return null;
          return { value: value, currency: currency };
        }

        function collectVisible(selector, tier) {
          document.querySelectorAll(selector).forEach(function(el) {
            if (!isVisible(el)) return;
            if (isInsideHiddenOrMeta(el)) return;
            var text = (el.textContent || '').trim();
            if (text.length > 60 || text.length < 2) return;
            if (!/\\d/.test(text)) return;
            if (!(/[\\u20AC$\\u00A3\\u00A5\\u20B9\\u20BA\\u20A9\\u20AA\\u20BD]/).test(text) &&
                !(/(?:EUR|USD|GBP|CHF)/).test(text)) return;
            var parsed = parsePrice(text);
            if (parsed && parsed.value >= 5) {
              allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(el), tier: tier });
            }
          });
        }

        // Tier 1: Elements with price-related classes or data attributes
        collectVisible('[class*="price"] span, [class*="price"] div, [class*="price"] strong, [class*="price"] b, [class*="price"] p, [class*="price"] em', 1);
        collectVisible('[class*="Price"] span, [class*="Price"] div, [class*="Price"] strong, [class*="Price"] b', 1);
        collectVisible('[data-testid*="price"] span, [data-testid*="price"] div, [data-testid*="Price"] span', 1);

        // Tier 2: Rate/cost/amount containers
        if (allPrices.length === 0) {
          collectVisible('[class*="rate"] span, [class*="Rate"] span, [class*="cost"] span, [class*="Cost"] span', 2);
          collectVisible('[class*="amount"] span, [class*="Amount"] span, [class*="total"] span, [class*="Total"] span', 2);
        }

        // Tier 3: Direct price-class elements (the element itself has a price class)
        if (allPrices.length === 0) {
          collectVisible('[class*="price"], [class*="Price"], [class*="rate"], [class*="Rate"]', 3);
        }

        // Tier 4: TreeWalker — visible text nodes with currency symbols
        if (allPrices.length === 0) {
          var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
          var node;
          while ((node = walker.nextNode())) {
            if (!node.parentElement || !isVisible(node.parentElement)) continue;
            if (isInsideHiddenOrMeta(node.parentElement)) continue;
            var text = (node.textContent || '').trim();
            if (text.length > 40 || text.length < 2) continue;
            if (/[\\u20AC$\\u00A3\\u00A5\\u20B9]\\s*[\\d,.]+/.test(text) || /[\\d,.]+\\s*[\\u20AC$\\u00A3\\u00A5\\u20B9]/.test(text)) {
              var parsed = parsePrice(text);
              if (parsed && parsed.value >= 10 && node.parentElement) {
                allPrices.push({ value: parsed.value, currency: parsed.currency, isStrikethrough: isStrikethrough(node.parentElement), tier: 4 });
              }
            }
          }
        }

        var nonStrikethrough = allPrices.filter(function(p) { return !p.isStrikethrough; });
        var candidates = nonStrikethrough.length > 0 ? nonStrikethrough : allPrices;
        candidates.sort(function(a, b) {
          if (a.tier !== b.tier) return a.tier - b.tier;
          return a.value - b.value;
        });
        var seen = {};
        var unique = [];
        for (var i = 0; i < candidates.length; i++) {
          if (!seen[candidates[i].value]) { seen[candidates[i].value] = true; unique.push(candidates[i]); }
        }
        var lowest = unique[0];

        var description = '';
        var h1 = document.querySelector('h1');
        if (h1 && h1.textContent && h1.textContent.trim()) {
          description = h1.textContent.trim();
        }

        return {
          price: lowest ? String(lowest.value) : '',
          currency: lowest ? lowest.currency : '',
          description: description,
          reserveUrl: '',
          allPricesFound: unique.length,
          hadStrikethroughPrices: allPrices.some(function(p) { return p.isStrikethrough; })
        };
      })()`);
    } catch (err: any) {
      if (attempt < retries - 1 && err.message?.includes('context was destroyed')) {
        await new Promise(r => setTimeout(r, 1000));
        continue;
      }
      throw err;
    }
  }
}

/**
 * Helper: extract price data from the current page state (with retry on context destruction)
 */
async function extractPriceFromPage(page: any, retries = 3): Promise<any> {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {});
      // Use string-based evaluate to bypass esbuild __name transformation
      return await page.evaluate(`(function() {
        var priceSelectors = [
          '[data-testid="price-for-x-nights"]',
          '[data-testid="price-and-discounted-price"]',
          '.hprt-price-price',
          '.bui-price-display__value',
          '.prco-valign-middle-helper',
          '[class*="price"] [class*="amount"]',
          '.hprt-table .hprt-price'
        ];

        var priceText = '';
        for (var i = 0; i < priceSelectors.length; i++) {
          var el = document.querySelector(priceSelectors[i]);
          if (el && el.textContent && el.textContent.trim()) {
            priceText = el.textContent.trim();
            break;
          }
        }

        var currencyMatch = priceText.match(/([\\u20AC$\\u00A3\\u00A5\\u20B9]|[A-Z]{3})/);
        var currency = currencyMatch ? currencyMatch[1] : '';
        var numericMatch = priceText.match(/[\\d,.\\s]+/);
        var price = numericMatch ? numericMatch[0].trim().replace(/\\s/g, '') : priceText;

        var descSelectors = [
          '.hprt-roomtype-icon-link',
          '[data-testid="property-section--content"] .room_link',
          '.hprt-table .hprt-roomtype-link',
          '.room_link strong'
        ];

        var description = '';
        for (var j = 0; j < descSelectors.length; j++) {
          var el2 = document.querySelector(descSelectors[j]);
          if (el2 && el2.textContent && el2.textContent.trim()) {
            description = el2.textContent.trim();
            break;
          }
        }

        var reserveUrl = '';
        var reserveLink = document.querySelector('a[href*="/book."]');
        if (reserveLink) {
          reserveUrl = reserveLink.href;
        }

        return { price: price, currency: currency, description: description, reserveUrl: reserveUrl };
      })()`);
    } catch (err: any) {
      if (attempt < retries - 1 && err.message?.includes('context was destroyed')) {
        await new Promise(r => setTimeout(r, 1000));
        continue;
      }
      throw err;
    }
  }
}

/**
 * POST /proxy/session/:sessionId/extract-price
 * Extract cheapest option price from a hotel page in both desktop and mobile viewports.
 * Supports Booking.com, Hotels.com and Airbnb — auto-detects provider from the current page URL.
 */
router.post('/session/:sessionId/extract-price', verifyToken, apiLimiter, async (req, res) => {
  try {
    const { sessionId } = req.params;
    const { liteMode } = req.body as { liteMode?: boolean };

    const session = await browserPool.getSession({
      sessionId,
      userId: (req as any).user.userId,
      deviceMode: 'desktop',
    });

    const page = session.page;
    // Use session's stored currentUrl (reliable after fetch-based nav) with page.url() as fallback
    const currentUrl = session.state.currentUrl || page.url();
    const provider = detectProvider(currentUrl);

    console.log(`[extract-price] provider=${provider}, currentUrl=${currentUrl}, page.url()=${page.url()}`);
    logger.info('Extracting price', { sessionId, provider, currentUrl });

    // Pick the right extractor based on provider
    // Dedicated extractors for major providers; Travelocity reuses Expedia (same UITK);
    // all other known providers use the visibility-checked generic extractor;
    // unknown/booking.com falls through to extractPriceFromPage.
    const dedicatedExtractors: Partial<Record<Provider, (page: any, retries?: number) => Promise<any>>> = {
      airbnb: extractAirbnbPriceFromPage,
      hotels: extractHotelsPriceFromPage,
      expedia: extractExpediaPriceFromPage,
      vrbo: extractVrboPriceFromPage,
      agoda: extractAgodaPriceFromPage,
      trip: extractTripPriceFromPage,
      travelocity: extractExpediaPriceFromPage,
    };
    const genericProviders: Provider[] = [
      'hostelworld', 'homestay', 'casasrurales', 'hometogo', 'plumguide',
      'hipcamp', 'misterbandb', 'redawning', 'ostrovok', 'fliggy',
      'makemytrip', 'oyo', 'goibibo', 'cleartrip', 'yatra',
    ];
    const extractFn = dedicatedExtractors[provider]
      ?? (genericProviders.includes(provider) ? extractGenericPriceFromPage : extractPriceFromPage);

    // Track network traffic
    let totalBytes = 0;
    let requestCount = 0;
    const trafficHandler = async (response: any) => {
      requestCount++;
      try {
        const body = await response.body().catch(() => null);
        if (body) totalBytes += body.length;
        const headers = response.headers();
        const headerSize = Object.entries(headers).reduce(
          (sum: number, [k, v]) => sum + k.length + String(v).length + 4, 0
        );
        totalBytes += headerSize;
      } catch {
        // ignore failed responses
      }
    };
    page.on('response', trafficHandler);

    // Save original viewport
    const originalViewport = page.viewportSize();

    let desktopData;
    let mobileData;

    if (liteMode) {
      // Lite mode: no reloads, just resize viewport and wait for DOM to settle
      logger.info('Lite mode — extracting without reloads', { sessionId, provider });

      const isSpaProvider = provider !== 'booking' && provider !== 'homestay' && provider !== 'casasrurales' && provider !== 'unknown';

      // Desktop extraction — resize + wait for DOM to settle
      await page.setViewportSize({ width: 1280, height: 900 });
      await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {});
      await page.waitForTimeout(isSpaProvider ? 3000 : 500);
      desktopData = await extractFn(page);

      // Mobile extraction — resize + wait for DOM to settle
      await page.setViewportSize({ width: 375, height: 812 });
      await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {});
      await page.waitForTimeout(isSpaProvider ? 3000 : 500);
      mobileData = await extractFn(page);

      // Restore viewport without reload
      if (originalViewport) {
        await page.setViewportSize(originalViewport);
      }
    } else {
      // Full mode: reload for each viewport to get server-rendered content
      // --- Desktop extraction ---
      await page.setViewportSize({ width: 1280, height: 900 });
      await page.reload({ waitUntil: 'networkidle', timeout: 20000 }).catch(() => {
        logger.warn('Desktop reload timeout, proceeding', { sessionId });
      });
      await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
      if (provider !== 'booking') await page.waitForTimeout(3000);
      desktopData = await extractFn(page);

      // --- Mobile extraction ---
      await page.setViewportSize({ width: 375, height: 812 });
      await page.reload({ waitUntil: 'networkidle', timeout: 20000 }).catch(() => {
        logger.warn('Mobile reload timeout, proceeding', { sessionId });
      });
      await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
      if (provider !== 'booking') await page.waitForTimeout(3000);
      mobileData = await extractFn(page);

      // --- Restore original viewport ---
      if (originalViewport) {
        await page.setViewportSize(originalViewport);
        await page.reload({ waitUntil: 'networkidle', timeout: 20000 }).catch(() => {});
      }
    }

    // Stop tracking traffic
    page.removeListener('response', trafficHandler);

    const result = {
      provider,
      desktop: { ...desktopData },
      mobile: { price: mobileData.price, currency: mobileData.currency, description: mobileData.description },
      reserveUrl: desktopData.reserveUrl,
      traffic: { totalBytes, requestCount },
    };

    logger.info('Price extracted (desktop + mobile)', { sessionId, provider, result });

    // Persist search + results to MongoDB (fire-and-forget)
    const userId = (req as any).user?.userId;
    SearchModel.create({
      userId,
      url: currentUrl,
      geo: [],
      results: result,
    }).catch((err: Error) => logger.warn('Failed to save search', { error: err.message }));

    res.json(result);
  } catch (error) {
    logger.error('Failed to extract price', {
      sessionId: req.params.sessionId,
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({
      error: error instanceof Error ? error.message : 'Failed to extract price',
    });
  }
});

const PLAN_MONTHLY_LIMITS: Record<string, number> = {
  free: Infinity,
  basic: 5,
  premium: 20,
  max: 100,
  business: Infinity,
};

/**
 * POST /proxy/scrape-price
 * Lightweight price extraction via Decodo scraping API (no browser needed).
 * Accepts { url, geo, currency } and returns same shape as extract-price.
 */
router.post('/scrape-price', optionalAuth, apiLimiter, async (req, res) => {
  try {
    const { url, geo, currency, includeMobile } = req.body as { url: string; geo?: string; currency?: string; includeMobile?: boolean };

    if (!url) {
      res.status(400).json({ error: 'url is required' });
      return;
    }

    // Enforce monthly search limit for authenticated paid plans only
    const userPlan: string = (req as any).user?.plan || 'free';
    const monthlyLimit = PLAN_MONTHLY_LIMITS[userPlan] ?? 5;
    if ((req as any).user && isFinite(monthlyLimit)) {
      const userId = (req as any).user?.userId;
      const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
      const searchCount = await SearchModel.countDocuments({ userId, createdAt: { $gte: thirtyDaysAgo } });
      if (searchCount >= monthlyLimit) {
        res.status(429).json({
          error: `Monthly search limit reached (${monthlyLimit} searches/month on the ${userPlan} plan). Upgrade to continue.`,
          limitReached: true,
          plan: userPlan,
          limit: monthlyLimit,
          used: searchCount,
        });
        return;
      }
    }

    const provider = detectProvider(url);
    const finalUrl = currency ? forceCurrencyUrl(url, currency) : url;

    logger.info('Scrape-price request', { provider, geo, currency, url: finalUrl });

    const geoValue = geo || 'United States';

    const callDecodo = async (deviceType: string) => {
      const scrapeRes = await fetch(config.decodo.apiUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': config.decodo.auth,
        },
        body: JSON.stringify({
          url: finalUrl,
          headless: 'html',
          geo: geoValue,
          device_type: deviceType,
        }),
      });

      if (!scrapeRes.ok) {
        const text = await scrapeRes.text();
        logger.error('Decodo API error', { status: scrapeRes.status, deviceType, body: text });
        return null;
      }

      const data = await scrapeRes.json() as { status?: string; message?: string; results?: Array<{ content: string; status_code: number }> };
      if (data.status === 'failed') {
        logger.warn('Decodo API scrape failed', { deviceType, message: data.message });
        return null;
      }
      if (!data.results?.[0]?.content) return null;
      const content = data.results[0].content;
      // Detect blocked / error pages (SPA sites that return generic error pages to scrapers)
      if (/Something went wrong|Access Denied|Please enable JavaScript/i.test(content) && content.length < 150000) {
        logger.warn('Scrape returned error/blocked page', { deviceType, provider });
        return null;
      }
      return extractPriceFromHtml(content, provider);
    };

    // Fetch desktop always; mobile only when requested
    const desktopPromise = callDecodo('desktop_chrome');
    const mobilePromise = includeMobile ? callDecodo('mobile') : Promise.resolve(null);
    const [desktopResult, mobileResult] = await Promise.all([desktopPromise, mobilePromise]);

    if (!desktopResult && !mobileResult) {
      res.status(502).json({ error: `Scraping API could not extract content from ${provider}. This site may block automated scraping — try the browser-based method instead.` });
      return;
    }

    const result = {
      provider,
      desktop: desktopResult
        ? { price: desktopResult.price, currency: desktopResult.currency, description: desktopResult.description }
        : { price: '', currency: '', description: '' },
      mobile: mobileResult
        ? { price: mobileResult.price, currency: mobileResult.currency, description: mobileResult.description }
        : { price: '', currency: '', description: '' },
      reserveUrl: desktopResult?.reserveUrl || mobileResult?.reserveUrl || '',
    };

    logger.info('Scrape-price result', { provider, geo: geoValue, result });

    // Persist search + results to MongoDB for authenticated users only (fire-and-forget)
    const userId = (req as any).user?.userId;
    if (userId) {
      SearchModel.create({
        userId,
        url: finalUrl,
        geo: Array.isArray(geo) ? geo : [geoValue],
        currency: currency || '',
        results: result,
      }).catch((err: Error) => logger.warn('Failed to save search', { error: err.message }));
    }

    // For free plan: return result but hide the geo/country identifier
    if (userPlan === 'free') {
      res.json({ ...result, countryRevealed: false, geo: null });
      return;
    }

    res.json({ ...result, countryRevealed: true, geo: geoValue });
  } catch (error) {
    logger.error('Failed to scrape price', {
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({
      error: error instanceof Error ? error.message : 'Failed to scrape price',
    });
  }
});

/**
 * GET /proxy/session/:sessionId/screenshot
 * Get screenshot of current page
 */
router.get('/session/:sessionId/screenshot', verifyToken, apiLimiter, async (req, res) => {
  try {
    const { sessionId } = req.params;

    const session = await browserPool.getSession({
      sessionId,
      userId: (req as any).user.userId,
      deviceMode: 'desktop',
    });

    const screenshot = await session.getScreenshot();

    res.type('png').send(screenshot);
  } catch (error) {
    logger.error('Failed to get screenshot', {
      sessionId: req.params.sessionId,
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({
      error: error instanceof Error ? error.message : 'Failed to get screenshot',
    });
  }
});

/**
 * DELETE /proxy/session/:sessionId
 * Close a browser session
 */
router.delete('/session/:sessionId', verifyToken, apiLimiter, async (req, res) => {
  try {
    const { sessionId } = req.params;

    logger.info('Closing session', { sessionId });

    await browserPool.closeSession(sessionId);

    res.json({
      sessionId,
      status: 'closed',
    });
  } catch (error) {
    logger.error('Failed to close session', {
      sessionId: req.params.sessionId,
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({
      error: error instanceof Error ? error.message : 'Failed to close session',
    });
  }
});

export default router;
