import { useState, useEffect, useRef } from 'react'
import { Save, Disc, Cpu, Database, Globe, AlertCircle, CheckCircle, HardDrive, Wrench, Send, Tv, KeyRound, Upload, Trash2, Download } from 'lucide-react'
import { api } from '../lib/api'
import { useToast } from '../context/ToastContext'
import StorageMounts from '../components/StorageMounts'
import WorkerVerwaltung from '../components/WorkerVerwaltung'
import { PageHeader } from '../components/ui/PageHeader'
import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card'
import { Button } from '../components/ui/Button'
import { Input, Select, Toggle } from '../components/ui/Input'
import { MEDIA_SERVER_OPTIONEN } from '../lib/mediaServer'
import { ENCODER_BADGES } from '../lib/design'
import { PRESET_KEINE, schwacheEncoderCpu, simdWarnung } from '../lib/encoder'

interface SettingsState {
  tmdbApiKey: string
  tvdbApiKey: string
  omdbApiKey: string
  outputDir: string
  movieDir: string
  seriesDir: string
  musicDir: string
  mainFeatureOnly: boolean
  autoEject: boolean
  autoRipStart: boolean
  notificationWebhook: string
  transcodeEnabled: boolean
  // transcodePreset bleibt als Rückfall für Bestandsinstallationen bestehen —
  // solange die drei Felder darunter nicht gespeichert sind, gilt weiter dieser
  // Wert (siehe preset_fuer() in docker/worker/ripping.py).
  transcodePreset: string
  transcodePresetDvd: string
  transcodePresetBluray: string
  transcodePresetUhd: string
  keepOriginal: boolean
  mediaServer: string
  jellyfinUrl: string
  jellyfinApiKey: string
  workDir: string
  makemkvAppKey: string
  // Wunschsprachen (ISO-639-2, Komma-Liste). Greifen bei der Kompression und
  // sind im Rip-Dialog vorausgewählt. Leer = alle Sprachen behalten.
  audioSprachen: string
  untertitelSprachen: string
}

const defaultSettings: SettingsState = {
  tmdbApiKey: '',
  tvdbApiKey: '',
  omdbApiKey: '',
  outputDir: '/app/media',
  movieDir: 'movies',
  seriesDir: 'series',
  musicDir: 'music',
  mainFeatureOnly: false,
  autoEject: true,
  autoRipStart: false,
  notificationWebhook: '',
  transcodeEnabled: true,
  transcodePreset: 'H.265 MKV 1080p30',
  transcodePresetDvd: 'H.265 MKV 576p25',
  transcodePresetBluray: 'H.265 MKV 1080p30',
  transcodePresetUhd: 'H.265 MKV 2160p60 4K',
  keepOriginal: false,
  mediaServer: 'none',
  jellyfinUrl: '',
  jellyfinApiKey: '',
  workDir: '',
  makemkvAppKey: '',
  // Leer = alles behalten. Bewusst kein Vorgabewert: Eine stille Vorauswahl
  // („deu") würde bei einem japanischen Original die Originaltonspur wegwerfen,
  // ohne dass jemand danach gefragt hat.
  audioSprachen: '',
  untertitelSprachen: '',
}

type SettingsTab = 'ripping' | 'verarbeitung' | 'worker' | 'speicherziele' | 'apis' | 'benachrichtigungen' | 'system'

interface WorkerInfo {
  name: string
  encoders: string[]
  // keydb: "ja" | "nein" | "unbekannt" — sagt, ob DIESER Worker eine KEYDB.cfg
  // in seinem MakeMKV-Datenverzeichnis sieht. Nur der Worker, der wirklich
  // rippt, zählt, deshalb steht die Angabe pro Worker und nicht global.
  // cpu_kerne/cpu_simd: Rechenleistung fürs Software-Encodieren. Ohne diese
  // Angaben war am 25.07.2026 nicht zu sehen, dass ein 4K-Encode auf der
  // Rippy-VM Tage statt Stunden braucht (kein AVX2, 4 Kerne).
  // handbrake_encoder: die ungefilterte Encoder-Auskunft von HandBrake selbst.
  // extern: "ja" = läuft AUSSERHALB des Rippy-Containers, sieht /app/... also
  // nur über eine Freigabe plus pfad_map. Der Worker meldet das selbst.
  info?: {
    makemkv?: string, handbrake?: string, makemkv_key?: string, keydb?: string,
    cpu_modell?: string, cpu_kerne?: string, cpu_simd?: string,
    handbrake_encoder?: string, extern?: string, pfad_map?: string,
  }
}

// Antwort von GET/POST/DELETE /system/keydb (Feldnamen exakt wie die API sie liefert).
interface KeydbStatus {
  vorhanden: boolean
  pfad: string
  groesse_bytes: number
  eintraege: number
  geaendert: string
}

// Ein Ablageziel aus GET /storage-targets (Feldnamen exakt wie die API liefert).
interface StorageZiel {
  name: string
  path: string
  is_mount: boolean
  free_gb: number | null
}

// Antwort von GET/POST /system/keystore — MakeMKVs eigener Schlüsselvorrat.
interface KeystoreStatus {
  vorhanden: boolean
  pfad: string
  groesse_bytes: number
  schluessel: number
  geaendert: string
}

// Ein AACS-Dump aus GET /system/aacs-dumps.
interface AacsDump {
  name: string
  groesse_bytes: number
  geaendert: string
}

interface SystemInfo {
  api_version: string
  plaetze: { name: string, frei_gb: number, gesamt_gb: number }[]
  workers: WorkerInfo[]
  makemkv_key_ui: boolean
  webhook_gesetzt: boolean
}

// Antwort von GET /presets — die Preset-Namen kommen aus
// `HandBrakeCLI --preset-list` des jeweiligen Workers, die Empfehlung samt
// Begründung aus api/presets.py (dort getestet). Bis 26.07.2026 standen die
// Namen fest verdrahtet in dieser Datei; sie unterscheiden sich aber zwischen
// HandBrake-Versionen, und ein Name, den das HandBrake des Workers nicht kennt,
// lässt die Kompression scheitern.
interface PresetEmpfehlung {
  preset: string
  grund: string
  gefunden: boolean
}

interface PresetTyp {
  empfehlung: PresetEmpfehlung
  auswahl: { name: string, verkleinert: boolean }[]
}

interface PresetUebersicht {
  quelle: 'worker' | 'rueckfall'
  hardware: string[]
  simd_schnell: boolean
  typen: Record<'uhd' | 'bluray' | 'dvd', PresetTyp>
}

// Welches Einstellungs-Feld gehört zu welchem Disc-Typ.
const PRESET_FELD: Record<'uhd' | 'bluray' | 'dvd', keyof SettingsState> = {
  uhd: 'transcodePresetUhd',
  bluray: 'transcodePresetBluray',
  dvd: 'transcodePresetDvd',
}

// Byte-Zahl menschenlesbar — eine echte KEYDB.cfg ist mehrere MB groß.
function bytesLesbar(bytes: number): string {
  if (!bytes) return '0 B'
  if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
  if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`
  return `${bytes} B`
}

// Die API liefert ISO-8601 in UTC (oder einen leeren String). Ohne Zonen-Kennung
// würde der Browser den Zeitstempel als Ortszeit lesen und die Uhrzeit um den
// Zonen-Versatz verschieben — deshalb notfalls ein Z anhaengen.
function zeitLesbar(iso: string): string {
  if (!iso) return 'unbekannt'
  const mitZone = /[Zz]$|[+-]\d{2}:?\d{2}$/.test(iso) ? iso : `${iso}Z`
  const d = new Date(mitZone)
  return isNaN(d.getTime()) ? 'unbekannt' : d.toLocaleString('de-DE')
}

export default function SettingsPage() {
  const [settings, setSettings] = useState<SettingsState>(defaultSettings)
  const [activeTab, setActiveTab] = useState<SettingsTab>('ripping')
  const [workers, setWorkers] = useState<WorkerInfo[]>([])
  const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null)
  const [loading, setLoading] = useState(true)
  const [saving, setSaving] = useState(false)
  const [saved, setSaved] = useState(false)
  const [testBusy, setTestBusy] = useState(false)
  const [quellenStatus, setQuellenStatus] = useState<Record<string, string> | null>(null)
  const [quellenBusy, setQuellenBusy] = useState(false)
  const [updates, setUpdates] = useState<Record<string, { installiert?: string, verfuegbar?: string, update?: boolean }> | null>(null)
  const [updatesBusy, setUpdatesBusy] = useState(false)
  // KEYDB.cfg und AACS-Dumps sind DATEIEN im MakeMKV-Datenverzeichnis, keine
  // Einstellungen — deshalb bewusst NICHT in SettingsState: der globale
  // Speichern-Knopf postet dieses Objekt komplett und würde sie mitschleifen.
  const [keydb, setKeydb] = useState<KeydbStatus | null>(null)
  const [keydbBusy, setKeydbBusy] = useState(false)
  const [dumps, setDumps] = useState<AacsDump[]>([])
  const keydbInput = useRef<HTMLInputElement>(null)
  // Ablageziele unter /app/media inkl. eingehängter Freigaben — speist die
  // Auswahl des Arbeitsverzeichnisses (vorher musste man den Pfad tippen).
  const [ziele, setZiele] = useState<StorageZiel[]>([])
  const [keystore, setKeystore] = useState<KeystoreStatus | null>(null)
  const [keystoreBusy, setKeystoreBusy] = useState(false)
  const keystoreInput = useRef<HTMLInputElement>(null)
  // Preset-Namen + Empfehlung, beides von der API (siehe PresetUebersicht).
  const [presetInfo, setPresetInfo] = useState<PresetUebersicht | null>(null)

  const { toast } = useToast()

  const updatesPruefen = async () => {
    setUpdatesBusy(true)
    try {
      const r = await api.get('/system/updates')
      setUpdates(r.data)
      if (r.data?.makemkv?.update) {
        toast('info', `MakeMKV ${r.data.makemkv.verfuegbar} ist verfügbar (installiert: ${r.data.makemkv.installiert})`)
      } else {
        toast('success', 'Update-Check abgeschlossen')
      }
    } catch {
      toast('error', 'Update-Check fehlgeschlagen (makemkv.com/GitHub nicht erreichbar?)')
    } finally {
      setUpdatesBusy(false)
    }
  }

  // Status der KEYDB.cfg + Liste der AACS-Dumps frisch holen. Wird beim Laden
  // der Seite und nach jeder Aktion aufgerufen — die API ist die Wahrheit,
  // nicht das, was wir gerade hochgeschickt haben.
  const keydbLaden = () => {
    api.get('/system/keydb').then(r => setKeydb(r.data)).catch(() => setKeydb(null))
    api.get('/system/keystore').then(r => setKeystore(r.data)).catch(() => setKeystore(null))
    api.get('/system/aacs-dumps').then(r => setDumps(r.data?.dumps || [])).catch(() => setDumps([]))
    // Die Worker-Plaketten oben kommen aus /capabilities und stammen aus dem
    // letzten Herzschlag des Workers (minuetlich) — ohne dieses Nachladen
    // widerspraeche die Kachel nach einem Upload sichtbar der Erfolgsmeldung.
    // Ganz sofort ist sie trotzdem nicht: der Worker meldet erst beim
    // nächsten Herzschlag neu. Genau so steht es auch im Erklaertext.
    api.get('/capabilities').then(r => setWorkers(r.data.workers || [])).catch(() => setWorkers([]))
  }

  const keydbHochladen = async (datei: File) => {
    setKeydbBusy(true)
    try {
      // Bewusst als JSON-Text und nicht als Multipart-Upload: der API fehlt
      // python-multipart, ein File()-Endpunkt würde sie beim Import killen.
      const inhalt = await datei.text()
      await api.post('/system/keydb', { inhalt })
      keydbLaden()
      toast('success', 'KEYDB.cfg gespeichert — sie wirkt ab dem nächsten Rip.')
    } catch (e: any) {
      toast('error', e?.response?.data?.detail || 'Hochladen fehlgeschlagen — ist das wirklich eine KEYDB.cfg, und läuft die API?')
    } finally {
      setKeydbBusy(false)
    }
  }

  const keystoreHochladen = async (datei: File) => {
    setKeystoreBusy(true)
    try {
      // Die Datei wandert als roher Anfrage-Körper zur API — _private_data.tar
      // ist binär, JSON oder Base64 wäre nur unnötiger Ballast, und Multipart
      // kann die API nicht (kein python-multipart).
      await api.post('/system/keystore', datei, {
        headers: { 'Content-Type': 'application/octet-stream' },
      })
      keydbLaden()
      toast('success', 'Schlüsselspeicher übernommen — wirkt ab dem nächsten Rip.')
    } catch (e: any) {
      toast('error', e?.response?.data?.detail
        || 'Übernahme fehlgeschlagen — ist das wirklich die Datei _private_data.tar, und läuft die API?')
    } finally {
      setKeystoreBusy(false)
    }
  }

  const keydbEntfernen = async () => {
    setKeydbBusy(true)
    try {
      await api.delete('/system/keydb')
      keydbLaden()
      toast('success', 'KEYDB.cfg entfernt — Rippy nutzt jetzt keine Schlüsseldatei mehr.')
    } catch (e: any) {
      toast('error', e?.response?.data?.detail || 'Entfernen fehlgeschlagen — läuft die API?')
    } finally {
      setKeydbBusy(false)
    }
  }

  const quellenPruefen = async () => {
    setQuellenBusy(true)
    try {
      const r = await api.get('/metadata/status')
      setQuellenStatus(r.data)
      const kaputt = Object.entries(r.data).filter(([, v]) => v === 'fehler')
      if (kaputt.length === 0) {
        toast('success', 'Metadaten-Quellen geprüft — alles in Ordnung')
      } else {
        toast('error', `Quelle mit Problem: ${kaputt.map(([k]) => k.toUpperCase()).join(', ')}`)
      }
    } catch {
      toast('error', 'Prüfung fehlgeschlagen — läuft die API?')
    } finally {
      setQuellenBusy(false)
    }
  }

  useEffect(() => {
    api.get('/capabilities')
      .then(r => setWorkers(r.data.workers || []))
      .catch(() => setWorkers([]))
    api.get('/system/info')
      .then(r => setSystemInfo(r.data))
      .catch(() => setSystemInfo(null))
    api.get('/system/keydb')
      .then(r => setKeydb(r.data))
      .catch(() => setKeydb(null))
    api.get('/system/keystore')
      .then(r => setKeystore(r.data))
      .catch(() => setKeystore(null))
    api.get('/storage-targets')
      .then(r => setZiele(Array.isArray(r.data) ? r.data : []))
      .catch(() => setZiele([]))
    api.get('/system/aacs-dumps')
      .then(r => setDumps(r.data?.dumps || []))
      .catch(() => setDumps([]))
    api.get('/presets')
      .then(r => setPresetInfo(r.data))
      .catch(() => setPresetInfo(null))
  }, [])

  // „Bestes automatisch wählen" (Commander-Anforderung 26.07.2026). Setzt alle
  // drei Disc-Typen auf die Empfehlung der API — und nur solche, die wirklich
  // gefunden wurden, damit nie ein Name landet, den HandBrake nicht kennt.
  const bestePresetsUebernehmen = () => {
    if (!presetInfo) return
    const neu: Partial<SettingsState> = {}
    const uebernommen: string[] = []
    for (const typ of ['uhd', 'bluray', 'dvd'] as const) {
      const e = presetInfo.typen[typ]?.empfehlung
      if (!e?.gefunden || !e.preset) continue
      neu[PRESET_FELD[typ]] = e.preset as any
      uebernommen.push(`${typ.toUpperCase()}: ${e.preset === PRESET_KEINE ? 'nicht komprimieren' : e.preset}`)
    }
    if (uebernommen.length === 0) {
      toast('error', 'Kein Preset übernommen — die Worker melden keine bekannten Namen.')
      return
    }
    setSettings(prev => ({ ...prev, ...neu }))
    setSaved(false)
    toast('success', `Übernommen — ${uebernommen.join(' · ')}. Noch speichern!`)
  }

  // Eine Preset-Auswahl je Disc-Typ. Die Optionen kommen vom WORKER (siehe
  // PresetUebersicht), nicht aus dieser Datei. Der aktuell gespeicherte Wert
  // steht immer mit drin — sonst würde eine Bestandseinstellung beim Öffnen der
  // Seite still auf etwas anderes springen.
  const presetAuswahl = (
    typ: 'uhd' | 'bluray' | 'dvd', label: string, keineText: string,
  ) => {
    const feld = PRESET_FELD[typ]
    const wert = (settings[feld] as string) || ''
    const daten = presetInfo?.typen?.[typ]
    const empfehlung = daten?.empfehlung
    const passend = (daten?.auswahl || []).filter(o => !o.verkleinert)
    const kleiner = (daten?.auswahl || []).filter(o => o.verkleinert)
    const bekannt = wert === PRESET_KEINE
      || (daten?.auswahl || []).some(o => o.name === wert)
    return (
      <div className="space-y-1.5">
        <Select label={label} value={wert} onChange={(e) => handleChange(feld, e.target.value)}>
          <option value={PRESET_KEINE}>{keineText}</option>
          {/* Ein gespeicherter Wert, den dieser Worker nicht kennt, wird
              BENANNT statt verschluckt — genau so fällt eine falsche
              Bestandseinstellung überhaupt auf. */}
          {!bekannt && wert && (
            <option value={wert}>{wert} — gespeichert, dieser Worker kennt es nicht</option>
          )}
          {passend.length > 0 && (
            <optgroup label="Passt zur Auflösung">
              {passend.map(o => (
                <option key={o.name} value={o.name}>
                  {o.name}{empfehlung?.gefunden && empfehlung.preset === o.name ? '  ★ empfohlen' : ''}
                </option>
              ))}
            </optgroup>
          )}
          {kleiner.length > 0 && (
            <optgroup label="Bewusst verkleinern — die 4K-Auflösung ist danach weg">
              {kleiner.map(o => <option key={o.name} value={o.name}>{o.name}</option>)}
            </optgroup>
          )}
        </Select>
        {empfehlung && empfehlung.preset !== wert && (
          <p className="text-xs text-slate-500 dark:text-slate-400">
            {empfehlung.gefunden ? (
              <>
                Empfohlen wäre{' '}
                <strong className="text-slate-700 dark:text-slate-300">
                  {empfehlung.preset === PRESET_KEINE ? 'nicht komprimieren' : empfehlung.preset}
                </strong>
                {' — '}{empfehlung.grund}
              </>
            ) : empfehlung.grund}
          </p>
        )}
      </div>
    )
  }

  const webhookTesten = async () => {
    if (!settings.notificationWebhook) return
    setTestBusy(true)
    try {
      await api.post('/notifications/test', { url: settings.notificationWebhook })
      toast('success', 'Test-Nachricht gesendet — prüfe deinen Kanal!')
    } catch (e: any) {
      toast('error', e?.response?.data?.detail || 'Test fehlgeschlagen')
    } finally {
      setTestBusy(false)
    }
  }

  useEffect(() => {
    const loadSettings = async () => {
      try {
        const response = await api.get('/settings')
        setSettings({ ...defaultSettings, ...response.data })
      } catch {
        setSettings(defaultSettings)
      } finally {
        setLoading(false)
      }
    }
    loadSettings()
  }, [])

  const handleChange = (field: keyof SettingsState, value: any) => {
    setSettings(prev => ({ ...prev, [field]: value }))
    setSaved(false)
  }

  const handleSave = async () => {
    setSaving(true)
    try {
      await api.post('/settings', settings)
      setSaved(true)
      toast('success', 'Einstellungen gespeichert')
      setTimeout(() => setSaved(false), 3000)
    } catch {
      toast('error', 'Speichern fehlgeschlagen — läuft die API?')
    } finally {
      setSaving(false)
    }
  }

  if (loading) {
    return (
      <div className="flex items-center justify-center min-h-[60vh]">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-amber-500"></div>
      </div>
    )
  }

  // Erreichbarkeit für EXTERNE Encoder. Ein Worker auf einem anderen Rechner
  // sieht die Container-Platte nicht — nur eingehängte Netzwerk-Freigaben, die
  // er selbst auch erreicht. Liegen Ablage und Arbeitsverzeichnis nicht BEIDE
  // auf einer solchen Freigabe, scheitert die Kompression dort zwangsläufig.
  //
  // Genau das ist am 25.07.2026 passiert: Rohdaten auf der NAS (erreichbar),
  // Ziel auf der VM-Platte (nicht erreichbar) — der Fehler kam erst nach einer
  // Stunde Rippen. Diese Prüfung sagt es vorher.
  // Der Worker sagt selbst, ob er außerhalb des Containers läuft (caps.py
  // meldet `extern`) — keine Heuristik über IPs oder Namen nötig.
  const externerWorkerVorhanden = workers.some(w => w.info?.extern === 'ja')
  const istFreigabe = (pfad: string) =>
    ziele.some(z => z.path === pfad && z.is_mount)
  const ablageWarnung = (() => {
    const ablage = settings.outputDir || '/app/media'
    const arbeit = settings.workDir
    if (!arbeit) {
      return 'Das Arbeitsverzeichnis steht auf der Container-Platte. Ein Encoder auf '
        + 'einem anderen Rechner kann die Rohdaten dort nicht lesen — lege beides auf '
        + 'dieselbe Netzwerk-Freigabe (Einstellungen → Verarbeitung).'
    }
    if (!istFreigabe(ablage) && istFreigabe(arbeit)) {
      return 'Arbeitsverzeichnis liegt auf einer Freigabe, die Ablage aber auf der '
        + 'Container-Platte. Ein externer Encoder kann dann lesen, aber nicht schreiben — '
        + 'genau daran scheiterte der erste Versuch. Stelle die Ablage auf dieselbe Freigabe.'
    }
    if (istFreigabe(ablage) && istFreigabe(arbeit) && ablage !== arbeit) {
      return 'Ablage und Arbeitsverzeichnis liegen auf VERSCHIEDENEN Freigaben. Das '
        + 'funktioniert, kostet am Ende aber eine Vollkopie statt eines Umhängens. '
        + 'Dieselbe Freigabe für beides ist deutlich schneller.'
    }
    return ''
  })()

  const tabs: { id: SettingsTab; label: string; icon: any }[] = [
    { id: 'ripping', label: 'Ripping', icon: Disc },
    { id: 'verarbeitung', label: 'Verarbeitung', icon: Cpu },
    { id: 'worker', label: 'Worker', icon: Cpu },
    { id: 'speicherziele', label: 'Speicherziele', icon: HardDrive },
    { id: 'apis', label: 'APIs', icon: Globe },
    { id: 'benachrichtigungen', label: 'Benachrichtigungen', icon: AlertCircle },
    { id: 'system', label: 'System', icon: Wrench },
  ]

  return (
    <div className="max-w-6xl mx-auto space-y-6">
      <PageHeader
        title="Einstellungen"
        subtitle="Konfiguriere Ripping-Parameter, Speicherziele, APIs und Benachrichtigungen"
      />

      {saved && (
        <div className="p-4 rounded-xl flex items-center gap-3 font-medium bg-emerald-500/15 border border-emerald-500/30 text-emerald-600 dark:text-emerald-400">
          <CheckCircle className="flex-shrink-0" size={22} />
          <span>Einstellungen erfolgreich gespeichert!</span>
        </div>
      )}

      <Card className="overflow-hidden">
        {/* Navigation Tabs */}
        <div className="flex border-b border-slate-800 overflow-x-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden bg-[#0c101d]">
          {tabs.map((tab) => {
            const isActive = activeTab === tab.id
            return (
              <button
                key={tab.id}
                onClick={() => setActiveTab(tab.id)}
                className={`px-5 py-3.5 text-sm font-medium transition-all flex items-center gap-2 border-b-2 whitespace-nowrap ${
                  isActive
                    ? 'border-amber-500 text-amber-400 bg-[#131929] font-bold'
                    : 'border-transparent text-slate-400 hover:text-slate-200 hover:bg-slate-900/50'
                }`}
              >
                <tab.icon size={17} className={isActive ? 'text-amber-400' : ''} />
                <span>{tab.label}</span>
              </button>
            )
          })}
        </div>

        {/* Tab Content */}
        <CardContent className="p-6 space-y-8">
          {/* Ripping Settings */}
          {activeTab === 'ripping' && (
            <section className="space-y-6">
              <div>
                <h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
                  <Disc size={20} className="text-amber-500" />
                  Ripping-Einstellungen
                </h2>
              </div>

              <div className="space-y-4">
                <p className="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500">
                  📀 Video — DVD · Blu-ray · 4K UHD
                </p>

                <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80">
                  <Toggle
                    label="Vollautomatik: sofort rippen"
                    description="Disc erkannt → Rip startet ohne Nachfrage in den passenden Ordner."
                    checked={settings.autoRipStart}
                    onChange={(v) => handleChange('autoRipStart', v)}
                  />
                </div>

                <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80">
                  <Toggle
                    label="Nur Hauptfeature"
                    description="Rippe bei DVDs/Blu-rays nur das Hauptfeature (längster Titel)."
                    checked={settings.mainFeatureOnly}
                    onChange={(v) => handleChange('mainFeatureOnly', v)}
                  />
                </div>

                <p className="text-xs text-slate-500 dark:text-slate-400 px-1">
                  🌐 Sprachen &amp; Untertitel: MakeMKV sichert grundsätzlich ALLE Ton-Spuren und Untertitel verlustfrei.
                </p>

                <p className="text-xs font-bold uppercase tracking-wider pt-2 text-slate-400 dark:text-slate-500">
                  🎵 Audio-CD
                </p>

                {/* Der Schalter „Alle Tracks rippen" ist am 25.07.2026 entfallen.
                    Er war ein Placebo: abcde bekommt keine Track-Auswahl (siehe
                    build_abcde_cmd) und es gibt auch keine Oberfläche, um eine
                    Teilmenge zu wählen — eine Audio-CD wurde also immer
                    vollständig gerippt, egal wie der Schalter stand. Statt einer
                    Wahl, die nichts bewirkt, steht hier jetzt die Wahrheit. */}
                <p className="text-xs text-slate-500 dark:text-slate-400 px-1">
                  💿 Eine Audio-CD wird immer vollständig gerippt — alle Tracks als
                  FLAC, mit MusicBrainz-Tags.
                </p>

                <p className="text-xs font-bold uppercase tracking-wider pt-2 text-slate-400 dark:text-slate-500">
                  Allgemein
                </p>

                <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80">
                  <Toggle
                    label="Automatischer Auswurf"
                    description="Disc nach erfolgreichem Ripping automatisch auswerfen."
                    checked={settings.autoEject}
                    onChange={(v) => handleChange('autoEject', v)}
                  />
                </div>

                {/* Media-Server Integration */}
                <div className="p-5 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-4">
                  <h3 className="font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
                    <Tv size={18} className="text-amber-500" />
                    Media-Server (Jellyfin &amp; Co.)
                  </h3>
                  <p className="text-sm text-slate-500 dark:text-slate-400">
                    Rippy benennt fertige Rips als „Titel (Jahr)" und legt NFO + Poster dazu.
                  </p>
                  <div className="grid grid-cols-2 md:grid-cols-5 gap-2">
                    {MEDIA_SERVER_OPTIONEN.map(o => (
                      <button
                        key={o.wert}
                        type="button"
                        onClick={() => handleChange('mediaServer', o.wert)}
                        title={o.hinweis}
                        className={`px-3 py-2 rounded-xl text-sm font-medium border transition-all ${
                          settings.mediaServer === o.wert
                            ? 'bg-amber-500/20 border-amber-500 text-amber-600 dark:text-amber-300 font-semibold'
                            : 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 hover:border-slate-300 dark:hover:border-slate-600'
                        }`}
                      >
                        {o.label}
                      </button>
                    ))}
                  </div>
                  <p className="text-xs text-slate-500 dark:text-slate-400">
                    {MEDIA_SERVER_OPTIONEN.find(o => o.wert === settings.mediaServer)?.hinweis}
                  </p>

                  {(settings.mediaServer === 'jellyfin' || settings.mediaServer === 'emby') && (
                    <div className="pt-3 border-t border-slate-200 dark:border-slate-800 space-y-3">
                      <p className="text-xs text-slate-600 dark:text-slate-300">
                        <strong>Bibliotheks-Refresh</strong>: Server-URL + API-Key für automatischen Scan nach dem Rip.
                      </p>
                      <div className="grid grid-cols-1 md:grid-cols-2 gap-3 items-end">
                        <Input
                          label="Server-URL"
                          value={settings.jellyfinUrl}
                          onChange={(e) => handleChange('jellyfinUrl', e.target.value)}
                          placeholder="z. B. http://192.168.1.50:8096"
                        />
                        <div className="flex gap-2 items-end">
                          <div className="flex-1">
                            <Input
                              type="password"
                              label="API-Key"
                              value={settings.jellyfinApiKey}
                              onChange={(e) => handleChange('jellyfinApiKey', e.target.value)}
                              placeholder="API-Key"
                            />
                          </div>
                          <Button
                            variant="amber"
                            size="md"
                            onClick={async () => {
                              try {
                                await api.post('/mediaserver/refresh', {
                                  url: settings.jellyfinUrl, api_key: settings.jellyfinApiKey,
                                })
                                toast('success', 'Verbunden — Bibliotheks-Scan wurde angestoßen!')
                              } catch (e: any) {
                                toast('error', e?.response?.data?.detail || 'Verbindung fehlgeschlagen')
                              }
                            }}
                            disabled={!settings.jellyfinUrl || !settings.jellyfinApiKey}
                          >
                            Testen
                          </Button>
                        </div>
                      </div>
                    </div>
                  )}
                </div>
              </div>
            </section>
          )}

          {/* Verarbeitung */}
          {activeTab === 'verarbeitung' && (
            <section className="space-y-6">
              <h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
                <Cpu size={20} className="text-amber-500" />
                Verarbeitung (Kompression)
              </h2>
              <p className="text-sm text-slate-500 dark:text-slate-400">
                MakeMKV rippt verlustfrei, HandBrake komprimiert danach auf Arbeitsgröße.
              </p>

              <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80">
                <p className="text-sm font-medium mb-2 text-slate-900 dark:text-slate-200">Verfügbare Encoder</p>
                {workers.length === 0 ? (
                  <p className="text-sm text-slate-500 dark:text-slate-400">Kein Worker gemeldet.</p>
                ) : (
                  workers.map(w => (
                    <div key={w.name} className="mb-1">
                      <span className="text-xs font-mono mr-2 text-slate-500 dark:text-slate-400">{w.name}: </span>
                      {w.encoders.map(e => (
                        <span key={e} className={`inline-block text-xs px-2.5 py-0.5 rounded-md font-medium border mr-1 ${
                          e.startsWith('cpu')
                            ? 'bg-slate-200 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700'
                            : 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/30'
                        }`}>
                          {ENCODER_BADGES[e]?.label || e}
                        </span>
                      ))}
                      {(w.info?.cpu_kerne || w.info?.cpu_simd) && (
                        <p className="mt-1 text-xs text-slate-500 dark:text-slate-400" title={w.info?.cpu_modell || ''}>
                          {w.info?.cpu_modell ? `${w.info.cpu_modell} · ` : ''}
                          {w.info?.cpu_kerne ? `${w.info.cpu_kerne} Kerne` : ''}
                          {w.info?.cpu_kerne && w.info?.cpu_simd ? ' · ' : ''}
                          {w.info?.cpu_simd ? `Vektorbefehle ${w.info.cpu_simd}` : ''}
                        </p>
                      )}
                      {simdWarnung(w.info?.cpu_simd) && (
                        <p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
                          {simdWarnung(w.info?.cpu_simd)}
                        </p>
                      )}
                      {w.info?.handbrake_encoder && (
                        <p className="mt-1 text-xs text-slate-400 dark:text-slate-500 font-mono break-all">
                          HandBrake meldet: {w.info.handbrake_encoder}
                        </p>
                      )}
                    </div>
                  ))
                )}
              </div>

              <div className="space-y-4">
                <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80">
                  <Toggle
                    label="Nach dem Rip komprimieren"
                    description="x265 statt 40-GB-Rohdatei (empfohlen)."
                    checked={settings.transcodeEnabled}
                    onChange={(v) => handleChange('transcodeEnabled', v)}
                  />
                </div>

                {/*
                  Ein Preset JE DISC-TYP (25.07.2026). Vorher lief alles durch
                  dasselbe Preset — eine 4K-UHD wurde damit auf 1080p
                  heruntergerechnet und eine DVD auf 1080p hochskaliert.

                  Seit 26.07.2026 kommen die NAMEN vom Worker selbst
                  (`HandBrakeCLI --preset-list`, siehe worker/caps.py) und die
                  Empfehlung von GET /presets. Vorher standen sie hier fest
                  verdrahtet — sie unterscheiden sich aber je HandBrake-Version,
                  und ein Name, den das HandBrake des Workers nicht kennt, lässt
                  die Kompression scheitern.
                */}
                <div className="space-y-3">
                  <div className="flex items-start justify-between gap-3">
                    <p className="text-xs text-slate-500 dark:text-slate-400">
                      Ein Preset je Quelle — Rippy erkennt den Disc-Typ selbst und wählt passend aus.
                      <strong className="text-slate-700 dark:text-slate-300"> Eine 4K-UHD gehört auf ein
                      2160p-Preset</strong>, sonst rechnet HandBrake sie auf 1080p herunter und die
                      4K-Auflösung ist weg.
                    </p>
                    <Button
                      variant="secondary"
                      onClick={bestePresetsUebernehmen}
                      disabled={!presetInfo}
                      className="flex-shrink-0"
                    >
                      Bestes wählen
                    </Button>
                  </div>

                  {presetInfo && (
                    <p className="text-xs text-slate-500 dark:text-slate-400">
                      {presetInfo.quelle === 'worker' ? (
                        <>
                          Die Liste kommt von deinen Workern selbst.
                          {presetInfo.hardware.length > 0
                            ? ` Hardware-Encoder gemeldet: ${presetInfo.hardware.join(', ')} — damit geht 4K in Minuten statt Tagen.`
                            : presetInfo.simd_schnell
                              ? ' Kein Hardware-Encoder, aber ein Worker mit AVX2 — Software-H.265 ist damit schnell genug.'
                              : ' Kein Hardware-Encoder und kein AVX2 — hier ist Software-4K keine Option (gemessen: 28–55 h je Film).'}
                        </>
                      ) : (
                        <>
                          Kein Worker meldet gerade eine Preset-Liste (alter Worker-Stand oder
                          keiner online) — angezeigt wird deshalb eine geprüfte Standardauswahl.
                        </>
                      )}
                    </p>
                  )}

                  {presetAuswahl('uhd', 'Preset für 4K-UHD',
                    'Nicht komprimieren — 4K verlustfrei behalten')}
                  {/* Die Warnung, die den 50-Stunden-Lauf verhindert haette. Sie
                      steht nur da, wenn die Maschine sie WIRKLICH braucht -
                      gemessen, nicht vermutet (worker/caps.py meldet cpu_simd). */}
                  {settings.transcodePresetUhd !== PRESET_KEINE
                    && settings.transcodePresetUhd.includes('2160')
                    && schwacheEncoderCpu(workers) && (
                    <p className="text-xs -mt-1 p-3 rounded-lg bg-amber-500/10 border border-amber-500/30 text-amber-700 dark:text-amber-300">
                      ⚠️ Keiner deiner Worker kann AVX2 — 4K in H.265 dauert damit
                      <strong> ein bis zwei Tage pro Film</strong> (gemessen, nicht geschätzt).
                      Besser „Nicht komprimieren" wählen und die verlustfreie Datei behalten,
                      oder einen Worker mit Hardware-Encoder anschließen.
                    </p>
                  )}

                  {presetAuswahl('bluray', 'Preset für Blu-ray',
                    'Nicht komprimieren — verlustfrei behalten')}
                  {presetAuswahl('dvd', 'Preset für DVD',
                    'Nicht komprimieren — verlustfrei behalten')}

                  <p className="text-xs text-slate-500 dark:text-slate-400">
                    „Nicht komprimieren" heißt: Der Rip bleibt so, wie MakeMKV ihn
                    gesichert hat — beste Qualität, aber 20–100 GB je Film. Sinnvoll für
                    4K, wenn die Ablage groß genug ist.
                  </p>
                </div>

                {/*
                  Wunschsprachen (Commander-Anforderung 26.07.2026: die Auswahl
                  muss „in der Automatik ebenfalls einstellbar" sein).

                  Sie greifen bei der KOMPRESSION, nicht beim Rippen — der Rip
                  bleibt vollständig und verlustfrei (Muss-Feature). Damit lässt
                  sich später mit anderer Sprachwahl neu komprimieren, ohne die
                  Disc wieder einzulegen.
                */}
                <div className="space-y-3 p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80">
                  <div>
                    <p className="text-sm font-medium text-slate-900 dark:text-slate-100">Wunschsprachen</p>
                    <p className="text-xs text-slate-500 dark:text-slate-400">
                      Gilt für Vollautomatik-Rips und ist im Rip-Dialog vorausgewählt.
                      <strong className="text-slate-700 dark:text-slate-300"> Leer = alle Sprachen behalten.</strong>
                    </p>
                  </div>

                  <Input
                    label="Tonspuren (ISO-639-2, mit Komma getrennt)"
                    value={settings.audioSprachen}
                    onChange={(e) => handleChange('audioSprachen', e.target.value)}
                    placeholder="z. B. deu,eng,jpn"
                  />
                  <Input
                    label="Untertitel (ISO-639-2, mit Komma getrennt)"
                    value={settings.untertitelSprachen}
                    onChange={(e) => handleChange('untertitelSprachen', e.target.value)}
                    placeholder="z. B. deu"
                  />

                  <p className="text-xs text-slate-500 dark:text-slate-400">
                    Codes sind dreibuchstabig: <span className="font-mono">deu</span> Deutsch,
                    {' '}<span className="font-mono">eng</span> Englisch,
                    {' '}<span className="font-mono">jpn</span> Japanisch,
                    {' '}<span className="font-mono">fra</span> Französisch,
                    {' '}<span className="font-mono">spa</span> Spanisch,
                    {' '}<span className="font-mono">ita</span> Italienisch.
                    Welche eine Disc wirklich hat, zeigt Rippy beim „Disc scannen" im
                    Rip-Dialog — dort ist die Auswahl auch je Disc änderbar.
                  </p>
                  <p className="text-xs text-slate-500 dark:text-slate-400">
                    Wichtig: Diese Wahl wirkt beim <strong>Komprimieren</strong>. Der Rip
                    selbst sichert immer alles — sonst wäre eine spätere Änderung nur mit
                    neuem Rip möglich.
                  </p>
                </div>

                <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80">
                  <Toggle
                    label="Original behalten"
                    description="Verlustfreie Roh-Datei zusätzlich aufheben (viel Speicher!)."
                    checked={settings.keepOriginal}
                    onChange={(v) => handleChange('keepOriginal', v)}
                  />
                </div>

                {/*
                  Auswahl statt Eintippen (Befund 25.07.2026): Das Feld war ein
                  freies Textfeld — man musste den Container-Pfad KENNEN
                  (/app/media/…), um eine Netzwerk-Freigabe zu treffen. Genau
                  daran ist es gescheitert, und der 74-GB-Rohschnitt landete auf
                  der VM-Platte, bis sie voll war. Die Ziele kommen aus
                  /storage-targets — dieselbe Liste wie bei den Speicherzielen,
                  inklusive freiem Platz.
                */}
                <Select
                  label="Arbeitsverzeichnis für Roh-Rips (Standard)"
                  value={settings.workDir}
                  onChange={(e) => handleChange('workDir', e.target.value)}
                >
                  <option value="">Container-Platte (Standard) — klein, nur für DVD/Blu-ray</option>
                  {ziele.map(z => (
                    <option key={z.path} value={z.path}>
                      {z.name}{z.is_mount ? ' (Netzwerk-Freigabe)' : ''}
                      {z.free_gb != null ? ` — ${z.free_gb} GB frei` : ''}
                    </option>
                  ))}
                </Select>
                <p className="text-xs text-slate-500 dark:text-slate-400">
                  <strong className="text-slate-700 dark:text-slate-300">Der Standard</strong> — beim
                  „Rippen starten" kannst du für jede Disc etwas anderes wählen. <strong className="text-slate-700 dark:text-slate-300">
                  Läuft die Vollautomatik</strong>, fragt dich niemand: dann gilt genau dieser Wert.
                  Wichtig für 4K-UHD: Rohdaten sind bis 100 GB groß und passen selten auf die
                  Container-Platte. Am besten dieselbe Freigabe wie das Ziel — dann muss Rippy die
                  Rohdatei am Ende nur umhängen statt sie zu kopieren.
                  {systemInfo?.plaetze?.length ? (
                    <> Aktuell frei: {systemInfo.plaetze.map(p => `${p.name}: ${p.frei_gb} GB`).join(' · ')}</>
                  ) : null}
                </p>
              </div>
            </section>
          )}

          {/* Worker */}
          {activeTab === 'worker' && (
            <section className="space-y-6">
              <h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
                <Cpu size={20} className="text-amber-500" />
                Encoding-Worker
              </h2>
              <WorkerVerwaltung />
            </section>
          )}

          {/* Speicherziele */}
          {activeTab === 'speicherziele' && (
            <section className="space-y-6">
              <h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
                <HardDrive size={20} className="text-amber-500" />
                Speicherziele
              </h2>

              {/* ABLAGE — bis 26.07.2026 gab es dieses Feld im UI GAR NICHT.
                  outputDir stand in den Einstellungen und wurde von der
                  Vollautomatik und der Schnellwahl gelesen, ließ sich aber
                  nirgends ändern: die Ablage klebte auf /app/media, also der
                  Container-Platte. Deshalb konnte man das Arbeitsverzeichnis auf
                  eine NAS legen (dafür gab es eine Auswahl), das ZIEL aber nicht
                  — und genau daran scheiterte der erste Rip mit externem
                  Encoder: Rohdaten auf der erreichbaren NAS, Ziel auf der
                  VM-Platte, die der andere Rechner nicht sieht. */}
              <div className="p-5 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
                <h3 className="font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
                  <HardDrive size={16} className="text-amber-500" /> Ablage — wohin die fertigen Filme kommen
                </h3>
                <Select
                  label="Ablage"
                  value={settings.outputDir}
                  onChange={(e) => handleChange('outputDir', e.target.value)}
                >
                  <option value="/app/media">Container-Platte (Standard)</option>
                  {ziele.filter(z => z.path !== '/app/media').map(z => (
                    <option key={z.path} value={z.path}>
                      {z.name}{z.is_mount ? ' (Netzwerk-Freigabe)' : ''}
                      {z.free_gb != null ? ` — ${z.free_gb} GB frei` : ''}
                    </option>
                  ))}
                </Select>
                <p className="text-xs text-slate-500 dark:text-slate-400">
                  Gilt für die Schnellwahl im Rip-Dialog <strong className="text-slate-700 dark:text-slate-300">und
                  für die Vollautomatik</strong> — dort fragt niemand nach, also entscheidet dieser Wert.
                  Die Unterordner darunter kommen aus der Schnellwahl.
                </p>
                {externerWorkerVorhanden && ablageWarnung && (
                  <p className="text-xs p-3 rounded-lg bg-amber-500/10 border border-amber-500/30 text-amber-700 dark:text-amber-300">
                    ⚠️ {ablageWarnung}
                  </p>
                )}
              </div>

              <div className="p-5 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
                <h3 className="font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
                  <Database size={16} className="text-amber-500" /> Schnellwahl beim Rippen
                </h3>
                <p className="text-xs text-slate-500 dark:text-slate-400">
                  Name der Ordner im „Rippen starten"-Dialog (🎥/📺/🎵) — jeweils unterhalb der Ablage.
                </p>
                <div className="grid grid-cols-3 gap-3">
                  {([['movieDir', '🎥 Filme'], ['seriesDir', '📺 Serien'], ['musicDir', '🎵 Musik']] as const).map(([feld, label]) => (
                    <Input
                      key={feld}
                      label={label}
                      value={settings[feld]}
                      onChange={(e) => handleChange(feld, e.target.value)}
                    />
                  ))}
                </div>
              </div>

              <StorageMounts />
            </section>
          )}

          {/* APIs */}
          {activeTab === 'apis' && (
            <section className="space-y-6">
              <h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
                <Globe size={20} className="text-amber-500" />
                API-Schlüssel
              </h2>

              <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 flex flex-wrap items-center gap-3">
                <Button variant="amber" size="sm" onClick={quellenPruefen} disabled={quellenBusy}>
                  {quellenBusy ? 'Prüfe…' : 'Verbindung prüfen'}
                </Button>
                {quellenStatus ? (
                  <div className="flex flex-wrap gap-2">
                    {Object.entries(quellenStatus).map(([quelle, status]) => (
                      <span
                        key={quelle}
                        className={`text-xs px-2.5 py-1 rounded-full font-medium border ${
                          status === 'ok' ? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/30'
                          : status === 'kein_key' ? 'bg-slate-200 dark:bg-slate-800 text-slate-500 border-slate-300 dark:border-slate-700'
                          : 'bg-rose-500/15 text-rose-600 dark:text-rose-400 border-rose-500/30'
                        }`}
                      >
                        {quelle.toUpperCase()}: {status === 'ok' ? '✓ funktioniert' : status === 'kein_key' ? 'kein Key' : '✗ Fehler'}
                      </span>
                    ))}
                  </div>
                ) : (
                  <span className="text-xs text-slate-500 dark:text-slate-400">
                    Prüft live gegen TMDB/OMDb — nach dem Speichern eines neuen Keys hier klicken.
                  </span>
                )}
              </div>

              <div className="space-y-4">
                <Input
                  type="password"
                  label="TMDB API Key"
                  value={settings.tmdbApiKey}
                  onChange={(e) => handleChange('tmdbApiKey', e.target.value)}
                  placeholder="Dein TMDB API Key"
                />
                <Input
                  type="password"
                  label="TVDb API Key"
                  value={settings.tvdbApiKey}
                  onChange={(e) => handleChange('tvdbApiKey', e.target.value)}
                  placeholder="Dein TVDb API Key"
                />
                <Input
                  type="password"
                  label="OMDb API Key (Fallback)"
                  value={settings.omdbApiKey}
                  onChange={(e) => handleChange('omdbApiKey', e.target.value)}
                  placeholder="Dein OMDb API Key"
                />
              </div>
            </section>
          )}

          {/* Benachrichtigungen */}
          {activeTab === 'benachrichtigungen' && (
            <section className="space-y-6">
              <h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
                <AlertCircle size={20} className="text-amber-500" />
                Webhook-Benachrichtigungen
              </h2>
              <p className="text-sm text-slate-500 dark:text-slate-400">
                Rippy meldet sich, sobald ein Job <strong>fertig</strong> ist, <strong>fehlschlägt</strong> oder <strong>abgebrochen</strong> wird.
              </p>

              <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3 text-sm">
                <div>
                  <p className="font-semibold text-slate-900 dark:text-slate-200">💬 Discord (einfachster Weg)</p>
                  <p className="text-xs text-slate-500 dark:text-slate-400">
                    Kanal-Zahnrad → Integrationen → Webhooks → „Neuer Webhook" → URL kopieren.
                  </p>
                </div>
                <div>
                  <p className="font-semibold text-slate-900 dark:text-slate-200">📱 ntfy (Push aufs Handy)</p>
                  <p className="text-xs text-slate-500 dark:text-slate-400">
                    Thema abonnieren und https://ntfy.sh/mein-thema eintragen.
                  </p>
                </div>
              </div>

              <div className="space-y-2">
                <div className="flex gap-2 items-end">
                  <div className="flex-1">
                    <Input
                      label="Webhook URL"
                      value={settings.notificationWebhook}
                      onChange={(e) => handleChange('notificationWebhook', e.target.value)}
                      placeholder="https://discord.com/api/webhooks/… oder https://ntfy.sh/mein-thema"
                    />
                  </div>
                  <Button
                    variant="amber"
                    size="md"
                    onClick={webhookTesten}
                    disabled={!settings.notificationWebhook || testBusy}
                  >
                    <Send size={15} />
                    {testBusy ? 'Sende…' : 'Test senden'}
                  </Button>
                </div>
              </div>
            </section>
          )}

          {/* System */}
          {activeTab === 'system' && (
            <section className="space-y-6">
              <h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
                <Wrench size={20} className="text-amber-500" />
                System &amp; Kern-Werkzeuge
              </h2>

              <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80">
                <p className="text-sm font-semibold mb-2 text-slate-900 dark:text-slate-200">Installierte Werkzeuge</p>
                {!systemInfo || systemInfo.workers.length === 0 ? (
                  <p className="text-sm text-slate-500 dark:text-slate-400">Noch kein Worker gemeldet.</p>
                ) : (
                  systemInfo.workers.map(w => (
                    <div key={w.name} className="flex flex-wrap items-center gap-2 mb-1.5">
                      <span className="text-xs font-mono text-slate-500 dark:text-slate-400">{w.name}</span>
                      {w.info?.makemkv && (
                        <span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-slate-200 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-700">
                          MakeMKV {w.info.makemkv}
                        </span>
                      )}
                      {w.info?.handbrake && (
                        <span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-slate-200 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-700">
                          HandBrake {w.info.handbrake}
                        </span>
                      )}
                      {/*
                        Nur das Verzeichnis des rippenden Workers zählt — deshalb je Worker.
                        Die Zahl der Disc-Schluessel ist die entscheidende Angabe für 4K-UHD:
                        0 heißt, dass JEDE unbekannte UHD-Disc scheitert.
                      */}
                      {w.info?.schluessel && w.info.schluessel !== '0' && w.info.schluessel !== 'unbekannt' && (
                        <span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30">
                          {w.info.schluessel} Disc-Schlüssel
                        </span>
                      )}
                      {w.info?.schluessel === '0' && (
                        <span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-amber-500/15 text-amber-600 dark:text-amber-400 border border-amber-500/30">
                          keine Disc-Schlüssel — 4K-UHD scheitert
                        </span>
                      )}
                      {w.info?.keydb === 'ja' && (
                        <span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-slate-200 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-300 dark:border-slate-700">
                          + KEYDB.cfg
                        </span>
                      )}
                    </div>
                  ))
                )}
                {/*
                  Bis 25.07.2026 stand hier, MakeMKV-Updates braechten die neueste
                  Disc-Schluessel-Datenbank mit. Nachgemessen: falsch — MakeMKV liefert
                  gar keine Schluessel mit, es holt sie zur Laufzeit. Und die
                  Linux-Version holt sie nie (kein einziger Verbindungsversuch, auf
                  beiden Maschinen verglichen). Deshalb der Schlüsselspeicher unten.
                */}
                <p className="text-xs mt-3 pt-3 border-t border-slate-200/60 dark:border-slate-800 text-slate-500 dark:text-slate-400">
                  <strong className="text-slate-700 dark:text-slate-300">MakeMKV</strong> lässt sich aktuell halten
                  (Update-Check unten; Version über <code>MAKEMKV_VERSION</code> + Rebuild) — neue Versionen bringen
                  vor allem Laufwerks-Unterstützung und Fehlerbehebungen. <strong>Disc-Schlüssel für 4K-UHD kommen
                  NICHT aus einem Update</strong> — die holt MakeMKV zur Laufzeit, und die Linux-Version tut das
                  nie. Deshalb der Block „Disc-Schlüssel für 4K-UHD" weiter unten.
                  {' '}<strong className="text-slate-700 dark:text-slate-300">HandBrake</strong> im eingebauten
                  Docker-Worker ist bewusst die stabile <strong>Debian-Version</strong> — für die Kompression völlig
                  ausreichend und wird <strong>nicht separat aktualisiert</strong>. Native Worker (Windows) holen
                  dagegen automatisch die neueste HandBrake-Version.
                </p>
              </div>

              <Input
                type="password"
                label="MakeMKV Beta-Key"
                value={settings.makemkvAppKey}
                onChange={(e) => handleChange('makemkvAppKey', e.target.value)}
                placeholder="T-… (Forum-Thread „MakeMKV is free while in beta”)"
              />

              {/*
                Schlüsselspeicher — der Hauptweg für 4K-UHD (Befund 25.07.2026,
                auf beiden Maschinen gemessen): makemkvcon holt Disc-Schluessel
                unter Linux NIE selbst, die Windows-Version tut es (Meldung 3338,
                Verbindung nach 185.84.108.20:443). Deshalb wird der Vorrat hier
                von Hand hereingereicht. Frueher stand an dieser Stelle die These,
                MakeMKVs Schluessel-Kanal sei abgeschaltet — das war falsch.
              */}
              <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
                <div className="flex items-center justify-between">
                  <p className="text-sm font-semibold text-slate-900 dark:text-slate-200 flex items-center gap-2">
                    <Database size={16} className="text-amber-500" />
                    Disc-Schlüssel für 4K-UHD
                  </p>
                  <div className="flex items-center gap-2">
                    <input
                      type="file"
                      className="hidden"
                      ref={keystoreInput}
                      accept=".tar"
                      onChange={(e) => {
                        const datei = e.target.files?.[0]
                        e.target.value = ''
                        if (datei) keystoreHochladen(datei)
                      }}
                    />
                    <Button
                      variant="amber"
                      size="sm"
                      onClick={() => keystoreInput.current?.click()}
                      disabled={keystoreBusy}
                    >
                      <Upload size={14} />
                      {keystoreBusy ? 'Übernehme…' : '_private_data.tar übernehmen'}
                    </Button>
                  </div>
                </div>

                {keystore?.vorhanden && keystore.schluessel > 0 ? (
                  <div className="p-3 rounded-lg text-sm bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30">
                    <p className="font-semibold">{keystore.schluessel} Disc-Schlüssel vorhanden.</p>
                    <p className="text-xs mt-1">
                      {bytesLesbar(keystore.groesse_bytes)} · zuletzt geändert {zeitLesbar(keystore.geaendert)}
                    </p>
                  </div>
                ) : (
                  <div className="p-3 rounded-lg text-sm bg-amber-500/15 text-amber-600 dark:text-amber-400 border border-amber-500/30">
                    <p className="font-semibold">Kein einziger Disc-Schlüssel vorhanden.</p>
                    <p className="text-xs mt-1">
                      DVDs und normale Blu-rays laufen trotzdem. 4K-UHD-Discs scheitern dagegen mit
                      „The volume key is unknown for this disc" — solange hier nichts liegt, jede einzelne.
                    </p>
                  </div>
                )}

                <p className="text-xs text-slate-500 dark:text-slate-400">
                  <strong className="text-slate-700 dark:text-slate-300">Warum das nötig ist:</strong> MakeMKV
                  holt sich diese Schlüssel eigentlich selbst aus dem Netz. Die <strong>Linux-Version tut das
                  nicht</strong> — am 25.07.2026 nachgemessen: sie baut dabei nicht eine einzige Verbindung auf.
                  Die Windows-Version schon. Ein MakeMKV-Update ändert daran nichts, es ist kein Fehler in Rippy.
                </p>
                <p className="text-xs text-slate-500 dark:text-slate-400">
                  <strong className="text-slate-700 dark:text-slate-300">So füllst du den Vorrat:</strong> MakeMKV
                  auf einem Windows-PC installieren (gleicher Beta-Key), das Laufwerk dort anstecken und die Disc
                  einmal öffnen. MakeMKV lädt die Schlüssel dabei nach. Danach unter <em>Preferences → General</em>
                  das „MakeMKV data directory" nachschlagen, die Datei <code>_private_data.tar</code> daraus hier
                  hochladen — fertig. Für neue Discs von Zeit zu Zeit wiederholen.
                </p>
                <p className="text-xs text-slate-500 dark:text-slate-400">
                  Das ist der Zwischenspeicher <strong className="text-slate-700 dark:text-slate-300">deiner
                  eigenen</strong> MakeMKV-Installation, über MakeMKVs offiziellen Weg mit deiner eigenen Lizenz
                  geholt. Rippy liefert keine Schlüssel mit und lädt keine herunter.
                </p>
              </div>

              {/*
                KEYDB.cfg — der NOTNAGEL, nicht der Hauptweg. Sie hilft bei Pressungen,
                die auch MakeMKV selbst nicht kennt. Der Regelfall läuft über den
                Schlüsselspeicher im Block darüber. Dateiname GROSS geschrieben, unter
                Linux case-sensitiv. Rippy stellt nur den Platz bereit.
              */}
              <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
                <div className="flex items-center justify-between">
                  <p className="text-sm font-semibold text-slate-900 dark:text-slate-200 flex items-center gap-2">
                    <KeyRound size={16} className="text-amber-500" />
                    KEYDB.cfg (Notnagel)
                  </p>
                  <div className="flex items-center gap-2">
                    {/*
                      Versteckter Datei-Dialog: der Inhalt wird im Browser gelesen und als
                      JSON gepostet. Kein Multipart — der API fehlt python-multipart.
                    */}
                    <input
                      type="file"
                      className="hidden"
                      ref={keydbInput}
                      accept=".cfg,text/plain"
                      onChange={(e) => {
                        const datei = e.target.files?.[0]
                        // Wert leeren, damit dieselbe Datei erneut gewählt werden kann.
                        e.target.value = ''
                        if (datei) keydbHochladen(datei)
                      }}
                    />
                    <Button
                      variant="amber"
                      size="sm"
                      onClick={() => keydbInput.current?.click()}
                      disabled={keydbBusy}
                    >
                      <Upload size={14} />
                      {keydbBusy ? 'Arbeite…' : 'KEYDB.cfg hochladen'}
                    </Button>
                    {keydb?.vorhanden && (
                      <Button variant="danger" size="sm" onClick={keydbEntfernen} disabled={keydbBusy}>
                        <Trash2 size={14} />
                        Entfernen
                      </Button>
                    )}
                  </div>
                </div>

                {keydb?.vorhanden ? (
                  <div className="p-3 rounded-lg text-sm bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30">
                    <p className="font-semibold">Eine KEYDB.cfg liegt bereit.</p>
                    <p className="text-xs mt-1">
                      {bytesLesbar(keydb.groesse_bytes)} · {keydb.eintraege} Zeilen mit Disc-Kennung ·
                      zuletzt geändert {zeitLesbar(keydb.geaendert)}
                    </p>
                    <p className="text-xs mt-1 font-mono opacity-80">{keydb.pfad}</p>
                  </div>
                ) : (
                  <div className="p-3 rounded-lg text-sm bg-slate-200 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-300 dark:border-slate-700">
                    <p className="font-semibold">Keine KEYDB.cfg hinterlegt.</p>
                    <p className="text-xs mt-1">
                      Das ist normal und meistens auch nicht nötig — der Regelfall läuft über den
                      Schlüsselspeicher oben.
                    </p>
                  </div>
                )}

                <p className="text-xs text-slate-500 dark:text-slate-400">
                  Die <strong className="text-slate-700 dark:text-slate-300">KEYDB.cfg</strong> ist eine reine
                  Textdatei mit Disc-Schlüsseln. Sie ist der <strong className="text-slate-700 dark:text-slate-300">
                  Notnagel</strong> für den Fall, dass eine Pressung selbst über den Schlüsselspeicher nicht
                  aufgeht — also auch MakeMKV sie nicht kennt. Zuerst immer den Weg darüber versuchen.
                </p>
                <p className="text-xs text-slate-500 dark:text-slate-400">
                  <strong className="text-slate-700 dark:text-slate-300">Wichtig:</strong> Rippy liefert keine
                  Schlüssel mit, lädt keine herunter und verteilt keine. Rippy stellt nur den Platz für eine Datei
                  bereit, die du selbst mitbringst, und zeigt dir ehrlich an, was dort liegt. Die Zahl oben ist
                  genau das: die gezählten Zeilen mit einer Disc-Kennung in deiner Datei — keine von MakeMKV
                  bestätigte Anzahl brauchbarer Schlüssel.
                </p>
                <p className="text-xs text-slate-500 dark:text-slate-400">
                  Eine hochgeladene Datei ersetzt die bisherige und wirkt
                  <strong className="text-slate-700 dark:text-slate-300"> ab dem nächsten Rip</strong> — laufende
                  Jobs bleiben unberührt, ein Neustart ist nicht nötig. Die Plakette am Worker weiter oben
                  folgt erst mit dessen nächstem Herzschlag (etwa eine Minute) — dieser Kasten hier ist sofort
                  aktuell.
                </p>
              </div>

              {/*
                AACS-Dumps: MakeMKV legt sie bei einer unbekannten UHD-Disc ab (Meldung 3332).
                Seit das Datenverzeichnis persistent gemountet ist, überleben sie den
                Container-Neustart — vorher waren sie nach jedem Rip weg.
              */}
              <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
                <div className="flex items-center justify-between">
                  <p className="text-sm font-semibold text-slate-900 dark:text-slate-200">AACS-Dumps</p>
                  <Button variant="secondary" size="sm" onClick={keydbLaden}>
                    Liste aktualisieren
                  </Button>
                </div>
                {dumps.length === 0 ? (
                  <p className="text-xs text-slate-500 dark:text-slate-400">
                    Noch kein Dump vorhanden. MakeMKV legt hier automatisch einen ab, sobald eine 4K-UHD-Disc
                    an einem unbekannten Schlüssel scheitert.
                  </p>
                ) : (
                  <div className="space-y-1.5">
                    {dumps.map(d => (
                      <a
                        key={d.name}
                        href={`/api/system/aacs-dumps/${encodeURIComponent(d.name)}`}
                        download
                        className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors text-amber-600 dark:text-amber-300 hover:bg-slate-100 dark:hover:bg-slate-800"
                      >
                        <Download size={14} className="flex-shrink-0" />
                        <span className="truncate min-w-0 font-mono">{d.name}</span>
                        <span className="ml-auto text-xs flex-shrink-0 text-slate-500 dark:text-slate-400 font-mono">
                          {bytesLesbar(d.groesse_bytes)} · {zeitLesbar(d.geaendert)}
                        </span>
                      </a>
                    ))}
                  </div>
                )}
                <p className="text-xs text-slate-500 dark:text-slate-400">
                  Ein AACS-Dump ist das, was das Laufwerk von der Disc gelesen hat, bevor der Schlüssel fehlte.
                  Er enthält keinen Schlüssel und nützt dir allein nichts — aber du kannst ihn herunterladen und
                  im MakeMKV-Forum im Bereich <strong className="text-slate-700 dark:text-slate-300">Ultra HD
                  Blu-ray</strong> einreichen. Dort können Leute daraus den Schlüssel für deine Pressung ermitteln;
                  den trägst du dann in deine KEYDB.cfg ein.
                </p>
              </div>

              <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
                <div className="flex items-center justify-between">
                  <p className="text-sm font-semibold text-slate-900 dark:text-slate-200">Update-Check</p>
                  <Button variant="secondary" size="sm" onClick={updatesPruefen} disabled={updatesBusy}>
                    {updatesBusy ? 'Prüfe…' : 'Auf Updates prüfen'}
                  </Button>
                </div>
                {updates ? (
                  <div className="space-y-3 text-sm text-slate-700 dark:text-slate-300">
                    {/* MakeMKV = aktualisierbar → hier ist ein Update ein echtes To-do */}
                    <div className={updates.makemkv?.update ? 'text-amber-600 dark:text-amber-400' : ''}>
                      <strong>MakeMKV:</strong> installiert {updates.makemkv?.installiert || '—'}, neueste {updates.makemkv?.verfuegbar || 'unbekannt'}
                      {updates.makemkv?.update
                        ? (
                          <p className="text-xs mt-1">
                            ⚠️ Update verfügbar — in der .env <code>MAKEMKV_VERSION={updates.makemkv.verfuegbar}</code> setzen,
                            dann auf der Rippy-Maschine <code>docker compose build worker &amp;&amp; docker compose up -d worker</code>.
                            {/* 25.07.2026 richtiggestellt: ein Update bringt KEINE Disc-Schluessel mit. */}
                            Neue Versionen bringen Laufwerks-Unterstützung und Fehlerbehebungen — aber
                            <strong> keine Disc-Schlüssel</strong>: die kommen aus dem Schlüsselspeicher.
                          </p>
                        )
                        : <span className="text-xs ml-1 text-emerald-600 dark:text-emerald-400">✓ aktuell</span>}
                    </div>
                    {/* HandBrake im Docker = bewusst Debian-stabil → KEIN To-do, nur Info */}
                    <div>
                      <strong>HandBrake (Docker-Worker):</strong> {updates.handbrake?.installiert || '—'}
                      <span className="text-xs ml-1 text-slate-500 dark:text-slate-400">— stabile Debian-Version, bewusst so, kein Update nötig</span>
                      <p className="text-xs mt-1 text-slate-500 dark:text-slate-400">
                        Neueste offizielle Version wäre {updates.handbrake?.verfuegbar || 'unbekannt'} — die nutzen die
                        <strong> nativen Worker (Windows)</strong> automatisch. Der Docker-Worker bleibt bei der
                        Debian-Version (für die Kompression identisch, robustere Builds).
                      </p>
                    </div>
                  </div>
                ) : (
                  <p className="text-xs text-slate-500 dark:text-slate-400">
                    Prüft gegen makemkv.com und die HandBrake-GitHub-Releases (Ergebnis wird 12 h gemerkt).
                  </p>
                )}
              </div>
            </section>
          )}

          {/* Global Save Button */}
          {activeTab !== 'worker' && (
            <div className="flex items-center justify-end gap-4 pt-6 border-t border-slate-200/80 dark:border-slate-800">
              <Button
                variant="amber"
                size="lg"
                onClick={handleSave}
                disabled={saving}
              >
                {saving ? (
                  <>
                    <div className="animate-spin rounded-full h-4 w-4 border-2 border-slate-950 border-t-transparent"></div>
                    Speichern...
                  </>
                ) : (
                  <>
                    <Save size={18} />
                    Einstellungen speichern
                  </>
                )}
              </Button>
            </div>
          )}
        </CardContent>
      </Card>
    </div>
  )
}
