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 API_BASE = 'https://banking.dkb.de/api';

// documentTypeId -> unsere Kategorie. UUIDs sind serverseitig stabil
// (entdeckt 2026-04-28 im /documentstorage/documents Response).
// Unbekannte UUIDs werden auf 'information' gemappt und mitgenommen.
const DOC_TYPE_TO_CATEGORY = {
  '9cae635a-fba3-11ec-b939-0242ac120002': 'accountStatement',  // Kontoauszug Giro/Tagesgeld
  '697e0136-e744-4d00-80c2-f41a276ebc94': 'taxInformation',    // Jahressteuerbescheinigung, Erträgnisaufstellung
  '33397ad6-e629-49a7-96ab-584f72648615': 'stocksDepot',       // HV-Anschreiben, Kapitalmaßnahmen, Kosteninfo
  '5a537190-1012-4ff0-a1fc-171a66bd99f8': 'stocksDepot',       // Depotauszug
  'e116311e-b4f9-4a7d-a2a2-2279358fd3e5': 'stocksDepot',       // Ertragsabrechnung Dividenden/Fonds
  '69f3bd1e-0c21-46d9-9e98-428fb9e708a2': 'stocksDepot',       // Verlustschwellenreporting
};

export class DkbAdapter extends SourceAdapter {
  static key = 'dkb';
  static label = 'DKB';
  static group = 'Banken';

  async login() {
    const { page } = this;
    await page.goto('https://banking.dkb.de/login', { waitUntil: 'domcontentloaded' });
    log.info(`[dkb] Page geladen: url=${page.url()} title="${(await page.title().catch(() => '')).slice(0, 80)}"`);

    const user = page.getByRole('textbox', { name: /Anmeldename/i });
    const pw = page.getByRole('textbox', { name: /Passwort/i });
    const submit = page.locator('button[data-t-id="login"]');

    log.info('[dkb] Warte auf Login-Form ...');
    const formReady = user.waitFor({ state: 'visible', timeout: 60_000 }).then(() => 'form').catch(() => null);
    const urlChanged = page.waitForURL((u) => !/\/login/.test(new URL(u).pathname), { timeout: 60_000 }).then(() => 'redirect').catch(() => null);
    const what = await Promise.race([formReady, urlChanged]);

    if (what === 'redirect') {
      log.info(`[dkb] Session noch aktiv (URL: ${page.url()})`);
      return;
    }
    if (what !== 'form') {
      const shot = join(ensureDir(LOG_DIR), `dkb-no-form-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      throw new Error(`[dkb] Login-Form erschien nicht innerhalb 60s. URL=${page.url()} Screenshot=${shot}`);
    }

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

    // Sanity: ist die Page noch INERT durch ein offenes <dialog>? Wenn ja, ist jeder fill()
    // ein stiller Timeout. Fail-fast mit Screenshot statt 30s zu warten.
    const stuck = await page.evaluate(() => {
      for (const d of document.querySelectorAll('dialog[open]')) {
        const id = d.id || '';
        const html = d.outerHTML || '';
        if (/uc-|cmp|consent|cookie|usercentrics|data-action-type/i.test(id + html.slice(0, 500))) {
          return { id, html: html.slice(0, 300) };
        }
      }
      return null;
    }).catch(() => null);
    if (stuck) {
      const shot = join(ensureDir(LOG_DIR), `dkb-cmp-stuck-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      throw new Error(`[dkb] Consent-Banner liess sich nicht schliessen — Body bleibt inert. id="${stuck.id}" Screenshot=${shot}`);
    }

    log.info('[dkb] Fülle Login-Felder');
    await user.fill(this.account.credentials.username);
    await pw.fill(this.account.credentials.password);

    await page.waitForTimeout(500);

    log.info('[dkb] Warte bis Anmelden-Button drückbar ist');
    await submit.waitFor({ state: 'visible', timeout: 10_000 });
    await page.waitForFunction(
      () => {
        const b = document.querySelector('button[data-t-id="login"]');
        return b && !b.disabled && b.getAttribute('aria-disabled') !== 'true';
      },
      null,
      { timeout: 10_000 },
    );

    log.info('[dkb] Drücke Anmelden');
    const urlBefore = page.url();
    await submit.click();

    // Friendly Captcha abfangen: iframe "frc-i-widget" erscheint nach Submit mit 🤖-Banner.
    await page.waitForTimeout(2000);
    const captchaIframe = page.locator('iframe.frc-i-widget');
    if (await captchaIframe.isVisible().catch(() => false)) {
      log.info('[dkb] Friendly Captcha erkannt');
      const frame = page.frameLocator('iframe.frc-i-widget');
      try {
        await frame.locator('button').first().click({ timeout: 3_000 });
        log.info('[dkb] Captcha-Widget geklickt');
      } catch {
        log.info('[dkb] Kein klickbarer Button im Widget — läuft vmtl. automatisch');
      }
      try {
        await page.locator('[role="alert"]').first().waitFor({ state: 'hidden', timeout: 60_000 });
        log.info('[dkb] Captcha gelöst — Banner weg');
      } catch {
        const shot = join(ensureDir(LOG_DIR), `dkb-captcha-stuck-${Date.now()}.png`);
        await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
        log.warn(`[dkb] Captcha-Banner bleibt — bitte manuell bestätigen. Screenshot: ${shot}`);
      }
    } else if (page.url() === urlBefore) {
      const alertText = await page.locator('[role="alert"]').first().innerText().catch(() => '');
      if (alertText) log.warn(`[dkb] Banner nach Submit: ${alertText.slice(0, 200)}`);
      const shot = join(ensureDir(LOG_DIR), `dkb-post-submit-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      log.info(`[dkb] Screenshot nach Submit: ${shot}`);
    }

    log.info('[dkb] Warte auf 2FA-Bestätigung in der DKB-App (bis zu 3 Min) ...');
    try {
      await page.waitForURL((url) => !/\/login/.test(new URL(url).pathname), { timeout: 180_000 });
    } catch (e) {
      const shot = join(ensureDir(LOG_DIR), `dkb-no-redirect-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      log.warn(`[dkb] Kein Redirect nach 3 Min. Screenshot: ${shot}`);
      throw e;
    }
    log.info(`[dkb] Login erfolgreich (URL: ${page.url()})`);
  }

  // Holt die Postbox-Dokumente direkt über die JSON-API. Vorher: Click-Through im UI mit
  // 50× "Mehr Dokumente laden", Popup-Fang, Autologout-Risiko. Jetzt: zwei API-Endpunkte
  // (Liste paginiert + PDF pro ID), keine UI-Interaktion mehr.
  async *iterateStatements() {
    const { page } = this;

    // Postbox einmal aufrufen, damit die SPA Cookies/Session frisch macht.
    await page.goto('https://banking.dkb.de/postbox', { waitUntil: 'domcontentloaded' }).catch(() => {});
    await page.waitForTimeout(1500);

    const cookies = await page.context().cookies();
    // DKB verwendet aktuell den secure-prefix Cookie-Namen "__Host-xsrf" (frueher: "XSRF-TOKEN").
    // Beide Namen pruefen, damit Aenderungen serverseitig nicht direkt brechen.
    const xsrf = cookies.find((c) => c.name === '__Host-xsrf' || c.name === 'XSRF-TOKEN')?.value;
    if (!xsrf) log.warn('[dkb] Kein xsrf-Cookie gefunden — API-Calls könnten 403 liefern');

    const apiHeaders = {
      accept: 'application/vnd.api+json',
      application_name: 'web-banking',
      ...(xsrf ? { 'x-xsrf-token': xsrf } : {}),
    };

    // Konten-Liste vorab — liefert pro Kontonummer Produkt + Inhaber.
    // Wir brauchen das, damit Subfolder sinnvolle Namen kriegen
    // ("Tagesgeld", "Girokonto-Familie") statt nur Kontonummern.
    const accountsMap = await this._fetchAccounts(apiHeaders);
    if (accountsMap.size) {
      log.info(`[dkb] ${accountsMap.size} Konten via /accounts:`);
      for (const [nr, info] of accountsMap) {
        log.info(`[dkb]   ${nr}: ${info.alias} (${info.displayName}, ${info.holder})`);
      }
    }

    // 1) Liste paginiert ziehen
    const allDocs = [];
    const limit = 200;
    for (let offset = 0; ; offset += limit) {
      const url = `${API_BASE}/documentstorage/documents?page%5Blimit%5D=${limit}&page%5Boffset%5D=${offset}&sort=-creationDate`;
      const resp = await page.request.get(url, { headers: apiHeaders });
      if (!resp.ok()) {
        const body = await resp.text().catch(() => '');
        throw new Error(`[dkb] documents API ${resp.status()}: ${body.slice(0, 300)}`);
      }
      const json = await resp.json();
      const data = json.data || [];
      allDocs.push(...data);
      log.info(`[dkb] Liste offset=${offset}: ${data.length} Items (gesamt ${allDocs.length})`);
      if (data.length < limit) break;
    }

    const requested = new Set(this.account.categories || [
      'taxInformation',
      'accountStatement',
      'creditCardStatement',
      'stocksDepot',
      'contractInformation',
      'information',
    ]);
    const categoryFolders = {
      taxInformation: 'Steuer',
      // Fallback wenn weder "zu Konto NNN" noch Auto-Alias matcht — sonst landet das
      // File direkt im Account-Root (subfolder=undefined). Selten, aber moeglich bei
      // DKB-Sammelmitteilungen die als Auszug klassifiziert sind.
      accountStatement: 'Konto',
      creditCardStatement: 'Kreditkarte',
      stocksDepot: 'Wertpapier',
      contractInformation: 'Verträge',
      information: 'Sonstiges',
      ...(this.account.categoryFolders || {}),
    };

    // _categorize einmal pro Doc. Sonst 3x: in sort, filter, loop.
    for (const doc of allDocs) doc._cat = this._categorize(doc);

    // Steuer zuerst (Priorität für den Steuerberater-Workflow), dann Rest
    const order = ['taxInformation', 'accountStatement', 'creditCardStatement', 'stocksDepot', 'contractInformation', 'information'];
    const orderRank = (cat) => { const i = order.indexOf(cat); return i === -1 ? 99 : i; };
    allDocs.sort((a, b) => orderRank(a._cat) - orderRank(b._cat));

    let unknownTypeWarned = false;
    const stats = new Map();

    // Progress-Marker fuer die GUI vorab — Anzahl der zu verarbeitenden Items.
    const todoCount = allDocs.filter((d) => requested.has(d._cat)).length;
    yield { kind: 'progress', total: todoCount };

    for (const doc of allDocs) {
      const cat = doc._cat;
      if (!requested.has(cat)) continue;
      stats.set(cat, (stats.get(cat) || 0) + 1);

      const a = doc.attributes || {};
      const meta = a.metadata || {};
      const subject = (meta.subject || a.fileName || `dkb-${doc.id.slice(0, 8)}`).replace(/\s+/g, ' ').trim();

      if (!DOC_TYPE_TO_CATEGORY[a.documentTypeId] && !unknownTypeWarned) {
        log.warn(`[dkb] Unbekannter documentTypeId=${a.documentTypeId} ("${subject.slice(0, 80)}") -> Kategorie 'information'. Bei Bedarf in DOC_TYPE_TO_CATEGORY ergänzen.`);
        unknownTypeWarned = true;
      }

      // Datum: bevorzugt metadata.creationDate (= echtes Dokumenten-Datum, nicht Posting-Datum),
      // sonst aus subject parsen ("vom DD.MM.YYYY"), letzter Fallback attributes.creationDate.
      const metaDate = (meta.creationDate || meta.statementDate || '').slice(0, 10);
      const sm = subject.match(/(\d{2})\.(\d{2})\.(\d{4})/);
      const dateIso =
        metaDate ||
        (sm ? `${sm[3]}-${sm[2]}-${sm[1]}` : (a.creationDate || '').slice(0, 10));
      const year = Number(dateIso.slice(0, 4)) || new Date().getFullYear();

      // Filename behaelt das ggf. doppelte "vom DD.MM.YYYY" — kosmetisch unschoen,
      // aber AENDERN wuerde alle existierenden DKB-Files unter neuem Namen Re-Downloaden
      // (Pre-Skip-Mismatch). Nur bei einem Migrations-Schritt darf der Filename brechen.
      const filename = safeFilename(`${dateIso} ${subject}.pdf`);

      // Subfolder:
      // 1) Manuelle account.kontoAliases[NNN] gewinnen immer (User-Override)
      // 2) Auto-Alias aus /accounts API (z. B. "Girokonto", "Tagesgeld-1234")
      // 3) "zu Depot NNN" -> Depotnummer wenn manueller Alias vorhanden
      // 4) sonst kategorie-spezifischer Fallback (Wertpapier, Steuer, ...)
      // 5) sonst nackte Kontonummer
      let subfolder;
      let altSubfolder;  // Fallback fuer Pre-Skip: alter "<NNN>/"-Pfad bei aktivem Auto-Alias
      const kontoMatch = subject.match(/zu Konto\s+(\d+)/i);
      const depotMatch = subject.match(/zu Depot\s+(\d+)/i);
      if (kontoMatch) {
        const kontoNr = kontoMatch[1];
        const manual = this.account.kontoAliases?.[kontoNr];
        const auto = accountsMap.get(kontoNr)?.alias;
        if (manual) {
          subfolder = manual;
          if (manual !== kontoNr) altSubfolder = kontoNr;
        } else if (auto) {
          subfolder = auto;
          altSubfolder = kontoNr;  // wer vorher per Nr gespeichert hatte, soll nicht neu downloaden
        } else {
          subfolder = kontoNr;
        }
      } else if (depotMatch && this.account.kontoAliases?.[depotMatch[1]]) {
        subfolder = this.account.kontoAliases[depotMatch[1]];
      } else if (categoryFolders[cat]) {
        subfolder = categoryFolders[cat];
      }

      // Pre-Skip: ist das File schon da? Spart den PDF-Download komplett.
      // Wir pruefen den aktuellen Subfolder UND ggf. den alten (legacy) Pfad,
      // damit ein Auto-Alias-Wechsel keine Doppel-Downloads ausloest.
      // Wir yielden {kind:'skip'} statt continue, damit der Runner item-skipped emitted
      // und der GUI-Progress-Bar mitwaechst.
      if (this.cfg) {
        const safeSub = subfolder ? safeFilename(subfolder) : undefined;
        let existing = fileExistsAtAnyLayout(this.cfg, this.account.folder, year, safeSub, filename);
        if (!existing && altSubfolder && altSubfolder !== subfolder) {
          existing = fileExistsAtAnyLayout(this.cfg, this.account.folder, year, safeFilename(altSubfolder), filename);
        }
        if (existing) {
          log.info(`[${this.account.id}] skip: ${existing}`);
          yield { kind: 'skip', path: existing };
          continue;
        }
      }

      // PDF laden mit Retry-Backoff. DKB schliesst ohne Vorwarnung Connections (ECONNRESET)
      // wenn man schneller als ~3 req/s durchballert. Wir drosseln auf ~5 req/s und retryn
      // transiente Fehler. Endgueltige Fails (nach 3 Versuchen) sind nicht fatal — der
      // naechste Lauf holt das File ueber den Pre-Skip nach.
      const pdfUrl = `${API_BASE}/documentstorage/documents/${doc.id}`;
      const pdfHeaders = {
        accept: 'application/pdf',
        application_name: 'web-banking',
        ...(xsrf ? { 'x-xsrf-token': xsrf } : {}),
      };

      let buffer = null;
      for (let attempt = 1; attempt <= 3; attempt++) {
        try {
          const pdfResp = await page.request.get(pdfUrl, { headers: pdfHeaders });
          if (!pdfResp.ok()) {
            log.warn(`[dkb] PDF ${pdfResp.status()} (Versuch ${attempt}/3) für ${doc.id} "${subject.slice(0, 60)}"`);
          } else {
            const ct = (pdfResp.headers()['content-type'] || '').toLowerCase();
            if (!ct.includes('pdf')) {
              const preview = (await pdfResp.text().catch(() => '')).slice(0, 200);
              log.warn(`[dkb] erwartet PDF, content-type=${ct} für ${doc.id} -> body: ${preview}`);
              break; // kein Retry: das ist kein transienter Fehler
            }
            buffer = await pdfResp.body();
            break;
          }
        } catch (e) {
          log.warn(`[dkb] PDF fetch fehlgeschlagen (Versuch ${attempt}/3) für ${doc.id}: ${e.message.split('\n')[0]}`);
        }
        if (attempt < 3) await page.waitForTimeout(1000 * attempt); // 1s, 2s
      }

      if (!buffer) {
        log.warn(`[dkb] gebe auf bei ${doc.id} "${subject.slice(0, 60)}" — beim nächsten Lauf erneut versucht`);
        continue;
      }

      yield { filename, year, buffer, subfolder };
      // Throttle: ~5 req/s, damit DKB die Connection nicht resetet.
      await page.waitForTimeout(200);
    }

    for (const [cat, count] of stats) log.info(`[dkb] ${cat}: ${count} Dokumente verarbeitet`);
  }

  // GET /api/accounts/accounts -> Map<Kontonummer, { iban, holder, displayName, alias }>.
  // Der Auto-Alias ist 'displayName' (z. B. "Girokonto", "Tagesgeld"); bei Kollision
  // (zwei "Girokonto"-Konten) wird die letzte 4 IBAN-Stellen angehaengt: "Girokonto-1035".
  // Fehler im Endpoint sind nicht fatal: leere Map -> Fallback ist die nackte Kontonummer als Subfolder.
  async _fetchAccounts(apiHeaders) {
    const url = `${API_BASE}/accounts/accounts?filter%5Bproduct.type%5D%5BNEQ%5D=loan`;
    let resp;
    try {
      resp = await this.page.request.get(url, { headers: apiHeaders });
    } catch (e) {
      log.warn(`[dkb] /accounts fetch fehlgeschlagen: ${e.message}`);
      return new Map();
    }
    if (!resp.ok()) {
      log.warn(`[dkb] /accounts ${resp.status()} -- Auto-Alias uebersprungen`);
      return new Map();
    }
    let json;
    try {
      json = await resp.json();
    } catch {
      return new Map();
    }
    const items = Array.isArray(json.data) ? json.data : [];

    const raw = new Map();
    for (const a of items) {
      const attrs = a.attributes || {};
      const iban = attrs.iban || '';
      // Kontonummer = letzte 10 Stellen der DE-IBAN (8 BLZ + 10 Konto = 22 Zeichen).
      // Robust waere ein iban.slice(-10), das matcht die "zu Konto NNN"-Captures aus den Subjects.
      const acctNum = iban.slice(-10);
      if (!acctNum) continue;
      raw.set(acctNum, {
        iban,
        holder: attrs.holderName || '',
        displayName: attrs.product?.displayName || 'Konto',
      });
    }

    // Disambiguierung: gleicher displayName mehrfach -> Suffix mit IBAN-Endung
    const dnCount = new Map();
    for (const v of raw.values()) {
      dnCount.set(v.displayName, (dnCount.get(v.displayName) || 0) + 1);
    }
    const out = new Map();
    for (const [nr, info] of raw) {
      const alias = dnCount.get(info.displayName) > 1
        ? `${info.displayName}-${nr.slice(-4)}`
        : info.displayName;
      out.set(nr, { ...info, alias });
    }
    return out;
  }

  _categorize(doc) {
    const a = doc.attributes || {};
    const meta = a.metadata || {};
    let cat = DOC_TYPE_TO_CATEGORY[a.documentTypeId] || 'information';
    // Kreditkarte vs. Konto: gleicher documentTypeId. Heuristik via Subject/IBAN.
    // (Ohne CC-Sample bewusst konservativ: erst dann umlabeln wenn Subject Kreditkarte sagt.)
    if (cat === 'accountStatement' && /Kreditkart/i.test(meta.subject || '')) {
      cat = 'creditCardStatement';
    }
    return cat;
  }
}
