import { chromium } from 'playwright';
import { browserDataDir } from '../paths.mjs';
import { attachPageDebug, log } from '../logger.mjs';

const CHROME_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';

const STEALTH_INIT = () => {
  // navigator.webdriver -> undefined
  // configurable:true, sonst wirft eine zweite Anwendung (z.B. nach Reload, in Iframes
  // oder bei pages, die das Init-Script via addInitScript erneut feuern) "TypeError:
  // Cannot redefine property". Stiller Init lieber als Adapter-Crash.
  try {
    Object.defineProperty(Navigator.prototype, 'webdriver', {
      get: () => undefined,
      configurable: true,
    });
  } catch { /* schon definiert, lassen */ }
  // Chrome-Objekt vortäuschen
  if (!window.chrome) window.chrome = { runtime: {} };
  // Sprachen-Liste realistisch halten
  try {
    Object.defineProperty(navigator, 'languages', {
      get: () => ['de-DE', 'de', 'en-US', 'en'],
      configurable: true,
    });
  } catch { /* schon definiert */ }
  // Plugins nicht leer lassen (klassisches Bot-Signal)
  try {
    Object.defineProperty(navigator, 'plugins', {
      get: () => [1, 2, 3, 4, 5].map((i) => ({ name: `Plugin ${i}` })),
      configurable: true,
    });
  } catch { /* schon definiert */ }
  // permissions.query für notifications liefert bei echten Browsern 'prompt' nicht 'denied'
  const origQuery = navigator.permissions?.query?.bind(navigator.permissions);
  if (origQuery) {
    navigator.permissions.query = (p) =>
      p && p.name === 'notifications'
        ? Promise.resolve({ state: Notification.permission, onchange: null })
        : origQuery(p);
  }
};

export class SourceAdapter {
  static key = '';
  static label = '';

  constructor(account, { headful = true, cfg } = {}) {
    this.account = account;
    this.headful = headful;
    this.cfg = cfg;
  }

  async openContext() {
    const userDataDir = browserDataDir(this.constructor.key, this.account.id);
    const baseOpts = {
      headless: !this.headful,
      viewport: { width: 1280, height: 900 },
      locale: 'de-DE',
      timezoneId: 'Europe/Berlin',
      userAgent: CHROME_UA,
      acceptDownloads: true,
      args: ['--disable-blink-features=AutomationControlled'],
      ignoreDefaultArgs: ['--enable-automation'],
    };
    // Standard: bundled Chromium (stabiles Profil). Opt-in: DOCUDOWN_BROWSER=chrome → installierter Chrome
    const useChrome = process.env.DOCUDOWN_BROWSER === 'chrome';
    const launchOpts = useChrome ? { ...baseOpts, channel: 'chrome' } : baseOpts;
    const context = await chromium.launchPersistentContext(userDataDir, launchOpts);
    await context.addInitScript(STEALTH_INIT);
    const page = context.pages()[0] ?? await context.newPage();
    attachPageDebug(page, this.constructor.key);
    log.debug(`[${this.constructor.key}] openContext: userDataDir=${userDataDir}, useChrome=${useChrome}, headful=${this.headful}`);
    this.context = context;
    this.page = page;
    return { context, page };
  }

  async close() {
    if (this.context) await this.context.close();
  }

  async dismissConsent() {
    const page = this.page;

    // CMP-Banner kommen oft lazy nach dem domcontentloaded — bis zu 5s warten.
    // Wenn keiner kommt, sparen wir uns den 15s langen Probe-Loop unten.
    // Wichtig: ein offener HTML5 <dialog open> macht den ganzen Body INERT — solange
    // der nicht weg ist, schlaegt jedes spaetere fill()/click() still fehl.
    const cmpDetected = await page.waitForFunction(() => {
      return !!(
        document.querySelector('#usercentrics-cmp-ui')
        || document.querySelector('dialog[open] button[data-action-type]')
        || document.querySelector('button[data-action-type="deny"]')
        || document.querySelector('button[data-action-type="accept"]')
        || document.querySelector('#onetrust-banner-sdk')
        || document.querySelector('#CybotCookiebotDialog')
        || document.querySelector('#didomi-host')
        || document.querySelector('.cmplz-cookiebanner')
        || document.querySelector('#cookie-modal.show')  // captrader.com Bootstrap-Modal
        || document.querySelector('#gdpr-reject-all')
        || document.querySelector('#cmpbox')  // consentmanager.net (gothaer u.a.)
        || document.querySelector('ing-dialog[opened]')  // ING Web-Components Cookie-Layer
      );
    }, null, { timeout: 5_000 }).then(() => true).catch(() => false);
    if (!cmpDetected) return false;

    // ING Cookie-Layer (Web-Components mit Shadow-DOM-Tree). Selektoren wie button:has-text
    // erreichen Buttons innerhalb von ing-cc-dialog-level0 nicht zuverlaessig — wir
    // durchsuchen den Shadow-DOM-Tree komplett und klicken Akzeptieren/Ablehnen-Texte.
    const ingHandled = await page.evaluate(() => {
      const root = document.querySelector('ing-dialog[opened]');
      if (!root) return 'no-host';
      // Rekursiv alle Buttons in allen Shadow-Roots sammeln.
      const collectButtons = (node, acc) => {
        if (node.shadowRoot) {
          for (const b of node.shadowRoot.querySelectorAll('button')) acc.push(b);
          for (const child of node.shadowRoot.querySelectorAll('*')) collectButtons(child, acc);
        }
        for (const child of node.querySelectorAll ? node.querySelectorAll('*') : []) {
          if (child.shadowRoot) collectButtons(child, acc);
        }
      };
      const buttons = [];
      collectButtons(root, buttons);
      // Light-DOM auch noch durchsuchen
      for (const b of root.querySelectorAll('button')) buttons.push(b);

      const denyRe = /^(ablehnen|alle ablehnen|nur notwendige|nur erforderlich)/i;
      const acceptRe = /^(alle? akzeptieren|akzeptieren|zustimmen|einwilligen)/i;
      // Erst Deny (privacy-first), dann Accept als Fallback.
      for (const re of [denyRe, acceptRe]) {
        for (const b of buttons) {
          const txt = (b.textContent || '').trim();
          if (re.test(txt)) { b.click(); return `clicked: ${txt.slice(0, 40)}`; }
        }
      }
      return 'no-button-matched';
    }).catch(() => 'eval-error');
    if (ingHandled.startsWith('clicked')) {
      log.info(`[consent] ING-Layer geschlossen (${ingHandled})`);
      await page.waitForFunction(() => !document.querySelector('ing-dialog[opened]'), null, { timeout: 5_000 }).catch(() => {});
      return true;
    }

    // Usercentrics rendert das CMP in einem Shadow-DOM unter <... id="usercentrics-cmp-ui">.
    // Playwright's CSS-Selektoren matchen dort i. d. R. nicht — wir muessen den ShadowRoot
    // gezielt durchgreifen. Reihenfolge: erst Deny (datenschutzfreundlich), Accept als Fallback.
    const ucHandled = await page.evaluate(() => {
      const host = document.querySelector('#usercentrics-cmp-ui');
      if (!host) return 'no-host';
      const root = host.shadowRoot;
      if (!root) return 'no-shadow';
      const tries = [
        '[data-testid="uc-deny-all-button"]',
        'button[data-action-type="deny"]',
        '[data-testid="uc-accept-all-button"]',
        'button[data-action-type="accept"]',
      ];
      for (const sel of tries) {
        const btn = root.querySelector(sel);
        if (btn) { btn.click(); return sel; }
      }
      return 'host-but-no-button';
    }).catch(() => 'eval-error');
    if (ucHandled.startsWith('[') || ucHandled.startsWith('button')) {
      // Verifizieren, dass der Host weg ist (= Banner geschlossen, Body nicht mehr inert)
      await page.waitForFunction(
        () => !document.querySelector('#usercentrics-cmp-ui'),
        null,
        { timeout: 5_000 },
      ).catch(() => {});
      return true;
    }

    // Haeufige CMP-Selektoren: OneTrust, Cookiebot, Usercentrics, TrustArc, Didomi, Complianz.
    // Reihenfolge: erst Deny (datenschutzfreundlich), dann Accept als Fallback.
    // Usercentrics hat zwei Render-Varianten: Shadow-DOM (oben separat behandelt) und ein
    // regulaeres <dialog id="uc-dialog-shell"> — die data-action-type-Selektoren funktionieren
    // fuer beide.
    const idSelectors = [
      // Deny-Buttons (privacy-first)
      'button[data-action-type="deny"]',
      'button.uc-deny-button',
      '#cmpbntnotxt',
      '#cmpbox button.cmpboxbtnno',  // gothaer.de & andere consentmanager.net (cmpbox-CMP)
      '#cmpbox .cmpboxbtnno',
      '#onetrust-reject-all-handler',
      '#gdpr-reject-all',  // captrader.com Bootstrap-Modal "Reject All Cookies"
      // Accept-Buttons (Fallback)
      'button[data-action-type="accept"]',
      'button.uc-accept-button',
      '#onetrust-accept-btn-handler',
      '#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll',
      '#CybotCookiebotDialogBodyButtonAccept',
      '[data-testid="uc-accept-all-button"]',
      'button#uc-accept-all-button',
      '#truste-consent-button',
      '#didomi-notice-agree-button',
      '#btn_accept_cookies',  // captrader.com Bootstrap-Modal "Accept Cookies"
      '.cmplz-btn.cmplz-accept',
      'button[aria-label*="Alle akzeptieren" i]',
      'button[aria-label*="Alles akzeptieren" i]',
      'button[aria-label*="accept all" i]',
      '[data-cy="accept-all"]',
      '[data-qa="accept-all"]',
    ];
    // Haeufige Button-Texte (deutsch und englisch). Deny zuerst, dann Accept.
    // Anchors absichtlich locker — Buttons enthalten oft Icons + Trailing-Whitespace
    // (z.B. captrader.com: "Reject All Cookies " mit <i> davor).
    const textPatterns = [
      /^ablehnen$/i,
      /^alle ablehnen$/i,
      /reject all cookies?/i,
      /^decline$/i,
      /^alle akzeptieren$/i,
      /^alles akzeptieren$/i,
      /^alle auswählen & akzeptieren$/i,
      /^alle (cookies )?(akzeptieren|annehmen|erlauben|zulassen)$/i,
      /^akzeptieren$/i,
      /^zustimmen$/i,
      /^einverstanden$/i,
      /^einwilligen$/i,
      /accept (all )?cookies?/i,
      /^allow all$/i,
      /^i agree$/i,
    ];

    // Nach einem erfolgreichen Click warten wir bis ein offener CMP-Dialog wirklich
    // geschlossen ist. Wichtig fuer HTML5-<dialog open>: solange der offen ist, ist
    // der Body INERT — jeder spaetere fill()/click() auf der Login-Form landet im Leeren.
    const waitForDialogClosed = async () => {
      try {
        await page.waitForFunction(() => {
          for (const d of document.querySelectorAll('dialog[open]')) {
            const id = d.id || '';
            const html = d.outerHTML || '';
            if (/uc-|cmp|consent|cookie|usercentrics/i.test(id) ||
                /uc-|cmp|consent|cookie|usercentrics|data-action-type/i.test(html.slice(0, 500))) {
              return false;
            }
          }
          if (document.querySelector('#usercentrics-cmp-ui')) return false;
          return true;
        }, null, { timeout: 5_000 });
      } catch { /* timeout — Banner haengt; weiterprobieren */ }
    };

    // 1. Selektor-Kandidaten durchgehen (auf der Hauptseite + in typischen CMP-iframes)
    const frames = [page, ...page.frames().filter((f) => /consent|sp-message|cmp|cookie|onetrust|didomi|usercentrics/i.test(f.url()))];
    for (const target of frames) {
      for (const sel of idSelectors) {
        try {
          const loc = target.locator(sel).first();
          if (await loc.isVisible({ timeout: 400 }).catch(() => false)) {
            await loc.click({ timeout: 2000 });
            await waitForDialogClosed();
            await page.waitForTimeout(200);
            return true;
          }
        } catch { /* weiter */ }
      }
      for (const re of textPatterns) {
        try {
          const loc = target.getByRole('button', { name: re }).first();
          if (await loc.isVisible({ timeout: 400 }).catch(() => false)) {
            await loc.click({ timeout: 2000 });
            await waitForDialogClosed();
            await page.waitForTimeout(200);
            return true;
          }
        } catch { /* weiter */ }
      }
    }
    return false;
  }

  async login() {
    throw new Error(`login() nicht implementiert für Quelle ${this.constructor.key}`);
  }

  async *iterateStatements() {
    throw new Error(`iterateStatements() nicht implementiert für Quelle ${this.constructor.key}`);
    // Ein Implementor liefert: { filename, year, download } — download = Playwright Download-Objekt
    // oder ein { filename, year, buffer: Buffer } für direkten Buffer.
  }
}
