import { ApiSourceAdapter } from './api-base.mjs';
import { log } from '../logger.mjs';
import { fileExistsAtAnyLayout } from '../paths.mjs';

// AutoDNS / InternetX JSON-API
//   Doku: https://help.internetx.com/display/APIXMLDE/Authentifizierung
//         https://help.internetx.com/display/APIXMLDE/API-Grundlagen+JSON
//         https://help.internetx.com/display/APIXMLDE/Invoice+List
const ENDPOINT_LIVE = 'https://api.autodns.com/v1';
const ENDPOINT_DEMO = 'https://api.demo.autodns.com/v1';

// API erlaubt 3 Anfragen/Sekunde. 350ms zwischen Calls = ~2.85 req/s mit Sicherheitspuffer.
const MIN_INTERVAL_MS = 350;

// AutoDNS liefert pro Invoice bis zu drei verschiedene Document-IDs:
//   - document.id                         → PDF
//   - addon.documentstore_csv_id          → CSV
//   - addon.documentstore_txt_id          → TXT (oft null)
// Alle drei werden vom selben Endpoint GET /document/{id} ausgeliefert.
const DOC_TYPES = [
  { flag: 'fetchInvoicePdf', idPath: ['document', 'id'],                  ext: 'pdf' },
  { flag: 'fetchInvoiceCsv', idPath: ['addon', 'documentstore_csv_id'],   ext: 'csv' },
  { flag: 'fetchInvoiceTxt', idPath: ['addon', 'documentstore_txt_id'],   ext: 'txt' },
];

export class InternetXAdapter extends ApiSourceAdapter {
  static key = 'internetx';
  static label = 'InternetX';
  static group = 'Hosting';

  endpoint() {
    // Explizit gesetzter Endpoint hat Vorrang
    if (this.account.endpoint) return this.account.endpoint;
    // Multi-Tenant-Setup: wenn account.customer (z.B. "37380") gesetzt ist,
    // bauen wir den Endpoint analog zum Web-Portal <customer>.cloud.autodns.com mit /api-Pfad
    if (this.account.customer) {
      const sub = String(this.account.customer).trim();
      return this.account.context === '1'
        ? `https://${sub}.cloud.demo.autodns.com/api/v1`
        : `https://${sub}.cloud.autodns.com/api/v1`;
    }
    // Standard-Endpoint (Hauptkunden ohne Subdomain-Setup)
    return this.account.context === '1' ? ENDPOINT_DEMO : ENDPOINT_LIVE;
  }

  context() {
    return this.account.context || '4'; // 4 = Live, 1 = Demo
  }

  /** Throttle: serialisiert API-Calls und haelt MIN_INTERVAL_MS Pause zwischen ihnen.
   *  AutoDNS limitiert auf 3 req/s — 350ms Pause => ~2.85 req/s mit Puffer. */
  async _throttle() {
    const now = Date.now();
    const elapsed = now - (this._lastCallAt || 0);
    if (elapsed < MIN_INTERVAL_MS) {
      await new Promise((r) => setTimeout(r, MIN_INTERVAL_MS - elapsed));
    }
    this._lastCallAt = Date.now();
  }

  async ixFetch(method, path, jsonBody) {
    const { username, password } = this.account.credentials || {};
    if (!username || !password) {
      throw new Error('[internetx] Credentials fehlen: username/password');
    }
    await this._throttle();
    const url = `${this.endpoint()}${path}`;
    const headers = {
      'Authorization': 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'),
      'X-Domainrobot-Context': this.context(),
      'Accept': 'application/json',
      // AutoDNS verlangt zwingend einen User-Agent — sonst HTTP 401/403
      'User-Agent': 'DocuDown/0.1 (https://docudown.example) DocuDown-InternetXAdapter',
    };
    if (jsonBody !== undefined) headers['Content-Type'] = 'application/json';

    log.debug(`[internetx] ${method} ${path}`);
    // Einmaliger Auth-Diagnose-Log pro Adapter-Instanz: zeigt ob Username korrekt durchgereicht wird
    if (!this._authLogged) {
      this._authLogged = true;
      const userPreview = username.length >= 2
        ? `${username.slice(0, Math.min(3, username.length - 1))}…${username.slice(-1)} (len=${username.length})`
        : `(len=${username.length})`;
      log.info(`[internetx] Auth-Check: user="${userPreview}", context=${this.context()}, endpoint=${this.endpoint()}`);
    }
    const resp = await fetch(url, {
      method,
      headers,
      body: jsonBody !== undefined ? JSON.stringify(jsonBody) : undefined,
    });
    if (resp.status === 429) {
      // Rate-Limit erwischt — Retry-After-Header beachten oder 1.5s warten und einmal retry
      const retryAfter = Number(resp.headers.get('retry-after')) * 1000 || 1500;
      log.warn(`[internetx] HTTP 429 — warte ${retryAfter}ms und versuche einmal erneut`);
      await new Promise((r) => setTimeout(r, retryAfter));
      await this._throttle();
      const resp2 = await fetch(url, {
        method,
        headers,
        body: jsonBody !== undefined ? JSON.stringify(jsonBody) : undefined,
      });
      if (!resp2.ok) {
        const text = await resp2.text().catch(() => '');
        throw new Error(`InternetX ${method} ${path}: HTTP ${resp2.status} (auch nach Retry) — ${text.slice(0, 240)}`);
      }
      return resp2;
    }
    if (!resp.ok) {
      const text = await resp.text().catch(() => '');
      throw new Error(`InternetX ${method} ${path}: HTTP ${resp.status} — ${text.slice(0, 240)}`);
    }
    return resp;
  }

  async ixJson(method, path, jsonBody) {
    const resp = await this.ixFetch(method, path, jsonBody);
    return resp.json();
  }

  async login() {
    // Sanity-Check via /hello (laut API-Grundlagen JSON: "no operation, tests connection and access credentials")
    try {
      await this.ixFetch('GET', '/hello');
      log.info(`[internetx] Auth OK (Context ${this.context()}, Endpoint ${this.endpoint()})`);
    } catch (e) {
      throw new Error(`[internetx] Auth fehlgeschlagen: ${e.message}`);
    }
  }

  /** Datei via GET /document/{docId} holen — funktioniert fuer PDF, CSV und TXT.
   *
   *  AutoDNS-Server-Eigenheit: Liefert oft **rohe Bytes mit Content-Type "application/json"**
   *  (z.B. "%PDF-1.3..."). Daher Magic-Byte-Heuristik:
   *    - Body beginnt mit '{'  → echter JSON-Wrapper { data: [{ data: <base64>, link, ... }] }
   *    - Body beginnt anders   → rohe Bytes direkt verwenden (PDF/CSV/TXT)
   *  Liefert Buffer bei Erfolg, sonst null. */
  async fetchDocument(docId, ext) {
    try {
      const resp = await this.ixFetch('GET', `/document/${docId}`);
      const buffer = Buffer.from(await resp.arrayBuffer());
      if (buffer.length === 0) {
        log.warn(`[internetx] /document/${docId} (${ext}): leere Response`);
        return null;
      }

      // Magic-Byte-Check: 0x7B = '{' → JSON-Wrapper, alles andere → rohe Bytes
      if (buffer[0] !== 0x7B) {
        log.debug(`[internetx] /document/${docId} (${ext}): ${buffer.length} bytes (raw, content-type wird ignoriert)`);
        return buffer;
      }

      // JSON-Wrapper-Pfad: Base64-Decode oder Link-Fallback
      let json;
      try {
        json = JSON.parse(buffer.toString('utf8'));
      } catch (e) {
        log.warn(`[internetx] /document/${docId} (${ext}): Body sieht aus wie JSON, ist aber kein valides JSON — speichere roh`);
        return buffer;
      }
      const sType = json.status?.type;
      if (sType === 'ERROR' || sType === 'E') {
        log.warn(`[internetx] /document/${docId} (${ext}): ${json.status.code} ${json.status.text}`);
        return null;
      }
      const doc = Array.isArray(json.data) ? json.data[0] : null;
      if (doc?.data && typeof doc.data === 'string') {
        const decoded = Buffer.from(doc.data, 'base64');
        if (decoded.length > 0) {
          log.debug(`[internetx] /document/${docId} (${ext}, ${doc.mimeType || '?'}): ${decoded.length} bytes (base64-decoded)`);
          return decoded;
        }
      }
      if (doc?.link) {
        const linkResp = await fetch(doc.link);
        if (!linkResp.ok) {
          log.warn(`[internetx] /document/${docId} link ${doc.link}: HTTP ${linkResp.status}`);
          return null;
        }
        const linkBuf = Buffer.from(await linkResp.arrayBuffer());
        log.debug(`[internetx] /document/${docId} (${ext}): ${linkBuf.length} bytes (via link)`);
        return linkBuf;
      }
      log.warn(`[internetx] /document/${docId} (${ext}): JSON ohne data/link — Inhalt unklar`);
      return null;
    } catch (e) {
      log.warn(`[internetx] /document/${docId} (${ext}): ${e.message}`);
      return null;
    }
  }

  async *iterateStatements() {
    // Welche Doc-Typen will der User? Default: nur PDF.
    const wantedTypes = DOC_TYPES.filter((t) => {
      const v = this.account[t.flag];
      if (v === undefined || v === null || v === '') return t.flag === 'fetchInvoicePdf';
      return v === true || v === 'true';
    });
    if (wantedTypes.length === 0) {
      log.warn('[internetx] Keine Doc-Typen ausgewaehlt — nichts zu holen');
      return;
    }
    log.info(`[internetx] Aktive Typen: ${wantedTypes.map((t) => t.ext.toUpperCase()).join(', ')}`);

    // Jahr fuer Jahr — sauberer Pre-Skip pro Jahr. Default: heutiges Jahr + 7 zurueck.
    const currentYear = new Date().getFullYear();
    const startYear = Number(this.account.startYear) || (currentYear - 7);
    const endYear   = Number(this.account.endYear)   || currentYear;
    log.info(`[internetx] Hole Rechnungen ${startYear}–${endYear}`);

    const PAGE_LIMIT = 50; // konservativ — exaktes Max ist nicht dokumentiert

    for (let year = endYear; year >= startYear; year--) {
      let offset = 0;
      let total = 0;

      while (true) {
        const body = {
          filters: [{ key: 'type', value: 'INVOICE', operator: 'EQUAL' }],
          view: {
            children: 1,
            limit: PAGE_LIMIT,
            offset,
            from: `${year}-01-01T00:00:00.000+0000`,
            to:   `${year}-12-31T23:59:59.000+0000`,
          },
          // Neueste Rechnungen zuerst — sinnvoll fuer effizienten Pre-Skip beim Folgelauf
          orders: [{ key: 'created', type: 'DESC' }],
        };
        let listResp;
        try {
          listResp = await this.ixJson('POST', '/invoice/_search', body);
        } catch (e) {
          log.warn(`[internetx] /invoice/_search Jahr ${year} offset ${offset}: ${e.message}`);
          break;
        }
        // AutoDNS-status.type kann ausgeschrieben (SUCCESS/ERROR/NOTIFICATION) oder kurz (S/E/N) kommen.
        const sType = listResp.status?.type;
        if (sType === 'ERROR' || sType === 'E') {
          log.warn(`[internetx] /invoice/_search Jahr ${year} offset ${offset}: ${listResp.status.code} ${listResp.status.text}`);
          break;
        }
        if (sType === 'NOTIFICATION' || sType === 'N') {
          // Async-Followup: bei Read-Only-Calls eigentlich nicht zu erwarten — wenn doch, weiter und hoffen
          log.warn(`[internetx] /invoice/_search Jahr ${year}: status=N (async) ${listResp.status.code} ${listResp.status.text} — fahre trotzdem fort`);
        }
        const items = Array.isArray(listResp.data) ? listResp.data : [];
        if (offset === 0) {
          const summary = listResp.object?.summary;
          log.info(`[internetx] -> Jahr ${year}${summary !== undefined ? ` (Total ueber alle Jahre: ${summary})` : ''}`);
        }
        if (items.length === 0) break;
        total += items.length;

        for (const inv of items) {
          // ISO 8601 (z. B. "2019-11-07T04:00:57.000+0100") — erste 10 Zeichen = Datum
          const dateIso = (inv.created || inv.updated || '').slice(0, 10);
          const yyyy = dateIso ? Number(dateIso.slice(0, 4)) : year;
          const number = inv.number || inv.id || `unknown-${offset}`;

          // Pro Doc-Typ pruefen ob bereits vorhanden, nur fehlende holen
          const todos = [];
          let already = 0;
          for (const t of wantedTypes) {
            const docId = inv[t.idPath[0]]?.[t.idPath[1]];
            if (!docId) continue; // dieser Format-Typ existiert fuer diese Invoice nicht
            // Pattern "JJJJ-MM-TT Rechnungsnummer.ext" — chronologisch sortierbar im Dateisystem,
            // Source-Prefix entfaellt (Folder ist eh "InternetX/<Jahr>/").
            const filename = `${dateIso || `${yyyy}-00-00`} ${number}.${t.ext}`;
            if (this.cfg) {
              const existing = fileExistsAtAnyLayout(this.cfg, this.account.folder, yyyy, undefined, filename);
              if (existing) {
                already++;
                continue;
              }
            }
            todos.push({ docId, filename, ext: t.ext });
          }
          if (todos.length === 0) {
            if (already > 0) log.info(`[${this.account.id}] skip ${number} (${dateIso}): ${already} bereits vorhanden`);
            continue;
          }
          if (already > 0) {
            log.info(`[${this.account.id}] ${number} (${dateIso}): ${already} bereits vorhanden, ${todos.length} werden geholt`);
          }

          for (const d of todos) {
            const buffer = await this.fetchDocument(d.docId, d.ext);
            if (buffer) yield { filename: d.filename, year: yyyy, buffer };
          }
        }

        if (items.length < PAGE_LIMIT) break;
        offset += PAGE_LIMIT;
      }
      log.info(`[internetx] Jahr ${year}: ${total} Rechnungen verarbeitet`);
    }
  }
}
