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

const LOGIN_URL = 'https://app.n26.com/login';

export class N26Adapter extends SourceAdapter {
  static key = 'n26';
  static label = 'N26';
  static group = 'Banken';

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

    // Session noch aktiv? Pfad verlässt /login
    if (!page.url().includes('/login')) {
      log.info(`[n26] Session noch aktiv`);
      return;
    }

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

    const email = page.locator('#email').or(page.locator('input[data-testid="login-email"]')).first();
    const pw = page.locator('#password').or(page.locator('input[data-testid="login-password"]')).first();
    const submit = page.locator('button[data-testid="login-button"]').first();

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

    await email.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('[n26] Drücke Einloggen');
    await submit.click();

    log.info('[n26] Warte auf 2FA-Bestätigung im gekoppelten Gerät (N26-App) — bis zu 3 Min ...');
    try {
      await page.waitForFunction(
        () => !location.pathname.includes('/login'),
        null,
        { timeout: 180_000 },
      );
    } catch (e) {
      const shot = join(ensureDir(LOG_DIR), `n26-no-redirect-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      log.warn(`[n26] Kein Redirect nach 3 Min. URL: ${page.url()} — Screenshot: ${shot}`);
      throw e;
    }
    log.info(`[n26] Login erfolgreich (URL: ${page.url()})`);
  }

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

    // Zur Downloads-Seite: entweder direkt per gespeicherter URL oder via Quick-Action-Link
    let navigated = false;
    if (this.account.downloadsUrl) {
      log.info(`[n26] Goto ${this.account.downloadsUrl}`);
      await page.goto(this.account.downloadsUrl, { waitUntil: 'domcontentloaded' });
      navigated = true;
    } else {
      const quickAction = page.locator('a[data-testid="Downloads-quick-action"]').first();
      try {
        await quickAction.waitFor({ state: 'visible', timeout: 30_000 });
      } catch {
        const shot = join(ensureDir(LOG_DIR), `n26-no-quickaction-${Date.now()}.png`);
        await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
        throw new Error(`[n26] Downloads-Quick-Action nicht sichtbar. URL: ${page.url()} — Screenshot: ${shot}`);
      }
      await quickAction.click();
      await page.waitForTimeout(1000);
      log.info(`[n26] Downloads-URL erkannt: ${page.url()} — mit "set-option ${this.account.id} downloadsUrl ${page.url()}" für direkten Zugriff speichern`);
      navigated = true;
    }

    if (!navigated) return;

    // Liste der Statement-Links abwarten
    const rows = page.locator('div[data-testid^="statement-https://app.n26.com/download-statement/"]');
    try {
      await rows.first().waitFor({ state: 'visible', timeout: 30_000 });
    } catch {
      log.warn('[n26] Keine Statement-Einträge sichtbar');
      return;
    }

    const total = await rows.count();
    log.info(`[n26] ${total} Kontoauszüge`);

    let consecSaved = 0; // Zähler für bursthaften Token-Bucket-Schutz
    for (let idx = 0; idx < total; idx++) {
      const row = rows.nth(idx);
      const link = row.locator('a[data-testid="statements-download-link"]').first();
      const href = await link.getAttribute('href').catch(() => null);
      if (!href) continue;

      const monthName = (await link.innerText().catch(() => '')).trim();
      const rowText = (await row.innerText().catch(() => '')).trim();
      const isPreliminary = /Vorläufig/i.test(rowText);

      const match = href.match(/statement-(\d{4})-(\d{2})$/);
      if (!match) {
        log.warn(`[n26] Unerwartetes Href-Format: ${href}`);
        continue;
      }
      const year = Number(match[1]);
      const month = match[2];
      const suffix = isPreliminary ? ' (Vorläufig)' : '';
      // Pattern "JJJJ-MM-01 Kontoauszug <Monatsname>.pdf" — Tag auf 01 fixiert,
      // weil N26 nur monatsweise abrechnet. Datum vorn fuer chronologische Sortierung.
      const filename = `${year}-${month}-01 Kontoauszug ${monthName}${suffix}.pdf`;
      // Final-Variant ohne (Vorläufig)-Suffix — fuer Pre-Skip-Erkennung wenn der vorherige
      // Lauf einen Vorläufig-Auszug hatte und der jetzt durch das finale Statement ersetzt
      // wird. Wir wollen weder doppelt downloaden noch die alte Vorläufig-Datei verlieren.
      const finalFilename = `${year}-${month}-01 Kontoauszug ${monthName}.pdf`;

      // Pre-Skip
      if (this.cfg) {
        const existing = fileExistsAtAnyLayout(this.cfg, this.account.folder, year, undefined, filename);
        if (existing) {
          log.info(`[${this.account.id}] skip (bereits vorhanden): ${existing}`);
          continue;
        }
        // Wenn das aktuelle Statement vorlaeufig ist, koennte das finale unter dem
        // suffix-losen Namen schon liegen → auch dann skippen.
        if (isPreliminary) {
          const existingFinal = fileExistsAtAnyLayout(this.cfg, this.account.folder, year, undefined, finalFilename);
          if (existingFinal) {
            log.info(`[${this.account.id}] skip (finale Variante existiert): ${existingFinal}`);
            continue;
          }
        }
      }

      // PDF per authentifiziertem Request-Context holen. N26 rate-limited aggressiv.
      let buffer;
      let lastStatus = 0;
      let hit429 = false;
      for (let attempt = 0; attempt < 2; attempt++) {
        try {
          const resp = await this.context.request.get(href);
          lastStatus = resp.status();
          if (resp.ok()) {
            buffer = await resp.body();
            break;
          }
          if (resp.status() === 429 && attempt === 0) {
            hit429 = true;
            log.info(`[n26] HTTP 429 bei "${filename}" — warte 30s, dann ein Retry`);
            await new Promise((r) => setTimeout(r, 30_000));
            continue;
          }
          break;
        } catch (e) {
          log.warn(`[n26] Request-Fehler bei "${filename}": ${e.message}`);
          break;
        }
      }
      if (buffer) {
        yield { filename, year, buffer };
        consecSaved = hit429 ? 0 : consecSaved + 1;
      } else {
        log.warn(`[n26] Download fehlgeschlagen bei "${filename}" (Status: ${lastStatus}) — beim nächsten Lauf erneut versucht`);
        consecSaved = 0;
        if (hit429) await new Promise((r) => setTimeout(r, 30_000));
      }

      // Proaktive Pause nach jedem 8. Download (vor dem nächsten), um N26's Token-Bucket nicht zu leeren
      if (consecSaved > 0 && consecSaved % 8 === 0) {
        log.info(`[n26] Zwischenpause nach ${consecSaved} Downloads: 15s`);
        await new Promise((r) => setTimeout(r, 15_000));
      } else {
        await new Promise((r) => setTimeout(r, 3_000));
      }
    }
  }
}
