#!/usr/bin/env node
import { parseArgs } from 'node:util';
import { existsSync, readdirSync, statSync, mkdirSync, renameSync, unlinkSync, rmdirSync, cpSync, copyFileSync } from 'node:fs';
import { join, dirname, resolve } from 'node:path';
import { log, setDebug } from './logger.mjs';
import { ask, askSecret, askConfirm } from './prompt.mjs';
import {
  SUPPORTED_SOURCES, SOURCE_DEFAULT_FOLDER,
  configExists, loadConfig, saveConfig, newConfig, addAccount, removeAccount, renameAccount, setAccountOption,
  getLayout, getMailConfig, exportConfigFile, importConfigFile,
} from './config.mjs';
import {
  CONFIG_PATH, DEFAULT_KONTO_ROOT, APP_DATA_DIR,
  BROWSER_DATA_ROOT, LOG_DIR,
  LEGACY_BROWSER_DATA_ROOT, LEGACY_LOG_DIR,
} from './paths.mjs';
import { runAll, loginOnly } from './runner.mjs';
import { sendTaxMail, sendMonthMail, assessMonth } from './mail.mjs';

function usage() {
  process.stdout.write(`\nDocuDown — Kontoauszüge, Rechnungen und Policen automatisch herunterladen

  init                        Neue verschlüsselte Config anlegen
  add-account                 Konto hinzufügen (interaktiv)
  remove-account <id>         Konto entfernen
  rename-account <alt> <neu>  Konto-ID umbenennen (Credentials bleiben)
  set-credentials <id>        Credentials für ein Konto neu setzen (source-spezifisch)
  set-option <id> <feld> <wert>  Zusatzfeld setzen (z.B. postboxFolderId, kontoAliases als JSON)
  set-layout <feld> <wert>    Layout-Setting setzen (z.B. yearSubfolder false)
  set-mail <feld> <wert>      Mail-Setting setzen (z.B. zipLayout flat-date-prefix, maxSizeMB 25)
  list                        Konten anzeigen
  change-password             Master-Passwort ändern
  set-root                    Ziel-Wurzelverzeichnis ändern
  set-smtp                    SMTP-Versanddaten für Mail-Feature setzen (Steuerberater)
  config-export <pfad>        Verschlüsselte Config an angegebenen Pfad sichern (Backup/USB)
  config-import <pfad>        Verschlüsselte Config von Pfad importieren (überschreibt aktuelle)
  migrate-app-data            Browser-Sessions + Logs vom Repo-Folder in den OS-Standard-Pfad verschieben
  run [--source=<key>] [--id=<id>] [--mail-note="..."] [--debug]
                              Dokumente holen, ggf. neue Items per Mail versenden
  login <id> [--debug]        Nur einloggen, Browser offen lassen (für Erkundung/Debug)
  rpc                         JSON-RPC-Modus für GUI (stdin/stdout, intern für Tauri)
  migrate-aliases <id>        Alte Subfolder (nur Nummer) in neue "Typ (Nummer)"-Ordner mergen

  --debug                     Ausführliches Log inkl. Browser-Console / failed Requests
                              in logs/debug-<zeitstempel>.log

Quellen: ${SUPPORTED_SOURCES.join(', ')}
Config-Datei: ${CONFIG_PATH}
`);
}

async function promptMasterPassword({ confirm = false } = {}) {
  const pw = await askSecret('Master-Passwort');
  if (!pw) throw new Error('Leeres Passwort nicht erlaubt.');
  if (confirm) {
    const pw2 = await askSecret('Master-Passwort (Wiederholung)');
    if (pw !== pw2) throw new Error('Passwörter stimmen nicht überein.');
  }
  return pw;
}

async function cmdInit() {
  if (configExists()) {
    const ok = await askConfirm(`Config existiert bereits (${CONFIG_PATH}). Überschreiben?`);
    if (!ok) return;
  }
  const rootDir = await ask('Ziel-Wurzelverzeichnis', { defaultValue: DEFAULT_KONTO_ROOT });
  const pw = await promptMasterPassword({ confirm: true });
  const cfg = newConfig(rootDir);
  saveConfig(cfg, pw);
  log.info(`Config angelegt: ${CONFIG_PATH}`);
  log.info('Nächster Schritt: `node src/cli.mjs add-account`');
}

async function promptCredentials(source) {
  if (source === 'ovh') {
    log.info('OVH-Credentials erzeugen — entweder:');
    log.info('  • Token Manager (alles auf einmal):  https://eu.api.ovh.com/createToken/');
    log.info('  • App-Registrierung (App Key + Secret):  https://eu.api.ovh.com/createApp/');
    log.info('    → Consumer Key kann unten leer bleiben, wird per Credential-Request geholt.');

    const appKey = await ask('OVH Application Key');
    const appSecret = await askSecret('OVH Application Secret');
    let consumerKey = await ask('OVH Consumer Key (leer lassen → automatisch holen)');

    if (!consumerKey) {
      log.info('Sende Credential-Request an OVH ...');
      const resp = await fetch('https://eu.api.ovh.com/1.0/auth/credential', {
        method: 'POST',
        headers: { 'X-Ovh-Application': appKey, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          accessRules: [
            { method: 'GET', path: '/me' },
            { method: 'GET', path: '/me/bill' },
            { method: 'GET', path: '/me/bill/*' },
            { method: 'GET', path: '/me/refund' },
            { method: 'GET', path: '/me/refund/*' },
          ],
        }),
      });
      if (!resp.ok) {
        const text = await resp.text().catch(() => '');
        throw new Error(`Credential-Request fehlgeschlagen: HTTP ${resp.status} — ${text.slice(0, 200)}`);
      }
      const data = await resp.json();
      consumerKey = data.consumerKey;
      process.stdout.write(`\nConsumer Key: ${consumerKey}\n`);
      process.stdout.write(`Bitte im Browser bestätigen:\n  ${data.validationUrl}\n\n`);
      await ask('ENTER drücken nach Bestätigung im Browser');
    }

    return { appKey, appSecret, consumerKey };
  }
  const username = await ask('Login / Benutzername / E-Mail');
  if (!username) throw new Error('Username darf nicht leer sein.');
  const password = await askSecret('Login-Passwort / PIN');
  if (!password) throw new Error('Passwort darf nicht leer sein. (Falls per Paste eingefügt: bitte erneut versuchen — Eingabe akzeptiert nun auch Clipboard-Paste.)');
  return { username, password };
}

async function cmdSetCredentials(id) {
  if (!id) throw new Error('Account-ID fehlt: set-credentials <id>');
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  const account = cfg.accounts.find((a) => a.id === id);
  if (!account) throw new Error(`Account "${id}" nicht gefunden.`);
  account.credentials = await promptCredentials(account.source);
  saveConfig(cfg, pw);
  log.info(`Credentials für "${id}" aktualisiert.`);
}

async function cmdAddAccount() {
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);

  const source = (await ask(`Quelle (${SUPPORTED_SOURCES.join('/')})`)).toLowerCase();
  if (!SUPPORTED_SOURCES.includes(source)) throw new Error(`Unbekannte Quelle: ${source}`);

  const defaultId = `${source}-${cfg.accounts.filter((a) => a.source === source).length + 1}`;
  const id = await ask('Account-ID (eindeutig, z.B. "dkb-giro", "dkb-wohnung")', { defaultValue: defaultId });
  const label = await ask('Label (Anzeige)', { defaultValue: id });
  const folder = await ask('Ziel-Unterordner unter dem Wurzelverzeichnis', { defaultValue: SOURCE_DEFAULT_FOLDER[source] });

  const credentials = await promptCredentials(source);

  addAccount(cfg, {
    id, label, source, folder,
    credentials,
  });
  saveConfig(cfg, pw);
  log.info(`Konto "${id}" hinzugefügt.`);
}

async function cmdRemoveAccount(id) {
  if (!id) throw new Error('Account-ID fehlt: remove-account <id>');
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  removeAccount(cfg, id);
  saveConfig(cfg, pw);
  log.info(`Konto "${id}" entfernt.`);
}

async function cmdRenameAccount(oldId, newId) {
  if (!oldId || !newId) throw new Error('Usage: rename-account <alte-id> <neue-id>');
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  renameAccount(cfg, oldId, newId);
  saveConfig(cfg, pw);
  log.info(`Konto "${oldId}" → "${newId}"`);
}

async function cmdSetOption(id, field, value) {
  if (!id || !field) throw new Error('Usage: set-option <id> <feld> <wert>');
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  const parsed = setAccountOption(cfg, id, field, value);
  saveConfig(cfg, pw);
  if (parsed === null) log.info(`Konto "${id}": ${field} entfernt`);
  else log.info(`Konto "${id}": ${field} = ${JSON.stringify(parsed)}`);
}

// Generischer Setter fuer cfg.<section>.<field> — JSON-parse, leer = entfernen.
function setNestedField(cfg, section, field, rawValue) {
  if (rawValue === undefined || rawValue === '' || rawValue === 'null') {
    if (cfg[section]) delete cfg[section][field];
    return null;
  }
  let value = rawValue;
  try { value = JSON.parse(rawValue); } catch { /* plain string */ }
  cfg[section] = cfg[section] || {};
  cfg[section][field] = value;
  return value;
}

async function cmdSetLayout(field, value) {
  if (!field) throw new Error('Usage: set-layout <feld> <wert>   Felder: yearSubfolder');
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  const parsed = setNestedField(cfg, 'layout', field, value);
  saveConfig(cfg, pw);
  log.info(`layout.${field} = ${JSON.stringify(parsed)}`);
  log.info(`Wirksam ab dem nächsten Run. Bestehende Files bleiben — Pre-Skip findet beide Layouts.`);
}

async function cmdSetMail(field, value) {
  if (!field) throw new Error('Usage: set-mail <feld> [wert]   Felder: zipLayout, maxSizeMB, zipPassword');
  const allowedZip = ['flat-date-prefix', 'folder'];
  if (field === 'zipLayout' && value && !allowedZip.includes(value)) {
    throw new Error(`zipLayout muss einer von: ${allowedZip.join(', ')}`);
  }
  // zipPassword darf NICHT als Plain-CLI-Argument uebergeben werden — landet sonst in der
  // Shell-History. Stattdessen interaktiv per askSecret abfragen. Leere Eingabe loescht.
  let effectiveValue = value;
  if (field === 'zipPassword') {
    if (value) {
      log.warn('Sicherheits-Hinweis: zipPassword als CLI-Argument landet in der Shell-History. Bitte ohne Wert aufrufen — wird interaktiv abgefragt.');
      throw new Error('Aus Sicherheitsgruenden: "set-mail zipPassword" ohne Wert aufrufen.');
    }
    effectiveValue = await askSecret('ZIP-Passwort (leer = Verschluesselung deaktivieren)');
  }
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  const parsed = setNestedField(cfg, 'mail', field, effectiveValue);
  saveConfig(cfg, pw);
  // zipPassword nicht im Log preisgeben
  const display = field === 'zipPassword' ? (effectiveValue ? '<gesetzt>' : '<entfernt>') : JSON.stringify(parsed);
  log.info(`mail.${field} = ${display}`);
}

async function cmdConfigExport(target) {
  if (!target) throw new Error('Usage: config-export <pfad>');
  const dest = exportConfigFile(target);
  log.info(`Config exportiert: ${dest}`);
  log.info(`(verschlüsselt — Master-Passwort weiterhin nötig zum Entsperren)`);
}

async function cmdConfigImport(source) {
  if (!source) throw new Error('Usage: config-import <pfad>');
  if (existsSync(CONFIG_PATH)) {
    const ok = await askConfirm(`Aktuelle Config (${CONFIG_PATH}) überschreiben?`);
    if (!ok) return;
  }
  const dest = importConfigFile(source);
  log.info(`Config importiert: ${source} → ${dest}`);
}

async function cmdMigrateAppData() {
  const moves = [
    { from: LEGACY_BROWSER_DATA_ROOT, to: BROWSER_DATA_ROOT, label: 'browser-data (Sessions/Cookies)' },
    { from: LEGACY_LOG_DIR,           to: LOG_DIR,           label: 'logs' },
  ];
  let didAnything = false;
  for (const m of moves) {
    if (!existsSync(m.from)) {
      log.info(`[skip] ${m.label}: Quelle existiert nicht (${m.from})`);
      continue;
    }
    // Ziel existiert ist OK, solange leer — Adapter/Tools koennen den Folder vorab angelegt haben.
    if (existsSync(m.to)) {
      let dstEntries = [];
      try { dstEntries = readdirSync(m.to); } catch { /* ignore */ }
      if (dstEntries.length > 0) {
        log.warn(`[skip] ${m.label}: Ziel existiert bereits und ist nicht leer (${m.to}, ${dstEntries.length} Eintraege) — manuell aufräumen`);
        continue;
      }
      log.info(`  Ziel existiert leer — wird mit Quell-Inhalten gefüllt`);
    }
    mkdirSync(dirname(m.to), { recursive: true });
    let copied = 0;
    let failed = 0;
    // Pro Top-Level-Eintrag einzeln kopieren — robuster gegen OneDrive-Reparse-Points/Permission-Quirks,
    // und Teil-Erfolge werden trotzdem geschrieben.
    for (const entry of readdirSync(m.from)) {
      const src = join(m.from, entry);
      const dst = join(m.to, entry);
      try {
        cpSync(src, dst, { recursive: true, errorOnExist: false, force: true });
        copied++;
      } catch (e) {
        failed++;
        log.warn(`  Fehler bei "${entry}": ${e.message}`);
      }
    }
    log.info(`${m.label}: ${copied} Eintrag/Eintraege kopiert${failed ? `, ${failed} Fehler` : ''} → ${m.to}`);
    if (copied > 0) log.info(`  Quelle bleibt vorerst stehen — nach erfolgreichem Run kannst du sie löschen.`);
    didAnything = true;
  }
  if (!didAnything) log.info('Nichts zu migrieren.');
  log.info(`APP_DATA_DIR: ${APP_DATA_DIR}`);
}

async function cmdList() {
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  process.stdout.write(`\nWurzelverzeichnis: ${cfg.rootDir}\n\n`);
  if (cfg.accounts.length === 0) {
    process.stdout.write('(keine Konten)\n');
    return;
  }
  for (const a of cfg.accounts) {
    process.stdout.write(`- ${a.id}  [${a.source}]  "${a.label}"  →  ${a.folder}/\n`);
  }
  process.stdout.write('\n');
}

async function cmdChangePassword() {
  const oldPw = await askSecret('Aktuelles Master-Passwort');
  const cfg = loadConfig(oldPw);
  process.stdout.write('Neues Master-Passwort setzen:\n');
  const newPw = await promptMasterPassword({ confirm: true });
  saveConfig(cfg, newPw);
  log.info('Master-Passwort geändert.');
}

async function cmdSetTaxKontos(id) {
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  const accounts = id
    ? cfg.accounts.filter((a) => a.id === id)
    : cfg.accounts;
  if (id && accounts.length === 0) throw new Error(`Account "${id}" nicht gefunden.`);

  // Heuristik: Standard-Vorschlag pro Subfolder-Name
  const TAX_HINTS = [
    /steuer/i, /rechnung/i, /beleg/i, /quittung/i,
    /beitrag/i, /lebensversicherung/i, /krankenversicherung/i,
    /geschäft/i, /business/i, /firma/i,
  ];
  const TAX_BLACKLIST = [
    /^\d{4}$/,             // reine Jahre (Amazon, OVH default)
    /^Mitteilungen$/i, /^Sonstiges$/i, /^Nachrichten$/i,
  ];
  const heuristicYes = (name) =>
    !TAX_BLACKLIST.some((p) => p.test(name)) && TAX_HINTS.some((p) => p.test(name));

  for (const account of accounts) {
    const accountRoot = join(cfg.rootDir, account.folder);
    process.stdout.write(`\n=== ${account.id} (${account.source}) → ${accountRoot}\n`);
    if (!existsSync(accountRoot)) {
      process.stdout.write('  (Verzeichnis existiert noch nicht — übersprungen)\n');
      continue;
    }
    const subfolders = readdirSync(accountRoot, { withFileTypes: true })
      .filter((e) => e.isDirectory()).map((e) => e.name).sort();
    if (subfolders.length === 0) {
      process.stdout.write('  (keine Subfolder gefunden)\n');
      continue;
    }
    const current = new Set(account.taxKontos || []);
    process.stdout.write(`  Aktuell: ${[...current].length ? JSON.stringify([...current]) : '(leer)'}\n`);

    const allMode = await askConfirm('  Alle Subfolder dieses Accounts (auch zukünftige)?', { defaultYes: current.has('*') });
    if (allMode) {
      account.taxKontos = ['*'];
      saveConfig(cfg, pw);
      log.info(`  ${account.id}: taxKontos = ["*"]`);
      continue;
    }

    const newSet = new Set();
    for (const sub of subfolders) {
      const inCurrent = current.has(sub);
      const defaultYes = inCurrent || heuristicYes(sub);
      const ans = await askConfirm(`    "${sub}" einschließen?`, { defaultYes });
      if (ans) newSet.add(sub);
    }
    account.taxKontos = [...newSet];
    saveConfig(cfg, pw);
    log.info(`  ${account.id}: taxKontos = ${JSON.stringify(account.taxKontos)}`);
  }
}

async function cmdSetSmtp() {
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  const cur = cfg.smtp || {};
  process.stdout.write('SMTP-Versanddaten (für Steuerberater-Mail). Leer lassen = aktuellen Wert behalten.\n');
  const host = await ask('SMTP-Host', { defaultValue: cur.host || '' });
  const port = await ask('SMTP-Port', { defaultValue: String(cur.port || 587) });
  const secure = (await ask('TLS/SSL secure (true/false)', { defaultValue: String(cur.secure ?? (Number(port) === 465)) })).toLowerCase() === 'true';
  const user = await ask('SMTP-User', { defaultValue: cur.user || '' });
  const password = await askSecret('SMTP-Passwort (leer = behalten)');
  const from = await ask('Absender (z.B. "DocuDown <bot@example.com>")', { defaultValue: cur.from || '' });
  const toRaw = await ask('Empfänger (komma-getrennt)', { defaultValue: (cur.to || []).join(', ') });
  const to = toRaw.split(',').map((s) => s.trim()).filter(Boolean);
  cfg.smtp = {
    host, port: Number(port), secure, user,
    password: password || cur.password || '',
    from, to,
  };
  saveConfig(cfg, pw);
  log.info('SMTP-Konfiguration gespeichert.');
}

async function cmdSetRoot() {
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  const v = await ask('Neues Wurzelverzeichnis', { defaultValue: cfg.rootDir });
  cfg.rootDir = v;
  saveConfig(cfg, pw);
  log.info(`Wurzelverzeichnis gesetzt: ${v}`);
}

async function cmdSendMonth(monthArg, opts) {
  if (!monthArg || !/^\d{4}-\d{2}$/.test(monthArg)) {
    log.error('Verwendung: docudown send-month YYYY-MM [--mail-note "..."]');
    process.exit(1);
  }
  const [yStr, mStr] = monthArg.split('-');
  const year = Number(yStr);
  const month = Number(mStr);
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);

  const status = assessMonth(cfg, year, month);
  if (!status.selectable) {
    log.error(`Monat ${monthArg} ist nicht auswählbar (Zukunft).`);
    process.exit(1);
  }
  if (status.warnings.length) {
    for (const w of status.warnings) log.warn(`[mail] ${w}`);
  }
  log.info(`[mail] ${status.fileCount} Dokumente fuer ${monthArg} aus ${status.accounts.length} Konten`);
  if (!status.fileCount) process.exit(0);

  const result = await sendMonthMail(cfg, year, month, opts['mail-note'] || '');
  if (!result.sent) {
    log.error(`[mail] Versand fehlgeschlagen: ${result.error}`);
    process.exit(1);
  }
  log.info(`[mail] Versand OK an ${result.recipients?.join(', ') || '(unbekannt)'}`);
}

async function cmdRun(opts) {
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  const filter = [];
  if (opts.source) filter.push(opts.source);
  if (opts.id) filter.push(opts.id);
  const results = await runAll(cfg, filter.length ? filter : null);
  if (cfg.smtp?.host) {
    const r = await sendTaxMail(cfg, results || [], opts['mail-note'] || '');
    if (r && !r.sent && r.error && r.error !== 'no-files') {
      log.warn(`[cli] Mail nicht versendet: ${r.error}`);
    }
  }
}

function moveDirContents(srcDir, tgtDir) {
  if (!existsSync(tgtDir)) mkdirSync(tgtDir, { recursive: true });
  const entries = readdirSync(srcDir, { withFileTypes: true });
  let moved = 0, duplicate = 0, conflict = 0;
  for (const e of entries) {
    const src = join(srcDir, e.name);
    const tgt = join(tgtDir, e.name);
    if (e.isDirectory()) {
      const sub = moveDirContents(src, tgt);
      moved += sub.moved; duplicate += sub.duplicate; conflict += sub.conflict;
      if (readdirSync(src).length === 0) rmdirSync(src);
    } else {
      if (existsSync(tgt)) {
        if (statSync(src).size === statSync(tgt).size) {
          unlinkSync(src);
          duplicate++;
        } else {
          renameSync(src, `${tgt}.duplicate`);
          log.warn(`[migrate] Konflikt (size diff) — src behalten als ${e.name}.duplicate`);
          conflict++;
        }
      } else {
        renameSync(src, tgt);
        moved++;
      }
    }
  }
  return { moved, duplicate, conflict };
}

async function cmdMigrateAliases(id) {
  if (!id) throw new Error('Account-ID fehlt: migrate-aliases <id>');
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  const account = cfg.accounts.find((a) => a.id === id);
  if (!account) throw new Error(`Account "${id}" nicht gefunden.`);

  const accountRoot = join(cfg.rootDir, account.folder);
  if (!existsSync(accountRoot)) {
    log.warn(`Account-Ordner existiert nicht: ${accountRoot}`);
    return;
  }

  const dirs = readdirSync(accountRoot, { withFileTypes: true })
    .filter((e) => e.isDirectory()).map((e) => e.name);

  // Reine-Nummer-Ordner: 39.428.367568, 95-48284561, 1234567890, ...
  const numberLike = /^[\d.\-]+$/;
  let migrated = 0;

  const aliases = account.kontoAliases || {};
  const normalize = (s) => s.replace(/[.\-]/g, '');

  for (const dirName of dirs) {
    if (!numberLike.test(dirName)) continue;
    const dirNorm = normalize(dirName);
    // 1) Bevorzugt: existierender Ziel-Ordner, dessen "(...)" normalisiert auf dirName matcht
    let target = dirs.find((d) => {
      if (d === dirName) return false;
      const m = d.match(/\(([^)]+)\)/);
      return m && normalize(m[1]) === dirNorm;
    });
    // 2) Fallback: Mapping aus account.kontoAliases (exakt oder normalisiert)
    if (!target) {
      if (aliases[dirName]) {
        target = aliases[dirName];
      } else {
        const hit = Object.entries(aliases).find(([k]) => normalize(k) === dirNorm);
        if (hit) target = hit[1];
      }
      if (target) log.info(`[migrate] Ziel aus kontoAliases: ${target}`);
    }
    if (!target) {
      log.info(`[migrate] kein Ziel für ${dirName} (weder existierend noch in kontoAliases) — skip`);
      continue;
    }
    log.info(`[migrate] ${dirName}  →  ${target}`);
    const result = moveDirContents(join(accountRoot, dirName), join(accountRoot, target));
    log.info(`[migrate]   verschoben=${result.moved}, dedup=${result.duplicate}, konflikt=${result.conflict}`);
    if (existsSync(join(accountRoot, dirName)) && readdirSync(join(accountRoot, dirName)).length === 0) {
      rmdirSync(join(accountRoot, dirName));
      log.info(`[migrate]   ${dirName} entfernt (leer)`);
    }
    migrated++;
  }

  log.info(`[migrate] Fertig — ${migrated} Ordner migriert.`);
}

async function cmdLogin(accountId) {
  if (!accountId) throw new Error('Account-ID fehlt: login <id>');
  const pw = await promptMasterPassword();
  const cfg = loadConfig(pw);
  await loginOnly(cfg, accountId);
}

async function main() {
  const { values, positionals } = parseArgs({
    args: process.argv.slice(2),
    options: {
      source: { type: 'string' },
      id: { type: 'string' },
      debug: { type: 'boolean' },
      'mail-note': { type: 'string' },
      help: { type: 'boolean', short: 'h' },
    },
    allowPositionals: true,
  });
  if (values.debug) setDebug(true);
  if (values.help || positionals.length === 0) { usage(); return; }
  const [cmd, ...rest] = positionals;
  switch (cmd) {
    case 'init':            return cmdInit();
    case 'add-account':     return cmdAddAccount();
    case 'remove-account':  return cmdRemoveAccount(rest[0]);
    case 'rename-account':  return cmdRenameAccount(rest[0], rest[1]);
    case 'set-credentials': return cmdSetCredentials(rest[0]);
    case 'set-option':      return cmdSetOption(rest[0], rest[1], rest.slice(2).join(' '));
    case 'set-layout':      return cmdSetLayout(rest[0], rest.slice(1).join(' '));
    case 'set-mail':        return cmdSetMail(rest[0], rest.slice(1).join(' '));
    case 'config-export':   return cmdConfigExport(rest[0]);
    case 'config-import':   return cmdConfigImport(rest[0]);
    case 'migrate-app-data': return cmdMigrateAppData();
    case 'list':            return cmdList();
    case 'change-password': return cmdChangePassword();
    case 'set-root':        return cmdSetRoot();
    case 'set-smtp':        return cmdSetSmtp();
    case 'run':             return cmdRun(values);
    case 'login':           return cmdLogin(rest[0]);
    case 'migrate-aliases': return cmdMigrateAliases(rest[0]);
    case 'send-month':      return cmdSendMonth(rest[0], values);
    case 'rpc':             { const { startRpc } = await import('./rpc.mjs'); return startRpc(); }
    default: usage(); process.exit(1);
  }
}

main().catch((e) => {
  log.error(e.message || String(e));
  process.exit(1);
});
