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

// Atruvia (Fiducia & GAD) — gemeinschaftliche Banking-Plattform aller ~700 Volks- und
// Raiffeisenbanken. Login + API sind plattformweit identisch, nur die Subdomain unterscheidet
// sich pro Bank. Konfigurierbar über account.bankUrl (Default vrbankgl.de = VR Bank GL).
const DEFAULT_BANK_URL = 'https://www.vrbankgl.de';

// API-Base unter dem Bank-Origin: /services_cloud/portal/proxy-gateway/serviceproxy/...
const EPF_PATH = '/services_cloud/portal/proxy-gateway/serviceproxy/epostfach-core-v1/rest/de.fiduciagad.epostfach.core.api.v1.CoreApi';
const TICKET_PATH = '/services_cloud/portal/datasharing/ticket/download';

// Custom Header — Atruvia prüft den Client-Identifier; ohne kommen 4xx.
const EPF_META = JSON.stringify({ clientName: 'epostfach-ui', version: '000', platform: 'WEB', insideApp: 'false' });

// VR-Bank-Unterkategorie -> DocuDown-Kategorie. Was wir nicht kennen, wird zu 'information'.
const SUBCAT_TO_CATEGORY = {
  'Kontoauszüge': 'accountStatement',
  'Mitteilungen': 'information',
  'Versicherungsdokumente': 'contractInformation',
};

// Heuristik fuer steuerrelevante Mitteilungen — Atruvia hat keine eigene "Steuer"-Subkategorie,
// alles laeuft unter "Mitteilungen". Match auf Titel mit eindeutigen Keywords.
const TAX_TITLE_RE = /Steuer(bescheinigung)?|Erträgnis|Jahressteuer/i;

export class VrBankAdapter extends SourceAdapter {
  static key = 'vr-bank';
  static label = 'VR-Bank (Atruvia)';
  static group = 'Banken';

  get bankUrl() {
    return (this.account.bankUrl || DEFAULT_BANK_URL).replace(/\/+$/, '');
  }

  async login() {
    const { page } = this;
    // Direkt zur Postfach-URL — Atruvia routet uns automatisch zur Auth-Form, wenn die Session
    // abgelaufen ist. Vorher haben wir die Portal-Landing-Seite geladen und nach 5s entschieden
    // "kein Login-Button → eingeloggt"; das war ein Race mit dem SPA-Init und brachte 401s
    // wenn die Session in Wirklichkeit ausgelaufen war.
    const postfachUrl = `${this.bankUrl}/services_cloud/portal/webcomp/epostfach/postfach`;
    await page.goto(postfachUrl, { waitUntil: 'domcontentloaded' });
    log.info(`[vr-bank] Initial-URL nach goto: ${page.url()}`);

    await this._dismissAtruviaConsent();

    // Drei mögliche Outcomes der SPA-Init:
    //  (a) Session aktiv → Postfach-WebComponent erscheint
    //  (b) Session abgelaufen → SPA redirected zum Auth-Frontend
    //  (c) Session abgelaufen → Portal-Landing-Seite mit "Login"-Button erscheint
    // Wir warten 45s, was OAuth-Refresh-Try + SPA-Init + initialen API-Call abdeckt.
    const result = await Promise.race([
      page.waitForFunction(
        () => !!document.querySelector('epostfach-ui, static-postfach-menu-element, app-banking'),
        null, { timeout: 45_000 },
      ).then(() => 'logged-in').catch(() => null),
      page.waitForURL(/\/services_auth\/auth-frontend/, { timeout: 45_000 })
        .then(() => 'auth-form').catch(() => null),
      page.locator('a[data-automation-id="navigate-to-portal"]').first()
        .waitFor({ state: 'visible', timeout: 45_000 })
        .then(() => 'login-btn').catch(() => null),
    ]);

    if (result === 'logged-in') {
      log.info(`[vr-bank] Session aktiv (URL: ${page.url()})`);
      return;
    }
    if (result === 'login-btn') {
      log.info('[vr-bank] Login-Button erschien → Klick auf Auth-Frontend');
      await page.locator('a[data-automation-id="navigate-to-portal"]').first().click();
      await page.waitForURL(/\/services_auth\/auth-frontend/, { timeout: 30_000 });
    } else if (result === 'auth-form') {
      log.info('[vr-bank] SPA hat zur Auth-Form redirected (Session abgelaufen)');
    } else {
      const shot = join(ensureDir(LOG_DIR), `vr-bank-detection-timeout-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      throw new Error(`[vr-bank] Login-Detection-Timeout nach 45s. URL: ${page.url()} — Screenshot: ${shot}`);
    }

    // Cookie-Layer auf Auth-Frontend (anderer Origin → eigener Banner möglich)
    await this._dismissAtruviaConsent();

    const userField = page.locator('input[data-automation-id="vrNetKey-input"]').first();
    const pinField = page.locator('input[data-automation-id="pin-input"]').first();
    const submit = page.locator('button[data-automation-id="sign-in-button"]').first();

    await userField.waitFor({ state: 'visible', timeout: 30_000 });

    log.info('[vr-bank] Fülle VR-NetKey + PIN');
    await userField.fill(this.account.credentials.username);
    await pinField.fill(this.account.credentials.password);
    await page.waitForTimeout(300);

    log.info('[vr-bank] Drücke Anmelden');
    await submit.click();

    // Nach Submit: entweder direkt Redirect zurueck zum Portal (vertraute Session) oder
    // 2FA-Prompt (SecureGo-Push, ChipTAN). Adapter wartet bis 3 Min auf Portal-URL.
    log.info('[vr-bank] Warte auf Redirect zurück zum Portal (ggf. 2FA in der App bestätigen) ...');
    try {
      await page.waitForURL(/\/services_cloud\/portal/, { timeout: 180_000 });
    } catch (e) {
      const shot = join(ensureDir(LOG_DIR), `vr-bank-no-redirect-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      log.warn(`[vr-bank] Kein Redirect nach 3 Min. URL: ${page.url()} — Screenshot: ${shot}`);
      throw e;
    }
    // SPA braucht noch einen Moment um OAuth-Tokens in die Cookies zu schreiben und
    // die Routing-Logik abzuschließen. Bevor wir in die API gehen, einen Indikator abwarten.
    await page.waitForFunction(
      () => !!document.querySelector('epostfach-ui, static-postfach-menu-element, app-banking'),
      null, { timeout: 30_000 },
    ).catch(() => log.warn('[vr-bank] Eingeloggt-Indikator nach Redirect nicht gefunden — versuche trotzdem die API'));
    log.info(`[vr-bank] Login erfolgreich (URL: ${page.url()})`);
  }

  // Atruvia rendert den Cookie-Layer als Material-Dialog. base.mjs kennt das Muster nicht,
  // wir handeln es lokal — Selektoren sind plattformweit stabil (data-automation-id).
  async _dismissAtruviaConsent() {
    const btn = this.page.locator('button[data-automation-id="consent-layer-accept-required"]').first();
    if (await btn.isVisible({ timeout: 3_000 }).catch(() => false)) {
      await btn.click().catch(() => {});
      await this.page.waitForTimeout(300);
      log.info('[vr-bank] Cookie-Layer bestätigt (Technisch Notwendige)');
      return true;
    }
    return false;
  }

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

    // Postfach einmal aufrufen, damit die SPA das OAuth-Token frisch in den Cookies hat.
    await page.goto(`${this.bankUrl}/services_cloud/portal/webcomp/epostfach/postfach`, { waitUntil: 'domcontentloaded' }).catch(() => {});
    await page.waitForTimeout(1500);

    const apiHeaders = {
      'accept': 'application/json, text/plain, */*',
      'content-type': 'application/json',
      'epf-meta-data': EPF_META,
      'x-vp-app-locale': 'de-DE',
    };

    // 1) Berechtigte Kunden + initialer JWT
    const kundenResp = await page.request.get(`${this.bankUrl}${EPF_PATH}/liefereBerechtigteKunden?institut=BANK`, { headers: apiHeaders });
    if (!kundenResp.ok()) {
      const body = await kundenResp.text().catch(() => '');
      throw new Error(`[vr-bank] liefereBerechtigteKunden ${kundenResp.status()}: ${body.slice(0, 300)}`);
    }
    const kundenJson = await kundenResp.json();
    const jwtKundeListe = kundenJson.jwtKundeListe;
    const kundenList = kundenJson.kundeMitHauptKategorieZaehlerListe || [];
    log.info(`[vr-bank] ${kundenList.length} Kunden im Postfach:`);
    for (const k of kundenList) log.info(`[vr-bank]   ${k.kunde.kundenId}: ${k.kunde.kundenName}`);

    // Subfolder-Strategie: bei Multi-Customer wird der Kundenname zum Top-Subfolder,
    // sonst nur die Subkategorie (Kontoauszüge / Mitteilungen / ...).
    const multiKunde = kundenList.length > 1;

    const requested = new Set(this.account.categories || [
      'taxInformation',
      'accountStatement',
      'contractInformation',
      'information',
    ]);
    const categoryFolders = {
      taxInformation: 'Steuer',
      contractInformation: 'Verträge',
      information: 'Sonstiges',
      ...(this.account.categoryFolders || {}),
    };

    // Sammeln: erst alle Dokumente listen (alle Kunden, beide Hauptkategorien), dann verarbeiten.
    // So koennen wir vorab den Total-Count fuer den GUI-Progress yielden.
    const allDocs = [];
    for (const k of kundenList) {
      const kundenId = k.kunde.kundenId;
      const kundenName = k.kunde.kundenName;
      const kundenAlias = this.account.kundenAliases?.[kundenId] || kundenName;

      for (const hauptKategorie of ['POSTEINGANG', 'ARCHIV']) {
        const docs = await this._fetchDocsPage(apiHeaders, kundenId, hauptKategorie, jwtKundeListe);
        for (const doc of docs.dokumentListe || []) {
          allDocs.push({
            doc,
            kundenId,
            kundenName,
            kundenAlias,
            hauptKategorie,
            jwtDokumentListe: docs.jwtDokumentListe,
          });
        }
        if (docs.zwischenSeitenAufsetzpunkte?.length) {
          // PAGINATION-TODO (offen): anzahlDokumente=500 deckt typische Privatkunden ab
          // (DKB hat in 5+ Jahren ~400 Items). Bei Postfaechern >500 Items pro Hauptkategorie
          // (Vereins-/Geschaeftskonten, alte Konten >10 Jahre) fehlen alte Dokumente.
          // Cursor-Format ist base64-encoded, vermutlich im Folge-Body als
          // 'zwischenSeitenAufsetzpunkte' (gleicher Key wie im Response) wieder mitzugeben.
          // Verifikation braucht: bei einem grossen Postfach in DevTools-Network den 2. Call
          // beim UI-Scrollen abfangen, das tatsaechliche Body-Schema dokumentieren, dann
          // _fetchDocsPage zu einer Schleife umbauen. Aktuell: WARN + Liste, was wir haben.
          log.warn(`[vr-bank] PAGINATION-TODO: Cursor erhalten fuer kunde=${kundenId} ${hauptKategorie} — Folge-Call nicht implementiert. Es werden NUR die ersten 500 Items geladen; ggf. fehlen alte Dokumente. Falls relevant, HAR-Sample beim Scrollen aufnehmen und Adapter erweitern.`);
        }
      }
    }

    log.info(`[vr-bank] Gesamt ${allDocs.length} Dokumente vor Filter`);

    // Kategorisieren + skip-filtern
    for (const item of allDocs) item._cat = this._categorize(item.doc);
    const todo = allDocs.filter((it) => requested.has(it._cat));
    log.info(`[vr-bank] ${todo.length} Dokumente nach Filter (categories=${[...requested].join(',')})`);

    yield { kind: 'progress', total: todo.length };

    const stats = new Map();
    for (const item of todo) {
      const { doc, kundenAlias, jwtDokumentListe } = item;
      const cat = item._cat;
      stats.set(cat, (stats.get(cat) || 0) + 1);

      const dateIso = (doc.erstellDatum || '').slice(0, 10);
      const year = Number(dateIso.slice(0, 4)) || new Date().getFullYear();
      const titel = (doc.dokumentTitel || `vr-${doc.dokumentId}`).replace(/\s+/g, ' ').trim();
      const filename = safeFilename(`${dateIso} ${titel}.pdf`);

      // Subfolder: <KundenAlias>/<Subkategorie> bei Multi-Customer, sonst nur Subkategorie.
      // Subkategorie kommt direkt aus dem Doc (Kontoauszüge / Mitteilungen / ...);
      // bei DocuDown-Tax-Match heben wir es in den Steuer-Folder.
      const subKat = doc.roUnterKategorie?.unterKategorieName || 'Sonstiges';
      let subfolder = cat === 'taxInformation' ? (categoryFolders.taxInformation) : subKat;
      if (multiKunde) subfolder = `${safeFilename(kundenAlias)}/${subfolder}`;

      if (this.cfg) {
        const existing = fileExistsAtAnyLayout(this.cfg, this.account.folder, year, subfolder, filename);
        if (existing) {
          log.info(`[${this.account.id}] skip: ${existing}`);
          yield { kind: 'skip', path: existing };
          continue;
        }
      }

      // JWT-Refresh-Callback: bei 401 holt _downloadPdf einen frischen jwtDokumentListe
      // fuer (kundenId, hauptKategorie). Schuetzt vor Lauf-Abbruch in grossen Postfaechern,
      // wenn die Iteration die 1800s-JWT-Lifetime ueberschreitet.
      const refreshJwt = async () => {
        const fresh = await this._fetchDocsPage(apiHeaders, item.kundenId, item.hauptKategorie, jwtKundeListe);
        const newJwt = fresh.jwtDokumentListe;
        // Frischen JWT auch fuer alle noch wartenden Items derselben (Kunde, Kategorie)
        // Kombination uebernehmen, damit der naechste Download nicht erneut 401 sieht.
        for (const it of todo) {
          if (it.kundenId === item.kundenId && it.hauptKategorie === item.hauptKategorie) {
            it.jwtDokumentListe = newJwt;
          }
        }
        return newJwt;
      };
      const buffer = await this._downloadPdf(apiHeaders, doc.dokumentId, item.jwtDokumentListe, refreshJwt);
      if (!buffer) {
        log.warn(`[vr-bank] Download fehlgeschlagen für ${doc.dokumentId} "${titel.slice(0, 60)}" — naechster Lauf retried`);
        continue;
      }

      yield { filename, year, buffer, subfolder };
      // Throttle: Atruvia hat keine bekannten Rate-Limits, aber 200ms ist freundlich.
      await page.waitForTimeout(200);
    }

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

  async _fetchDocsPage(apiHeaders, kundenId, hauptKategorie, jwtKundeListe) {
    const body = {
      dokumenteZuKundenListe: [kundenId],
      hauptKategorie,
      dokumenteZurUnterKategorie: null,
      liefereDokumente: true,
      liefereUnterKategorienMitZaehler: true,
      anzahlDokumente: 500,
      jwtNextVersion: true,
      jwtKundeListe,
    };
    const resp = await this.page.request.post(`${this.bankUrl}${EPF_PATH}/liefereDokumenteZuKunden`, {
      headers: apiHeaders,
      data: JSON.stringify(body),
    });
    if (!resp.ok()) {
      const errText = await resp.text().catch(() => '');
      throw new Error(`[vr-bank] liefereDokumenteZuKunden ${resp.status()} (kunde=${kundenId} ${hauptKategorie}): ${errText.slice(0, 200)}`);
    }
    return await resp.json();
  }

  // Zwei-Schritt-Download: documentUpload liefert ein Ticket, dann das eigentliche PDF.
  // Atruvia bezeichnet das System als "datasharing" — vermutlich derselbe Pfad fuer
  // Document-Sharing zwischen Bank und Kunde generell.
  //
  // refreshJwt: optional async () => newJwtDokumentListe. Wird bei 401 vom documentUpload
  // (= JWT abgelaufen, Standard-Lifetime 1800s) gerufen, dann ein einzelner Retry mit
  // dem frischen JWT. Schuetzt grosse Postfaecher vor Mid-Iteration-Aussterben.
  async _downloadPdf(apiHeaders, dokumentId, jwtDokumentListe, refreshJwt = null) {
    const tryUpload = async (jwt) => {
      const resp = await this.page.request.post(`${this.bankUrl}${EPF_PATH}/datasharing/documentUpload`, {
        headers: apiHeaders,
        data: JSON.stringify({ dokumentId, jwtDokumentListe: jwt }),
      });
      return resp;
    };

    try {
      let ticketResp = await tryUpload(jwtDokumentListe);
      if (ticketResp.status() === 401 && refreshJwt) {
        log.info(`[vr-bank] documentUpload 401 fuer ${dokumentId} → JWT-Refresh + Retry`);
        try {
          const newJwt = await refreshJwt();
          ticketResp = await tryUpload(newJwt);
        } catch (e) {
          log.warn(`[vr-bank] JWT-Refresh fehlgeschlagen: ${e.message.split('\n')[0]}`);
        }
      }
      if (!ticketResp.ok()) {
        log.warn(`[vr-bank] documentUpload ${ticketResp.status()} fuer ${dokumentId}`);
        return null;
      }
      const ticket = await ticketResp.json();
      const { uploadId, downloadTicketId } = ticket;
      if (!uploadId || !downloadTicketId) {
        log.warn(`[vr-bank] Ticket-Response unvollständig: ${JSON.stringify(ticket).slice(0, 200)}`);
        return null;
      }

      const pdfUrl = `${this.bankUrl}${TICKET_PATH}/${encodeURIComponent(uploadId)}?downloadticket=${encodeURIComponent(downloadTicketId)}&directDownload=true&requester=epostfach-document-download`;
      const pdfHeaders = {
        accept: 'application/json, text/plain, */*',
        'epf-meta-data': EPF_META,
      };
      const pdfResp = await this.page.request.get(pdfUrl, { headers: pdfHeaders });
      if (!pdfResp.ok()) {
        log.warn(`[vr-bank] ticket/download ${pdfResp.status()} fuer ${dokumentId}`);
        return null;
      }
      return await pdfResp.body();
    } catch (e) {
      log.warn(`[vr-bank] Download-Exception fuer ${dokumentId}: ${e.message.split('\n')[0]}`);
      return null;
    }
  }

  _categorize(doc) {
    const subKat = doc.roUnterKategorie?.unterKategorieName;
    const base = SUBCAT_TO_CATEGORY[subKat] || 'information';
    // Steuer-Heuristik: Mitteilungen mit Steuer-Keywords werden hochgehoben.
    if (base === 'information' && TAX_TITLE_RE.test(doc.dokumentTitel || '')) {
      return 'taxInformation';
    }
    return base;
  }
}
