import { Browser, BrowserContext, Page, Response, chromium } from "playwright";

const DEFAULT_BLOCK_TTL_MS = 6 * 60 * 60 * 1000;
const blockedPortals = new Map<string, number>();

const STEALTH_USER_AGENTS = [
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
];

const STEALTH_VIEWPORTS = [
  { width: 1366, height: 768 },
  { width: 1280, height: 800 },
  { width: 1920, height: 1080 },
];

const STEALTH_INIT_SCRIPT = `
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
Object.defineProperty(navigator, 'languages', { get: () => ['es-ES', 'es'] });
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
window.chrome = { runtime: {} };
`;

function randomItem<T>(arr: T[]): T {
  return arr[Math.floor(Math.random() * arr.length)];
}

export class PortalBlockedError extends Error {
  constructor(public readonly portal: string, public readonly blockedUntil: number) {
    super(`${portal} ha presentado un bloqueo temporal; en cuarentena hasta ${new Date(blockedUntil).toISOString()}`);
    this.name = "PortalBlockedError";
  }
}

export interface ChallengeEvidence {
  status?: number;
  url?: string;
  title?: string;
  html?: string;
}

/**
 * Reconoce páginas de challenge; no intenta resolverlas ni eludir controles.
 * Se combinan varias señales para no confundir una página de resultados vacía
 * con un bloqueo basándose únicamente en el tamaño del HTML.
 */
export function isChallengePage(evidence: ChallengeEvidence): boolean {
  if (evidence.status === 403 || evidence.status === 429) return true;

  const url = (evidence.url || "").toLowerCase();
  if (/\/antibot\/|captcha|datadome|incapsula|imperva/.test(url)) return true;

  const title = (evidence.title || "").toLowerCase();
  if (/captcha|robot|access denied|acceso denegado|just a moment/.test(title)) return true;

  const html = (evidence.html || "").toLowerCase();
  const markers = [
    "cf-chl-", "datadome", "captcha-delivery", "incapsula incident",
    "_incapsula_resource", "verify you are human", "verifica que eres humano",
    "access denied", "acceso denegado", "unusual traffic", "tráfico inusual",
  ];
  return markers.some(marker => html.includes(marker));
}

function envNumber(name: string, fallback: number): number {
  const parsed = Number(process.env[name]);
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
}

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/** Una identidad y un contexto estables durante todo el ciclo de un portal. */
export class PortalBrowserSession {
  private browser: Browser | null = null;
  private context: BrowserContext | null = null;
  private page: Page | null = null;
  private navigations = 0;

  constructor(
    private readonly portal: string,
    private readonly timeoutMs = 45_000
  ) {}

  static isQuarantined(portal: string): boolean {
    const until = blockedPortals.get(portal) || 0;
    if (until <= Date.now()) {
      blockedPortals.delete(portal);
      return false;
    }
    return true;
  }

  static quarantineRemainingMs(portal: string): number {
    return Math.max(0, (blockedPortals.get(portal) || 0) - Date.now());
  }

  private quarantine(): never {
    const ttl = envNumber("SCRAPER_CHALLENGE_COOLDOWN_MINUTES", DEFAULT_BLOCK_TTL_MS / 60_000) * 60_000;
    const until = Date.now() + ttl;
    blockedPortals.set(this.portal, until);
    throw new PortalBlockedError(this.portal, until);
  }

  private async ensurePage(): Promise<Page> {
    if (this.page) return this.page;
    if (PortalBrowserSession.isQuarantined(this.portal)) {
      throw new PortalBlockedError(this.portal, Date.now() + PortalBrowserSession.quarantineRemainingMs(this.portal));
    }

    this.browser = await chromium.launch({
      headless: true,
      args: [
        "--disable-blink-features=AutomationControlled",
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-dev-shm-usage",
        "--lang=es-ES",
      ],
    });
    this.context = await this.browser.newContext({
      viewport: randomItem(STEALTH_VIEWPORTS),
      locale: "es-ES",
      timezoneId: "Europe/Madrid",
      userAgent: randomItem(STEALTH_USER_AGENTS),
      javaScriptEnabled: true,
      ignoreHTTPSErrors: true,
      extraHTTPHeaders: {
        "Accept-Language": "es-ES,es;q=0.9",
        Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
      },
    });
    await this.context.addInitScript(STEALTH_INIT_SCRIPT);
    this.page = await this.context.newPage();
    return this.page;
  }

  async fetchHtml(
    url: string,
    options: {
      waitForSelector?: string;
      afterNavigation?: (page: Page) => Promise<void>;
    } = {}
  ): Promise<string> {
    const page = await this.ensurePage();
    if (this.navigations > 0) {
      const min = envNumber("SCRAPER_NAVIGATION_DELAY_MIN_MS", 4_000);
      const max = Math.max(min, envNumber("SCRAPER_NAVIGATION_DELAY_MAX_MS", 8_000));
      await sleep(min + Math.random() * (max - min));
    }

    let response: Response | null = null;
    try {
      response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: this.timeoutMs });
      this.navigations++;
      await this.dismissCookieBanner(page);
      if (options.afterNavigation) await options.afterNavigation(page);
      if (options.waitForSelector) {
        await page.waitForSelector(options.waitForSelector, { timeout: Math.min(this.timeoutMs, 15_000) }).catch(() => null);
      }
      const [title, html] = await Promise.all([
        page.title().catch(() => ""),
        page.content().catch(() => ""),
      ]);
      if (isChallengePage({ status: response?.status(), url: page.url(), title, html })) {
        this.quarantine();
      }
      return html;
    } catch (error) {
      if (error instanceof PortalBlockedError) throw error;
      const status = response?.status();
      if (status === 403 || status === 429) this.quarantine();
      throw error;
    }
  }

  private async dismissCookieBanner(page: Page): Promise<void> {
    const selectors = [
      "#onetrust-accept-btn-handler", "#didomi-notice-agree-button",
      ".didomi-continue-btn", "button[data-testid='accept-cookies']",
      "button[aria-label*='Aceptar']", "button:text('Aceptar todo')",
      "button:text('Aceptar todas')", "button:text('Accept all')",
    ];
    for (const selector of selectors) {
      const button = page.locator(selector).first();
      if (await button.isVisible({ timeout: 500 }).catch(() => false)) {
        await button.click({ timeout: 2_000 }).catch(() => null);
        break;
      }
    }
  }

  async close(): Promise<void> {
    this.page = null;
    if (this.context) await this.context.close().catch(() => null);
    if (this.browser) await this.browser.close().catch(() => null);
    this.context = null;
    this.browser = null;
  }
}
