import './warnungen'; // muss vor dem Laden von node:sqlite stehen
import express from 'express';
import * as fs from 'fs';
import * as path from 'path';
import * as dns from 'node:dns';
import { EINSTUFUNGEN, PROGRAMMARTEN, type Einstufung, type Programmart, type QuelleTyp, type SoftwareEingabe } from 'bibliothek-types';
import {
  initialisiereDatenbank,
  alleSoftware,
  softwareNachId,
  softwareAnlegen,
  softwareAktualisieren,
  softwareLoeschen,
  abgleichSpeichern,
  zugriffProtokollieren,
  alleZugriffe,
  DATEIEN_ORDNER,
} from './datenbank';
import { gleicheSoftwareAb, gleicheAlleAb } from './abgleich';
import { erzeugeZip } from './zip';
import { pruefeSignatur } from './signatur';
import { loescheGeheimnisse, sichereGeheimnisOrdner, speichereGeheimnisse, uebersichtGeheimnisse } from './geheimnisse';
import { erstelleUmzugsPaket, uebernehmeUmzugsPaket } from './umzug';
import { istEinzeldatei, oberflaechenOrdner } from './pfade';
import { aktuelleEinstellungen, speichereEinstellungen, effektiverDatenOrdner, effektiverPort, konfigurationsDatei } from './konfiguration';

const PORT = effektiverPort();
const ABGLEICH_INTERVALL_MS = 24 * 60 * 60 * 1000; // täglicher Auto-Check

initialisiereDatenbank();
sichereGeheimnisOrdner();

const anwendung = express();
// Umzugs-Pakete können je nach Kataloggröße etwas umfangreicher sein.
anwendung.use(express.json({ limit: '25mb' }));

// --- Zugriffs-Herkunft ---

function anfrageIp(anfrage: express.Request): string {
  const roh = anfrage.ip ?? anfrage.socket.remoteAddress ?? '';
  return roh.replace(/^::ffff:/, ''); // IPv4-mapped IPv6 auf reine IPv4 normalisieren
}

function istLokalerZugriff(anfrage: express.Request): boolean {
  const ip = anfrageIp(anfrage);
  return ip === '127.0.0.1' || ip === '::1';
}

/** Middleware: erlaubt die Route nur bei direktem Zugriff am Server (localhost). */
function nurLokal(anfrage: express.Request, antwort: express.Response, next: express.NextFunction): void {
  if (!istLokalerZugriff(anfrage)) {
    antwort.status(403).json({ fehler: 'Diese Aktion ist nur direkt am Server möglich.' });
    return;
  }
  next();
}

// --- Umgebung & Status ---

anwendung.get('/api/umgebung', (anfrage, antwort) => {
  antwort.json({ lokal: istLokalerZugriff(anfrage) });
});

anwendung.get('/api/status', (_anfrage, antwort) => {
  antwort.json({ status: 'ok', dienst: 'software-bibliothek', softwareAnzahl: alleSoftware().length });
});

// --- Katalog lesen (auch aus dem Netzwerk erreichbar) ---

anwendung.get('/api/software', (_anfrage, antwort) => {
  antwort.json(alleSoftware());
});

anwendung.get('/api/software/:id', (anfrage, antwort) => {
  const eintrag = softwareNachId(Number(anfrage.params.id));
  if (!eintrag) {
    antwort.status(404).json({ fehler: 'Software nicht gefunden' });
    return;
  }
  antwort.json(eintrag);
});

// --- Datei-Download (auch aus dem Netzwerk, wird protokolliert) ---

anwendung.get('/api/software/:id/download', async (anfrage, antwort) => {
  const eintrag = softwareNachId(Number(anfrage.params.id));
  if (!eintrag || !eintrag.dateiname) {
    antwort.status(404).json({ fehler: 'Für diese Software liegt (noch) keine Datei vor.' });
    return;
  }
  const dateipfad = path.join(DATEIEN_ORDNER, String(eintrag.id), eintrag.dateiname);
  if (!fs.existsSync(dateipfad)) {
    antwort.status(404).json({ fehler: 'Die Datei fehlt auf dem Server.' });
    return;
  }
  await protokolliereDownload(anfrage, eintrag.name);
  antwort.download(dateipfad, eintrag.dateiname);
});

// --- Sammel-Download mehrerer Programme als ZIP (auch aus dem Netzwerk) ---

anwendung.get('/api/download-mehrere', async (anfrage, antwort) => {
  const ids = String(anfrage.query.ids ?? '')
    .split(',')
    .map((eintrag) => Number(eintrag.trim()))
    .filter((id) => Number.isInteger(id) && id > 0);
  if (ids.length === 0) {
    antwort.status(400).json({ fehler: 'Keine Auswahl übergeben' });
    return;
  }

  const eintraege = [];
  const namen: string[] = [];
  for (const id of ids) {
    const software = softwareNachId(id);
    if (!software?.dateiname) {
      continue;
    }
    const dateipfad = path.join(DATEIEN_ORDNER, String(id), software.dateiname);
    if (!fs.existsSync(dateipfad)) {
      continue;
    }
    // Je Programm ein eigener Ordner im Archiv — verhindert Namenskollisionen.
    eintraege.push({ dateiname: `${dateinameSaeubern(software.name)}/${software.dateiname}`, inhalt: fs.readFileSync(dateipfad) });
    namen.push(software.name);
  }
  if (eintraege.length === 0) {
    antwort.status(404).json({ fehler: 'Zu der Auswahl liegen keine Dateien vor.' });
    return;
  }

  await protokolliereDownload(anfrage, `${eintraege.length} Programme: ${namen.join(', ')}`);
  antwort.type('application/zip');
  antwort.setHeader('Content-Disposition', 'attachment; filename="software-auswahl.zip"');
  antwort.send(erzeugeZip(eintraege));
});

// --- Katalog verwalten (nur lokal am Server) ---

anwendung.post('/api/software', nurLokal, (anfrage, antwort) => {
  const eingabe = normalisiereEingabe(anfrage.body);
  if (!eingabe.name) {
    antwort.status(400).json({ fehler: 'Name ist erforderlich' });
    return;
  }
  const angelegt = softwareAnlegen(eingabe);
  void gleicheSoftwareAb(angelegt); // Datei im Hintergrund holen, Antwort nicht blockieren
  antwort.status(201).json(angelegt);
});

anwendung.put('/api/software/:id', nurLokal, (anfrage, antwort) => {
  const eingabe = normalisiereEingabe(anfrage.body);
  if (!eingabe.name) {
    antwort.status(400).json({ fehler: 'Name ist erforderlich' });
    return;
  }
  const aktualisiert = softwareAktualisieren(Number(anfrage.params.id), eingabe);
  if (!aktualisiert) {
    antwort.status(404).json({ fehler: 'Software nicht gefunden' });
    return;
  }
  void gleicheSoftwareAb(aktualisiert); // Quelle könnte geändert sein → neu holen
  antwort.json(aktualisiert);
});

anwendung.delete('/api/software/:id', nurLokal, (anfrage, antwort) => {
  const id = Number(anfrage.params.id);
  const geloescht = softwareLoeschen(id);
  if (geloescht) {
    loescheGeheimnisse(id);
  }
  antwort.status(geloescht ? 204 : 404).end();
});

// --- Bezugsquellen-Abgleich (nur lokal am Server) ---

anwendung.post('/api/software/:id/abgleichen', nurLokal, async (anfrage, antwort) => {
  const software = softwareNachId(Number(anfrage.params.id));
  if (!software) {
    antwort.status(404).json({ fehler: 'Software nicht gefunden' });
    return;
  }
  await gleicheSoftwareAb(software);
  antwort.json(softwareNachId(software.id));
});

anwendung.post('/api/abgleich/alle', nurLokal, async (_anfrage, antwort) => {
  await gleicheAlleAb(alleSoftware());
  antwort.json(alleSoftware());
});

// --- Datei von Hand hinterlegen (Quellentyp „manuell", nur lokal am Server) ---

anwendung.post('/api/software/:id/datei', nurLokal, (anfrage, antwort) => {
  const software = softwareNachId(Number(anfrage.params.id));
  if (!software) {
    antwort.status(404).json({ fehler: 'Software nicht gefunden' });
    return;
  }
  // basename schützt davor, über den Dateinamen aus dem Ablageordner auszubrechen.
  const dateiname = path.basename(String(anfrage.query.dateiname ?? '')).trim();
  if (!dateiname) {
    antwort.status(400).json({ fehler: 'Dateiname fehlt' });
    return;
  }
  const zielordner = path.join(DATEIEN_ORDNER, String(software.id));
  fs.rmSync(zielordner, { recursive: true, force: true }); // nur eine aktuelle Datei je Software
  fs.mkdirSync(zielordner, { recursive: true });

  const zielpfad = path.join(zielordner, dateiname);
  const zielDatei = fs.createWriteStream(zielpfad);
  anfrage.pipe(zielDatei);
  zielDatei.on('finish', () => {
    void (async () => {
      const befund = await pruefeSignatur(zielpfad);
      abgleichSpeichern(software.id, {
        version: software.version,
        dateiname,
        erfolg: true,
        meldung: 'Manuell hinterlegt.',
        geaendert: true,
        signaturStatus: befund.status,
        signaturHerausgeber: befund.herausgeber,
        veroeffentlichtAm: software.veroeffentlichtAm,
        benoetigteGeheimnisse: software.benoetigteGeheimnisse,
      });
      antwort.json(softwareNachId(software.id));
    })();
  });
  zielDatei.on('error', (fehler) => {
    antwort.status(500).json({ fehler: `Datei konnte nicht gespeichert werden: ${fehler.message}` });
  });
});

// --- Bau-Geheimnisse (nur lokal am Server; Werte verlassen den Server nie) ---

anwendung.get('/api/software/:id/geheimnisse', nurLokal, (anfrage, antwort) => {
  antwort.json(uebersichtGeheimnisse(Number(anfrage.params.id)));
});

anwendung.put('/api/software/:id/geheimnisse', nurLokal, (anfrage, antwort) => {
  const eingehende = Array.isArray(anfrage.body) ? anfrage.body : [];
  antwort.json(speichereGeheimnisse(Number(anfrage.params.id), eingehende));
});

// --- Umzug auf einen anderen Rechner (nur lokal am Server) ---

anwendung.get('/api/export', nurLokal, (anfrage, antwort) => {
  const mitGeheimnissen = anfrage.query.geheimnisse === 'ja';
  const paket = erstelleUmzugsPaket(mitGeheimnissen);
  const namenszusatz = mitGeheimnissen ? '-mit-schluesseln' : '';
  antwort.setHeader('Content-Disposition', `attachment; filename="software-bibliothek${namenszusatz}.json"`);
  antwort.type('application/json').send(JSON.stringify(paket, null, 2));
});

anwendung.post('/api/import', nurLokal, (anfrage, antwort) => {
  try {
    antwort.json(uebernehmeUmzugsPaket(anfrage.body));
  } catch (fehler) {
    antwort.status(400).json({ fehler: fehler instanceof Error ? fehler.message : 'Paket konnte nicht eingelesen werden.' });
  }
});

// --- Einstellungen (nur lokal am Server) ---

anwendung.get('/api/einstellungen', nurLokal, (_anfrage, antwort) => {
  antwort.json({
    einstellungen: aktuelleEinstellungen(),
    konfigurationsDatei: konfigurationsDatei(),
    effektiverDatenOrdner: effektiverDatenOrdner(),
    laufenderPort: PORT,
  });
});

anwendung.put('/api/einstellungen', nurLokal, (anfrage, antwort) => {
  const gespeicherte = speichereEinstellungen(anfrage.body ?? {});
  antwort.json({
    einstellungen: gespeicherte,
    konfigurationsDatei: konfigurationsDatei(),
    // Nach dem Speichern gilt der neue Ordner erst beim nächsten Start —
    // die Datenbank ist bereits geöffnet.
    effektiverDatenOrdner: effektiverDatenOrdner(),
    laufenderPort: PORT,
  });
});

// --- Zugriffsprotokoll (nur lokal am Server) ---

anwendung.get('/api/zugriffslog', nurLokal, (_anfrage, antwort) => {
  antwort.json(alleZugriffe());
});

// --- Hilfsfunktionen ---

const ERLAUBTE_QUELLEN: Array<QuelleTyp | ''> = ['', 'direkt', 'github', 'winget', 'svn', 'git', 'manuell'];

/** Säubert die Eingabe und stellt einen gültigen Quell-Typ sicher. */
function normalisiereEingabe(koerper: Partial<SoftwareEingabe>): SoftwareEingabe {
  const typ = ERLAUBTE_QUELLEN.includes((koerper.quelleTyp ?? '') as QuelleTyp) ? (koerper.quelleTyp ?? '') : '';
  return {
    name: (koerper.name ?? '').trim(),
    beschreibung: (koerper.beschreibung ?? '').trim(),
    elternId: Number(koerper.elternId ?? 0) || 0,
    quelleTyp: typ as QuelleTyp | '',
    quelleWert: (koerper.quelleWert ?? '').trim(),
    bauBefehl: (koerper.bauBefehl ?? '').trim(),
    bauErgebnis: (koerper.bauErgebnis ?? '').trim(),
    einstufung: EINSTUFUNGEN.some((eintrag) => eintrag.id === koerper.einstufung) ? (koerper.einstufung as Einstufung) : 'lizenzfrei',
    programmart: PROGRAMMARTEN.some((eintrag) => eintrag.id === koerper.programmart) ? (koerper.programmart as Programmart) : 'unbestimmt',
    hinweis: (koerper.hinweis ?? '').trim(),
    erwarteterHerausgeber: (koerper.erwarteterHerausgeber ?? '').trim(),
    anleitungUrl: (koerper.anleitungUrl ?? '').trim(),
  };
}

/** Ersetzt Zeichen, die in Datei-/Ordnernamen nicht zulässig sind. */
function dateinameSaeubern(name: string): string {
  return name.replace(/[\\/:*?"<>|]/g, '_').trim() || 'Programm';
}

/** Schreibt einen Download in das Zugriffsprotokoll (IP, Reverse-DNS-Rechnername, angegebener Name). */
async function protokolliereDownload(anfrage: express.Request, softwareName: string): Promise<void> {
  const ip = anfrageIp(anfrage);
  let rechnername = '';
  try {
    const namen = await dns.promises.reverse(ip);
    rechnername = namen[0] ?? '';
  } catch {
    // Kein Reverse-DNS-Eintrag verfügbar – Rechnername bleibt leer.
  }
  const angegebenerName = typeof anfrage.query.name === 'string' ? anfrage.query.name : '';
  zugriffProtokollieren({
    zeitpunkt: new Date().toISOString(),
    ipAdresse: ip,
    rechnername,
    angegebenerName,
    aktion: `Heruntergeladen: ${softwareName}`,
  });
}

/** Liest ein in die exe eingebettetes Asset als Puffer (undefined, falls nicht vorhanden). */
function eingebettetesAsset(name: string): Buffer | undefined {
  try {
    const sea = require('node:sea') as { getRawAsset(schluessel: string): ArrayBuffer };
    return Buffer.from(sea.getRawAsset(name));
  } catch {
    return undefined; // Asset nicht eingebettet
  }
}

/**
 * Liefert die gebaute Oberfläche aus: als exe aus den eingebetteten Assets,
 * sonst aus dem dist-Ordner des Client-Pakets. Muss nach den API-Routen stehen.
 */
function richteOberflaecheEin(): void {
  if (istEinzeldatei()) {
    anwendung.use((anfrage, antwort, next) => {
      if (anfrage.path.startsWith('/api/')) {
        next();
        return;
      }
      const gewuenscht = anfrage.path === '/' ? 'index.html' : anfrage.path.replace(/^\//, '');
      const inhalt = eingebettetesAsset(gewuenscht);
      if (inhalt) {
        antwort.type(path.extname(gewuenscht) || '.html').send(inhalt);
        return;
      }
      const startseite = eingebettetesAsset('index.html');
      if (!startseite) {
        next();
        return;
      }
      antwort.type('.html').send(startseite);
    });
    return;
  }
  if (fs.existsSync(oberflaechenOrdner())) {
    anwendung.use(express.static(oberflaechenOrdner()));
  }
}

richteOberflaecheEin();

// Täglicher Hintergrund-Abgleich aller Quellen.
setInterval(() => {
  void gleicheAlleAb(alleSoftware());
}, ABGLEICH_INTERVALL_MS);

anwendung.listen(PORT, () => {
  console.log(`Software-Bibliothek läuft auf http://localhost:${PORT}`);
  console.log(`Daten:         ${effektiverDatenOrdner()}`);
  console.log(`Einstellungen: ${konfigurationsDatei()}`);
});
