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://my.fyrst.de/#/login';

export class FyrstAdapter extends SourceAdapter {
  static key = 'fyrst';
  static label = 'FYRST (Deutsche Bank)';
  static group = 'Banken';

  async login() {
    const { page } = this;
    await page.goto(LOGIN_URL, { waitUntil: 'commit' });
    log.info(`[fyrst] URL nach goto: ${page.url()}`);
    await page.waitForTimeout(1500);
    log.info(`[fyrst] URL nach SPA-Init: ${page.url()}`);

    if (!page.url().includes('/login')) {
      log.info(`[fyrst] Session noch aktiv`);
      return;
    }

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

    // Schritt 1: Username-Feld (db-*-Klassen vom Deutsche-Bank-Designsystem)
    const user = page.locator('#db-input--0').or(page.locator('input[name="oneid"]')).first();

    try {
      await user.waitFor({ state: 'visible', timeout: 180_000 });
    } catch {
      const shot = join(ensureDir(LOG_DIR), `fyrst-no-form-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      throw new Error(`[fyrst] Username-Feld nicht sichtbar. URL: ${page.url()} — Screenshot: ${shot}`);
    }

    await user.fill(this.account.credentials.username);
    await page.waitForTimeout(300);

    // Schritt 2: "Weiter"-Button klicken
    const weiter = page.getByRole('button', { name: /^Weiter$|^Next$|^Continue$/i }).first();
    await weiter.waitFor({ state: 'visible', timeout: 10_000 });
    log.info('[fyrst] Drücke Weiter');
    await weiter.click();

    // Schritt 3: FYRST kann zwei Wege gehen — entweder Passwort-Seite ODER direkt 2FA (wenn Session-Cookie da ist).
    // Race: warten bis entweder Passwort-Feld erscheint oder die URL den Login verlässt.
    const pw = page.locator('input[type="password"]').first();
    const pwReady = pw.waitFor({ state: 'visible', timeout: 180_000 }).then(() => 'pw').catch(() => null);
    const redirected = page.waitForFunction(
      () => !location.hash.includes('/login') && !location.pathname.includes('/login'),
      null,
      { timeout: 180_000 },
    ).then(() => 'redirect').catch(() => null);

    const what = await Promise.race([pwReady, redirected]);

    if (what === 'redirect') {
      log.info(`[fyrst] Direkt nach 2FA eingeloggt (Passwort-Schritt übersprungen, URL: ${page.url()})`);
      return;
    }
    if (what !== 'pw') {
      const shot = join(ensureDir(LOG_DIR), `fyrst-no-password-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      throw new Error(`[fyrst] Weder Passwort-Feld noch Redirect. URL: ${page.url()} — Screenshot: ${shot}`);
    }

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

    // Schritt 4: Einloggen-Button (data-test="passwordButton")
    const submit = page.locator('[data-test="passwordButton"] button').first();
    await submit.waitFor({ state: 'visible', timeout: 10_000 });
    log.info('[fyrst] Drücke Einloggen');
    try {
      await submit.click({ timeout: 5_000 });
    } catch {
      log.warn('[fyrst] Click blockiert — Fallback auf DOM-Click + Enter');
      await submit.evaluate((el) => el.click()).catch(() => {});
      await pw.press('Enter').catch(() => {});
    }

    log.info('[fyrst] Warte auf 2FA-Bestätigung (photoTAN/pushTAN) — 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), `fyrst-no-redirect-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      log.warn(`[fyrst] Kein Redirect nach 3 Min. URL: ${page.url()} — Screenshot: ${shot}`);
      throw e;
    }
    log.info(`[fyrst] Login erfolgreich (URL: ${page.url()})`);
  }

  async *iterateStatements() {
    const { page } = this;

    // Angular-Dashboard rendert nach Login noch ein paar Sekunden — kurz abwarten
    await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => {});
    await page.waitForTimeout(1000);
    log.info(`[fyrst] Dashboard-URL: ${page.url()}`);

    // Bevorzugter Weg: direkte URL zur Postbox. Braucht account.postboxFolderId (=10-stellige FYRST-Kontonr)
    let navigated = false;
    if (this.account.postboxFolderId) {
      const url = `https://my.fyrst.de/#/messages/postbox/dashboard/${this.account.postboxFolderId}`;
      log.info(`[fyrst] Goto ${url}`);
      await page.goto(url, { waitUntil: 'commit' });
      await page.waitForTimeout(1000);
      navigated = true;
    }

    // Fallback: Klick auf Nachrichten-Button (wenn postboxFolderId nicht gesetzt)
    if (!navigated) {
      const msgBtn = page.locator('button[data-test="metaNavBarMenuMessagesMetaItem"]').first();
      try {
        await msgBtn.waitFor({ state: 'visible', timeout: 30_000 });
      } catch {
        const shot = join(ensureDir(LOG_DIR), `fyrst-no-msg-btn-${Date.now()}.png`);
        await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
        throw new Error(`[fyrst] Nachrichten-Button nicht sichtbar. URL: ${page.url()} — Screenshot: ${shot}`);
      }

      log.info('[fyrst] Klicke Nachrichten (kein postboxFolderId in Config)');
      await msgBtn.scrollIntoViewIfNeeded().catch(() => {});
      try {
        await msgBtn.click({ timeout: 5_000 });
      } catch {
        log.warn('[fyrst] Normaler Click blockiert — Fallback auf DOM-Click');
        await msgBtn.evaluate((el) => el.click()).catch(() => {});
      }
      await page.waitForTimeout(1500);
      log.info(`[fyrst] URL nach Nachrichten-Click: ${page.url()}`);

      // Auto-Persist: URL enthält folderId? Dann für künftige Läufe merken.
      const urlMatch = page.url().match(/messages\/postbox\/dashboard\/(\d+)/);
      if (urlMatch) {
        log.info(`[fyrst] Folder-ID erkannt: ${urlMatch[1]} — setze mit "set-option ${this.account.id} postboxFolderId ${urlMatch[1]}" für direkte Navigation`);
      }
    }

    // Liste abwarten — Zeilen erscheinen via XHR
    const rowsLocator = page.locator('cirrus-file-row[data-test^="fileRow"]');
    await rowsLocator.first().waitFor({ state: 'visible', timeout: 30_000 }).catch(() => {
      log.warn('[fyrst] Keine Dateizeilen sichtbar');
    });

    const total = await rowsLocator.count();
    log.info(`[fyrst] ${total} Einträge`);

    for (let idx = 0; idx < total; idx++) {
      const row = rowsLocator.nth(idx);
      const title = (await row.locator('[data-test="name"]').first().innerText().catch(() => '')).trim();
      const dateStr = (await row.locator('[data-test="date"]').first().innerText().catch(() => '')).trim();
      const category = (await row.locator('[data-test="category"]').first().innerText().catch(() => '')).trim();
      const accountStr = (await row.locator('[data-test="account"]').first().innerText().catch(() => '')).trim();

      // Jahr aus Datum (DD.MM.YYYY) oder aus Titel extrahieren
      const dateMatch = dateStr.match(/(\d{2})\.(\d{2})\.(\d{4})/);
      const titleYearMatch = title.match(/\b(20\d{2})\b/);
      const year = titleYearMatch ? Number(titleYearMatch[1])
        : dateMatch ? Number(dateMatch[3])
        : new Date().getFullYear();

      // Kontonummer extrahieren. Fyrst-Format ist meist "BLZ-Filiale Kontonr Unterkonto Waehrung",
      // z.B. "209 5253018 10 EUR" → echte Kontonummer ist "5253018", NICHT die slice(0,10) der
      // konkatenierten Ziffern (2095253018 mischt BLZ+Konto+1 Unterkonto-Ziffer und kann zu
      // falschen kontoAliases-Lookups fuehren). Stattdessen das laengste pure-digit-Token nehmen
      // — robust auch fuer IBAN-artige Anzeigen (dort ist's der einzige Token).
      const tokens = accountStr.split(/\s+/).filter((t) => /^\d+$/.test(t));
      const kontoNr = tokens.length
        ? tokens.reduce((a, b) => (a.length >= b.length ? a : b))
        : accountStr.replace(/\D/g, '').slice(0, 10);

      // Subfolder-Logik:
      //  - Steuerdokument / Steuer im Titel → "Steuer"
      //  - Mitteilungen → "Mitteilungen"
      //  - sonst Kontonummer (Konto-Kategorie)
      let subfolder;
      if (/Steuer/i.test(category) || /Steuer/i.test(title)) {
        subfolder = 'Steuer';
      } else if (/Mitteilung/i.test(category)) {
        subfolder = 'Mitteilungen';
      } else if (kontoNr) {
        subfolder = this.account.kontoAliases?.[kontoNr] || kontoNr;
      }

      // Dateiname: "JJJJ-MM-TT <Titel> (<Konto>).pdf" — Datum vorn fuer chronologische Sortierung.
      const dateIso = dateMatch ? `${dateMatch[3]}-${dateMatch[2]}-${dateMatch[1]}` : '';
      const datePrefix = dateIso ? `${dateIso} ` : '';
      const filename = `${datePrefix}${title}${accountStr ? ` (${accountStr})` : ''}.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;
        }
      }

      // 3-Punkte-Menü der Zeile öffnen → "Herunterladen" klicken → Download abfangen
      const menuBtn = row.locator('button[aria-label="Dokumentaktion Menü"]').first();
      try {
        await menuBtn.click({ timeout: 5_000 });
      } catch (e) {
        log.warn(`[fyrst] Menü-Button-Click fehlgeschlagen bei "${title}": ${e.message.split('\n')[0]}`);
        continue;
      }

      // Auf das Menü warten (im document-level Overlay, nicht in der Row)
      const downloadBtn = page.locator('button[data-test="download"][role="menuitem"]').first();
      try {
        await downloadBtn.waitFor({ state: 'visible', timeout: 5_000 });
      } catch {
        log.warn(`[fyrst] Download-Menüeintrag nicht sichtbar bei "${title}"`);
        await page.keyboard.press('Escape').catch(() => {}); // Menü schließen
        continue;
      }

      // Manuelle one-shot Listener (kein page.waitForEvent), damit nach Auflosung GENAU EIN
      // Outcome zaehlt und die Listener garantiert removed werden — kein Risiko, dass der
      // verlierende Listener im naechsten Iterations-Cycle ungewollt feuert.
      let resolveEvent;
      let settled = false;
      const eventP = new Promise((r) => { resolveEvent = r; });
      const settle = (val) => { if (!settled) { settled = true; resolveEvent(val); } };
      const onDownload = (d) => settle({ kind: 'main-download', download: d });
      const onPopup = (p) => settle({ kind: 'popup', popup: p });
      page.on('download', onDownload);
      page.on('popup', onPopup);

      let event;
      let timeoutHandle;
      try {
        await downloadBtn.click();
        const timeoutP = new Promise((resolve) => {
          timeoutHandle = setTimeout(() => resolve(null), 20_000);
        });
        event = await Promise.race([eventP, timeoutP]);
      } finally {
        // Race-Timer aufraeumen, sonst leakt ein 20s-Handle pro Item.
        if (timeoutHandle) clearTimeout(timeoutHandle);
        page.off('download', onDownload);
        page.off('popup', onPopup);
      }

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