import * as fs from 'fs';
import * as path from 'path';

/** Alles, was eine Bezugsquelle für einen Abgleich braucht. */
export interface Abgleichauftrag {
  /** Quell-spezifische Angabe: URL, `besitzer/repo`, Paket-ID, Repo-URL … */
  wert: string;
  /** Bisher bekannte Versions-/Stand-Kennung. */
  aktuelleVersion: string;
  /** Ordner, in dem die bereitgestellte Datei liegen soll. */
  zielordner: string;
  /** Arbeitskopie für Quellen, die etwas auschecken/bauen müssen. */
  arbeitsordner: string;
  /** Befehl zum Bauen (nur für die Git-Quelle). */
  bauBefehl: string;
  /** Pfad des Bau-Ergebnisses relativ zur Arbeitskopie (nur für die Git-Quelle). */
  bauErgebnis: string;
  /** Größe der bisher bereitgestellten Datei — für die Plausibilitätsprüfung. */
  vorherigeGroesse: number;
  /** Hinterlegte Bau-Geheimnisse, die dem Bau als Umgebungsvariablen mitgegeben werden. */
  geheimnisse: Record<string, string>;
  /** SSH-Aufruf für Git-Zugriffe aus den Einstellungen (leer = SSH-Konfiguration entscheidet). */
  gitSshBefehl: string;
}

/** Ein Geheimnis, das ein Repository für seinen Bau anfordert. */
export interface GeheimnisBedarf {
  name: string;
  beschreibung: string;
}

/** Ergebnis eines Quellen-Abgleichs. */
export interface QuellenAbgleich {
  erfolg: boolean;
  /** Wurde eine neue Datei geholt (true) oder war schon alles aktuell (false)? */
  geaendert: boolean;
  /** Neue Versions-/Stand-Kennung. */
  version: string;
  /** Name der aktuell vorliegenden Datei. */
  dateiname: string;
  meldung: string;
  /** Veröffentlichungsdatum beim Anbieter (ISO), falls die Quelle es kennt. */
  veroeffentlichtAm?: string;
  /** Vom Repository angeforderte Bau-Geheimnisse (nur die Git-Quelle liefert das). */
  benoetigteGeheimnisse?: GeheimnisBedarf[];
}

/**
 * Eine Bezugsquelle prüft ihre Quelle und stellt bei Bedarf die aktuelle Datei bereit.
 * Jede neue Quellenart implementiert nur dieses Interface.
 */
export interface Bezugsquelle {
  gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich>;
}

export function abgleichMisserfolg(version: string, meldung: string): QuellenAbgleich {
  return { erfolg: false, geaendert: false, version, dateiname: '', meldung };
}

/** Wandelt eine Datumsangabe der Quelle in ISO um (leer, wenn unbrauchbar). */
export function alsIsoDatum(rohwert: string | null | undefined): string {
  if (!rohwert) {
    return '';
  }
  const zeitpunkt = new Date(rohwert);
  return Number.isNaN(zeitpunkt.getTime()) ? '' : zeitpunkt.toISOString();
}

// --- Plausibilitätsprüfung ---

/** Unterhalb dieser Größe gilt eine Datei als verdächtig, wenn sie zudem stark geschrumpft ist. */
const VERDAECHTIG_KLEIN_BYTES = 100 * 1024;

/**
 * Prüft eine frisch geholte Datei auf typische Anzeichen eines toten oder
 * übernommenen Links (Fehler-/Anmeldeseite statt Datei, leere oder plötzlich
 * winzige Datei). Liefert einen Beanstandungstext oder `undefined`, wenn alles passt.
 */
export function inhaltBeanstanden(inhalt: Buffer, dateiname: string, inhaltstyp: string | null, vorherigeGroesse: number): string | undefined {
  if (inhalt.length === 0) {
    return 'Die Quelle hat eine leere Datei geliefert.';
  }

  const istWebseiteErwartet = /\.(html?|htm)$/i.test(dateiname);
  if (!istWebseiteErwartet) {
    const anfang = inhalt.subarray(0, 200).toString('latin1').trimStart().toLowerCase();
    if (anfang.startsWith('<!doctype html') || anfang.startsWith('<html')) {
      return 'Die Quelle hat eine Webseite statt der Datei geliefert — der Link ist vermutlich veraltet oder verlangt eine Anmeldung.';
    }
    if (inhaltstyp && inhaltstyp.toLowerCase().includes('text/html')) {
      return `Die Quelle hat eine Webseite geliefert (${inhaltstyp}) — der Link ist vermutlich veraltet.`;
    }
  }

  // Ein plötzlicher Einbruch auf eine winzige Datei deutet auf eine Fehlerseite hin.
  if (vorherigeGroesse > 0 && inhalt.length < vorherigeGroesse / 2 && inhalt.length < VERDAECHTIG_KLEIN_BYTES) {
    return `Die geholte Datei ist auffällig klein (${inhalt.length} statt bisher ${vorherigeGroesse} Bytes) — bitte die Quelle prüfen.`;
  }

  return undefined;
}

// --- Dateiablage: erst vollständig vorbereiten, dann ersetzen ---

function temporaerenOrdnerAnlegen(zielordner: string): string {
  const temporaer = `${zielordner}.neu`;
  fs.rmSync(temporaer, { recursive: true, force: true });
  fs.mkdirSync(temporaer, { recursive: true });
  return temporaer;
}

/**
 * Ersetzt den Inhalt des Zielordners durch den des temporären Ordners.
 * Der alte Stand wird erst angefasst, wenn der neue vollständig und geprüft ist —
 * ein fehlgeschlagener Abgleich kann die funktionierende Datei damit nicht zerstören.
 */
function uebernehmeOrdner(temporaer: string, zielordner: string): void {
  fs.rmSync(zielordner, { recursive: true, force: true });
  fs.renameSync(temporaer, zielordner);
}

export function verwerfeOrdner(temporaer: string): void {
  fs.rmSync(temporaer, { recursive: true, force: true });
}

/** Legt eine Datei als neuen, alleinigen Inhalt des Zielordners ab (alter Stand bleibt bis zuletzt erhalten). */
export function ersetzeDateiSicher(zielordner: string, dateiname: string, inhalt: Buffer): void {
  const temporaer = temporaerenOrdnerAnlegen(zielordner);
  fs.writeFileSync(path.join(temporaer, dateiname), inhalt);
  uebernehmeOrdner(temporaer, zielordner);
}

/**
 * Für Quellen, die über ein Kommandozeilen-Werkzeug in einen Ordner schreiben:
 * liefert einen leeren temporären Ordner und übernimmt ihn erst nach erfolgreicher Prüfung.
 */
export function mitTemporaeremOrdner(zielordner: string): { ordner: string; uebernehmen: () => void; verwerfen: () => void } {
  const temporaer = temporaerenOrdnerAnlegen(zielordner);
  return {
    ordner: temporaer,
    uebernehmen: () => uebernehmeOrdner(temporaer, zielordner),
    verwerfen: () => verwerfeOrdner(temporaer),
  };
}

/** Prüft, ob im Zielordner bereits eine bestimmte Datei liegt. */
export function dateiVorhanden(zielordner: string, dateiname: string): boolean {
  return !!dateiname && fs.existsSync(path.join(zielordner, dateiname));
}

/** Leitet einen Dateinamen aus einer URL ab (letztes Pfadsegment ohne Query). */
export function dateinameAusUrl(url: string): string {
  try {
    const segment = decodeURIComponent(new URL(url).pathname.split('/').pop() ?? '');
    return segment || 'download.bin';
  } catch {
    return 'download.bin';
  }
}
