/**
 * Zentrale Härtung der Electron-Laufzeit.
 *
 * Alles hier ist bewusst „standardmäßig verboten“: Berechtigungen werden
 * abgelehnt, Navigation nach außen wird unterbunden, neue Fenster werden nicht
 * geöffnet, sondern an den Standardbrowser des Systems übergeben.
 */

import { app, shell, session, type Session } from 'electron';

import { cspRichtlinie, type CspModus } from '../shared/csp';
import { UNTERSTUETZUNG_URL, unterstuetzungsziel } from '../shared/unterstuetzung';
import { istGleicheDatei, oberflaechenUrl } from './oberflaeche';

/** Herkünfte, zu denen der Renderer navigieren darf. */
function erlaubteHerkuenfte(): readonly string[] {
  const dev = process.env['ELECTRON_RENDERER_URL'];
  return dev ? [new URL(dev).origin] : [];
}

/**
 * Darf der Renderer dorthin navigieren?
 *
 * Bei `file:` genügt das Protokoll ausdrücklich **nicht**. Sonst käme jede
 * Datei auf der Platte in Frage – etwa eine heruntergeladene HTML-Datei. Das
 * Preload-Skript hängt am `webContents` und würde auch für die neue Seite
 * wieder eingespielt: Die fremde Seite hätte die volle Bridge in der Hand.
 * Verglichen wird deshalb der aufgelöste Pfad gegen die eine Datei, die
 * tatsächlich geladen wird.
 *
 * Hash und Query bleiben dabei außer Betracht – Sprungziele innerhalb der
 * eigenen Seite sind unbedenklich.
 */
export function istErlaubteNavigation(ziel: string): boolean {
  let url: URL;
  try {
    url = new URL(ziel);
  } catch {
    return false;
  }

  if (url.protocol === 'file:') {
    return istGleicheDatei(ziel, oberflaechenUrl());
  }

  return erlaubteHerkuenfte().includes(url.origin);
}

/**
 * Setzt die Content-Security-Policy als HTTP-Kopf.
 *
 * Nicht nur für den Dev-Server, wie hier früher stand: `onHeadersReceived`
 * greift auch für `file://`-Antworten – nachgemessen am gebauten Programm,
 * die Rückrufe kamen für index.html, das JS-Bündel und das Stilblatt. Damit
 * ist dieser Weg auch im gepackten Zustand der einzige, über den
 * `frame-ancestors` überhaupt wirken kann; im `<meta>`-Element ignoriert
 * Chromium die Direktive (siehe `src/shared/csp.ts`).
 *
 * Beide Richtlinien gelten nebeneinander, ausgewertet wird die Schnittmenge.
 */
function cspHeaderSetzen(zielSession: Session, modus: CspModus): void {
  const richtlinie = cspRichtlinie(modus, 'kopf');

  zielSession.webRequest.onHeadersReceived((details, callback) => {
    // Vorhandene CSP-Header werden nicht ergänzt, sondern ersetzt: mehrere
    // Richtlinien würden als Schnittmenge ausgewertet und im Dev-Modus
    // unbemerkt zu Fehlern führen.
    const header = Object.fromEntries(
      Object.entries(details.responseHeaders ?? {}).filter(
        ([schluessel]) => schluessel.toLowerCase() !== 'content-security-policy',
      ),
    );
    header['Content-Security-Policy'] = [richtlinie];

    callback({ responseHeaders: header });
  });
}

/** Lehnt sämtliche Geräte- und Medienberechtigungen ab. */
function berechtigungenSperren(zielSession: Session): void {
  zielSession.setPermissionRequestHandler((_webContents, berechtigung, callback) => {
    console.warn(`[sicherheit] Berechtigung abgelehnt: ${berechtigung}`);
    callback(false);
  });

  zielSession.setPermissionCheckHandler(() => false);

  zielSession.setDevicePermissionHandler(() => false);
}

/** Verhindert Navigation nach außen und das Öffnen fremder Fenster. */
function navigationSperren(): void {
  app.on('web-contents-created', (_ereignis, inhalte) => {
    inhalte.setWindowOpenHandler(({ url }) => {
      // Externe Links bewusst im Systembrowser öffnen, nie in einem
      // Electron-Fenster (das hätte Zugriff auf unsere Preload-Bridge).
      if (url.startsWith('https://')) {
        void shell.openExternal(url);
      }
      return { action: 'deny' };
    });

    inhalte.on('will-navigate', (ereignis, url) => {
      if (!istErlaubteNavigation(url)) {
        ereignis.preventDefault();
        console.warn(`[sicherheit] Navigation blockiert: ${url}`);
      }
    });

    /* `will-navigate` deckt serverseitige Weiterleitungen nicht ab. Praktisch
       nur im Entwicklungsbetrieb erreichbar – die Sperre kostet nichts. */
    inhalte.on('will-redirect', (ereignis, url) => {
      if (!istErlaubteNavigation(url)) {
        ereignis.preventDefault();
        console.warn(`[sicherheit] Weiterleitung blockiert: ${url}`);
      }
    });

    inhalte.on('will-attach-webview', (ereignis) => {
      // <webview> wird nicht verwendet und bleibt deshalb komplett gesperrt.
      ereignis.preventDefault();
    });
  });
}

/**
 * Öffnet die Unterstützungsseite im Standardbrowser – und sonst nichts.
 *
 * ## Warum das hier steht
 *
 * Diese Datei ist die Stelle, an der geregelt ist, was nach außen darf. Bis
 * jetzt stand hier nur ein Verbot: {@link navigationSperren} übergibt neue
 * Fenster an den Systembrowser, statt sie in Electron zu öffnen. Der
 * Spendenknopf braucht einen Weg hinaus, der ausdrücklich angestoßen wird –
 * er gehört deshalb daneben und nicht in ein Fachmodul, wo ihn beim nächsten
 * Sicherheitsdurchgang niemand suchen würde.
 *
 * ## Warum die Adresse trotzdem geprüft wird
 *
 * Der Renderer schickt die Adresse mit, damit prüfbar bleibt, dass die
 * Oberfläche genau das anfordert, was sie anzeigt. Geöffnet wird darum aber
 * nicht, was ankommt, sondern ausschließlich die eine eingetragene Adresse:
 * `shell.openExternal` reicht an das Betriebssystem weiter, und was dort
 * ankommt, ist nicht mehr einzufangen. Käme fremder Code doch einmal in den
 * Renderer, wäre dies sonst der bequemste Ausgang, den er finden könnte.
 *
 * Ist keine Adresse eingetragen, öffnet dieser Weg nichts, und die Oberfläche
 * zeigt das Angebot dann ohnehin nicht an. Das war bis 0.21.1 der Zustand;
 * seit 0.22.0 führt `shared/unterstuetzung.ts` eine Adresse, und der
 * Kommentar behauptete bis 0.24.2 weiter das Gegenteil – ein Satz, der beim
 * Lesen des Bausteins die Lage falsch darstellte. Die Zusage darunter bleibt
 * dieselbe: Geöffnet wird ausschließlich die eine eingetragene Adresse.
 *
 * @returns `true`, wenn der Browser angestoßen wurde.
 */
export async function unterstuetzungOeffnen(gewuenscht: unknown): Promise<boolean> {
  const ziel = unterstuetzungsziel(UNTERSTUETZUNG_URL);
  if (ziel === null) {
    console.warn('[sicherheit] Unterstützungsseite nicht geöffnet: keine Adresse eingetragen.');
    return false;
  }

  /* Verglichen wird die Zeichenkette, nicht ein wieder zusammengesetzter
     URL-Wert: Jede Umformung dazwischen wäre eine Stelle, an der zwei
     Schreibweisen als gleich durchgingen. Die gewünschte Adresse wird
     bewusst nicht mitprotokolliert – ins Protokoll gehört, dass etwas
     abgewiesen wurde, nicht der fremde Text selbst. */
  if (gewuenscht !== ziel) {
    console.warn('[sicherheit] Außenverweis abgewiesen: nicht die eingetragene Adresse.');
    return false;
  }

  try {
    await shell.openExternal(ziel);
    return true;
  } catch (fehler) {
    console.warn(`[sicherheit] Der Standardbrowser ließ sich nicht öffnen: ${String(fehler)}`);
    return false;
  }
}

/**
 * Muss NACH `app.whenReady()` aufgerufen werden (benötigt die Default-Session).
 */
export function sicherheitEinrichten(modus: CspModus): void {
  const standardSession = session.defaultSession;

  cspHeaderSetzen(standardSession, modus);
  berechtigungenSperren(standardSession);
  navigationSperren();
}
