import type { Page, Locator, Frame, Response, BrowserContext } from 'playwright';
import type { CsfLoginSession } from './sat-csf-login.js';

async function tryFetchPdfFromUrl(frame: Frame, url: string): Promise<Buffer | null> {
  if (!url || url === 'about:blank') return null;

  // Blob / data URI → fetchear dentro del navegador para respetar cookies/sesión
  if (url.startsWith('blob:') || url.startsWith('data:')) {
    try {
      const page = frame.page();
      const arr = await page.evaluate(async (u) => {
        const r = await fetch(u);
        const buf = await r.arrayBuffer();
        return Array.from(new Uint8Array(buf));
      }, url);
      const buf = Buffer.from(arr);
      return buf.subarray(0, 5).toString().startsWith('%PDF-') ? buf : null;
    } catch {
      return null;
    }
  }

  // URL http(s) relativa o absoluta → fetchear desde el frame para mantener sesión
  if (url.startsWith('http')) {
    try {
      const response = await frame.page().context().request.get(url);
      if (!response.ok()) return null;
      const ct = response.headers()['content-type'] ?? '';
      if (!ct.includes('application/pdf') && !url.toLowerCase().includes('.pdf')) return null;
      const buf = Buffer.from(await response.body());
      return buf.subarray(0, 5).toString().startsWith('%PDF-') ? buf : null;
    } catch {
      return null;
    }
  }

  return null;
}

async function findPdfInFrames(appPage: Page, deadlineMs: number): Promise<Buffer | null> {
  const deadline = Date.now() + deadlineMs;

  while (Date.now() < deadline) {
    const frames = appPage.frames();

    for (const frame of frames) {
      try {
        const frameUrl = frame.url();

        // 1. Frame cuya URL sea directamente un PDF
        if (
          frameUrl.toLowerCase().includes('.pdf') ||
          frameUrl.includes('application/pdf')
        ) {
          const body = await frame.content().catch(() => null);
          if (!body) continue;
          // content() de un PDF no es util; intentar fetch por URL
          const pdf = await tryFetchPdfFromUrl(frame, frameUrl);
          if (pdf) return pdf;
        }

        // 2. <embed type="application/pdf">
        const embed = frame.locator('embed[type="application/pdf"]').first();
        if ((await embed.count()) > 0) {
          const src = await embed.getAttribute('src');
          if (src) {
            const pdf = await tryFetchPdfFromUrl(frame, src);
            if (pdf) return pdf;
          }
        }

        // 3. <iframe> cuyo src apunte al visor/constancia (IdcSiat, .pdf, etc.)
        const iframes = await frame.locator('iframe').all();
        for (const iframe of iframes) {
          const src = await iframe.getAttribute('src');
          if (src) {
            const lower = src.toLowerCase();
            if (
              lower.includes('.pdf') ||
              lower.includes('idcsiat') ||
              lower.includes('reimpresion') ||
              lower.includes('consultatramite')
            ) {
              const pdf = await tryFetchPdfFromUrl(frame, src);
              if (pdf) return pdf;
            }
          }
        }

        // 4. <a download> o link con data/blob generado por jsPDF
        const downloadLinks = await frame.locator('a[download], a[href*="data:"], a[href*="blob:"]').all();
        for (const link of downloadLinks) {
          const href = await link.getAttribute('href');
          if (href) {
            const pdf = await tryFetchPdfFromUrl(frame, href);
            if (pdf) return pdf;
          }
        }
      } catch {
        // Frame puede estar navegando; ignorar y continuar
      }
    }

    await appPage.waitForTimeout(1000);
  }

  return null;
}

async function findGenerarButton(appPage: Page, timeoutMs: number): Promise<Locator | null> {
  const GENERAR_SELECTORS = [
    'button:has-text("Generar Constancia")',
    'button:has-text("Generar constancia")',
    'input[type="button"][value*="Generar" i]',
    'input[type="submit"][value*="Generar" i]',
    'a:has-text("Generar Constancia")',
    'a:has-text("Generar constancia")',
  ].join(', ');

  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    for (const frame of appPage.frames()) {
      const loc = frame.locator(GENERAR_SELECTORS).first();
      const count = await loc.count().catch(() => 0);
      if (count > 0 && (await loc.isVisible().catch(() => false))) {
        return loc;
      }
    }
    await appPage.waitForTimeout(1000);
  }
  return null;
}

/**
 * Busca "Generar Constancia" en cualquiera de los frames del appPage,
 * hace click, y extrae el PDF desde el visor/iframe que genera el SAT.
 *
 * El portal SAT actual (2025-2026) genera la CSF dentro de un iframe JSF
 * legacy (rfcampc.siat.sat.gob.mx/PTSC/.../ConsultaTramite.jsf). El PDF
 * no siempre se descarga como evento de download ni abre popup; a veces
 * se renderiza en un <embed> o en un iframe cuyo src devuelve el PDF.
 */
export async function extractCsfPdf(session: CsfLoginSession): Promise<Buffer> {
  const { context, appPage } = session;

  let interceptedPdf: Buffer | null = null;
  const responseListener = async (response: Response) => {
    const ct = response.headers()['content-type'] ?? '';
    if (ct.includes('application/pdf')) {
      try {
        interceptedPdf = Buffer.from(await response.body());
      } catch {
        /* ok */
      }
    }
  };
  context.on('response', responseListener);

  try {
    const generarLocator = await findGenerarButton(appPage, 90_000);
    if (!generarLocator) {
      throw new Error('Botón "Generar Constancia" no encontrado en ningún frame del portal SAT (tras 90s)');
    }

    await generarLocator.scrollIntoViewIfNeeded();
    await appPage.waitForTimeout(500);

    // Algunos botones del SAT usan JSF/ajax; un click simple a veces no basta.
    // Hacemos click normal y, como fallback, dispatchEvent si no hay reacción.
    await generarLocator.click();
    await appPage.waitForTimeout(2000);

    // Intentar extraer el PDF del iframe/visor
    let pdf = await findPdfInFrames(appPage, 60_000);
    if (pdf) return pdf;

    // Si aún no hay PDF, algunos flujos abren popup clásico
    const popupPromise = context.waitForEvent('page', { timeout: 10_000 }).catch(() => null);
    const downloadPromise = appPage.waitForEvent('download', { timeout: 10_000 }).catch(() => null);

    // Reintentar click por si el primero no disparó el handler
    const stillVisible = await generarLocator.isVisible().catch(() => false);
    if (stillVisible) {
      await generarLocator.dispatchEvent('click');
    }

    const [popup, download] = await Promise.all([popupPromise, downloadPromise]);

    if (download) {
      const stream = await download.createReadStream();
      const chunks: Buffer[] = [];
      for await (const chunk of stream) chunks.push(chunk as Buffer);
      const downloaded = Buffer.concat(chunks);
      if (downloaded.subarray(0, 5).toString().startsWith('%PDF-')) return downloaded;
    }

    if (popup) {
      await popup.waitForLoadState('domcontentloaded').catch(() => undefined);
      await popup.waitForTimeout(2000);

      pdf = await findPdfInFrames(popup, 20_000);
      if (pdf) return pdf;

      const embedSrc = await popup.locator('embed[type="application/pdf"], iframe').first().getAttribute('src').catch(() => null);
      if (embedSrc) {
        const absolute = new URL(embedSrc, popup.url()).toString();
        pdf = await tryFetchPdfFromUrl(popup.mainFrame(), absolute).catch(() => null);
        if (pdf) return pdf;
      }

      if (interceptedPdf) return interceptedPdf;
      throw new Error('El visor abrió pero no se pudo extraer el PDF');
    }

    // Último intento: esperar un poco más a que el iframe termine de cargar
    await appPage.waitForTimeout(5000);
    pdf = await findPdfInFrames(appPage, 20_000);
    if (pdf) return pdf;

    if (interceptedPdf) return interceptedPdf;

    throw new Error('Click en "Generar Constancia" no produjo un PDF descargable ni visible');
  } finally {
    context.off('response', responseListener);
  }
}
