import { SourceAdapter } from './base.mjs';
import { log } from '../logger.mjs';
import { join } from 'node:path';
import { ensureDir, LOG_DIR, fileExistsAtAnyLayout, safeFilename } from '../paths.mjs';

const LOGIN_URL = 'https://banking.volkswagenbank.de/#/login';

export class VwBankAdapter extends SourceAdapter {
  static key = 'vw-bank';
  static label = 'Volkswagen Bank';
  static group = 'Banken';

  async login() {
    const { page } = this;
    await page.goto(LOGIN_URL, { waitUntil: 'domcontentloaded' });
    log.info(`[vw-bank] URL nach goto: ${page.url()}`);

    // SPA braucht einen Tick, um zu booten + ggf. Auto-Redirect, wenn Session noch aktiv.
    await page.waitForTimeout(1500);
    log.info(`[vw-bank] URL nach SPA-Init: ${page.url()}`);

    // Session noch aktiv? Dann ist der Hash nicht mehr /login.
    if (!page.url().includes('/login')) {
      log.info(`[vw-bank] Session noch aktiv`);
      return;
    }

    if (await this.dismissConsent()) log.info('[vw-bank] Consent-Banner bestätigt');

    // Form-Selektoren der echten VW-Bank-Login-Seite (banking.volkswagenbank.de)
    const user = page.locator('#username');
    const pw = page.locator('#password');
    const submit = page.locator('#login-button');

    // Auf Login-Form warten — mit langem Timeout für den Fall, dass eine Bot-Challenge manuell gelöst werden muss.
    try {
      await user.waitFor({ state: 'visible', timeout: 180_000 });
    } catch {
      const shot = join(ensureDir(LOG_DIR), `vwbank-no-form-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      throw new Error(`[vw-bank] Login-Form (#username) nicht sichtbar. URL: ${page.url()} — Screenshot: ${shot}`);
    }

    await user.fill(this.account.credentials.username);
    await pw.fill(this.account.credentials.password);
    await page.waitForTimeout(500);

    await submit.waitFor({ state: 'visible', timeout: 10_000 });
    log.info('[vw-bank] Drücke Login');
    await submit.click();

    // Hash-basiertes Routing: waitForURL greift hier nicht zuverlässig — stattdessen in der Browser-Umgebung pollen.
    log.info('[vw-bank] Warte auf 2FA-Bestätigung (photoTAN/pushTAN-App) — bis zu 3 Min ...');
    try {
      await page.waitForFunction(
        () => !location.hash.includes('/login') && !location.pathname.includes('/login'),
        null,
        { timeout: 180_000 },
      );
    } catch (e) {
      const shot = join(ensureDir(LOG_DIR), `vwbank-no-redirect-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      log.warn(`[vw-bank] Kein Redirect nach 3 Min. URL: ${page.url()} — Screenshot: ${shot}`);
      throw e;
    }
    log.info(`[vw-bank] Login erfolgreich (URL: ${page.url()})`);
  }

  async *iterateStatements() {
    const { page } = this;
    // Default-Reihenfolge: Korrespondenz + aktuelle Kontoauszüge (letzte 2 Jahre).
    // Das Archiv (accountstatements/archive) ist NICHT im Default — ist ein einmaliger
    // Nachhol-Vorgang für Alt-Bestände. Bei Bedarf in account.categories mit aufnehmen:
    //   ["correspondences", "accountstatements", "accountstatements/archive"]
    const categories = this.account.categories || [
      'correspondences',
      'accountstatements',
    ];
    const categoryFolders = {
      correspondences: 'Korrespondenz',
      ...(this.account.categoryFolders || {}),
    };

    for (const cat of categories) {
      log.info(`[vw-bank] Kategorie: ${cat}`);
      const listUrl = `https://banking.volkswagenbank.de/#/mailbox/${cat}`;

      // Datenzeilen: haben einen Subject-Link. Divider-Zeilen (Jahres-Header) werden so automatisch ausgeschlossen.
      const rowsLocator = page.locator('tr.c-inbox-table__tr:has(a.c-inbox-table__link)');

      // Lädt die Liste — mit einem Retry falls beim ersten Versuch die API noch nicht antwortet.
      let total = 0;
      for (let attempt = 1; attempt <= 2; attempt++) {
        // Response-Wait auf Mailbox-API VOR dem goto registrieren, damit nichts zwischen den Aktionen verloren geht.
        const apiReady = page.waitForResponse(
          (r) => r.url().toLowerCase().includes('mailbox') && r.request().method() === 'GET' && r.ok(),
          { timeout: 20_000 },
        ).catch(() => null);

        log.info(`[vw-bank] ${cat}: navigiere zu ${listUrl}`);
        // waitUntil: 'commit' — feuert sofort, sobald URL committed ist. 'domcontentloaded' würde bei Hash-Navigation
        // einer SPA in den Default-Timeout laufen (keine volle Seitenlade, kein DOMContentLoaded-Event).
        await page.goto(listUrl, { waitUntil: 'commit' });
        log.info(`[vw-bank] ${cat}: warte auf Mailbox-API`);
        const resp = await apiReady;
        log.info(`[vw-bank] ${cat}: API-Response ${resp ? 'erhalten' : 'Timeout'}`);
        await page.waitForTimeout(300);

        // Archiv: Start-Datum muss gesetzt werden, sonst bleibt die Liste leer.
        // Das Feld ist tastatursperr — <calendar-range>-Web-Component gateet alles. Zwei Strategien:
        //  A) Native Value-Setter + change-Event (umgeht Angular's Value-Accessor)
        //  B) UI-Fallback: Calendar-Popup öffnen, "Vorheriger Monat" rückwärts klicken, Tag 1 klicken.
        if (cat.includes('archive')) {
          const rangeInput = page.locator('#range');
          // waitFor statt isVisible — letzteres wartet NICHT, der Range-Input rendert
          // beim Archive-Tab oft erst nach API-Lazy-Load. Ohne wait verfehlen wir ihn
          // und ueberspringen den Date-Setter still → Archiv liefert 0 Eintraege.
          const rangeReady = await rangeInput.waitFor({ state: 'visible', timeout: 3000 }).then(() => true).catch(() => false);
          if (rangeReady) {
            const targetYear = this.account.archiveStartYear || 2015; // default 10 Jahre zurück
            const targetDateStr = `01.01.${targetYear}`;
            log.info(`[vw-bank] ${cat}: Zieldatum ${targetDateStr}`);

            const filterApi = page.waitForResponse(
              (r) => r.url().toLowerCase().includes('mailbox') && r.ok(),
              { timeout: 20_000 },
            ).catch(() => null);

            // Strategie A: Native-Setter + Events
            await rangeInput.evaluate((el, val) => {
              const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
              setter.call(el, val);
              el.dispatchEvent(new Event('input', { bubbles: true }));
              el.dispatchEvent(new Event('change', { bubbles: true }));
              el.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
            }, targetDateStr).catch(() => {});

            let val = await rangeInput.inputValue().catch(() => '');
            log.info(`[vw-bank] ${cat}: nach Native-Setter Input="${val}"`);

            // Strategie B: Falls Native-Setter nicht durchgreift → Kalender-UI bedienen
            if (val !== targetDateStr) {
              log.info(`[vw-bank] ${cat}: Fallback auf Kalender-Picker-UI`);
              const toggleBtn = page.locator('button[aria-label="Toggle Datepicker"]').first();
              if (await toggleBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
                await toggleBtn.click();
                await page.waitForTimeout(300);

                // Rückwärts-Navigation: Anzahl Monate von heute zum Zielmonat (Januar Target-Jahr)
                const now = new Date();
                const monthsBack = (now.getFullYear() - targetYear) * 12 + now.getMonth(); // Januar = 0 Monate ab Anfang Jahr
                log.info(`[vw-bank] ${cat}: ${monthsBack} Monate zurück klicken`);

                const prevBtn = page.locator('calendar-range i[slot="previous"], [aria-label="Vorheriger Monat"]').first();
                for (let i = 0; i < monthsBack; i++) {
                  await prevBtn.click({ timeout: 2000 }).catch(() => {});
                  await page.waitForTimeout(40);
                }

                // Tag 1 klicken — typischerweise <button> oder <td> mit Text "1"
                const dayOne = page.locator('calendar-month button', { hasText: /^1$/ })
                  .or(page.locator('calendar-month [role="button"]', { hasText: /^1$/ }))
                  .first();
                if (await dayOne.isVisible({ timeout: 2000 }).catch(() => false)) {
                  await dayOne.click();
                  log.info(`[vw-bank] ${cat}: Tag 1 geklickt`);
                } else {
                  log.warn(`[vw-bank] ${cat}: Tag 1 im Kalender nicht gefunden`);
                }

                val = await rangeInput.inputValue().catch(() => '');
                log.info(`[vw-bank] ${cat}: nach Kalender-UI Input="${val}"`);
              }
            }

            await filterApi;
            await page.waitForTimeout(500);
          }
        }

        await rowsLocator.first().waitFor({ state: 'visible', timeout: 30_000 }).catch(() => {});
        total = await rowsLocator.count();
        if (total > 0) break;
        // Archiv nie retryen — der 2. Lauf würde den Kalender ausgehend von der bereits gesetzten
        // Position nochmal 10 Jahre rückwärts klicken und ins Jahr 2003+ abdriften.
        if (cat.includes('archive')) {
          log.warn(`[vw-bank] ${cat}: 0 Einträge — kein Retry fürs Archiv`);
          break;
        }
        log.warn(`[vw-bank] ${cat}: 0 Einträge im Versuch ${attempt} — erneut laden`);
      }

      if (total === 0) {
        log.warn(`[vw-bank] ${cat}: keine Einträge nach 2 Versuchen — überspringe`);
        continue;
      }
      log.info(`[vw-bank] ${cat}: ${total} Einträge`);

      for (let idx = 0; idx < total; idx++) {
        const row = rowsLocator.nth(idx);
        const link = row.locator('a.c-inbox-table__link').first();
        const title = (await link.innerText().catch(() => '')).replace(/\s+/g, ' ').trim();
        const datetimeAttr = await row.locator('time[datetime]').first().getAttribute('datetime').catch(() => '');
        const rowId = (await row.getAttribute('id').catch(() => '')) || '';

        // Jahr: aus Titel bevorzugt (z.B. "Steuerbescheinigung 2025"), sonst aus datetime-Attribut
        const titleYear = title.match(/\b(20\d{2})\b/);
        const year = titleYear ? Number(titleYear[1])
          : datetimeAttr ? Number(datetimeAttr.slice(0, 4))
          : new Date().getFullYear();

        // Subfolder-Logik:
        //  1. "Steuerbescheinigung" / "Ertr.gnis" im Titel → "Steuer"
        //  2. Row-ID enthält Kontonummer (Muster "item-<10-stellige-zahl>-...") → Kontonummer
        //  3. sonst Kategorie-Fallback (z.B. "Korrespondenz")
        const kontoIdMatch = rowId.match(/^#?item-(\d{6,})/);
        let subfolder;
        if (/Steuerbescheinigung|Jahressteuerbescheinigung|Ertr.gnis/i.test(title)) {
          subfolder = 'Steuer';
        } else if (kontoIdMatch) {
          const kontoNr = kontoIdMatch[1];
          subfolder = this.account.kontoAliases?.[kontoNr] || kontoNr;
        } else if (categoryFolders[cat]) {
          subfolder = categoryFolders[cat];
        }

        const safeCat = cat.replace(/\//g, '-');
        // Pattern "JJJJ-MM-TT <Titel>.pdf" — Datum aus datetime-Attribut wenn ISO,
        // sonst kein Prefix (selten — die VW-Banking-UI liefert standardisiert ISO).
        const dateIsoMatch = datetimeAttr.match(/^(\d{4}-\d{2}-\d{2})/);
        const datePrefix = dateIsoMatch ? `${dateIsoMatch[1]} ` : '';
        const filename = `${datePrefix}${title || `vwbank-${safeCat}-${idx + 1}`}.pdf`;

        // Pre-Skip
        if (this.cfg) {
          const safeSub = subfolder ? safeFilename(subfolder) : undefined;
          const existing = fileExistsAtAnyLayout(this.cfg, this.account.folder, year, safeSub, filename);
          if (existing) {
            log.info(`[${this.account.id}] skip (bereits vorhanden): ${existing}`);
            continue;
          }
        }

        // Click-Target: expliziter Download-Button (Korrespondenz) bevorzugt, sonst der Subject-Link.
        const dlBtn = row.locator('button[aria-label="Dokument herunterladen"]').first();
        const useBtn = (await dlBtn.count().catch(() => 0)) > 0;
        const trigger = useBtn ? dlBtn : link;

        // Click kann Download direkt auf der Hauptseite auslösen ODER ein Popup öffnen.
        // Manuelle one-shot Listener: GENAU EIN Outcome zaehlt, Listener werden garantiert
        // removed — sonst kann der verlierende Listener im naechsten Iterations-Cycle
        // ungewollt feuern (gleicher Bug wie gestern in captrader gefixt).
        let resolveFirst;
        let firstSettled = false;
        const firstP = new Promise((r) => { resolveFirst = r; });
        const firstSettle = (val) => { if (!firstSettled) { firstSettled = true; resolveFirst(val); } };
        const onMainDl = (d) => firstSettle({ kind: 'main-download', download: d });
        const onPopup = (p) => firstSettle({ kind: 'popup', popup: p });
        page.on('download', onMainDl);
        page.on('popup', onPopup);

        let firstEvent;
        let timeoutHandle;
        try {
          await trigger.click().catch((e) => log.warn(`[vw-bank] Click fehlgeschlagen "${title}": ${e.message.split('\n')[0]}`));
          const timeoutP = new Promise((resolve) => {
            timeoutHandle = setTimeout(() => resolve(null), 20_000);
          });
          firstEvent = await Promise.race([firstP, timeoutP]);
        } finally {
          // Race-Timer aufraeumen, sonst leakt ein 20s-Handle pro Item.
          if (timeoutHandle) clearTimeout(timeoutHandle);
          page.off('download', onMainDl);
          page.off('popup', onPopup);
        }

        if (firstEvent?.kind === 'main-download') {
          yield { filename, year, download: firstEvent.download, subfolder };
        } else if (firstEvent?.kind === 'popup') {
          const popup = firstEvent.popup;
          try {
            const respPromise = popup.waitForResponse(
              (r) => (r.headers()['content-type'] || '').toLowerCase().includes('pdf'),
              { timeout: 15_000 },
            ).then((r) => ({ kind: 'response', response: r })).catch(() => null);
            const dlPromise = popup.waitForEvent('download', { timeout: 15_000 })
              .then((d) => ({ kind: 'download', download: d })).catch(() => null);
            const winner = await Promise.race([respPromise, dlPromise]);

            if (winner?.kind === 'download') {
              yield { filename, year, download: winner.download, subfolder };
            } else if (winner?.kind === 'response') {
              yield { filename, year, buffer: await winner.response.body(), subfolder };
            } else {
              log.warn(`[vw-bank] Kein PDF im Popup bei "${title}"`);
            }
          } finally {
            await popup.close().catch(() => {});
          }
        } else {
          const shot = join(ensureDir(LOG_DIR), `vwbank-no-event-${idx}-${Date.now()}.png`);
          await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
          log.warn(`[vw-bank] Weder Download noch Popup bei "${title}" — Screenshot: ${shot}`);
        }
      }
    }
  }
}
