import { useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { Card } from 'primereact/card';
import { DataView, DataViewLayoutOptions } from 'primereact/dataview';
import { InputText } from 'primereact/inputtext';
import { Dropdown } from 'primereact/dropdown';
import { Button } from 'primereact/button';
import { Tag } from 'primereact/tag';
import { Toast } from 'primereact/toast';
import { Dialog } from 'primereact/dialog';
import { api } from '../api/client';
import JobDetailDialog from '../components/JobDetailDialog';
import MetadataSelectionDialog from '../components/MetadataSelectionDialog';
import CdMetadataDialog from '../components/CdMetadataDialog';
import ReencodeConflictModal from '../components/ReencodeConflictModal';
import blurayIndicatorIcon from '../assets/media-bluray.svg';
import discIndicatorIcon from '../assets/media-disc.svg';
import otherIndicatorIcon from '../assets/media-other.svg';
import {
  getStatusLabel,
  getStatusSeverity,
  normalizeStatus,
  STATUS_FILTER_OPTIONS
} from '../utils/statusPresentation';
import { confirmModal } from '../utils/confirmModal';
import { isSeriesDvdJob } from '../utils/jobTaxonomy';

const MEDIA_FILTER_OPTIONS = [
  { label: 'Alle Medien', value: '' },
  { label: 'Blu-ray', value: 'bluray' },
  { label: 'DVD', value: 'dvd' },
  { label: 'DVD Serie', value: 'dvd_series' },
  { label: 'Audio CD', value: 'cd' },
  { label: 'Audiobook', value: 'audiobook' },
  { label: 'Sonstiges', value: 'other' }
];

const SORT_OPTIONS = [
  { label: 'Startzeit: Neu -> Alt', value: '!sortStartTime' },
  { label: 'Startzeit: Alt -> Neu', value: 'sortStartTime' },
  { label: 'Endzeit: Neu -> Alt', value: '!sortEndTime' },
  { label: 'Endzeit: Alt -> Neu', value: 'sortEndTime' },
  { label: 'Titel: A -> Z', value: 'sortTitle' },
  { label: 'Titel: Z -> A', value: '!sortTitle' },
  { label: 'Medium: A -> Z', value: 'sortMediaType' },
  { label: 'Medium: Z -> A', value: '!sortMediaType' }
];

const CD_FORMAT_LABELS = {
  flac: 'FLAC',
  wav: 'WAV',
  mp3: 'MP3',
  opus: 'Opus',
  ogg: 'Ogg Vorbis'
};

function normalizePositiveInteger(value) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed <= 0) {
    return null;
  }
  return Math.trunc(parsed);
}

function resolveMediaType(row) {
  const encodePlan = row?.encodePlan && typeof row.encodePlan === 'object' ? row.encodePlan : null;
  const candidates = [
    row?.mediaType,
    row?.media_type,
    row?.mediaProfile,
    row?.media_profile,
    encodePlan?.mediaProfile,
    row?.makemkvInfo?.analyzeContext?.mediaProfile,
    row?.makemkvInfo?.mediaProfile,
    row?.mediainfoInfo?.mediaProfile
  ];
  for (const candidate of candidates) {
    const raw = String(candidate || '').trim().toLowerCase();
    if (!raw) {
      continue;
    }
    if (['bluray', 'blu-ray', 'blu_ray', 'bd', 'bdmv', 'bdrom', 'bd-rom', 'bd-r', 'bd-re'].includes(raw)) {
      return 'bluray';
    }
    if (['dvd', 'disc', 'dvdvideo', 'dvd-video', 'dvdrom', 'dvd-rom', 'video_ts', 'iso9660'].includes(raw)) {
      return 'dvd';
    }
    if (['cd', 'audio_cd', 'audio cd'].includes(raw)) {
      return 'cd';
    }
    if (['audiobook', 'audio_book', 'audio book', 'book'].includes(raw)) {
      return 'audiobook';
    }
    if (raw === 'converter') {
      return 'converter';
    }
  }
  const statusCandidates = [
    row?.status,
    row?.last_state,
    row?.makemkvInfo?.lastState
  ];
  if (statusCandidates.some((value) => String(value || '').trim().toUpperCase().startsWith('CD_'))) {
    return 'cd';
  }
  const planFormat = String(encodePlan?.format || '').trim().toLowerCase();
  const hasCdTracksInPlan = Array.isArray(encodePlan?.selectedTracks) && encodePlan.selectedTracks.length > 0;
  if (hasCdTracksInPlan && ['flac', 'wav', 'mp3', 'opus', 'ogg'].includes(planFormat)) {
    return 'cd';
  }
  if (String(row?.handbrakeInfo?.mode || '').trim().toLowerCase() === 'cd_rip') {
    return 'cd';
  }
  if (Array.isArray(row?.makemkvInfo?.tracks) && row.makemkvInfo.tracks.length > 0) {
    return 'cd';
  }
  if (['audiobook_encode', 'audiobook_encode_split'].includes(String(row?.handbrakeInfo?.mode || '').trim().toLowerCase())) {
    return 'audiobook';
  }
  if (String(encodePlan?.mode || '').trim().toLowerCase() === 'audiobook') {
    return 'audiobook';
  }
  return 'other';
}

function resolveMediaTypeMeta(row) {
  const mediaType = resolveMediaType(row);
  const isSeriesDvd = mediaType === 'dvd' && isSeriesDvdJob(row);
  if (mediaType === 'bluray') {
    return {
      mediaType,
      icon: blurayIndicatorIcon,
      label: 'Blu-ray',
      alt: 'Blu-ray'
    };
  }
  if (mediaType === 'dvd') {
    return {
      mediaType,
      icon: discIndicatorIcon,
      label: isSeriesDvd ? 'DVD Serie' : 'DVD',
      alt: isSeriesDvd ? 'DVD Serie' : 'DVD'
    };
  }
  if (mediaType === 'cd') {
    return {
      mediaType,
      icon: otherIndicatorIcon,
      label: 'Audio CD',
      alt: 'Audio CD'
    };
  }
  if (mediaType === 'audiobook') {
    return {
      mediaType,
      icon: otherIndicatorIcon,
      label: 'Audiobook',
      alt: 'Audiobook'
    };
  }
  if (mediaType === 'converter') {
    return {
      mediaType,
      icon: otherIndicatorIcon,
      label: 'Converter',
      alt: 'Converter'
    };
  }
  return {
    mediaType,
    icon: otherIndicatorIcon,
    label: 'Sonstiges',
    alt: 'Sonstiges Medium'
  };
}

function isSeriesBatchChildHistoryRow(row) {
  const encodePlan = row?.encodePlan && typeof row.encodePlan === 'object' ? row.encodePlan : null;
  if (!encodePlan) {
    return false;
  }
  return Boolean(encodePlan?.seriesBatchChild || encodePlan?.seriesBatchVirtualEpisode);
}

function formatDurationSeconds(totalSeconds) {
  const parsed = Number(totalSeconds);
  if (!Number.isFinite(parsed) || parsed <= 0) {
    return null;
  }
  const rounded = Math.max(0, Math.trunc(parsed));
  const hours = Math.floor(rounded / 3600);
  const minutes = Math.floor((rounded % 3600) / 60);
  const seconds = rounded % 60;
  if (hours > 0) {
    return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  }
  return `${minutes}:${String(seconds).padStart(2, '0')}`;
}

function resolveCdDetails(row) {
  const encodePlan = row?.encodePlan && typeof row.encodePlan === 'object' ? row.encodePlan : {};
  const makemkvInfo = row?.makemkvInfo && typeof row.makemkvInfo === 'object' ? row.makemkvInfo : {};
  const selectedMetadata = makemkvInfo?.selectedMetadata && typeof makemkvInfo.selectedMetadata === 'object'
    ? makemkvInfo.selectedMetadata
    : {};
  const tracksSource = Array.isArray(makemkvInfo?.tracks) && makemkvInfo.tracks.length > 0
    ? makemkvInfo.tracks
    : (Array.isArray(encodePlan?.tracks) ? encodePlan.tracks : []);
  const tracks = tracksSource
    .map((track) => {
      const position = normalizePositiveInteger(track?.position);
      if (!position) {
        return null;
      }
      return {
        ...track,
        position,
        selected: track?.selected !== false
      };
    })
    .filter(Boolean);
  const selectedTracksFromPlan = Array.isArray(encodePlan?.selectedTracks)
    ? encodePlan.selectedTracks
      .map((value) => normalizePositiveInteger(value))
      .filter(Boolean)
    : [];
  const selectedTrackPositions = selectedTracksFromPlan.length > 0
    ? selectedTracksFromPlan
    : tracks.filter((track) => track.selected !== false).map((track) => track.position);
  const fallbackArtist = tracks
    .map((track) => String(track?.artist || '').trim())
    .find(Boolean) || null;
  const totalDurationSec = tracks.reduce((sum, track) => {
    const durationMs = Number(track?.durationMs);
    const durationSec = Number(track?.durationSec);
    if (Number.isFinite(durationMs) && durationMs > 0) {
      return sum + (durationMs / 1000);
    }
    if (Number.isFinite(durationSec) && durationSec > 0) {
      return sum + durationSec;
    }
    return sum;
  }, 0);
  const format = String(encodePlan?.format || '').trim().toLowerCase();
  const mbId = String(
    selectedMetadata?.mbId
    || selectedMetadata?.musicBrainzId
    || selectedMetadata?.musicbrainzId
    || selectedMetadata?.musicbrainz_id
    || selectedMetadata?.music_brainz_id
    || selectedMetadata?.musicbrainz
    || selectedMetadata?.mbid
    || row?.imdb_id
    || ''
  ).trim() || null;

  return {
    artist: String(selectedMetadata?.artist || '').trim() || fallbackArtist || null,
    trackCount: tracks.length,
    selectedTrackCount: selectedTrackPositions.length,
    format,
    formatLabel: format ? (CD_FORMAT_LABELS[format] || format.toUpperCase()) : null,
    totalDurationLabel: formatDurationSeconds(totalDurationSec),
    mbId
  };
}

function resolveAudiobookDetails(row) {
  const encodePlan = row?.encodePlan && typeof row.encodePlan === 'object' ? row.encodePlan : {};
  const selectedMetadata = row?.makemkvInfo?.selectedMetadata && typeof row.makemkvInfo.selectedMetadata === 'object'
    ? row.makemkvInfo.selectedMetadata
    : (encodePlan?.metadata && typeof encodePlan.metadata === 'object' ? encodePlan.metadata : {});
  const chapters = Array.isArray(selectedMetadata?.chapters)
    ? selectedMetadata.chapters
    : (Array.isArray(row?.makemkvInfo?.chapters) ? row.makemkvInfo.chapters : []);
  const format = String(
    row?.handbrakeInfo?.format
    || encodePlan?.format
    || ''
  ).trim().toLowerCase() || null;
  return {
    author: String(selectedMetadata?.author || selectedMetadata?.artist || '').trim() || null,
    narrator: String(selectedMetadata?.narrator || '').trim() || null,
    chapterCount: chapters.length,
    formatLabel: format ? format.toUpperCase() : null
  };
}

function resolveConverterMediaType(row) {
  const encodePlan = row?.encodePlan && typeof row.encodePlan === 'object' ? row.encodePlan : {};
  const value = String(
    encodePlan?.converterMediaType
    || row?.converterMediaType
    || ''
  ).trim().toLowerCase();
  if (value === 'audio') {
    return 'audio';
  }
  if (value === 'video' || value === 'iso') {
    return 'video';
  }
  return null;
}

function hasAudioOutput(row) {
  const mediaType = resolveMediaType(row);
  if (mediaType === 'cd' || mediaType === 'audiobook') {
    return true;
  }
  if (mediaType === 'converter') {
    return resolveConverterMediaType(row) === 'audio';
  }
  return false;
}

function getOutputLabelForRow(row) {
  if (resolveMediaType(row) === 'audiobook') {
    return 'Audiobook-Datei(en)';
  }
  if (hasAudioOutput(row)) {
    return 'Audio-Dateien';
  }
  return 'Movie-Datei(en)';
}

function getOutputShortLabelForRow(row) {
  if (resolveMediaType(row) === 'audiobook') {
    return 'Audiobook';
  }
  if (hasAudioOutput(row)) {
    return 'Audio';
  }
  return 'Movie';
}

function normalizeJobId(value) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed <= 0) {
    return null;
  }
  return Math.trunc(parsed);
}

function getQueueActionResult(response) {
  return response?.result && typeof response.result === 'object' ? response.result : {};
}

function normalizeSortText(value) {
  return String(value || '').trim().toLocaleLowerCase('de-DE');
}

function sanitizeRating(value) {
  const raw = String(value || '').trim();
  if (!raw || raw.toUpperCase() === 'N/A') {
    return null;
  }
  return raw;
}

function findOmdbRatingBySource(omdbInfo, sourceName) {
  const ratings = Array.isArray(omdbInfo?.Ratings) ? omdbInfo.Ratings : [];
  const source = String(sourceName || '').trim().toLowerCase();
  const entry = ratings.find((item) => String(item?.Source || '').trim().toLowerCase() === source);
  return sanitizeRating(entry?.Value);
}

function resolveRatings(row) {
  const omdbInfo = row?.omdbInfo && typeof row.omdbInfo === 'object' ? row.omdbInfo : null;
  if (!omdbInfo) {
    return [];
  }

  const imdb = sanitizeRating(omdbInfo?.imdbRating)
    || findOmdbRatingBySource(omdbInfo, 'Internet Movie Database');
  const rotten = findOmdbRatingBySource(omdbInfo, 'Rotten Tomatoes');
  const metascore = sanitizeRating(omdbInfo?.Metascore);

  const ratings = [];
  if (imdb) {
    ratings.push({ key: 'imdb', label: 'IMDb', value: imdb });
  }
  if (rotten) {
    ratings.push({ key: 'rt', label: 'RT', value: rotten });
  }
  if (metascore) {
    ratings.push({ key: 'meta', label: 'Meta', value: metascore });
  }
  return ratings;
}

function formatDateTime(value) {
  if (!value) {
    return '-';
  }
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) {
    return String(value);
  }
  return date.toLocaleString('de-DE', {
    dateStyle: 'short',
    timeStyle: 'short'
  });
}

function parseSortableTimestamp(value) {
  if (!value) {
    return null;
  }
  const ts = Date.parse(value);
  return Number.isFinite(ts) ? ts : null;
}

function resolveSortTimestamp(...candidates) {
  for (const candidate of candidates) {
    const ts = parseSortableTimestamp(candidate);
    if (ts != null) {
      return ts;
    }
  }
  return 0;
}

export default function HistoryPage({ refreshToken = 0 }) {
  const location = useLocation();
  const navigate = useNavigate();
  const [jobs, setJobs] = useState([]);
  const [search, setSearch] = useState('');
  const [status, setStatus] = useState('');
  const [mediumFilter, setMediumFilter] = useState('');
  const [layout, setLayout] = useState('grid');
  const [sortKey, setSortKey] = useState('!sortStartTime');
  const [sortField, setSortField] = useState('sortStartTime');
  const [sortOrder, setSortOrder] = useState(-1);
  const [selectedJob, setSelectedJob] = useState(null);
  const [detailVisible, setDetailVisible] = useState(false);
  const [detailLoading, setDetailLoading] = useState(false);
  const [logLoadingMode, setLogLoadingMode] = useState(null);
  const [actionBusy, setActionBusy] = useState(false);
  const [reencodeBusyJobId, setReencodeBusyJobId] = useState(null);
  const [deleteEntryBusy, setDeleteEntryBusy] = useState(false);
  const [deleteEntryDialogVisible, setDeleteEntryDialogVisible] = useState(false);
  const [deleteEntryDialogRow, setDeleteEntryDialogRow] = useState(null);
  const [deleteEntryPreview, setDeleteEntryPreview] = useState(null);
  const [deleteEntryPreviewLoading, setDeleteEntryPreviewLoading] = useState(false);
  const [deleteEntryTargetBusy, setDeleteEntryTargetBusy] = useState(null);
  const [deleteEntrySelectedMoviePaths, setDeleteEntrySelectedMoviePaths] = useState([]);
  const [downloadBusyTarget, setDownloadBusyTarget] = useState(null);
  const [downloadFolderBusyPath, setDownloadFolderBusyPath] = useState(null);
  const [metadataDialogVisible, setMetadataDialogVisible] = useState(false);
  const [metadataDialogContext, setMetadataDialogContext] = useState(null);
  const [omdbAssignBusy, setOmdbAssignBusy] = useState(false);
  const [cdMetadataDialogVisible, setCdMetadataDialogVisible] = useState(false);
  const [cdMetadataDialogContext, setCdMetadataDialogContext] = useState(null);
  const [cdMetadataAssignBusy, setCdMetadataAssignBusy] = useState(false);
  const [acknowledgeErrorBusy, setAcknowledgeErrorBusy] = useState(false);
  const [loading, setLoading] = useState(false);
  const [queuedJobIds, setQueuedJobIds] = useState([]);
  const [conflictModalVisible, setConflictModalVisible] = useState(false);
  const [conflictModalJob, setConflictModalJob] = useState(null);
  const [conflictModalFolders, setConflictModalFolders] = useState([]);
  const [conflictModalAction, setConflictModalAction] = useState(null); // 'reencode' | 'restart_encode' | 'restart_review' | 'restart_cd_review' | 'delete_output'
  const [conflictModalMode, setConflictModalMode] = useState('reencode'); // 'reencode' | 'delete'
  const [conflictModalBusy, setConflictModalBusy] = useState(false);
  const toastRef = useRef(null);

  const queuedJobIdSet = useMemo(() => {
    const next = new Set();
    for (const value of Array.isArray(queuedJobIds) ? queuedJobIds : []) {
      const id = normalizeJobId(value);
      if (id) {
        next.add(id);
      }
    }
    return next;
  }, [queuedJobIds]);

  const preparedJobs = useMemo(
    () => jobs
      .filter((job) => !isSeriesBatchChildHistoryRow(job))
      .map((job) => ({
        ...job,
        sortTitle: normalizeSortText(job?.title || job?.detected_title || ''),
        sortMediaType: resolveMediaType(job),
        sortStartTime: resolveSortTimestamp(job?.start_time, job?.updated_at, job?.created_at, job?.end_time),
        sortEndTime: resolveSortTimestamp(job?.end_time, job?.updated_at, job?.created_at, job?.start_time)
      })),
    [jobs]
  );

  const visibleJobs = useMemo(
    () => (mediumFilter
      ? preparedJobs.filter((job) => {
        if (mediumFilter === 'dvd_series') {
          return job.sortMediaType === 'dvd' && isSeriesDvdJob(job);
        }
        return job.sortMediaType === mediumFilter;
      })
      : preparedJobs),
    [preparedJobs, mediumFilter]
  );

  const load = async () => {
    setLoading(true);
    try {
      const [jobsResponse, queueResponse] = await Promise.allSettled([
        api.getJobs({ search, status }),
        api.getPipelineQueue()
      ]);
      if (jobsResponse.status === 'fulfilled') {
        setJobs(jobsResponse.value.jobs || []);
      } else {
        setJobs([]);
      }
      if (queueResponse.status === 'fulfilled') {
        const queuedRows = Array.isArray(queueResponse.value?.queue?.queuedJobs)
          ? queueResponse.value.queue.queuedJobs
          : [];
        const queuedIds = queuedRows
          .map((item) => normalizeJobId(item?.jobId))
          .filter(Boolean);
        setQueuedJobIds(queuedIds);
      } else {
        setQueuedJobIds([]);
      }
    } catch (error) {
      toastRef.current?.show({ severity: 'error', summary: 'Fehler', detail: error.message });
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    api.getPref('history_layout').then((res) => {
      if (res?.value === 'list' || res?.value === 'grid') {
        setLayout(res.value);
      }
    }).catch(() => {});
  }, []);

  useEffect(() => {
    const timer = setTimeout(() => {
      load();
    }, 300);

    return () => clearTimeout(timer);
  }, [search, status, refreshToken]);

  useEffect(() => {
    const params = new URLSearchParams(location.search);
    const openJobId = Number(params.get('open') || 0);
    if (!openJobId) {
      return;
    }
    // URL-Parameter entfernen, dann Job-Modal öffnen
    navigate('/history', { replace: true });
    openDetail({ id: openJobId });
  }, [location.search]);

  const onSortChange = (event) => {
    const value = String(event.value || '').trim();
    if (!value) {
      setSortKey('!sortStartTime');
      setSortField('sortStartTime');
      setSortOrder(-1);
      return;
    }

    if (value.startsWith('!')) {
      setSortOrder(-1);
      setSortField(value.substring(1));
    } else {
      setSortOrder(1);
      setSortField(value);
    }
    setSortKey(value);
  };

  const openDetail = async (row) => {
    const jobId = Number(row?.id || 0);
    if (!jobId) {
      return;
    }

    setSelectedJob({
      ...row,
      logs: [],
      log: '',
      logMeta: {
        loaded: false,
        total: Number(row?.log_count || 0),
        returned: 0,
        truncated: false
      }
    });
    setDetailVisible(true);
    setDetailLoading(true);

    try {
      const response = await api.getJob(jobId, { includeLogs: false, forceRefresh: true });
      setSelectedJob(response.job);
    } catch (error) {
      toastRef.current?.show({ severity: 'error', summary: 'Fehler', detail: error.message });
    } finally {
      setDetailLoading(false);
    }
  };

  const handleLoadLog = async (job, mode = 'tail') => {
    const jobId = Number(job?.id || selectedJob?.id || 0);
    if (!jobId) {
      return;
    }

    setLogLoadingMode(mode);
    try {
      const response = await api.getJob(jobId, {
        includeLogs: true,
        includeAllLogs: mode === 'all',
        logTailLines: mode === 'all' ? null : 800
      });
      setSelectedJob(response.job);
    } catch (error) {
      toastRef.current?.show({ severity: 'error', summary: 'Log konnte nicht geladen werden', detail: error.message });
    } finally {
      setLogLoadingMode(null);
    }
  };

  const refreshDetailIfOpen = async (jobId) => {
    if (!detailVisible || Number(selectedJob?.id || 0) !== Number(jobId || 0)) {
      return;
    }
    const response = await api.getJob(jobId, { includeLogs: false, forceRefresh: true });
    setSelectedJob(response.job);
  };

  const refreshDetailAfterReplacement = async (sourceJobId, replacementJobId = null) => {
    if (!detailVisible) {
      return;
    }

    const sourceId = Number(sourceJobId || 0);
    const replacementId = Number(replacementJobId || 0);
    const selectedId = Number(selectedJob?.id || 0);
    const hasReplacement = replacementId > 0 && replacementId !== sourceId;

    if (hasReplacement && selectedId === sourceId) {
      const response = await api.getJob(replacementId, { includeLogs: false, forceRefresh: true });
      setSelectedJob(response.job);
      return;
    }

    await refreshDetailIfOpen(hasReplacement ? replacementId : sourceId);
  };

  // ── Conflict Modal Helpers ─────────────────────────────────────────────────

  const mergeOutputFoldersForJob = (job, folders = []) => {
    const normalizedJobId = Number(job?.id || 0);
    const merged = [];
    const seen = new Set();
    const addFolder = (folder) => {
      const outputPath = String(folder?.output_path || '').trim();
      if (!outputPath || seen.has(outputPath)) {
        return;
      }
      seen.add(outputPath);
      merged.push({
        id: folder?.id ?? 0,
        job_id: folder?.job_id ?? (normalizedJobId || null),
        output_path: outputPath,
        label: folder?.label || null,
        created_at: folder?.created_at || null
      });
    };

    for (const folder of (Array.isArray(folders) ? folders : [])) {
      addFolder(folder);
    }
    const currentPath = String(job?.output_path || '').trim();
    if (currentPath && !seen.has(currentPath) && Boolean(job?.outputStatus?.exists)) {
      addFolder({
        id: 0,
        job_id: normalizedJobId,
        output_path: currentPath,
        label: 'Aktuelle Ausgabe',
        created_at: null
      });
    }
    return merged;
  };

  const loadOutputFoldersForJob = async (job) => {
    if (!job?.id) {
      return [];
    }
    try {
      const response = await api.getOutputFolders(job.id);
      return mergeOutputFoldersForJob(job, response?.folders);
    } catch (_error) {
      return mergeOutputFoldersForJob(job, []);
    }
  };

  const openConflictModal = async (job, action, mode = 'reencode', preloadedFolders = null) => {
    const folders = Array.isArray(preloadedFolders)
      ? mergeOutputFoldersForJob(job, preloadedFolders)
      : await loadOutputFoldersForJob(job);
    if (folders.length === 0) {
      return false;
    }
    setConflictModalJob(job);
    setConflictModalAction(action);
    setConflictModalMode(mode);
    setConflictModalFolders(folders);
    setConflictModalVisible(true);
    return true;
  };

  const closeConflictModal = () => {
    if (conflictModalBusy) return;
    setConflictModalVisible(false);
    setConflictModalJob(null);
    setConflictModalAction(null);
    setConflictModalMode('reencode');
    setConflictModalFolders([]);
  };

  const executeHistoryAction = async (job, action, options = {}, executionOptions = {}) => {
    const skipConfirm = Boolean(executionOptions?.skipConfirm);
    if (action === 'reencode') {
      setReencodeBusyJobId(job.id);
      try {
        await api.reencodeJob(job.id, options);
        toastRef.current?.show({ severity: 'success', summary: 'Re-Encode gestartet', detail: 'Job wurde in die Mediainfo-Prüfung gesetzt.', life: 3500 });
        await load();
        await refreshDetailIfOpen(job.id);
      } catch (error) {
        toastRef.current?.show({ severity: 'error', summary: 'Re-Encode fehlgeschlagen', detail: error.message, life: 4500 });
      } finally {
        setReencodeBusyJobId(null);
      }
    } else if (action === 'restart_encode') {
      setActionBusy(true);
      try {
        const response = await api.restartEncodeWithLastSettings(job.id, options);
        const result = getQueueActionResult(response);
        const replacementJobId = normalizeJobId(result?.jobId);
        if (result.queued) {
          toastRef.current?.show({ severity: 'info', summary: 'Encode-Neustart in Queue', detail: result.queuePosition > 0 ? `Position ${result.queuePosition}` : 'In der Warteschlange eingeplant.', life: 3500 });
        } else {
          toastRef.current?.show({ severity: 'success', summary: 'Encode-Neustart gestartet', detail: 'Letzte bestätigte Einstellungen werden verwendet.', life: 3500 });
        }
        await load();
        await refreshDetailAfterReplacement(job.id, replacementJobId);
      } catch (error) {
        toastRef.current?.show({ severity: 'error', summary: 'Encode-Neustart fehlgeschlagen', detail: error.message, life: 4500 });
      } finally {
        setActionBusy(false);
      }
    } else if (action === 'restart_review') {
      const title = job?.title || job?.detected_title || `Job #${job?.id}`;
      if (!skipConfirm) {
        const confirmed = await confirmModal({
          header: 'Review neu starten',
          message: `Review für "${title}" neu starten?\nDer Job wird erneut analysiert. Spur- und Skriptauswahl kann danach im Ripper neu getroffen werden.`,
          acceptLabel: 'Review starten',
          rejectLabel: 'Abbrechen'
        });
        if (!confirmed) {
          return;
        }
      }
      setActionBusy(true);
      try {
        const response = await api.restartReviewFromRaw(job.id, options);
        const result = getQueueActionResult(response);
        const replacementJobId = normalizeJobId(result?.jobId);
        toastRef.current?.show({
          severity: 'success',
          summary: 'Review-Neustart',
          detail: 'Analyse gestartet. Job ist jetzt im Ripper verfügbar.',
          life: 3500
        });
        await load();
        await refreshDetailAfterReplacement(job.id, replacementJobId);
      } catch (error) {
        toastRef.current?.show({ severity: 'error', summary: 'Review-Neustart fehlgeschlagen', detail: error.message, life: 4500 });
      } finally {
        setActionBusy(false);
      }
    } else if (action === 'restart_cd_review') {
      const title = job?.title || job?.detected_title || `Job #${job?.id}`;
      if (!skipConfirm) {
        const confirmed = await confirmModal({
          header: 'CD-Vorprüfung starten',
          message: `CD-Vorprüfung für "${title}" starten?\nMusicBrainz-Suche, Trackauswahl und Ausgabeeinstellungen werden im Ripper geöffnet.`,
          acceptLabel: 'Vorprüfung starten',
          rejectLabel: 'Abbrechen'
        });
        if (!confirmed) {
          return;
        }
      }
      setActionBusy(true);
      try {
        await api.restartCdReviewFromRaw(job.id, options);
        toastRef.current?.show({
          severity: 'success',
          summary: 'CD-Vorprüfung gestartet',
          detail: 'Job ist jetzt im Ripper verfügbar — bitte Metadaten und Einstellungen wählen.',
          life: 4000
        });
        await load();
        await refreshDetailIfOpen(job.id);
      } catch (error) {
        toastRef.current?.show({ severity: 'error', summary: 'CD-Vorprüfung fehlgeschlagen', detail: error.message, life: 4500 });
      } finally {
        setActionBusy(false);
      }
    } else if (action === 'delete_output') {
      const deleteFolders = Array.isArray(options?.deleteFolders)
        ? options.deleteFolders.filter((value) => typeof value === 'string' && value.trim())
        : [];
      if (deleteFolders.length === 0) {
        toastRef.current?.show({
          severity: 'warn',
          summary: 'Keine Ordner gewählt',
          detail: 'Es wurden keine Output-Ordner zum Löschen erkannt.',
          life: 3500
        });
        return;
      }
      setActionBusy(true);
      try {
        const response = await api.deleteOutputFolders(job.id, deleteFolders);
        const result = response?.result && typeof response.result === 'object' ? response.result : {};
        const deletedCount = Array.isArray(result.deleted) ? result.deleted.length : 0;
        const failedCount = Array.isArray(result.failed) ? result.failed.length : 0;
        toastRef.current?.show({
          severity: failedCount > 0 ? 'warn' : 'success',
          summary: failedCount > 0 ? 'Output teilweise gelöscht' : 'Output gelöscht',
          detail: failedCount > 0
            ? `${deletedCount} Ordner gelöscht, ${failedCount} mit Fehler.`
            : `${deletedCount} Ordner gelöscht.`,
          life: 4000
        });
        await load();
        await refreshDetailIfOpen(job.id);
      } catch (error) {
        toastRef.current?.show({ severity: 'error', summary: 'Output löschen fehlgeschlagen', detail: error.message, life: 4500 });
      } finally {
        setActionBusy(false);
      }
    }
  };

  const handleConflictKeepBoth = async () => {
    const job = conflictModalJob;
    const action = conflictModalAction;
    setConflictModalBusy(true);
    try {
      await executeHistoryAction(job, action, { keepBoth: true }, { skipConfirm: true });
      closeConflictModal();
    } finally {
      setConflictModalBusy(false);
    }
  };

  const handleConflictDeleteSelected = async (selectedPaths) => {
    const job = conflictModalJob;
    const action = conflictModalAction;
    const selected = Array.isArray(selectedPaths)
      ? selectedPaths.filter((value) => typeof value === 'string' && value.trim())
      : [];
    const allKnownPaths = conflictModalFolders
      .map((folder) => String(folder?.output_path || '').trim())
      .filter(Boolean);
    const deleteFolders = selected.length > 0 ? selected : allKnownPaths;
    setConflictModalBusy(true);
    try {
      const options = deleteFolders.length > 0 ? { deleteFolders } : {};
      await executeHistoryAction(job, action, options, { skipConfirm: true });
      closeConflictModal();
    } finally {
      setConflictModalBusy(false);
    }
  };

  // ── File deletion ──────────────────────────────────────────────────────────

  const handleDeleteFiles = async (row, target) => {
    if (target === 'movie') {
      const outputFolders = await loadOutputFoldersForJob(row);
      if (outputFolders.length > 1) {
        await openConflictModal(row, 'delete_output', 'delete', outputFolders);
        return;
      }
    }

    const outputLabel = getOutputLabelForRow(row);
    const outputShortLabel = getOutputShortLabelForRow(row);
    const label = target === 'raw' ? 'RAW-Dateien' : target === 'movie' ? outputLabel : `RAW + ${outputShortLabel}`;
    const title = row.title || row.detected_title || `Job #${row.id}`;
    const confirmed = await confirmModal({
      header: 'Dateien löschen',
      message: `${label} für "${title}" wirklich löschen?`,
      acceptLabel: 'Löschen',
      rejectLabel: 'Abbrechen',
      danger: true
    });
    if (!confirmed) {
      return;
    }

    setActionBusy(true);
    try {
      const response = await api.deleteJobFiles(row.id, target);
      const summary = response.summary || {};
      const rawSummary = summary.raw || {};
      const movieSummary = summary.movie || {};
      const deletedSomething = target === 'raw'
        ? Boolean(rawSummary.deleted)
        : target === 'movie'
          ? Boolean(movieSummary.deleted)
          : Boolean(rawSummary.deleted || movieSummary.deleted);

      if (!deletedSomething) {
        const reason = target === 'raw'
          ? (rawSummary.reason || 'Keine passenden RAW-Dateien/Ordner gefunden.')
          : target === 'movie'
            ? (movieSummary.reason || `Keine passenden ${outputShortLabel}-Dateien/Ordner gefunden.`)
            : (movieSummary.reason || rawSummary.reason || 'Keine passenden Dateien/Ordner gefunden.');
        toastRef.current?.show({
          severity: 'warn',
          summary: 'Nichts gelöscht',
          detail: reason,
          life: 4200
        });
      } else {
        toastRef.current?.show({
          severity: 'success',
          summary: 'Dateien gelöscht',
          detail: `RAW: ${rawSummary.filesDeleted ?? 0}, ${outputShortLabel}: ${movieSummary.filesDeleted ?? 0}`,
          life: 3500
        });
      }
      await load();
      await refreshDetailIfOpen(row.id);
    } catch (error) {
      toastRef.current?.show({ severity: 'error', summary: 'Löschen fehlgeschlagen', detail: error.message, life: 4500 });
    } finally {
      setActionBusy(false);
    }
  };

  const handleDownloadArchive = async (row, target) => {
    const jobId = Number(row?.id || selectedJob?.id || 0);
    const normalizedTarget = String(target || '').trim().toLowerCase();
    if (!jobId || !['raw', 'output'].includes(normalizedTarget)) {
      return;
    }

    setDownloadBusyTarget(normalizedTarget);
    try {
      const response = await api.requestJobArchive(jobId, normalizedTarget);
      const item = response?.item && typeof response.item === 'object' ? response.item : null;
      const label = normalizedTarget === 'raw' ? 'RAW' : 'Encode';
      const isReady = String(item?.status || '').trim().toLowerCase() === 'ready';
      const detail = isReady
        ? `${label}-ZIP ist bereits auf der Downloads-Seite verfuegbar.`
        : `${label}-ZIP wird im Hintergrund erstellt und erscheint danach auf der Downloads-Seite.`;
      toastRef.current?.show({
        severity: isReady ? 'success' : 'info',
        summary: isReady ? 'ZIP bereit' : 'ZIP wird erstellt',
        detail,
        life: 4000
      });
    } catch (error) {
      toastRef.current?.show({
        severity: 'error',
        summary: 'Download fehlgeschlagen',
        detail: error.message,
        life: 4500
      });
    } finally {
      setDownloadBusyTarget(null);
    }
  };

  const handleDownloadOutputFolder = async (row, folderPath) => {
    const jobId = Number(row?.id || selectedJob?.id || 0);
    if (!jobId || !folderPath) return;
    setDownloadFolderBusyPath(folderPath);
    try {
      const response = await api.requestJobArchive(jobId, 'output', { outputPath: folderPath });
      const item = response?.item && typeof response.item === 'object' ? response.item : null;
      const isReady = String(item?.status || '').trim().toLowerCase() === 'ready';
      toastRef.current?.show({
        severity: isReady ? 'success' : 'info',
        summary: isReady ? 'ZIP bereit' : 'ZIP wird erstellt',
        detail: isReady
          ? 'Output-ZIP ist bereits auf der Downloads-Seite verfügbar.'
          : 'Output-ZIP wird im Hintergrund erstellt und erscheint danach auf der Downloads-Seite.',
        life: 4000
      });
    } catch (error) {
      toastRef.current?.show({ severity: 'error', summary: 'Download fehlgeschlagen', detail: error.message, life: 4500 });
    } finally {
      setDownloadFolderBusyPath(null);
    }
  };

  const maybeOpenOutputConflictModal = async (row, action) => {
    const outputFolders = await loadOutputFoldersForJob(row);
    if (outputFolders.length === 0) {
      return false;
    }
    await openConflictModal(row, action, 'reencode', outputFolders);
    return true;
  };

  const handleReencode = async (row) => {
    if (await maybeOpenOutputConflictModal(row, 'reencode')) {
      return;
    }
    await executeHistoryAction(row, 'reencode');
  };

  const handleRestartEncode = async (row) => {
    if (await maybeOpenOutputConflictModal(row, 'restart_encode')) {
      return;
    }
    await executeHistoryAction(row, 'restart_encode');
  };

  const handleRestartReview = async (row) => {
    if (await maybeOpenOutputConflictModal(row, 'restart_review')) {
      return;
    }
    await executeHistoryAction(row, 'restart_review');
  };

  const handleRestartCdReview = async (row) => {
    if (await maybeOpenOutputConflictModal(row, 'restart_cd_review')) {
      return;
    }
    await executeHistoryAction(row, 'restart_cd_review');
  };

  const handleRetry = async (row) => {
    const title = row?.title || row?.detected_title || `Job #${row?.id}`;
    const mediaType = resolveMediaType(row);
    const actionLabel = mediaType === 'cd' ? 'CD-Rip' : 'Retry';
    const confirmed = await confirmModal({
      header: `${actionLabel} neu starten`,
      message: `${actionLabel} für "${title}" neu starten?`,
      acceptLabel: 'Neu starten',
      rejectLabel: 'Abbrechen'
    });
    if (!confirmed) {
      return;
    }

    setActionBusy(true);
    try {
      const response = await api.retryJob(row.id);
      const result = getQueueActionResult(response);
      const replacementJobId = normalizeJobId(result?.jobId);
      toastRef.current?.show({
        severity: result.queued ? 'info' : 'success',
        summary: mediaType === 'cd' ? 'CD-Rip neu gestartet' : 'Retry gestartet',
        detail: result.queued
          ? 'Job wurde in die Warteschlange eingeplant.'
          : (replacementJobId ? `Neuer Job #${replacementJobId} wurde erstellt.` : 'Job wurde neu gestartet.'),
        life: 4000
      });
      await load();
      if (replacementJobId) {
        const detailResponse = await api.getJob(replacementJobId, { includeLogs: false, forceRefresh: true });
        setSelectedJob(detailResponse.job);
        setDetailVisible(true);
      } else {
        await refreshDetailIfOpen(row.id);
      }
    } catch (error) {
      toastRef.current?.show({
        severity: 'error',
        summary: mediaType === 'cd' ? 'CD-Rip Neustart fehlgeschlagen' : 'Retry fehlgeschlagen',
        detail: error.message,
        life: 4500
      });
    } finally {
      setActionBusy(false);
    }
  };

  const handleAssignOmdb = (row) => {
    const jobId = Number(row?.id || 0);
    if (!jobId) return;
    const makemkvInfo = row?.makemkvInfo && typeof row.makemkvInfo === 'object' ? row.makemkvInfo : {};
    const analyzeContext = makemkvInfo?.analyzeContext && typeof makemkvInfo.analyzeContext === 'object'
      ? makemkvInfo.analyzeContext
      : {};
    const selectedMetadata = analyzeContext?.selectedMetadata && typeof analyzeContext.selectedMetadata === 'object'
      ? analyzeContext.selectedMetadata
      : (makemkvInfo?.selectedMetadata && typeof makemkvInfo.selectedMetadata === 'object'
        ? makemkvInfo.selectedMetadata
        : {});
    const isSeriesDvd = resolveMediaType(row) === 'dvd' && isSeriesDvdJob(row);
    const metadataProvider = isSeriesDvd ? 'tmdb' : 'omdb';
    const seasonNumber = selectedMetadata?.seasonNumber ?? analyzeContext?.seriesLookupHint?.seasonNumber ?? null;
    const discNumber = selectedMetadata?.discNumber ?? analyzeContext?.seriesLookupHint?.discNumber ?? null;
    const context = {
      jobId,
      detectedTitle: row?.detected_title || row?.title || '',
      selectedMetadata: {
        ...(selectedMetadata && typeof selectedMetadata === 'object' ? selectedMetadata : {}),
        title: row?.title || selectedMetadata?.title || row?.detected_title || '',
        year: row?.year || selectedMetadata?.year || null,
        imdbId: row?.imdb_id || selectedMetadata?.imdbId || null,
        poster: row?.poster_url || selectedMetadata?.poster || null,
        metadataProvider,
        tmdbId: selectedMetadata?.tmdbId || null,
        providerId: selectedMetadata?.providerId || null,
        metadataKind: selectedMetadata?.metadataKind || (isSeriesDvd ? 'season' : null),
        seasonNumber,
        seasonName: selectedMetadata?.seasonName || null,
        episodeCount: selectedMetadata?.episodeCount || 0,
        episodes: Array.isArray(selectedMetadata?.episodes) ? selectedMetadata.episodes : [],
        ...(discNumber ? { discNumber } : {})
      },
      metadataProvider,
      metadataCandidates: Array.isArray(analyzeContext?.metadataCandidates) ? analyzeContext.metadataCandidates : [],
      seriesAnalysis: analyzeContext?.seriesAnalysis || null,
      seriesLookupHint: analyzeContext?.seriesLookupHint || null,
      omdbCandidates: []
    };
    setMetadataDialogContext(context);
    setMetadataDialogVisible(true);
  };

  const handleOmdbSearch = async (query, options = {}) => {
    const provider = String(
      options?.metadataProvider
      || metadataDialogContext?.metadataProvider
      || 'omdb'
    ).trim().toLowerCase() || 'omdb';
    try {
      const tmdbSeasonHint = metadataDialogContext?.selectedMetadata?.seasonNumber
        ?? metadataDialogContext?.seriesLookupHint?.seasonNumber
        ?? null;
      const response = provider === 'tmdb'
        ? await api.searchTmdbSeries(query, tmdbSeasonHint)
        : await api.searchOmdb(query);
      return response.results || [];
    } catch (error) {
      toastRef.current?.show({
        severity: 'error',
        summary: provider === 'tmdb' ? 'TMDb-Suche fehlgeschlagen' : 'OMDb-Suche fehlgeschlagen',
        detail: error.message
      });
      return [];
    }
  };

  const handleOmdbSubmit = async (payload) => {
    const provider = String(payload?.metadataProvider || metadataDialogContext?.metadataProvider || 'omdb').trim().toLowerCase() || 'omdb';
    setOmdbAssignBusy(true);
    try {
      await api.assignJobOmdb(payload.jobId, payload);
      toastRef.current?.show({
        severity: 'success',
        summary: 'Metadaten zugewiesen',
        detail: provider === 'tmdb' ? 'TMDb-Metadaten wurden aktualisiert.' : 'OMDb-Metadaten wurden aktualisiert.',
        life: 3000
      });
      setMetadataDialogVisible(false);
      setMetadataDialogContext(null);
      await load();
      await refreshDetailIfOpen(payload.jobId);
    } catch (error) {
      toastRef.current?.show({
        severity: 'error',
        summary: provider === 'tmdb' ? 'TMDb-Zuweisung fehlgeschlagen' : 'OMDb-Zuweisung fehlgeschlagen',
        detail: error.message
      });
    } finally {
      setOmdbAssignBusy(false);
    }
  };

  const handleAssignCdMetadata = (row) => {
    const jobId = Number(row?.id || 0);
    if (!jobId) return;
    const makemkvInfo = row?.makemkvInfo && typeof row.makemkvInfo === 'object' ? row.makemkvInfo : {};
    const encodePlan = row?.encodePlan && typeof row.encodePlan === 'object' ? row.encodePlan : {};
    const tracks = Array.isArray(makemkvInfo?.tracks) && makemkvInfo.tracks.length > 0
      ? makemkvInfo.tracks
      : (Array.isArray(encodePlan?.tracks) ? encodePlan.tracks : []);
    const context = {
      jobId,
      detectedTitle: row?.detected_title || row?.title || '',
      tracks,
      selectedMetadata: makemkvInfo?.selectedMetadata || {}
    };
    setCdMetadataDialogContext(context);
    setCdMetadataDialogVisible(true);
  };

  const handleMusicBrainzSearch = async (query) => {
    try {
      const response = await api.searchMusicBrainz(query);
      return response.results || [];
    } catch (error) {
      toastRef.current?.show({ severity: 'error', summary: 'MusicBrainz-Suche fehlgeschlagen', detail: error.message });
      return [];
    }
  };

  const handleMusicBrainzReleaseFetch = async (mbId) => {
    try {
      const response = await api.getMusicBrainzRelease(mbId);
      return response?.release || null;
    } catch (error) {
      toastRef.current?.show({ severity: 'error', summary: 'MusicBrainz-Abruf fehlgeschlagen', detail: error.message });
      return null;
    }
  };

  const handleCdMetadataSubmit = async (payload) => {
    setCdMetadataAssignBusy(true);
    try {
      await api.assignJobCdMetadata(payload.jobId, payload);
      toastRef.current?.show({ severity: 'success', summary: 'Metadaten zugewiesen', detail: 'MusicBrainz-Metadaten wurden aktualisiert.', life: 3000 });
      setCdMetadataDialogVisible(false);
      setCdMetadataDialogContext(null);
      await load();
      await refreshDetailIfOpen(payload.jobId);
    } catch (error) {
      toastRef.current?.show({ severity: 'error', summary: 'Metadaten-Zuweisung fehlgeschlagen', detail: error.message });
    } finally {
      setCdMetadataAssignBusy(false);
    }
  };

  const handleAcknowledgeError = async (job) => {
    const jobId = normalizeJobId(job?.id || selectedJob?.id);
    if (!jobId) {
      return;
    }
    setAcknowledgeErrorBusy(true);
    try {
      const response = await api.acknowledgeJobError(jobId);
      const updated = response?.job || null;
      if (updated) {
        setSelectedJob(updated);
      }
      await load();
      toastRef.current?.show({
        severity: 'success',
        summary: 'Fehler quittiert',
        detail: `Job #${jobId} wurde quittiert.`,
        life: 2500
      });
    } catch (error) {
      toastRef.current?.show({
        severity: 'error',
        summary: 'Quittierung fehlgeschlagen',
        detail: error.message,
        life: 4500
      });
    } finally {
      setAcknowledgeErrorBusy(false);
    }
  };

  const closeDeleteEntryDialog = () => {
    if (deleteEntryTargetBusy) {
      return;
    }
    setDeleteEntryDialogVisible(false);
    setDeleteEntryDialogRow(null);
    setDeleteEntryPreview(null);
    setDeleteEntryPreviewLoading(false);
    setDeleteEntryTargetBusy(null);
    setDeleteEntrySelectedMoviePaths([]);
  };

  const handleDeleteEntry = async (row) => {
    const jobId = Number(row?.id || 0);
    if (!jobId) {
      return;
    }
    setDeleteEntryDialogRow(row);
    setDeleteEntryPreview(null);
    setDeleteEntrySelectedMoviePaths([]);
    setDeleteEntryDialogVisible(true);
    setDeleteEntryPreviewLoading(true);
    setDeleteEntryBusy(true);
    try {
      const response = await api.getJobDeletePreview(jobId, { includeRelated: true });
      const preview = response?.preview || null;
      setDeleteEntryPreview(preview);
      const movieCandidates = Array.isArray(preview?.pathCandidates?.movie) ? preview.pathCandidates.movie : [];
      const defaultSelectedMoviePaths = movieCandidates
        .filter((item) => Boolean(item?.exists))
        .map((item) => String(item?.path || '').trim())
        .filter(Boolean);
      setDeleteEntrySelectedMoviePaths(defaultSelectedMoviePaths);
    } catch (error) {
      toastRef.current?.show({
        severity: 'error',
        summary: 'Löschvorschau fehlgeschlagen',
        detail: error.message,
        life: 4500
      });
      setDeleteEntryDialogVisible(false);
      setDeleteEntryDialogRow(null);
      setDeleteEntryPreview(null);
    } finally {
      setDeleteEntryPreviewLoading(false);
      setDeleteEntryBusy(false);
    }
  };

  const confirmDeleteEntry = async (target) => {
    const normalizedTarget = String(target || '').trim().toLowerCase();
    if (!['raw', 'movie', 'both', 'none'].includes(normalizedTarget)) {
      return;
    }
    const jobId = Number(deleteEntryDialogRow?.id || 0);
    if (!jobId) {
      return;
    }
    if (
      (normalizedTarget === 'movie' || normalizedTarget === 'both')
      && previewMovieExisting.length > 0
      && deleteEntrySelectedMoviePaths.length === 0
    ) {
      toastRef.current?.show({
        severity: 'warn',
        summary: `${deleteEntryOutputShortLabel}-Ordner auswählen`,
        detail: `Bitte mindestens einen ${deleteEntryOutputShortLabel}-Ordner zum Löschen auswählen.`,
        life: 3500
      });
      return;
    }

    setDeleteEntryBusy(true);
    setDeleteEntryTargetBusy(normalizedTarget);
    try {
      const response = await api.deleteJobEntry(jobId, normalizedTarget, {
        includeRelated: true,
        selectedMoviePaths: deleteEntrySelectedMoviePaths
      });
      const deletedJobIds = Array.isArray(response?.deletedJobIds) ? response.deletedJobIds : [];
      const fileSummary = response?.fileSummary || {};
      const rawFiles = Number(fileSummary?.raw?.filesDeleted || 0);
      const movieFiles = Number(fileSummary?.movie?.filesDeleted || 0);
      const rawDirs = Number(fileSummary?.raw?.dirsRemoved || 0);
      const movieDirs = Number(fileSummary?.movie?.dirsRemoved || 0);

      const detail = normalizedTarget === 'none'
        ? `${deletedJobIds.length || 1} Eintrag/Einträge entfernt (Dateien bleiben erhalten)`
        : `${deletedJobIds.length || 1} Eintrag/Einträge entfernt | RAW: ${rawFiles} Dateien, ${rawDirs} Ordner | ${deleteEntryOutputShortLabel}: ${movieFiles} Dateien, ${movieDirs} Ordner`;
      toastRef.current?.show({
        severity: 'success',
        summary: 'Historie gelöscht',
        detail,
        life: 5000
      });

      closeDeleteEntryDialog();
      setDetailVisible(false);
      setSelectedJob(null);
      await load();
    } catch (error) {
      toastRef.current?.show({
        severity: 'error',
        summary: 'Löschen fehlgeschlagen',
        detail: error.message,
        life: 5000
      });
    } finally {
      setDeleteEntryTargetBusy(null);
      setDeleteEntryBusy(false);
    }
  };

  const toggleDeleteMoviePathSelection = (moviePath, checked) => {
    const normalizedPath = String(moviePath || '').trim();
    if (!normalizedPath) {
      return;
    }
    setDeleteEntrySelectedMoviePaths((previous) => {
      const nextSet = new Set((Array.isArray(previous) ? previous : []).map((item) => String(item || '').trim()).filter(Boolean));
      if (checked) {
        nextSet.add(normalizedPath);
      } else {
        nextSet.delete(normalizedPath);
      }
      return previewMoviePaths
        .map((item) => String(item?.path || '').trim())
        .filter((itemPath) => itemPath && nextSet.has(itemPath));
    });
  };

  const handleRemoveFromQueue = async (row) => {
    const jobId = normalizeJobId(row?.id || row);
    if (!jobId) {
      return;
    }

    setActionBusy(true);
    try {
      await api.cancelPipeline(jobId);
      toastRef.current?.show({
        severity: 'success',
        summary: 'Aus Queue entfernt',
        detail: `Job #${jobId} wurde aus der Warteschlange entfernt.`,
        life: 3200
      });
      await load();
      await refreshDetailIfOpen(jobId);
    } catch (error) {
      toastRef.current?.show({
        severity: 'error',
        summary: 'Queue-Entfernung fehlgeschlagen',
        detail: error.message,
        life: 4500
      });
    } finally {
      setActionBusy(false);
    }
  };

  const renderStatusTag = (row) => {
    const normalizedStatus = normalizeStatus(row?.status);
    const rowId = normalizeJobId(row?.id);
    const isQueued = Boolean(rowId && queuedJobIdSet.has(rowId));
    return (
      <Tag
        value={getStatusLabel(row?.status, { queued: isQueued })}
        severity={getStatusSeverity(normalizedStatus, { queued: isQueued })}
      />
    );
  };

  const renderPoster = (row, className = 'history-dv-poster') => {
    const mediaMeta = resolveMediaTypeMeta(row);
    const isAudioSquare = ['cd', 'audiobook'].includes(mediaMeta.mediaType);
    const audioClass = className === 'history-dv-poster-grid' ? 'history-dv-poster-grid-audio' : 'history-dv-poster-audio';
    const imgClass = isAudioSquare ? audioClass : className;
    const title = row?.title || row?.detected_title || 'Poster';
    if (row?.poster_url && row.poster_url !== 'N/A') {
      return <img src={row.poster_url} alt={title} className={imgClass} loading="lazy" />;
    }
    return <div className="history-dv-poster-fallback">{isAudioSquare ? 'Kein Cover' : 'Kein Poster'}</div>;
  };

  const renderPresenceChip = (label, available, tone = null, suffix = '') => {
    const resolvedTone = tone || (available ? 'tone-ok' : 'tone-no');
    const iconClass = available ? 'pi-check-circle' : 'pi-times-circle';
    return (
      <span className={`history-dv-chip ${resolvedTone}`}>
        <i className={`pi ${iconClass}`} aria-hidden="true" />
        <span>{label}: {available ? 'Ja' : 'Nein'}{suffix}</span>
      </span>
    );
  };

  const renderSeriesOutputChip = (row) => {
    const summary = row?.seriesOutputSummary || null;
    if (!summary) {
      return renderPresenceChip('Episoden', Boolean(row?.outputStatus?.exists));
    }
    const expected = Number(summary.expected || 0);
    const existing = Number(summary.existing || 0);
    const hasExpected = Number.isFinite(expected) && expected > 0;
    const safeExpected = hasExpected ? expected : 0;
    const safeExisting = Number.isFinite(existing) && existing >= 0 ? existing : 0;
    const suffix = hasExpected ? ` (${safeExisting}/${safeExpected})` : '';
    if (safeExisting <= 0) {
      return renderPresenceChip('Episoden', false, 'tone-no', suffix || ' (0/0)');
    }
    if (hasExpected && safeExisting < safeExpected) {
      return renderPresenceChip('Episoden', true, 'tone-warn', suffix);
    }
    return renderPresenceChip('Episoden', true, 'tone-ok', suffix || ` (${safeExisting}/${safeExpected || safeExisting})`);
  };

  const renderSeriesRawChip = (row) => {
    const summary = row?.seriesChildSummary?.raw || null;
    if (!summary) {
      return renderPresenceChip('RAW', Boolean(row?.rawStatus?.exists));
    }
    const expected = Number(summary.expected || 0);
    const existing = Number(summary.existing || 0);
    const hasExpected = Number.isFinite(expected) && expected > 0;
    const safeExpected = hasExpected ? expected : 0;
    const safeExisting = Number.isFinite(existing) && existing >= 0 ? existing : 0;
    const suffix = hasExpected ? ` (${safeExisting}/${safeExpected})` : '';
    if (safeExisting <= 0) {
      return renderPresenceChip('RAW', false, 'tone-no', suffix || ' (0/0)');
    }
    if (hasExpected && safeExisting < safeExpected) {
      return renderPresenceChip('RAW', true, 'tone-warn', suffix);
    }
    return renderPresenceChip('RAW', true, 'tone-ok', suffix || ` (${safeExisting}/${safeExpected || safeExisting})`);
  };

  const renderSupplementalInfo = (row) => {
    if (resolveMediaType(row) === 'cd') {
      const cdDetails = resolveCdDetails(row);
      const infoItems = [];
      if (cdDetails.trackCount > 0) {
        infoItems.push({
          key: 'tracks',
          label: 'Tracks',
          value: cdDetails.selectedTrackCount > 0 && cdDetails.selectedTrackCount !== cdDetails.trackCount
            ? `${cdDetails.selectedTrackCount}/${cdDetails.trackCount}`
            : String(cdDetails.trackCount)
        });
      }
      if (cdDetails.formatLabel) {
        infoItems.push({ key: 'format', label: 'Format', value: cdDetails.formatLabel });
      }
      if (cdDetails.totalDurationLabel) {
        infoItems.push({ key: 'duration', label: 'Dauer', value: cdDetails.totalDurationLabel });
      }
      if (infoItems.length === 0) {
        return <span className="history-dv-subtle">Keine CD-Details</span>;
      }
      return infoItems.map((item) => (
        <span key={`${row?.id}-${item.key}`} className="history-dv-rating-chip">
          <strong>{item.label}</strong>
          <span>{item.value}</span>
        </span>
      ));
    }

    if (resolveMediaType(row) === 'audiobook') {
      const audiobookDetails = resolveAudiobookDetails(row);
      const infoItems = [];
      if (audiobookDetails.author) {
        infoItems.push({ key: 'author', label: 'Autor', value: audiobookDetails.author });
      }
      if (audiobookDetails.narrator) {
        const narratorShort = audiobookDetails.narrator.length > 48
          ? `${audiobookDetails.narrator.slice(0, 48).trimEnd()}…`
          : audiobookDetails.narrator;
        infoItems.push({ key: 'narrator', label: 'Sprecher', value: narratorShort });
      }
      if (audiobookDetails.chapterCount > 0) {
        infoItems.push({ key: 'chapters', label: 'Kapitel', value: String(audiobookDetails.chapterCount) });
      }
      if (audiobookDetails.formatLabel) {
        infoItems.push({ key: 'format', label: 'Format', value: audiobookDetails.formatLabel });
      }
      if (infoItems.length === 0) {
        return <span className="history-dv-subtle">Keine Audiobook-Details</span>;
      }
      return infoItems.map((item) => (
        <span key={`${row?.id}-${item.key}`} className="history-dv-rating-chip">
          <strong>{item.label}</strong>
          <span>{item.value}</span>
        </span>
      ));
    }

    const ratings = resolveRatings(row);
    if (ratings.length === 0) {
      return <span className="history-dv-subtle">Keine Ratings</span>;
    }

    return ratings.map((rating) => (
      <span key={`${row?.id}-${rating.key}`} className="history-dv-rating-chip">
        <strong>{rating.label}</strong>
        <span>{rating.value}</span>
      </span>
    ));
  };

  const onItemKeyDown = (event, row) => {
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      void openDetail(row);
    }
  };

  const listItem = (row) => {
    const mediaMeta = resolveMediaTypeMeta(row);
    const isCdJob = mediaMeta.mediaType === 'cd';
    const outputIsAudio = hasAudioOutput(row);
    const cdDetails = isCdJob ? resolveCdDetails(row) : null;
    const selectedMetadata = row?.makemkvInfo?.selectedMetadata && typeof row.makemkvInfo.selectedMetadata === 'object'
      ? row.makemkvInfo.selectedMetadata
      : {};
    const isSeriesDvd = resolveMediaType(row) === 'dvd' && isSeriesDvdJob(row);
    const seasonLabel = isSeriesDvd && selectedMetadata?.seasonNumber
      ? `Staffel ${selectedMetadata.seasonNumber}`
      : null;
    const tmdbLabel = isSeriesDvd && selectedMetadata?.tmdbId
      ? `TMDb ${selectedMetadata.tmdbId}`
      : null;
    const subtitle = isCdJob
      ? [
        `#${row?.id || '-'}`,
        cdDetails?.artist || '-',
        row?.year || null,
        cdDetails?.mbId ? 'MusicBrainz' : null
      ].filter(Boolean).join(' | ')
      : [
        `#${row?.id || '-'}`,
        row?.year || '-',
        isSeriesDvd ? seasonLabel : (row?.imdb_id || '-'),
        tmdbLabel
      ].filter(Boolean).join(' | ');

    return (
      <div className="col-12" key={row.id}>
        <div
          className="history-dv-item history-dv-item-list"
          role="button"
          tabIndex={0}
          onKeyDown={(event) => onItemKeyDown(event, row)}
          onClick={() => {
            void openDetail(row);
          }}
        >
          <div className="history-dv-poster-wrap">
            {renderPoster(row)}
          </div>

          <div className="history-dv-main">
            <div className="history-dv-head">
              <div className="history-dv-title-block">
                <strong className="history-dv-title">{row?.title || row?.detected_title || '-'}</strong>
                <small className="history-dv-subtle">{subtitle}</small>
              </div>
              {renderStatusTag(row)}
            </div>

            <div className="history-dv-meta-row">
              <span className="job-step-cell">
                <img src={mediaMeta.icon} alt={mediaMeta.alt} title={mediaMeta.label} className="media-indicator-icon" />
                <span>{mediaMeta.label}</span>
              </span>
              <span className="history-dv-subtle">Start: {formatDateTime(row?.start_time)}</span>
              <span className="history-dv-subtle">Ende: {formatDateTime(row?.end_time)}</span>
            </div>

            <div className="history-dv-flags-row">
              {isSeriesDvd
                ? renderSeriesRawChip(row)
                : renderPresenceChip('RAW', Boolean(row?.rawStatus?.exists))}
              {isSeriesDvd
                ? renderSeriesOutputChip(row)
                : renderPresenceChip(outputIsAudio ? 'Audio' : 'Movie', Boolean(row?.outputStatus?.exists))}
              {renderPresenceChip('Encode', Boolean(row?.encodeSuccess))}
            </div>

            <div className="history-dv-ratings-row">{renderSupplementalInfo(row)}</div>
          </div>


        </div>
      </div>
    );
  };

  const gridItem = (row) => {
    const mediaMeta = resolveMediaTypeMeta(row);
    const isCdJob = mediaMeta.mediaType === 'cd';
    const outputIsAudio = hasAudioOutput(row);
    const cdDetails = isCdJob ? resolveCdDetails(row) : null;
    const subtitle = isCdJob
      ? [
        `#${row?.id || '-'}`,
        cdDetails?.artist || '-',
        row?.year || null,
        cdDetails?.mbId ? 'MusicBrainz' : null
      ].filter(Boolean).join(' | ')
      : `#${row?.id || '-'} | ${row?.year || '-'} | ${row?.imdb_id || '-'}`;

    return (
      <div className="col-12 md-col-6 xl-col-4" key={row.id}>
        <div
          className="history-dv-item history-dv-item-grid"
          role="button"
          tabIndex={0}
          onKeyDown={(event) => onItemKeyDown(event, row)}
          onClick={() => {
            void openDetail(row);
          }}
        >
          <div className="history-dv-grid-status-overlay">
            {renderStatusTag(row)}
          </div>

          <div className="history-dv-grid-poster-wrap">
            {renderPoster(row, 'history-dv-poster-grid')}
          </div>

          <div className="history-dv-grid-main">
            <div className="history-dv-head">
              <strong className="history-dv-title">{row?.title || row?.detected_title || '-'}</strong>
            </div>

            <small className="history-dv-subtle">{subtitle}</small>

            <div className="history-dv-meta-row">
              <span className="job-step-cell">
                <img src={mediaMeta.icon} alt={mediaMeta.alt} title={mediaMeta.label} className="media-indicator-icon" />
                <span>{mediaMeta.label}</span>
              </span>
              <span className="history-dv-subtle">Start: {formatDateTime(row?.start_time)}</span>
              <span className="history-dv-subtle">Ende: {formatDateTime(row?.end_time)}</span>
            </div>

            <div className="history-dv-flags-row">
              {isSeriesDvd
                ? renderSeriesRawChip(row)
                : renderPresenceChip('RAW', Boolean(row?.rawStatus?.exists))}
              {isSeriesDvd
                ? renderSeriesOutputChip(row)
                : renderPresenceChip(outputIsAudio ? 'Audio' : 'Movie', Boolean(row?.outputStatus?.exists))}
              {renderPresenceChip('Encode', Boolean(row?.encodeSuccess))}
            </div>

            <div className="history-dv-ratings-row">{renderSupplementalInfo(row)}</div>
          </div>
        </div>
      </div>
    );
  };

  const itemTemplate = (row, currentLayout) => {
    if (!row) {
      return null;
    }
    return currentLayout === 'list' ? listItem(row) : gridItem(row);
  };

  const previewRelatedJobs = Array.isArray(deleteEntryPreview?.relatedJobs) ? deleteEntryPreview.relatedJobs : [];
  const previewRawPaths = Array.isArray(deleteEntryPreview?.pathCandidates?.raw) ? deleteEntryPreview.pathCandidates.raw : [];
  const previewMoviePaths = Array.isArray(deleteEntryPreview?.pathCandidates?.movie) ? deleteEntryPreview.pathCandidates.movie : [];
  const previewRawExisting = previewRawPaths.filter((item) => Boolean(item?.exists));
  const previewMovieExisting = previewMoviePaths.filter((item) => Boolean(item?.exists));
  const previewRawDisplay = previewRawExisting.length > 0 ? previewRawExisting : previewRawPaths.slice(0, 1);
  const previewMovieDisplay = previewMovieExisting.length > 0 ? previewMovieExisting : previewMoviePaths.slice(0, 1);
  const selectedDeleteMoviePathSet = useMemo(
    () => new Set(deleteEntrySelectedMoviePaths.map((item) => String(item || '').trim()).filter(Boolean)),
    [deleteEntrySelectedMoviePaths]
  );
  const movieDeleteSelectionRequired = previewMovieExisting.length > 0 && deleteEntrySelectedMoviePaths.length === 0;
  const deleteTargetActionsDisabled = deleteEntryPreviewLoading || Boolean(deleteEntryTargetBusy) || !deleteEntryPreview;
  const deleteEntryOutputLabel = getOutputLabelForRow(deleteEntryDialogRow);
  const deleteEntryOutputShortLabel = getOutputShortLabelForRow(deleteEntryDialogRow);

  const header = (
    <div className="history-dv-toolbar">
      <InputText
        value={search}
        onChange={(event) => setSearch(event.target.value)}
        placeholder="Suche nach Titel, Interpret oder IMDb"
      />

      <Dropdown
        value={status}
        options={STATUS_FILTER_OPTIONS}
        optionLabel="label"
        optionValue="value"
        onChange={(event) => setStatus(event.value)}
        placeholder="Status"
      />

      <Dropdown
        value={mediumFilter}
        options={MEDIA_FILTER_OPTIONS}
        optionLabel="label"
        optionValue="value"
        onChange={(event) => setMediumFilter(event.value || '')}
        placeholder="Medium"
      />

      <Dropdown
        value={sortKey}
        options={SORT_OPTIONS}
        optionLabel="label"
        optionValue="value"
        onChange={onSortChange}
        placeholder="Sortieren"
      />

      <Button label="Neu laden" icon="pi pi-refresh" onClick={load} loading={loading} />

      <div className="history-dv-layout-toggle">
        <DataViewLayoutOptions layout={layout} onChange={(event) => {
          setLayout(event.value);
          api.setPref('history_layout', event.value).catch(() => {});
        }} />
      </div>
    </div>
  );

  return (
    <div className="page-grid">
      <Toast ref={toastRef} />

      <Card title="Historie" subTitle="PrimeReact DataView">
        <DataView
          value={visibleJobs}
          layout={layout}
          header={header}
          itemTemplate={itemTemplate}
          paginator
          rows={12}
          rowsPerPageOptions={[12, 24, 48]}
          sortField={sortField}
          sortOrder={sortOrder}
          loading={loading}
          emptyMessage="Keine Einträge"
          className="history-dataview"
        />
      </Card>

      <JobDetailDialog
        visible={detailVisible}
        job={selectedJob}
        detailLoading={detailLoading}
        onLoadLog={handleLoadLog}
        logLoadingMode={logLoadingMode}
        onRestartEncode={resolveMediaType(selectedJob) === 'converter' ? null : handleRestartEncode}
        onRestartReview={resolveMediaType(selectedJob) === 'converter' ? null : handleRestartReview}
        onRestartCdReview={resolveMediaType(selectedJob) === 'converter' ? null : handleRestartCdReview}
        onReencode={resolveMediaType(selectedJob) === 'converter' ? null : handleReencode}
        onRetry={resolveMediaType(selectedJob) === 'converter' ? null : handleRetry}
        onAssignOmdb={handleAssignOmdb}
        onAssignCdMetadata={handleAssignCdMetadata}
        onAcknowledgeError={handleAcknowledgeError}
        onDeleteFiles={handleDeleteFiles}
        onDeleteEntry={handleDeleteEntry}
        onDownloadArchive={handleDownloadArchive}
        onDownloadOutputFolder={handleDownloadOutputFolder}
        onRemoveFromQueue={handleRemoveFromQueue}
        isQueued={Boolean(selectedJob?.id && queuedJobIdSet.has(normalizeJobId(selectedJob.id)))}
        actionBusy={actionBusy}
        omdbAssignBusy={omdbAssignBusy}
        cdMetadataAssignBusy={cdMetadataAssignBusy}
        acknowledgeErrorBusy={acknowledgeErrorBusy}
        reencodeBusy={reencodeBusyJobId === selectedJob?.id}
        deleteEntryBusy={deleteEntryBusy}
        downloadBusyTarget={downloadBusyTarget}
        downloadFolderBusyPath={downloadFolderBusyPath}
        onHide={() => {
          setDetailVisible(false);
          setDetailLoading(false);
          setLogLoadingMode(null);
          setDownloadBusyTarget(null);
          setDownloadFolderBusyPath(null);
        }}
      />

      <Dialog
        header="Historien-Eintrag löschen"
        visible={deleteEntryDialogVisible}
        onHide={closeDeleteEntryDialog}
        style={{ width: '56rem', maxWidth: '96vw' }}
        className="history-delete-dialog"
        modal
      >
        <p>
          {`Es werden ${previewRelatedJobs.length || 1} Historien-Eintrag/Einträge entfernt.`}
        </p>

        {deleteEntryDialogRow ? (
          <small className="muted-inline">
            Job: {deleteEntryDialogRow?.title || deleteEntryDialogRow?.detected_title || `Job #${deleteEntryDialogRow?.id || '-'}`}
          </small>
        ) : null}

        {deleteEntryPreviewLoading ? (
          <p>Löschvorschau wird geladen ...</p>
        ) : (
          <div className="history-delete-preview-grid">
            <div>
              <h4>Rip/Encode Historie</h4>
              {previewRelatedJobs.length > 0 ? (
                <ul className="history-delete-preview-list">
                  {previewRelatedJobs.map((item) => (
                    <li key={`delete-related-${item.id}`}>
                      <strong>#{item.id}</strong> | {item.title || '-'} | {getStatusLabel(item.status)} {item.isPrimary ? '(aktuell)' : '(Alt-Eintrag)'}
                    </li>
                  ))}
                </ul>
              ) : (
                <small className="history-dv-subtle">Keine verknüpften Alt-Einträge erkannt.</small>
              )}
            </div>

            <div>
              <h4>RAW</h4>
              {previewRawPaths.length > 0 ? (() => {
                return (
                  <ul className="history-delete-preview-list">
                    {previewRawDisplay.map((item) => (
                      <li key={`delete-raw-${item.path}`}>
                        <span className={item.exists ? 'exists-yes' : 'exists-no'}>
                          {item.exists ? 'vorhanden' : 'nicht gefunden'}
                        </span>
                        {' '}| {item.path}
                      </li>
                    ))}
                  </ul>
                );
              })() : (
                <small className="history-dv-subtle">Keine RAW-Pfade.</small>
              )}
            </div>

            <div>
              <h4>{deleteEntryOutputShortLabel}</h4>
              {previewMoviePaths.length > 0 ? (() => {
                return (
                  <ul className="history-delete-preview-list">
                    {previewMovieDisplay.map((item) => (
                      <li key={`delete-movie-${item.path}`}>
                        {item.exists ? (
                          <label className="history-delete-preview-checkbox-row">
                            <input
                              type="checkbox"
                              checked={selectedDeleteMoviePathSet.has(String(item.path || '').trim())}
                              onChange={(event) => toggleDeleteMoviePathSelection(item.path, Boolean(event.target.checked))}
                            />
                            <span className={item.exists ? 'exists-yes' : 'exists-no'}>
                              {item.exists ? 'vorhanden' : 'nicht gefunden'}
                            </span>
                            <span>| {item.path}</span>
                          </label>
                        ) : (
                          <>
                            <span className={item.exists ? 'exists-yes' : 'exists-no'}>
                              {item.exists ? 'vorhanden' : 'nicht gefunden'}
                            </span>
                            {' '}| {item.path}
                          </>
                        )}
                      </li>
                    ))}
                  </ul>
                );
              })() : (
                <small className="history-dv-subtle">Keine Movie-Pfade.</small>
              )}
            </div>
          </div>
        )}

        <div className="dialog-actions">
          <Button
            label="Nur RAW löschen"
            icon="pi pi-trash"
            severity="warning"
            outlined
            onClick={() => confirmDeleteEntry('raw')}
            loading={deleteEntryTargetBusy === 'raw'}
            disabled={deleteTargetActionsDisabled}
          />
          <Button
            label={`Nur ${deleteEntryOutputShortLabel} löschen`}
            icon="pi pi-trash"
            severity="warning"
            outlined
            onClick={() => confirmDeleteEntry('movie')}
            loading={deleteEntryTargetBusy === 'movie'}
            disabled={deleteTargetActionsDisabled || movieDeleteSelectionRequired}
          />
          <Button
            label="Beides löschen"
            icon="pi pi-times"
            severity="danger"
            onClick={() => confirmDeleteEntry('both')}
            loading={deleteEntryTargetBusy === 'both'}
            disabled={deleteTargetActionsDisabled || movieDeleteSelectionRequired}
          />
          <Button
            label="Nur Eintrag löschen"
            icon="pi pi-database"
            severity="secondary"
            outlined
            onClick={() => confirmDeleteEntry('none')}
            loading={deleteEntryTargetBusy === 'none'}
            disabled={deleteTargetActionsDisabled}
          />
          <Button
            label="Abbrechen"
            severity="secondary"
            outlined
            onClick={closeDeleteEntryDialog}
            disabled={Boolean(deleteEntryTargetBusy)}
          />
        </div>
      </Dialog>

      <MetadataSelectionDialog
        visible={metadataDialogVisible}
        context={metadataDialogContext || {}}
        onHide={() => {
          setMetadataDialogVisible(false);
          setMetadataDialogContext(null);
        }}
        onSubmit={handleOmdbSubmit}
        onSearch={handleOmdbSearch}
        busy={omdbAssignBusy}
      />

      <CdMetadataDialog
        visible={cdMetadataDialogVisible}
        context={cdMetadataDialogContext || {}}
        onHide={() => {
          setCdMetadataDialogVisible(false);
          setCdMetadataDialogContext(null);
        }}
        onSubmit={handleCdMetadataSubmit}
        onSearch={handleMusicBrainzSearch}
        onFetchRelease={handleMusicBrainzReleaseFetch}
        busy={cdMetadataAssignBusy}
      />

      <ReencodeConflictModal
        visible={conflictModalVisible}
        onHide={closeConflictModal}
        job={conflictModalJob}
        existingFolders={conflictModalFolders}
        mode={conflictModalMode}
        onKeepBoth={handleConflictKeepBoth}
        onDeleteSelected={handleConflictDeleteSelected}
        busy={conflictModalBusy}
      />
    </div>
  );
}
