import nodemailer from 'nodemailer';
import archiver from 'archiver';
import archiverZipEncrypted from 'archiver-zip-encrypted';
import { createWriteStream, mkdtempSync, rmSync, existsSync, readdirSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, basename, relative, dirname } from 'node:path';
import { log } from './logger.mjs';
import { getMailConfig, SOURCE_DEFAULT_FOLDER } from './config.mjs';

// AES-256 ZIP-Encryption fuer DSGVO-konformen Versand (ZipCrypto ist gebrochen, deshalb AES).
// Empfaenger braucht 7-Zip / Keka / aehnliches — Windows-Explorer kann AES-ZIPs nicht oeffnen.
// Plugin wird einmal global beim archiver registriert. Idempotent: registerFormat wirft, wenn
// schon angemeldet — wir schlucken den Fehler, damit Module-Reloads nicht knallen.
//
// ESM-Default-Import-Robustheit: archiver-zip-encrypted exportiert die Factory entweder
// direkt oder unter .default — je nach Bundler/Node-Version. Beide Varianten abfangen,
// sonst kommt erst beim ZIP-Bauen ein kryptisches "format zip-encrypted not registered".
const zipEncryptedFactory = archiverZipEncrypted?.default ?? archiverZipEncrypted;
try { archiver.registerFormat('zip-encrypted', zipEncryptedFactory); }
catch { /* schon registriert */ }

// Mindest-Anforderungen an das ZIP-Passwort. Atruvia-Tax-Files koennen Geburtsdaten,
// Steuer-IDs etc. enthalten → triviale PWs ("123") schwaechen den DSGVO-Schutz.
// Exportiert, damit setMailSettings das Passwort schon beim Setzen pruefen kann
// (nicht erst beim ersten ZIP-Bauen mit kryptischer User-Erfahrung).
export const ZIP_PW_MIN_LEN = 12;
export function validateZipPassword(pw) {
  if (!pw) return; // leer = unverschluesselt, OK
  if (typeof pw !== 'string') throw new Error('zipPassword muss ein String sein.');
  if (pw.length < ZIP_PW_MIN_LEN) {
    throw new Error(`zipPassword zu kurz (mindestens ${ZIP_PW_MIN_LEN} Zeichen).`);
  }
  // Triviale Pattern abfangen — nicht erschoepfend, aber faengt die offensichtlichen Faelle
  if (/^(.)\1{3,}$/.test(pw) || /^(0123|1234|abcd|password|geheim|qwertz)/i.test(pw)) {
    throw new Error('zipPassword zu trivial — bitte ein zufaelliges starkes Passwort waehlen.');
  }
}

// Verifiziert SMTP-Settings (Verbindung + Auth) ohne eine Mail zu verschicken.
// Wird aus der GUI vor dem Speichern aufgerufen. nodemailer.verify() macht
// SMTP HELO + AUTH und schliesst sauber — laesst den Server in Ruhe wenn die
// Daten passen, liefert klaren Fehler wenn nicht.
// Timeouts: Default-nodemailer-Werte sind 2min — viel zu lang fuer einen
// Settings-Dialog. 10/15s halten die GUI responsive.
export async function testSmtpConnection(smtp) {
  if (!smtp || !smtp.host || !smtp.user) {
    throw new Error('Host und User sind Pflicht.');
  }
  if (!smtp.password) {
    throw new Error('Kein Passwort hinterlegt — bitte zuerst Passwort setzen.');
  }
  const port = Number(smtp.port) || 587;
  const transport = nodemailer.createTransport({
    host: smtp.host,
    port,
    secure: smtp.secure ?? (port === 465),
    auth: { user: smtp.user, pass: smtp.password },
    connectionTimeout: 10000,
    greetingTimeout: 10000,
    socketTimeout: 15000,
  });
  try {
    await transport.verify();
    return { ok: true, host: smtp.host, port, user: smtp.user };
  } finally {
    transport.close();
  }
}

// Filtert pro Account die in diesem Lauf neu gespeicherten Files anhand
// account.taxKontos: Liste von Subfolder-Namen (Top-Level-Segment), deren Items in die Mail gehen.
// Items ohne Subfolder oder mit nicht-passendem Subfolder fallen raus.
function filterTaxFiles(accountResults) {
  const matched = [];
  for (const { account, savedFiles } of accountResults) {
    const taxKontos = account.taxKontos || [];
    if (!taxKontos.length || !savedFiles?.length) continue;
    const allMode = taxKontos.includes('*');
    const sourceFolder = account.folder || SOURCE_DEFAULT_FOLDER[account.source] || account.source;
    for (const f of savedFiles) {
      if (allMode) {
        matched.push({ ...f, account: account.id, sourceFolder, subfolder: f.subfolder || '' });
        continue;
      }
      if (!f.subfolder) continue;
      const topSub = f.subfolder.split(/[\\/]/)[0];
      if (taxKontos.includes(topSub)) {
        matched.push({ ...f, account: account.id, sourceFolder });
      }
    }
  }
  return matched;
}

// Liefert den Pfad innerhalb des ZIPs gemaess gewaehltem Layout.
//   'flat-date-prefix' (Default): "<JJJJ-MM-TT> <Source> [<Subfolder> ]<Filename>" — flach,
//                                 chronologisch sortiert, Source + Subfolder im Namen
//                                 → Datei bleibt eindeutig auch bei Multi-Account-gleicher-Source.
//   'folder':                     "<Source>/<Subfolder>/<Filename>" — strukturierte Ordner.
// Wenn der Filename bereits mit "JJJJ-MM-TT " beginnt, wird beim flat-date-prefix-Modus das Datum
// nicht doppelt davorgehaengt — wir schieben nur Source + Subfolder dazwischen.
function pathInZip(file, layout) {
  const fname = basename(file.path);
  const src = file.sourceFolder;
  const sub = file.subfolder ? `${file.subfolder}/` : '';
  if (layout === 'folder') {
    return `${src}/${sub}${fname}`.replace(/\\/g, '/');
  }
  // flat-date-prefix: Subfolder normalisieren (Slashes → Spaces, Filesystem-Trenner raus)
  // damit der ZIP-Eintrag wirklich flach bleibt (sonst wuerde "/" einen Unterordner erzwingen).
  const subToken = file.subfolder
    ? `${file.subfolder.replace(/[\\/]+/g, ' ')} `
    : '';
  const dateMatch = fname.match(/^(\d{4}-\d{2}-\d{2})\s+(.+)$/);
  if (dateMatch) {
    return `${dateMatch[1]} ${src} ${subToken}${dateMatch[2]}`;
  }
  // Kein erkennbares Datum am Anfang → Source + Subfolder vor den Namen, kein Datum erfunden
  return `${src} ${subToken}${fname}`;
}

async function buildZip(files, zipPath, layout, zipPassword) {
  validateZipPassword(zipPassword);
  await new Promise((resolve, reject) => {
    const output = createWriteStream(zipPath);
    // Bei gesetztem Passwort: AES-256-Encryption. Sonst: regulaeres ZIP.
    // Empfaenger braucht 7-Zip/Keka — Windows-Explorer kann AES-ZIPs nicht oeffnen.
    const archive = zipPassword
      ? archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: zipPassword })
      : archiver('zip', { zlib: { level: 9 } });
    output.on('close', resolve);
    output.on('error', reject);
    archive.on('error', reject);
    archive.pipe(output);
    for (const f of files) {
      archive.file(f.path, { name: pathInZip(f, layout) });
    }
    archive.finalize();
  });
}

// Gemeinsamer Send-Path fuer post-run-Versand und Monatsversand. Baut ZIP, verschickt
// per SMTP, raeumt temp auf. Liefert ein strukturiertes Ergebnis fuer GUI/CLI.
async function sendMailWithZip(cfg, files, { zipName, subject, intro, note = '' }) {
  const { smtp, zipLayout, zipPassword } = getMailConfig(cfg);
  if (!smtp || !smtp.host || !smtp.user || !smtp.password || !smtp.from || !smtp.to?.length) {
    log.warn('[mail] SMTP-Config unvollständig (host/user/password/from/to[]) — überspringe Versand');
    return { sent: false, fileCount: files.length, error: 'smtp-incomplete' };
  }

  const tmpDir = mkdtempSync(join(tmpdir(), 'docudown-mail-'));
  const zipPath = join(tmpDir, zipName);

  const enc = zipPassword ? ' (AES-256-verschlüsselt)' : '';
  log.info(`[mail] Erstelle ZIP (${zipLayout})${enc} mit ${files.length} Files: ${zipPath}`);
  try {
    await buildZip(files, zipPath, zipLayout, zipPassword);

    // maxSizeMB durchsetzen: Mailserver rejecten oft >25MB. Statt SMTP-Reject mit kryptischer
    // Meldung lieber sauberes pre-Send-Error mit klarer Message.
    const maxSizeMB = cfg?.mail?.maxSizeMB;
    if (maxSizeMB && maxSizeMB > 0) {
      const sizeBytes = statSync(zipPath).size;
      const limitBytes = maxSizeMB * 1024 * 1024;
      if (sizeBytes > limitBytes) {
        const sizeMB = (sizeBytes / 1024 / 1024).toFixed(1);
        log.error(`[mail] ZIP ${sizeMB}MB > Limit ${maxSizeMB}MB — Versand abgebrochen`);
        return { sent: false, fileCount: files.length, error: `zip-too-large:${sizeMB}MB>${maxSizeMB}MB` };
      }
    }

    const transport = nodemailer.createTransport({
      host: smtp.host,
      port: smtp.port || 587,
      secure: smtp.secure ?? (smtp.port === 465),
      auth: { user: smtp.user, pass: smtp.password },
    });

    const lines = [intro, '', 'Übersicht:', ...files.map((f) => `  • ${pathInZip(f, zipLayout)}`)];
    if (zipPassword) {
      lines.push('');
      lines.push('Hinweis: Das ZIP ist AES-256-verschlüsselt (DSGVO). Zum Öffnen 7-Zip oder Keka benötigt — Windows-Explorer kann AES-ZIPs nicht direkt öffnen. Das Passwort erhalten Sie auf separatem Weg.');
    }
    if (note) { lines.push(''); lines.push(note); }

    log.info(`[mail] Sende an ${smtp.to.join(', ')}`);
    const info = await transport.sendMail({
      from: smtp.from,
      to: smtp.to.join(', '),
      subject,
      text: lines.join('\n'),
      attachments: [{ filename: zipName, path: zipPath }],
    });
    log.info(`[mail] Versand erfolgreich (messageId=${info.messageId})`);
    return { sent: true, fileCount: files.length, messageId: info.messageId, recipients: smtp.to };
  } catch (e) {
    log.error(`[mail] Versand fehlgeschlagen: ${e.message}`);
    return { sent: false, fileCount: files.length, error: e.message };
  } finally {
    // tmpDir-Cleanup: Antivirus auf Windows kann Files locken → rmSync wirft.
    // Nicht-fatal, aber loggen damit /tmp nicht mit DocuDown-Resten vollaeuft.
    try { rmSync(tmpDir, { recursive: true, force: true }); }
    catch (e) { log.warn(`[mail] tmpDir cleanup fehlgeschlagen (${tmpDir}): ${e.message}`); }
  }
}

export async function sendTaxMail(cfg, accountResults, note = '') {
  const matched = filterTaxFiles(accountResults);
  if (matched.length === 0) {
    log.info('[mail] Keine neuen taxKontos-Files in diesem Lauf — keine Mail');
    return { sent: false, fileCount: 0, error: 'no-files' };
  }
  const today = new Date().toISOString().slice(0, 10);
  return await sendMailWithZip(cfg, matched, {
    zipName: `Steuer-${today}.zip`,
    subject: `Kontoauszüge ${today}`,
    intro: `Anbei ${matched.length} neue Dokumente vom ${today}.`,
    note,
  });
}

// Walkt rekursiv durch ein Account-Folder und sammelt alle Files, deren Filename mit
// "JJJJ-MM-" beginnt. Year-Subfolder werden uebersprungen wenn sie nicht zum gesuchten
// Year passen (Optimierung). Subfolder relativ zum Account-Root, Year-Segmente
// rausgefiltert (das Year ist im Filename, nicht in der ZIP-Struktur relevant).
function walkMonth(accountRoot, year, monthStr, onMatch) {
  const yearStr = String(year);
  const prefix = `${yearStr}-${monthStr}-`;
  const stack = [accountRoot];
  while (stack.length) {
    const dir = stack.pop();
    let entries;
    try { entries = readdirSync(dir, { withFileTypes: true }); }
    catch { continue; }
    for (const e of entries) {
      const p = join(dir, e.name);
      if (e.isDirectory()) {
        // Wenn der Folder-Name ein 4-stelliges Jahr ist und nicht zum gesuchten passt,
        // sparen wir den ganzen Subtree (kein File darin kann den Monatsprefix matchen).
        if (/^\d{4}$/.test(e.name) && e.name !== yearStr) continue;
        stack.push(p);
      } else if (e.isFile() && e.name.startsWith(prefix)) {
        onMatch(p);
      }
    }
  }
}

// Findet alle Files eines Monats, gefiltert nach account.taxKontos (Top-Level-Subfolder).
// Wie filterTaxFiles, aber Filesystem-basiert statt run-result-basiert. Year-Folder
// werden im Subfolder-Pfad rausgenommen (sind kein logisches Subfolder-Segment).
function findFilesForMonth(cfg, year, month) {
  const monthStr = String(month).padStart(2, '0');
  const matched = [];
  for (const account of cfg.accounts) {
    const taxKontos = account.taxKontos || [];
    if (!taxKontos.length) continue;
    const allMode = taxKontos.includes('*');
    const sourceFolder = account.folder || SOURCE_DEFAULT_FOLDER[account.source] || account.source;
    const accountRoot = join(cfg.rootDir, sourceFolder);
    if (!existsSync(accountRoot)) continue;

    walkMonth(accountRoot, year, monthStr, (filePath) => {
      const relDir = relative(accountRoot, dirname(filePath));
      const segments = relDir ? relDir.split(/[\\/]/).filter(Boolean) : [];
      // Year-Segmente raus — der Subfolder ist die fachliche Gliederung (z.B. "Familie/Tagesgeld").
      const logical = segments.filter((s) => !/^\d{4}$/.test(s));
      const subfolder = logical.join('/');
      const topSub = logical[0] || '';
      if (allMode || (topSub && taxKontos.includes(topSub))) {
        matched.push({ path: filePath, account: account.id, sourceFolder, subfolder });
      }
    });
  }
  return matched;
}

// Statusinfo fuer GUI: ist der Monat auswaehlbar, gibt's Warnungen, wieviele Files.
// - Zukunft: nicht selectable
// - Aktueller Monat: Warnung "noch nicht abgeschlossen"
// - Vormonat in den ersten Tagen: Warnung "Doks erscheinen meist erst am Monatsanfang"
// - Files=0: Warnung "Keine Dokumente gefunden"
// - smtpReady: false → GUI kann Senden-Button vorab disablen
export function assessMonth(cfg, year, month) {
  const now = new Date();
  const curY = now.getFullYear();
  const curM = now.getMonth() + 1;
  const isFuture = year > curY || (year === curY && month > curM);
  const isCurrent = year === curY && month === curM;
  // Vormonat: (year, month) ist genau einen Monat vor (curY, curM)
  const isPrevious = (curM > 1 && year === curY && month === curM - 1)
                  || (curM === 1 && year === curY - 1 && month === 12);

  const warnings = [];
  if (isFuture) warnings.push('Zukünftige Monate sind nicht auswählbar.');
  if (isCurrent) warnings.push('Aktueller Monat — Doks erscheinen meist erst nach Monatsende.');
  if (isPrevious && now.getDate() <= 5) warnings.push('Doks für den Vormonat erscheinen meist in den ersten Tagen des Folgemonats.');

  const files = isFuture ? [] : findFilesForMonth(cfg, year, month);
  if (!isFuture && files.length === 0) warnings.push('Keine Dokumente gefunden.');

  // SMTP-Ready-Check fuer GUI: Senden-Button kann vorab disabled werden
  const smtp = cfg?.smtp || {};
  const smtpReady = !!(smtp.host && smtp.user && smtp.password && smtp.from && smtp.to?.length);
  if (!smtpReady && !isFuture && files.length > 0) {
    warnings.push('SMTP nicht vollständig konfiguriert — Versand wird scheitern.');
  }

  return {
    year, month,
    selectable: !isFuture,
    isCurrent,
    isPrevious,
    smtpReady,
    fileCount: files.length,
    accounts: aggregateByAccount(cfg, files),
    warnings,
  };
}

// Aggregiert Files pro Account; Account-Label aus cfg holen, damit das Frontend
// keinen extra Lookup machen muss.
function aggregateByAccount(cfg, files) {
  const labelMap = new Map();
  for (const a of cfg.accounts || []) {
    labelMap.set(a.id, a.label || a.id);
  }
  const counts = new Map();
  for (const f of files) {
    counts.set(f.account, (counts.get(f.account) || 0) + 1);
  }
  return [...counts.entries()]
    .map(([accountId, count]) => ({ accountId, label: labelMap.get(accountId) || accountId, count }))
    .sort((a, b) => a.label.localeCompare(b.label));
}

// Schickt alle taxKontos-Files eines Monats als ZIP an den Steuerberater.
export async function sendMonthMail(cfg, year, month, note = '') {
  const matched = findFilesForMonth(cfg, year, month);
  if (matched.length === 0) {
    log.info(`[mail] Keine Files für ${year}-${String(month).padStart(2, '0')} unter taxKontos gefunden`);
    return { sent: false, fileCount: 0, error: 'no-files' };
  }
  const monthLabel = `${year}-${String(month).padStart(2, '0')}`;
  return await sendMailWithZip(cfg, matched, {
    zipName: `Steuer-${monthLabel}.zip`,
    subject: `Kontoauszüge ${monthLabel}`,
    intro: `Anbei ${matched.length} Dokumente für ${monthLabel}.`,
    note,
  });
}
