import { join } from 'node:path';
import { existsSync, statSync, writeFileSync, renameSync, unlinkSync, readdirSync } from 'node:fs';
import { targetDirFor, fileExistsAtAnyLayout, safeFilename } from './paths.mjs';
import { getLayout } from './config.mjs';
import { log } from './logger.mjs';
import { withAccount } from './run-context.mjs';
import { DkbAdapter } from './sources/dkb.mjs';
import { VwBankAdapter } from './sources/vw-bank.mjs';
import { N26Adapter } from './sources/n26.mjs';
import { FyrstAdapter } from './sources/fyrst.mjs';
import { IngAdapter } from './sources/ing.mjs';
import { GothaerAdapter } from './sources/gothaer.mjs';
import { OvhAdapter } from './sources/ovh.mjs';
import { AmazonAdapter } from './sources/amazon.mjs';
import { NetcologneAdapter } from './sources/netcologne.mjs';
import { InternetXAdapter } from './sources/internetx.mjs';
import { CaptraderAdapter } from './sources/captrader.mjs';
import { VrBankAdapter } from './sources/vr-bank.mjs';

const ADAPTERS = {
  'dkb': DkbAdapter,
  'vw-bank': VwBankAdapter,
  'n26': N26Adapter,
  'fyrst': FyrstAdapter,
  'ing': IngAdapter,
  'gothaer': GothaerAdapter,
  'ovh': OvhAdapter,
  'amazon': AmazonAdapter,
  'netcologne': NetcologneAdapter,
  'internetx': InternetXAdapter,
  'captrader': CaptraderAdapter,
  'vr-bank': VrBankAdapter,
};

const YEAR_RE = /(19|20)\d{2}/;

function extractYear(filename, fallbackDate = new Date()) {
  const m = filename.match(YEAR_RE);
  if (m) return Number(m[0]);
  return fallbackDate.getFullYear();
}

async function saveDownload(item, cfg, account) {
  const filename = safeFilename(item.filename);
  const year = item.year ?? extractYear(filename);
  // Subfolder darf verschachtelt sein (z.B. "Leonard Glück/Mitteilungen") — pro Segment sanitizen, Slashes behalten
  const subfolder = item.subfolder
    ? item.subfolder.split(/[\\/]/).map(safeFilename).filter(Boolean).join('/')
    : undefined;
  const layoutOpts = getLayout(cfg);
  const folder = targetDirFor(cfg.rootDir, account.folder, year, subfolder, layoutOpts);
  const target = join(folder, filename);

  // Pre-Skip: prueft beide Layout-Varianten (mit/ohne Year-Subfolder),
  // damit ein Setting-Wechsel nicht zu Re-Downloads alter Files fuehrt.
  const existingPath = fileExistsAtAnyLayout(cfg, account.folder, year, subfolder, filename);
  if (existingPath) {
    log.info(`[${account.id}] skip (bereits vorhanden): ${existingPath}`);
    return { skipped: true, path: existingPath };
  }

  const tmp = `${target}.part`;
  // Leftover .part von einem frueheren Crash zwischen saveAs und rename aufraeumen,
  // sonst sammelt sich Muell und saveAs koennte auf einer Stale-Datei aufsetzen.
  if (existsSync(tmp)) {
    try { unlinkSync(tmp); } catch { /* nicht-fatal */ }
  }
  if (item.download) {
    await item.download.saveAs(tmp);
  } else if (item.buffer) {
    writeFileSync(tmp, item.buffer);
  } else {
    throw new Error('Statement ohne download/buffer.');
  }
  renameSync(tmp, target);
  log.info(`[${account.id}] saved: ${target}`);
  return { skipped: false, path: target };
}

const noopEmit = () => {};

// Marker-Yields aus Adaptern:
//   { kind: 'progress', total: N } -> emit progress-total
//   { kind: 'skip', path: '...' }  -> Adapter hat selbst pre-geskippt (z.B. File schon da),
//                                     Runner emitted item-skipped damit Progress-UI mitkriegt.
function isProgressMarker(item) {
  return item && typeof item === 'object' && item.kind === 'progress';
}
function isSkipMarker(item) {
  return item && typeof item === 'object' && item.kind === 'skip';
}

export async function runAccount(cfg, account, emit = noopEmit, opts = {}) {
  const { signal } = opts;
  return withAccount(account.id, async () => {
    const AdapterClass = ADAPTERS[account.source];
    if (!AdapterClass) throw new Error(`Keine Implementierung für Quelle "${account.source}".`);
    log.info(`[${account.id}] Starte ${AdapterClass.label} / ${account.source}`);
    emit('account-start', { accountId: account.id, source: account.source, label: AdapterClass.label });
    const adapter = new AdapterClass(account, { headful: true, cfg });
    const results = { saved: 0, skipped: 0, errors: 0, savedFiles: [], aborted: false };

    // Idempotent close — sowohl der Abort-Handler als auch der finally-Block rufen
    // dasselbe auf, ohne Doppel-Close auf einem schon geschlossenen Context.
    let closed = false;
    const closeOnce = async () => {
      if (closed) return;
      closed = true;
      await adapter.close().catch(() => {});
    };

    // Bei Abort proaktiv den Browser-Context schliessen. Das laesst alle pending
    // Playwright-Ops (z.B. waitForURL beim 2FA, fill, click, request.get) sofort werfen
    // — sonst wartet der Runner weiter im 3-Min-2FA-Timeout und Abbrechen tut nix.
    const onAbort = () => { closeOnce().catch(() => {}); };
    if (signal) {
      if (signal.aborted) onAbort();
      else signal.addEventListener('abort', onAbort, { once: true });
    }

    try {
      await adapter.openContext();
      if (signal?.aborted) throw new AbortError();
      await adapter.login();
      if (signal?.aborted) throw new AbortError();
      emit('account-login-done', { accountId: account.id });
      for await (const item of adapter.iterateStatements()) {
        if (signal?.aborted) throw new AbortError();
        if (isProgressMarker(item)) {
          emit('progress-total', { accountId: account.id, total: item.total });
          continue;
        }
        if (isSkipMarker(item)) {
          results.skipped++;
          emit('item-skipped', { accountId: account.id, path: item.path });
          continue;
        }
        try {
          const r = await saveDownload(item, cfg, account);
          if (r.skipped) {
            results.skipped++;
            emit('item-skipped', { accountId: account.id, path: r.path });
          } else {
            results.saved++;
            results.savedFiles.push({ path: r.path, subfolder: item.subfolder || null });
            emit('item-saved', { accountId: account.id, path: r.path, subfolder: item.subfolder || null });
          }
        } catch (e) {
          // Auch hier: wenn signal aborted, ist der Fehler vom Context-Close. Nicht als
          // Item-Fehler werten.
          if (signal?.aborted) throw new AbortError();
          results.errors++;
          log.error(`[${account.id}] Fehler bei "${item.filename}": ${e.message}`);
          emit('item-error', { accountId: account.id, filename: item.filename, error: e.message });
        }
      }
    } catch (e) {
      // Abort-Detect: explizite AbortError ODER signal aborted (Playwright-Ops werfen mit
      // diversen Messages wenn der Context unter ihnen weggeschlossen wird).
      if (e?.name === 'AbortError' || signal?.aborted) {
        results.aborted = true;
        log.warn(`[${account.id}] Abgebrochen (saved=${results.saved} skipped=${results.skipped} errors=${results.errors})`);
        emit('account-aborted', { accountId: account.id, saved: results.saved, skipped: results.skipped, errors: results.errors });
        return results;
      }
      throw e;
    } finally {
      if (signal) signal.removeEventListener?.('abort', onAbort);
      await closeOnce();
      // Cleanup: Beim Abort kann ein .part-File mitten im Schreiben liegen geblieben
      // sein. Beim naechsten Run wuerde saveDownload es zwar selbst ueberschreiben,
      // aber bei abgebrochenen Multi-Account-Runs sammeln sich sonst .part-Files
      // pro Adapter-Verzeichnis. Bester Effort, nicht-fatal.
      if (results.aborted && account.folder) {
        try {
          const layoutOpts = getLayout(cfg);
          const accRoot = layoutOpts?.yearSubfolder
            ? join(cfg.rootDir, account.folder)
            : join(cfg.rootDir, account.folder);
          if (existsSync(accRoot)) sweepPartFiles(accRoot);
        } catch { /* ignore */ }
      }
    }
    log.info(`[${account.id}] fertig — saved=${results.saved} skipped=${results.skipped} errors=${results.errors}`);
    emit('account-done', { accountId: account.id, saved: results.saved, skipped: results.skipped, errors: results.errors });
    return results;
  });
}

class AbortError extends Error {
  constructor() { super('Abort'); this.name = 'AbortError'; }
}

// Rekursives Aufraeumen aller .part-Dateien unter einem Account-Root nach Abort.
// Best-Effort: einzelne Locks/Permission-Probleme werden ignoriert.
function sweepPartFiles(dir) {
  let entries;
  try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
  for (const e of entries) {
    const full = join(dir, e.name);
    if (e.isDirectory()) sweepPartFiles(full);
    else if (e.name.endsWith('.part')) { try { unlinkSync(full); } catch { /* ignore */ } }
  }
}

export async function runAll(cfg, filter, emit = noopEmit, opts = {}) {
  // acquireSignal/releaseSignal: pro-Account-Lifecycle.
  // - acquireSignal(id) wird VOR dem Account-Start gerufen → frisches AbortSignal zurueck
  // - releaseSignal(id) wird NACH dem Account-Run gerufen (auch bei Error/Abort)
  // Ergebnis: listRunning() im Aufrufer zeigt nur tatsaechlich laufende Accounts,
  // abortAll trifft keine bereits-fertigen.
  const { acquireSignal, releaseSignal } = opts;
  // Explizit gefiltert (per id/source): deaktivierte trotzdem ausführen — bewusste Auswahl.
  // Ohne Filter ("Alle holen"): deaktivierte überspringen.
  const accounts = cfg.accounts.filter((a) => {
    if (filter) return filter.includes(a.id) || filter.includes(a.source);
    return !a.disabled;
  });
  if (accounts.length === 0) {
    log.warn('Keine passenden Konten gefunden.');
    emit('run-done', { accounts: [] });
    return [];
  }
  emit('run-start', { accountIds: accounts.map((a) => a.id) });
  const accountResults = [];
  for (const account of accounts) {
    const signal = acquireSignal ? acquireSignal(account.id) : undefined;
    try {
      const result = await runAccount(cfg, account, emit, { signal });
      accountResults.push({ account, savedFiles: result.savedFiles, aborted: result.aborted });
    } catch (e) {
      log.error(`[${account.id}] Abbruch: ${e.message}`);
      emit('account-error', { accountId: account.id, error: e.message });
      accountResults.push({ account, savedFiles: [], error: e.message });
    } finally {
      if (releaseSignal) releaseSignal(account.id);
    }
  }
  emit('run-done', { accounts: accountResults.map((a) => ({ accountId: a.account.id, savedCount: a.savedFiles.length })) });
  return accountResults;
}

export async function loginOnly(cfg, accountId, emit = noopEmit) {
  const account = cfg.accounts.find((a) => a.id === accountId);
  if (!account) throw new Error(`Konto "${accountId}" nicht gefunden.`);
  const AdapterClass = ADAPTERS[account.source];
  if (!AdapterClass) throw new Error(`Keine Implementierung für Quelle "${account.source}".`);
  const isApi = AdapterClass.isApi === true;
  log.info(`[${account.id}] Login-Only — ${isApi ? 'API-Auth-Verifikation' : 'Browser bleibt nach Login offen'}`);
  const adapter = new AdapterClass(account, { headful: true, cfg });
  try {
    await adapter.openContext();
    await adapter.login();
    if (isApi) {
      log.info(`[${account.id}] Auth verifiziert. (API-Adapter — kein Browser, kein Wait nötig.)`);
    } else {
      log.info(`[${account.id}] Login durch. Browser offen — tippe "exit" + Enter im Terminal zum Schließen.`);
      await new Promise((resolve) => {
        process.stdin.resume();
        process.stdin.setEncoding('utf8');
        const onData = (chunk) => {
          if (String(chunk).trim().toLowerCase() === 'exit') {
            process.stdin.removeListener('data', onData);
            resolve();
          }
        };
        process.stdin.on('data', onData);
      });
    }
  } finally {
    await adapter.close();
  }
}
