import { useEffect, useMemo, useRef, useState } from 'react';
import { Card } from 'primereact/card';
import { Tag } from 'primereact/tag';
import { ProgressBar } from 'primereact/progressbar';
import { Button } from 'primereact/button';
import { Dropdown } from 'primereact/dropdown';
import MediaInfoReviewPanel from './MediaInfoReviewPanel';
import { api } from '../api/client';
import { getStatusLabel, getStatusSeverity } from '../utils/statusPresentation';

const CONVERTER_VIDEO_OUTPUT_FORMATS = [
  { label: 'MKV', value: 'mkv' },
  { label: 'MP4', value: 'mp4' },
  { label: 'M4V', value: 'm4v' }
];

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

function normalizeTitleIdList(values) {
  const list = Array.isArray(values) ? values : [];
  const seen = new Set();
  const output = [];
  for (const value of list) {
    const normalized = normalizeTitleId(value);
    if (normalized === null) {
      continue;
    }
    const key = String(normalized);
    if (seen.has(key)) {
      continue;
    }
    seen.add(key);
    output.push(normalized);
  }
  return output;
}

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

function normalizePlaylistId(value) {
  const raw = String(value || '').trim().toLowerCase();
  if (!raw) {
    return null;
  }
  const match = raw.match(/(\d{1,5})(?:\.mpls)?$/i);
  return match ? String(match[1]).padStart(5, '0') : null;
}

function formatDurationClock(seconds) {
  const total = Number(seconds || 0);
  if (!Number.isFinite(total) || total <= 0) {
    return null;
  }
  const rounded = Math.max(0, Math.trunc(total));
  const h = Math.floor(rounded / 3600);
  const m = Math.floor((rounded % 3600) / 60);
  const s = rounded % 60;
  return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}

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

function normalizeTrackIdList(values) {
  const list = Array.isArray(values) ? values : [];
  const seen = new Set();
  const output = [];
  for (const value of list) {
    const normalized = normalizeTrackId(value);
    if (normalized === null) {
      continue;
    }
    const key = String(normalized);
    if (seen.has(key)) {
      continue;
    }
    seen.add(key);
    output.push(normalized);
  }
  return output;
}

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

function normalizeScriptIdList(values) {
  const list = Array.isArray(values) ? values : [];
  const seen = new Set();
  const output = [];
  for (const value of list) {
    const normalized = normalizeScriptId(value);
    if (normalized === null) {
      continue;
    }
    const key = String(normalized);
    if (seen.has(key)) {
      continue;
    }
    seen.add(key);
    output.push(normalized);
  }
  return output;
}

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

function normalizeMediaProfile(value) {
  const raw = String(value || '').trim().toLowerCase();
  if (!raw) {
    return null;
  }
  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 (['audiobook', 'audio_book', 'audio book', 'book'].includes(raw)) {
    return 'audiobook';
  }
  if (raw === 'converter') {
    return 'converter';
  }
  if (['other', 'sonstiges', 'unknown'].includes(raw)) {
    return 'other';
  }
  return null;
}

function resolvePipelineMediaProfile(pipeline, mediaInfoReview) {
  const context = pipeline?.context && typeof pipeline.context === 'object' ? pipeline.context : {};
  const device = context?.device && typeof context.device === 'object' ? context.device : {};
  const review = mediaInfoReview && typeof mediaInfoReview === 'object' ? mediaInfoReview : {};
  const candidates = [
    context?.mediaProfile,
    context?.media_profile,
    review?.mediaProfile,
    review?.media_profile,
    device?.mediaProfile,
    device?.media_profile,
    device?.profile,
    device?.type
  ];
  for (const candidate of candidates) {
    const normalized = normalizeMediaProfile(candidate);
    if (normalized) {
      return normalized;
    }
  }
  return null;
}

function isBurnedSubtitleTrack(track) {
  const flags = Array.isArray(track?.subtitlePreviewFlags)
    ? track.subtitlePreviewFlags
    : (Array.isArray(track?.flags) ? track.flags : []);
  const hasBurnedFlag = flags.some((flag) => String(flag || '').trim().toLowerCase() === 'burned');
  const summary = `${track?.subtitlePreviewSummary || ''} ${track?.subtitleActionSummary || ''}`;
  return Boolean(
    track?.subtitlePreviewBurnIn
    || track?.burnIn
    || hasBurnedFlag
    || /burned/i.test(summary)
  );
}

function isDuplicateSubtitleTrack(track) {
  return Boolean(track?.duplicate);
}

function normalizeSubtitleLanguage(value) {
  const raw = String(value || '').trim().toLowerCase();
  if (!raw) {
    return 'und';
  }
  if (raw.length >= 3) {
    return raw.slice(0, 3);
  }
  return raw;
}

function normalizeSubtitleVariantSelectionMap(rawSelection) {
  const map = {};
  if (!rawSelection || typeof rawSelection !== 'object') {
    return map;
  }
  for (const [language, value] of Object.entries(rawSelection)) {
    const normalizedLanguage = normalizeSubtitleLanguage(language);
    const full = Boolean(value?.full);
    const forced = Boolean(value?.forced);
    map[normalizedLanguage] = { full, forced };
  }
  return map;
}

function normalizeSubtitleLanguageOrder(rawOrder = []) {
  const list = Array.isArray(rawOrder) ? rawOrder : [];
  const seen = new Set();
  const output = [];
  for (const value of list) {
    const language = normalizeSubtitleLanguage(value);
    if (!language || seen.has(language)) {
      continue;
    }
    seen.add(language);
    output.push(language);
  }
  return output;
}

function resolveSubtitleTrackVariantFlags(track) {
  const subtitleType = String(track?.subtitleType || '').trim().toLowerCase();
  const isForcedOnly = Boolean(
    track?.isForcedOnly
    ?? track?.subtitlePreviewForcedOnly
    ?? track?.forcedOnly
    ?? subtitleType === 'forced'
  );
  const fullHasForced = !isForcedOnly && Boolean(
    track?.fullHasForced
    ?? track?.subtitleFullHasForced
    ?? track?.hasForcedVariant
  );
  return { isForcedOnly, fullHasForced };
}

function buildDefaultSubtitleVariantSelection(subtitleTracks, selectedTracks) {
  const tracks = Array.isArray(subtitleTracks) ? subtitleTracks : [];
  const selected = Array.isArray(selectedTracks) ? selectedTracks : [];
  const orderedTracks = [...tracks].sort((a, b) => {
    const aId = normalizeTrackId(a?.id) ?? Number.MAX_SAFE_INTEGER;
    const bId = normalizeTrackId(b?.id) ?? Number.MAX_SAFE_INTEGER;
    if (aId !== bId) {
      return aId - bId;
    }
    return String(a?.language || a?.languageLabel || '').localeCompare(String(b?.language || b?.languageLabel || ''));
  });

  const order = [];
  const orderSeen = new Set();
  for (const track of orderedTracks) {
    const language = normalizeSubtitleLanguage(track?.language || track?.languageLabel || 'und');
    if (!orderSeen.has(language)) {
      orderSeen.add(language);
      order.push(language);
    }
  }

  const selectionMap = {};
  for (const track of selected) {
    if (isBurnedSubtitleTrack(track) || isDuplicateSubtitleTrack(track)) {
      continue;
    }
    const language = normalizeSubtitleLanguage(track?.language || track?.languageLabel || 'und');
    if (!selectionMap[language]) {
      selectionMap[language] = { full: false, forced: false };
    }
    const flags = resolveSubtitleTrackVariantFlags(track);
    if (flags.isForcedOnly) {
      selectionMap[language].forced = true;
    } else {
      selectionMap[language].full = true;
    }
  }

  return {
    subtitleVariantSelection: selectionMap,
    subtitleLanguageOrder: order
  };
}

function buildDefaultTrackSelection(review) {
  const titles = Array.isArray(review?.titles) ? review.titles : [];
  const selection = {};
  const reviewEncodeInputTitleId = normalizeTitleId(review?.encodeInputTitleId);
  const manualSelection = review?.manualTrackSelection && typeof review.manualTrackSelection === 'object'
    ? review.manualTrackSelection
    : null;
  const manualTitleId = normalizeTitleId(manualSelection?.titleId);

  for (const title of titles) {
    const titleId = normalizeTitleId(title?.id);
    if (!titleId) {
      continue;
    }

    const audioTracks = Array.isArray(title?.audioTracks) ? title.audioTracks : [];
    const subtitleTracks = Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : [];
    const isEncodeInputTitle = Boolean(
      title?.selectedForEncode
      || title?.encodeInput
      || (reviewEncodeInputTitleId && reviewEncodeInputTitleId === titleId)
    );
    const audioSelectionSource = isEncodeInputTitle
      ? audioTracks.filter((track) => Boolean(track?.selectedForEncode))
      : audioTracks.filter((track) => Boolean(track?.selectedByRule));
    const subtitleSelectionSource = isEncodeInputTitle
      ? subtitleTracks.filter((track) => Boolean(track?.selectedForEncode))
      : subtitleTracks.filter((track) => Boolean(track?.selectedByRule));
    const defaultSubtitleVariantSelection = buildDefaultSubtitleVariantSelection(
      subtitleTracks,
      subtitleSelectionSource
    );
    const manualSelectionMatchesTitle = Boolean(
      isEncodeInputTitle
      && manualSelection
      && (!manualTitleId || manualTitleId === titleId)
    );
    const manualAudioTrackIds = manualSelectionMatchesTitle
      ? normalizeTrackIdList(manualSelection?.audioTrackIds || [])
      : [];
    const manualSubtitleTrackIds = manualSelectionMatchesTitle
      ? normalizeTrackIdList(
        Array.isArray(manualSelection?.subtitleTrackIdsOrdered)
          ? manualSelection.subtitleTrackIdsOrdered
          : manualSelection?.subtitleTrackIds || []
      )
      : [];
    const manualSubtitleVariantSelection = manualSelectionMatchesTitle
      ? normalizeSubtitleVariantSelectionMap(manualSelection?.subtitleVariantSelection || {})
      : {};
    const manualSubtitleLanguageOrder = manualSelectionMatchesTitle
      ? normalizeSubtitleLanguageOrder(manualSelection?.subtitleLanguageOrder || [])
      : [];

    selection[titleId] = {
      audioTrackIds: manualAudioTrackIds.length > 0
        ? manualAudioTrackIds
        : normalizeTrackIdList(audioSelectionSource.map((track) => track?.id)),
      subtitleTrackIds: manualSubtitleTrackIds.length > 0
        ? manualSubtitleTrackIds
        : normalizeTrackIdList(
          subtitleSelectionSource
            .filter((track) => !isBurnedSubtitleTrack(track) && !isDuplicateSubtitleTrack(track))
            .map((track) => track?.id)
        ),
      subtitleVariantSelection: Object.keys(manualSubtitleVariantSelection).length > 0
        ? manualSubtitleVariantSelection
        : defaultSubtitleVariantSelection.subtitleVariantSelection,
      subtitleLanguageOrder: manualSubtitleLanguageOrder.length > 0
        ? manualSubtitleLanguageOrder
        : defaultSubtitleVariantSelection.subtitleLanguageOrder
    };
  }

  return selection;
}

function defaultTrackSelectionForTitle(review, titleId) {
  const defaults = buildDefaultTrackSelection(review);
  return defaults[titleId] || defaults[String(titleId)] || {
    audioTrackIds: [],
    subtitleTrackIds: [],
    subtitleVariantSelection: {},
    subtitleLanguageOrder: []
  };
}

function buildSettingsMap(categories) {
  const map = {};
  const list = Array.isArray(categories) ? categories : [];
  for (const category of list) {
    for (const setting of (Array.isArray(category?.settings) ? category.settings : [])) {
      map[setting.key] = setting.value;
    }
  }
  return map;
}

function buildPresetDisplayMap(options) {
  const map = {};
  const list = Array.isArray(options) ? options : [];
  for (const option of list) {
    if (!option || option.disabled) {
      continue;
    }
    const value = String(option.value || '').trim();
    if (!value) {
      continue;
    }
    const category = String(option.category || '').trim();
    map[value] = category ? `${category}/${value}` : value;
  }
  return map;
}

function transliterateForFilename(input) {
  return String(input || '')
    .replace(/ä/g, 'ae').replace(/Ä/g, 'Ae')
    .replace(/ö/g, 'oe').replace(/Ö/g, 'Oe')
    .replace(/ü/g, 'ue').replace(/Ü/g, 'Ue')
    .replace(/ß/g, 'ss')
    .replace(/å/g, 'a').replace(/Å/g, 'A')
    .replace(/æ/g, 'ae').replace(/Æ/g, 'Ae')
    .replace(/ø/g, 'o').replace(/Ø/g, 'O')
    .replace(/ð/g, 'd').replace(/Ð/g, 'D')
    .replace(/þ/g, 'th').replace(/Þ/g, 'Th')
    .normalize('NFD')
    .replace(/\p{M}+/gu, '')
    .replace(/[^\x00-\x7F]/g, '');
}

function sanitizeFileName(input) {
  return transliterateForFilename(String(input || 'untitled'))
    .replace(/[^a-zA-Z0-9 ()[\]_-]/g, '')
    .replace(/\s+/g, ' ')
    .trim()
    .slice(0, 180);
}

function renderTemplate(template, values) {
  const parseToken = (rawToken) => {
    const token = String(rawToken || '').trim();
    if (!token) {
      return { format: '', key: '' };
    }
    const separatorIndex = token.indexOf(':');
    if (separatorIndex <= 0) {
      return { format: '', key: token };
    }
    return {
      format: token.slice(0, separatorIndex).trim(),
      key: token.slice(separatorIndex + 1).trim()
    };
  };
  const applyFormat = (value, format) => {
    const normalizedFormat = String(format || '').trim().toLowerCase();
    if (!normalizedFormat) {
      return String(value);
    }
    if (normalizedFormat === '0') {
      const raw = String(value);
      const match = raw.match(/^(-?)(\d+)$/);
      if (match) {
        const sign = match[1] || '';
        const digits = String(match[2] || '');
        return `${sign}${digits.padStart(2, '0')}`;
      }
    }
    return String(value);
  };
  return String(template || '${title} (${year})').replace(/\$\{([^}]+)\}|\{([^{}]+)\}/g, (_, keyA, keyB) => {
    const { format, key } = parseToken(keyA || keyB || '');
    const value = values[key];
    if (value === undefined || value === null || value === '') {
      return 'unknown';
    }
    return applyFormat(value, format);
  });
}

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

function normalizeEpisodeReference(entry = {}) {
  const source = entry && typeof entry === 'object' ? entry : {};
  const episodeId = normalizePositiveInt(source?.episodeId ?? source?.id ?? null);
  const episodeNumber = normalizePositiveInt(source?.episodeNumber ?? source?.number ?? null);
  const seasonNumber = normalizePositiveInt(source?.seasonNumber ?? source?.season ?? null);
  if (!episodeId && !episodeNumber) {
    return null;
  }
  return {
    episodeId,
    episodeNumber,
    seasonNumber
  };
}

function buildEpisodeReferenceKey(entry = {}) {
  const normalized = normalizeEpisodeReference(entry);
  if (!normalized) {
    return null;
  }
  if (normalized.episodeId) {
    return `id:${normalized.episodeId}`;
  }
  if (normalized.seasonNumber && normalized.episodeNumber) {
    return `se:${normalized.seasonNumber}:${normalized.episodeNumber}`;
  }
  return normalized.episodeNumber ? `ep:${normalized.episodeNumber}` : null;
}

function isSeriesBatchEpisodePlan(plan = null) {
  const source = plan && typeof plan === 'object' ? plan : {};
  if (Boolean(source?.seriesBatchChild) || Boolean(source?.seriesBatchVirtualEpisode)) {
    return true;
  }
  const parentJobId = normalizePositiveInt(source?.seriesBatchParentJobId);
  const childIndex = normalizePositiveInt(source?.seriesBatchChildIndex);
  const childCount = normalizePositiveInt(source?.seriesBatchChildCount);
  const titleId = normalizeTitleId(source?.seriesBatchTitleId);
  return Boolean(parentJobId && (childIndex || childCount || titleId));
}

function collectEpisodeReferencesFromPlan(plan = null) {
  const source = plan && typeof plan === 'object' ? plan : {};
  const refs = [];
  const assignments = source?.episodeAssignments && typeof source.episodeAssignments === 'object'
    ? Object.values(source.episodeAssignments)
    : [];
  for (const assignment of assignments) {
    const normalized = normalizeEpisodeReference(assignment);
    if (normalized) {
      refs.push(normalized);
    }
  }

  const titles = Array.isArray(source?.titles) ? source.titles : [];
  for (const title of titles) {
    if (!Boolean(title?.selectedForEncode) && !Boolean(title?.encodeInput)) {
      continue;
    }
    const normalized = normalizeEpisodeReference({
      episodeId: title?.episodeId,
      episodeNumber: title?.episodeNumber,
      seasonNumber: title?.seasonNumber
    });
    if (normalized) {
      refs.push(normalized);
    }
  }
  return refs;
}

function normalizeSeriesLanguage(value) {
  const raw = String(value || '').trim();
  if (!raw) {
    return 'unknown';
  }
  return raw;
}

function normalizeSeriesBatchStatus(value) {
  return String(value || '').trim().toUpperCase() || 'UNKNOWN';
}

function normalizeSeriesBatchProgress(value, fallback = 0) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) {
    const fallbackValue = Number(fallback);
    return Number.isFinite(fallbackValue) ? Math.max(0, Math.min(100, fallbackValue)) : 0;
  }
  return Math.max(0, Math.min(100, parsed));
}

function formatPercentTwoDecimals(value) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) {
    return '0.00%';
  }
  return `${parsed.toFixed(2)}%`;
}

function getSeriesBatchTagSeverity(status) {
  const normalized = normalizeSeriesBatchStatus(status);
  if (normalized === 'FINISHED') {
    return 'success';
  }
  if (normalized === 'ERROR') {
    return 'danger';
  }
  if (normalized === 'CANCELLED') {
    return 'warning';
  }
  if (normalized === 'ENCODING' || normalized === 'RIPPING' || normalized === 'MEDIAINFO_CHECK') {
    return 'info';
  }
  return 'secondary';
}

function buildDvdSeriesEpisodeOutputPathPreview(settings, metadata, title, assignment = null, fallbackJobId = null) {
  const seriesRootDir = String(
    settings?.series_dir_dvd
    || settings?.movie_dir_dvd
    || settings?.movie_dir
    || ''
  ).trim();
  if (!seriesRootDir) {
    return null;
  }

  const seasonNumber = normalizePositiveInt(
    assignment?.seasonNumber ?? metadata?.seasonNumber ?? title?.seasonNumber ?? 1
  ) || 1;
  const episodeNumber = normalizePositiveInt(
    assignment?.episodeNumber ?? title?.episodeNumber ?? title?.id
  ) || 1;
  const discNumber = normalizePositiveInt(metadata?.discNumber ?? assignment?.discNumber ?? null);
  const seriesTitleRaw = String(
    metadata?.title
    || metadata?.seriesTitle
    || (fallbackJobId ? `job-${fallbackJobId}` : 'series')
  ).trim();
  const seriesTitle = seriesTitleRaw || 'series';
  const episodeTitleRaw = String(
    assignment?.episodeTitle
    || title?.episodeTitle
    || title?.fileName
    || `Episode ${episodeNumber}`
  ).trim();
  const episodeTitle = episodeTitleRaw || `Episode ${episodeNumber}`;
  const year = String(metadata?.year || new Date().getFullYear()).trim();
  const language = normalizeSeriesLanguage(settings?.dvd_series_language || metadata?.language || null);

  const templateRaw = String(
    settings?.output_template_dvd_series_episode
    || '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeNr} - {episodeTitle}'
  ).trim();
  const template = templateRaw || '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeNr} - {episodeTitle}';
  const rendered = renderTemplate(template, {
    seriesTitle,
    seasonNr: seasonNumber,
    seasonNo: String(seasonNumber),
    episodeNr: episodeNumber,
    episodeNo: String(episodeNumber),
    episodeTitle,
    discNr: discNumber || '',
    year,
    language
  });

  const segments = rendered
    .replace(/\\/g, '/')
    .replace(/\/+/g, '/')
    .replace(/^\/+|\/+$/g, '')
    .split('/')
    .map((seg) => sanitizeFileName(seg))
    .filter(Boolean);
  const baseName = segments.length > 0 ? segments[segments.length - 1] : 'untitled';
  const folderParts = segments.slice(0, -1);
  const ext = String(settings?.output_extension_dvd || settings?.output_extension || 'mkv').trim() || 'mkv';
  const root = seriesRootDir.replace(/\/+$/g, '');
  if (folderParts.length > 0) {
    return `${root}/${folderParts.join('/')}/${baseName}.${ext}`;
  }
  return `${root}/${baseName}.${ext}`;
}

function resolveProfiledSetting(settings, key, mediaProfile) {
  const profileKey = mediaProfile ? `${key}_${mediaProfile}` : null;
  if (profileKey && settings?.[profileKey] != null && settings[profileKey] !== '') {
    return settings[profileKey];
  }
  const fallbackProfiles = mediaProfile === 'bluray'
    ? ['dvd']
    : mediaProfile === 'dvd'
      ? ['bluray']
      : [];
  for (const fb of fallbackProfiles) {
    const fbKey = `${key}_${fb}`;
    if (settings?.[fbKey] != null && settings[fbKey] !== '') {
      return settings[fbKey];
    }
  }
  return settings?.[key] ?? null;
}

function buildOutputPathPreview(settings, mediaProfile, metadata, fallbackJobId = null) {
  const movieDir = String(resolveProfiledSetting(settings, 'movie_dir', mediaProfile) || '').trim();
  if (!movieDir) {
    return null;
  }

  const title = metadata?.title || (fallbackJobId ? `job-${fallbackJobId}` : 'job');
  const author = metadata?.author || metadata?.artist || 'unknown';
  const narrator = metadata?.narrator || 'unknown';
  const year = metadata?.year || new Date().getFullYear();
  const imdbId = metadata?.imdbId || (fallbackJobId ? `job-${fallbackJobId}` : 'noimdb');
  const DEFAULT_TEMPLATE = '${title} (${year})/${title} (${year})';
  const rawTemplate = resolveProfiledSetting(settings, 'output_template', mediaProfile);
  const template = String(rawTemplate || DEFAULT_TEMPLATE).trim() || DEFAULT_TEMPLATE;
  const rendered = renderTemplate(template, { title, year, imdbId, author, narrator });
  const segments = rendered
    .replace(/\\/g, '/')
    .replace(/\/+/g, '/')
    .replace(/^\/+|\/+$/g, '')
    .split('/')
    .map((seg) => sanitizeFileName(seg))
    .filter(Boolean);
  const baseName = segments.length > 0 ? segments[segments.length - 1] : 'untitled';
  const folderParts = segments.slice(0, -1);
  const rawExt = resolveProfiledSetting(settings, 'output_extension', mediaProfile);
  const ext = String(rawExt || 'mkv').trim() || 'mkv';
  const root = movieDir.replace(/\/+$/g, '');
  if (folderParts.length > 0) {
    return `${root}/${folderParts.join('/')}/${baseName}.${ext}`;
  }
  return `${root}/${baseName}.${ext}`;
}

function buildConverterOutputPathPreview(settings, review, metadata, fallbackJobId = null) {
  const explicitPath = String(review?.outputPath || review?.outputDir || '').trim();
  if (explicitPath) {
    return explicitPath;
  }

  const movieDir = String(settings?.converter_movie_dir || '').trim();
  if (!movieDir) {
    return null;
  }
  const title = metadata?.title || (fallbackJobId ? `job-${fallbackJobId}` : 'job');
  const year = metadata?.year || new Date().getFullYear();
  const template = String(settings?.converter_output_template_video || '{title}').trim() || '{title}';
  const rendered = renderTemplate(template, { title, year });
  const segments = rendered
    .replace(/\\/g, '/')
    .replace(/\/+/g, '/')
    .replace(/^\/+|\/+$/g, '')
    .split('/')
    .map((seg) => sanitizeFileName(seg))
    .filter(Boolean);
  const baseName = segments.length > 0 ? segments[segments.length - 1] : 'untitled';
  const folderParts = segments.slice(0, -1);
  const ext = String(review?.outputFormat || 'mkv').trim() || 'mkv';
  const root = movieDir.replace(/\/+$/g, '');
  if (folderParts.length > 0) {
    return `${root}/${folderParts.join('/')}/${baseName}.${ext}`;
  }
  return `${root}/${baseName}.${ext}`;
}

export default function PipelineStatusCard({
  jobId = null,
  pipeline,
  onAnalyze,
  onReanalyze,
  onOpenMetadata,
  onReassignOmdb,
  onStart,
  onRemoveFromQueue,
  onRestartEncode,
  onRestartReview,
  onConfirmReview,
  onSelectPlaylist,
  onSelectHandBrakeTitle,
  onCancel,
  onRetry,
  onDeleteJob,
  onInputsChanged,
  isQueued = false,
  busy
}) {
  const state = pipeline?.state || 'IDLE';
  const stateLabel = getStatusLabel(state);
  const progress = Number(pipeline?.progress || 0);
  const running = state === 'ANALYZING' || state === 'RIPPING' || state === 'ENCODING' || state === 'MEDIAINFO_CHECK';
  const explicitJobId = normalizeJobId(jobId);
  const contextJobId = normalizeJobId(pipeline?.context?.jobId);
  const retryJobId = explicitJobId || contextJobId;
  const queueLocked = Boolean(isQueued && retryJobId);
  const selectedMetadata = pipeline?.context?.selectedMetadata || null;
  const mediaInfoReview = pipeline?.context?.mediaInfoReview || null;
  const playlistAnalysis = pipeline?.context?.playlistAnalysis || null;
  const encodeInputPath = pipeline?.context?.inputPath || mediaInfoReview?.encodeInputPath || null;
  const reviewMode = String(mediaInfoReview?.mode || '').trim().toLowerCase();
  const isPreRipReview = reviewMode === 'pre_rip' || Boolean(mediaInfoReview?.preRip);
  const jobMediaProfile = resolvePipelineMediaProfile(pipeline, mediaInfoReview);
  const isDvdTitleSelectionRequired = Boolean(
    jobMediaProfile === 'dvd'
    && (mediaInfoReview?.titleSelectionRequired || mediaInfoReview?.handBrakeTitleDecisionRequired)
  );
  const [liveJobLog, setLiveJobLog] = useState('');
  const [selectedEncodeTitleId, setSelectedEncodeTitleId] = useState(null);
  const [selectedEncodeTitleIds, setSelectedEncodeTitleIds] = useState([]);
  const [selectedPlaylistId, setSelectedPlaylistId] = useState(null);
  const [selectedHandBrakeTitleIdsState, setSelectedHandBrakeTitleIdsState] = useState([]);
  const [trackSelectionByTitle, setTrackSelectionByTitle] = useState({});
  const [episodeAssignmentsByTitle, setEpisodeAssignmentsByTitle] = useState({});
  const [selectedEpisodeFillStart, setSelectedEpisodeFillStart] = useState(null);
  const [containerUsedEpisodeKeys, setContainerUsedEpisodeKeys] = useState([]);
  const [settingsMap, setSettingsMap] = useState({});
  const [presetDisplayMap, setPresetDisplayMap] = useState({});
  const [scriptCatalog, setScriptCatalog] = useState([]);
  const [chainCatalog, setChainCatalog] = useState([]);
  // Unified ordered lists: [{type: 'script'|'chain', id: number}]
  const [preEncodeItems, setPreEncodeItems] = useState([]);
  const [postEncodeItems, setPostEncodeItems] = useState([]);
  const [userPresets, setUserPresets] = useState([]);
  const [selectedUserPresetId, setSelectedUserPresetId] = useState(null);
  const [handBrakePresetOptions, setHandBrakePresetOptions] = useState([]);
  const [selectedHandBrakePreset, setSelectedHandBrakePreset] = useState('');
  const [selectedOutputFormat, setSelectedOutputFormat] = useState('mkv');
  const [converterConfigSaving, setConverterConfigSaving] = useState(false);
  const [converterConfigError, setConverterConfigError] = useState(null);
  const converterConfigLatestRef = useRef(null);
  const converterConfigSaveTimeoutRef = useRef(null);
  const selectedMetadataEpisodes = useMemo(() => {
    const rows = Array.isArray(selectedMetadata?.episodes) ? selectedMetadata.episodes : [];
    const normalizedRows = rows
      .map((item) => {
        const episodeId = Number(item?.id || 0);
        const episodeNumber = Number(item?.number ?? item?.episodeNumber ?? 0);
        const seasonNumber = Number(item?.seasonNumber ?? item?.season ?? 0);
        const title = String(item?.name || item?.title || '').trim();
        return {
          episodeId: Number.isFinite(episodeId) && episodeId > 0 ? Math.trunc(episodeId) : null,
          episodeNumber: Number.isFinite(episodeNumber) && episodeNumber > 0 ? Math.trunc(episodeNumber) : null,
          seasonNumber: Number.isFinite(seasonNumber) && seasonNumber > 0 ? Math.trunc(seasonNumber) : null,
          episodeTitle: title || null
        };
      })
      .filter((item) => item.episodeId || item.episodeNumber !== null)
      .sort((a, b) => {
        const aSeason = Number.isFinite(a.seasonNumber) ? a.seasonNumber : Number.MAX_SAFE_INTEGER;
        const bSeason = Number.isFinite(b.seasonNumber) ? b.seasonNumber : Number.MAX_SAFE_INTEGER;
        if (aSeason !== bSeason) {
          return aSeason - bSeason;
        }
        const aEpisode = Number.isFinite(a.episodeNumber) ? a.episodeNumber : Number.MAX_SAFE_INTEGER;
        const bEpisode = Number.isFinite(b.episodeNumber) ? b.episodeNumber : Number.MAX_SAFE_INTEGER;
        if (aEpisode !== bEpisode) {
          return aEpisode - bEpisode;
        }
        const aId = Number.isFinite(a.episodeId) ? a.episodeId : Number.MAX_SAFE_INTEGER;
        const bId = Number.isFinite(b.episodeId) ? b.episodeId : Number.MAX_SAFE_INTEGER;
        return aId - bId;
      });
    if (!Array.isArray(containerUsedEpisodeKeys) || containerUsedEpisodeKeys.length === 0) {
      return normalizedRows;
    }
    const usedKeySet = new Set(containerUsedEpisodeKeys);
    return normalizedRows.filter((episode) => {
      const key = buildEpisodeReferenceKey(episode);
      return !key || !usedKeySet.has(key);
    });
  }, [selectedMetadata?.episodes, containerUsedEpisodeKeys]);

  useEffect(() => {
    let cancelled = false;
    const loadSettings = async () => {
      try {
        const [settingsResponse, presetsResponse, scriptsResponse, chainsResponse, userPresetsResponse] = await Promise.allSettled([
          api.getSettings(),
          api.getHandBrakePresets(),
          api.getScripts(),
          api.getScriptChains(),
          api.getUserPresets(null, { forceRefresh: true })
        ]);
        if (!cancelled) {
          const categories = settingsResponse.status === 'fulfilled'
            ? (settingsResponse.value?.categories || [])
            : [];
          setSettingsMap(buildSettingsMap(categories));
          const presetOptions = presetsResponse.status === 'fulfilled'
            ? (presetsResponse.value?.options || [])
            : [];
          setPresetDisplayMap(buildPresetDisplayMap(presetOptions));
          setHandBrakePresetOptions(Array.isArray(presetOptions) ? presetOptions : []);
          const scripts = scriptsResponse.status === 'fulfilled'
            ? (Array.isArray(scriptsResponse.value?.scripts) ? scriptsResponse.value.scripts : [])
            : [];
          setScriptCatalog(
            scripts.map((item) => ({
              id: item?.id,
              name: item?.name
            }))
          );
          const chains = chainsResponse.status === 'fulfilled'
            ? (Array.isArray(chainsResponse.value?.chains) ? chainsResponse.value.chains : [])
            : [];
          setChainCatalog(chains.map((item) => ({ id: item?.id, name: item?.name })));
          const allUserPresets = userPresetsResponse.status === 'fulfilled'
            ? (Array.isArray(userPresetsResponse.value?.presets) ? userPresetsResponse.value.presets : [])
            : [];
          setUserPresets(allUserPresets);
        }
      } catch (_error) {
        if (!cancelled) {
          setSettingsMap({});
          setPresetDisplayMap({});
          setScriptCatalog([]);
          setChainCatalog([]);
          setUserPresets([]);
          setHandBrakePresetOptions([]);
        }
      }
    };
    loadSettings();
    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    const fromReview = normalizeTitleId(mediaInfoReview?.encodeInputTitleId);
    const reviewTitles = Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [];
    const selectedFromReview = normalizeTitleIdList([
      ...(Array.isArray(mediaInfoReview?.selectedTitleIds) ? mediaInfoReview.selectedTitleIds : []),
      ...reviewTitles.filter((title) => Boolean(title?.selectedForEncode)).map((title) => title?.id),
      ...(fromReview ? [fromReview] : [])
    ]);
    const defaultAllTitles = Boolean(
      (mediaInfoReview?.titleSelectionRequired || mediaInfoReview?.handBrakeTitleDecisionRequired)
      && reviewTitles.length > 0
    );
    const effectiveSelected = defaultAllTitles
      ? normalizeTitleIdList(reviewTitles.map((title) => title?.id))
      : selectedFromReview;
    setSelectedEncodeTitleIds(effectiveSelected);
    setSelectedEncodeTitleId(effectiveSelected[0] || fromReview || null);
    setTrackSelectionByTitle(buildDefaultTrackSelection(mediaInfoReview));
    const initialEpisodeAssignments = {};
    for (const title of reviewTitles) {
      const titleId = normalizeTitleId(title?.id);
      if (!titleId) {
        continue;
      }
      const episodeId = Number(title?.episodeId || 0);
      const episodeNumber = Number(title?.episodeNumber || 0);
      const seasonNumber = Number(title?.seasonNumber || 0);
      const episodeTitle = String(title?.episodeTitle || '').trim();
      if (!episodeTitle && !episodeId && !episodeNumber && !seasonNumber) {
        continue;
      }
      initialEpisodeAssignments[titleId] = {
        episodeId: Number.isFinite(episodeId) && episodeId > 0 ? Math.trunc(episodeId) : null,
        episodeNumber: Number.isFinite(episodeNumber) && episodeNumber > 0 ? Math.trunc(episodeNumber) : null,
        seasonNumber: Number.isFinite(seasonNumber) && seasonNumber > 0 ? Math.trunc(seasonNumber) : null,
        episodeTitle: episodeTitle || null
      };
    }
    setEpisodeAssignmentsByTitle(initialEpisodeAssignments);
    setSelectedEpisodeFillStart(null);
    const normChain = (raw) => (Array.isArray(raw) ? raw : []).map(Number).filter((id) => Number.isFinite(id) && id > 0);
    setPreEncodeItems([
      ...normalizeScriptIdList(mediaInfoReview?.preEncodeScriptIds || []).map((id) => ({ type: 'script', id })),
      ...normChain(mediaInfoReview?.preEncodeChainIds).map((id) => ({ type: 'chain', id }))
    ]);
    setPostEncodeItems([
      ...normalizeScriptIdList(mediaInfoReview?.postEncodeScriptIds || []).map((id) => ({ type: 'script', id })),
      ...normChain(mediaInfoReview?.postEncodeChainIds).map((id) => ({ type: 'chain', id }))
    ]);
    const nextOutputFormat = String(mediaInfoReview?.outputFormat || 'mkv').trim().toLowerCase() || 'mkv';
    setSelectedOutputFormat(nextOutputFormat);
    const userPresetId = Number(mediaInfoReview?.userPreset?.id);
    if (Number.isFinite(userPresetId) && userPresetId > 0) {
      setSelectedUserPresetId(Math.trunc(userPresetId));
      setSelectedHandBrakePreset('');
    } else {
      setSelectedUserPresetId(null);
      const hbPresetName = String(mediaInfoReview?.userPreset?.handbrakePreset || '').trim();
      setSelectedHandBrakePreset(hbPresetName);
    }
  }, [
    mediaInfoReview?.encodeInputTitleId,
    mediaInfoReview?.generatedAt,
    mediaInfoReview?.reviewConfirmedAt,
    mediaInfoReview?.prefilledFromPreviousRunAt,
    mediaInfoReview?.outputFormat,
    mediaInfoReview?.userPreset?.id,
    mediaInfoReview?.userPreset?.handbrakePreset,
    retryJobId
  ]);

  useEffect(() => {
    if (mediaInfoReview) {
      return;
    }
    setSelectedEncodeTitleIds([]);
    setSelectedEncodeTitleId(null);
    setTrackSelectionByTitle({});
    setEpisodeAssignmentsByTitle({});
    setSelectedEpisodeFillStart(null);
    setPreEncodeItems([]);
    setPostEncodeItems([]);
    setSelectedUserPresetId(null);
    setSelectedHandBrakePreset('');
  }, [mediaInfoReview, retryJobId]);

  useEffect(() => {
    const normalizedSelectedIds = normalizeTitleIdList(selectedEncodeTitleIds);
    const primaryTitleId = normalizeTitleId(selectedEncodeTitleId);
    if (normalizedSelectedIds.length === 0) {
      if (primaryTitleId !== null) {
        setSelectedEncodeTitleId(null);
      }
      return;
    }
    if (!primaryTitleId || !normalizedSelectedIds.includes(primaryTitleId)) {
      setSelectedEncodeTitleId(normalizedSelectedIds[0]);
    }
    setTrackSelectionByTitle((prev) => {
      const defaults = buildDefaultTrackSelection(mediaInfoReview);
      let next = prev;
      let changed = false;
      for (const titleId of normalizedSelectedIds) {
        if (next?.[titleId] || next?.[String(titleId)]) {
          continue;
        }
        const fallback = defaults[titleId] || {
          audioTrackIds: [],
          subtitleTrackIds: [],
          subtitleVariantSelection: {},
          subtitleLanguageOrder: []
        };
        if (!changed) {
          next = { ...(prev || {}) };
          changed = true;
        }
        next[titleId] = fallback;
      }
      return changed ? next : prev;
    });
  }, [selectedEncodeTitleId, selectedEncodeTitleIds, mediaInfoReview?.generatedAt]);

  const applyEpisodeFillFrom = (startEpisodeRef = null) => {
    const episodeRows = Array.isArray(selectedMetadataEpisodes) ? selectedMetadataEpisodes : [];
    if (episodeRows.length === 0) {
      return;
    }
    const selectedTitleIds = normalizeTitleIdList(selectedEncodeTitleIds);
    if (selectedTitleIds.length === 0) {
      return;
    }
    const orderedTitles = (Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [])
      .map((title) => ({
        id: normalizeTitleId(title?.id),
        title
      }))
      .filter((row) => row.id !== null);
    const orderedSelectedTitleIds = orderedTitles
      .map((row) => row.id)
      .filter((titleId) => selectedTitleIds.includes(titleId));
    const fallbackSelectedTitleIds = orderedSelectedTitleIds.length > 0
      ? orderedSelectedTitleIds
      : selectedTitleIds;
    if (fallbackSelectedTitleIds.length === 0) {
      return;
    }
    const normalizedRef = String(startEpisodeRef || '').trim();
    let startIndex = 0;
    if (normalizedRef) {
      const foundIndex = episodeRows.findIndex((episode) =>
        String(episode?.episodeId || '') === normalizedRef
        || String(episode?.episodeNumber || '') === normalizedRef
      );
      if (foundIndex >= 0) {
        startIndex = foundIndex;
      }
    }

    const nextAssignments = {};
    fallbackSelectedTitleIds.forEach((titleId, idx) => {
      const episode = episodeRows[startIndex + idx];
      if (!episode) {
        return;
      }
      nextAssignments[titleId] = {
        episodeId: episode?.episodeId || null,
        episodeNumber: episode?.episodeNumber ?? null,
        seasonNumber: episode?.seasonNumber ?? null,
        episodeTitle: episode?.episodeTitle || null
      };
    });
    setEpisodeAssignmentsByTitle((prev) => ({
      ...(prev && typeof prev === 'object' ? prev : {}),
      ...nextAssignments
    }));
  };

  useEffect(() => {
    if (!Array.isArray(selectedMetadataEpisodes) || selectedMetadataEpisodes.length === 0) {
      return;
    }
    const hasAnyEpisodeAssignment = Object.values(episodeAssignmentsByTitle || {}).some((assignment) => {
      const episodeId = Number(assignment?.episodeId || 0);
      const episodeNumber = Number(assignment?.episodeNumber || 0);
      const seasonNumber = Number(assignment?.seasonNumber || 0);
      const episodeTitle = String(assignment?.episodeTitle || '').trim();
      return Boolean(episodeTitle || episodeId > 0 || episodeNumber > 0 || seasonNumber > 0);
    });
    if (hasAnyEpisodeAssignment) {
      return;
    }
    const defaultStartRef = selectedMetadataEpisodes[0]?.episodeId || selectedMetadataEpisodes[0]?.episodeNumber || null;
    if (!defaultStartRef) {
      return;
    }
    setSelectedEpisodeFillStart(String(defaultStartRef));
    applyEpisodeFillFrom(defaultStartRef);
  }, [selectedMetadataEpisodes, selectedEncodeTitleIds, mediaInfoReview?.generatedAt, episodeAssignmentsByTitle]);

  const reviewPlaylistDecisionRequired = Boolean(mediaInfoReview?.playlistDecisionRequired);
  const hasSelectedEncodeTitle = normalizeTitleIdList(selectedEncodeTitleIds).length > 0;
  const canConfirmReview = !reviewPlaylistDecisionRequired || hasSelectedEncodeTitle;
  const trimmedHandBrakePreset = String(selectedHandBrakePreset || '').trim();
  const hasSelectedPreset = Boolean(selectedUserPresetId) || Boolean(trimmedHandBrakePreset);
  const converterMediaType = String(mediaInfoReview?.converterMediaType || 'video').trim().toLowerCase() || 'video';
  const showConverterConfig = jobMediaProfile === 'converter' && converterMediaType !== 'audio';
  const requiresPresetSelection = showConverterConfig;

  // Filter user presets by job media profile ('all' presets always shown)
  const filteredUserPresets = (Array.isArray(userPresets) ? userPresets : []).filter((p) => {
    const presetMediaType = normalizeMediaProfile(p?.mediaType) || 'all';
    if (!jobMediaProfile) {
      return true;
    }
    // Converter jobs may reuse any user preset; do not restrict by preset media type here.
    if (jobMediaProfile === 'converter') {
      return true;
    }
    return presetMediaType === 'all' || presetMediaType === jobMediaProfile;
  });
  const effectiveUserPresets = filteredUserPresets;
  const canStartReadyJob = (isPreRipReview
    ? Boolean(retryJobId)
    : Boolean(retryJobId && encodeInputPath))
    && (!requiresPresetSelection || hasSelectedPreset);
  const canRestartEncodeFromLastSettings = Boolean(
    (state === 'ERROR' || state === 'CANCELLED')
    && retryJobId
    && pipeline?.context?.canRestartEncodeFromLastSettings
  );
  const canRestartReviewFromRaw = Boolean(
    retryJobId
    && !running
    && (pipeline?.context?.canRestartReviewFromRaw || pipeline?.context?.rawPath)
  );
  const allowConverterConfigEdit = showConverterConfig && !queueLocked && state === 'READY_TO_ENCODE';

  const converterConfigPayload = useMemo(() => {
    if (!showConverterConfig) {
      return null;
    }
    let userPreset = null;
    if (selectedUserPresetId) {
      const preset = (Array.isArray(userPresets) ? userPresets : [])
        .find((p) => Number(p?.id) === Number(selectedUserPresetId));
      userPreset = preset
        ? { id: preset.id, handbrakePreset: preset.handbrake_preset || null, extraArgs: preset.extra_args || '' }
        : { id: selectedUserPresetId };
    } else if (trimmedHandBrakePreset) {
      userPreset = { handbrakePreset: trimmedHandBrakePreset, extraArgs: '' };
    }
    return {
      converterMediaType,
      outputFormat: selectedOutputFormat,
      userPreset
    };
  }, [
    showConverterConfig,
    converterMediaType,
    selectedOutputFormat,
    selectedUserPresetId,
    trimmedHandBrakePreset,
    userPresets
  ]);

  const converterConfigSerialized = useMemo(
    () => (converterConfigPayload ? JSON.stringify(converterConfigPayload) : null),
    [converterConfigPayload]
  );

  useEffect(() => {
    converterConfigLatestRef.current = null;
    setConverterConfigError(null);
    setConverterConfigSaving(false);
  }, [retryJobId]);

  useEffect(() => {
    if (!allowConverterConfigEdit || !converterConfigPayload || !retryJobId) {
      return;
    }
    if (converterConfigLatestRef.current === null) {
      converterConfigLatestRef.current = converterConfigSerialized;
      return;
    }
    if (converterConfigSerialized === converterConfigLatestRef.current) {
      return;
    }
    if (converterConfigSaveTimeoutRef.current) {
      clearTimeout(converterConfigSaveTimeoutRef.current);
    }
    converterConfigSaveTimeoutRef.current = setTimeout(async () => {
      setConverterConfigSaving(true);
      setConverterConfigError(null);
      try {
        await api.converterUpdateJobConfig(retryJobId, converterConfigPayload);
        converterConfigLatestRef.current = converterConfigSerialized;
        onInputsChanged?.(retryJobId, converterConfigPayload);
      } catch (error) {
        setConverterConfigError(error?.message || 'Konfiguration konnte nicht gespeichert werden.');
      } finally {
        setConverterConfigSaving(false);
      }
    }, 650);
    return () => {
      if (converterConfigSaveTimeoutRef.current) {
        clearTimeout(converterConfigSaveTimeoutRef.current);
        converterConfigSaveTimeoutRef.current = null;
      }
    };
  }, [
    allowConverterConfigEdit,
    converterConfigPayload,
    converterConfigSerialized,
    retryJobId,
    onInputsChanged
  ]);

  const waitingPlaylistRows = useMemo(() => {
    const evaluated = Array.isArray(playlistAnalysis?.evaluatedCandidates)
      ? playlistAnalysis.evaluatedCandidates
      : [];

    const rows = evaluated.length > 0
      ? evaluated
      : (Array.isArray(pipeline?.context?.playlistCandidates) ? pipeline.context.playlistCandidates : []);

    const normalized = rows
      .map((item) => {
        const playlistId = normalizePlaylistId(item?.playlistId || item?.playlistFile || item);
        if (!playlistId) {
          return null;
        }
        const playlistFile = `${playlistId}.mpls`;
        const score = Number(item?.score);
        const sequenceCoherence = Number(
          item?.structuralMetrics?.sequenceCoherence ?? item?.sequenceCoherence
        );
        const handBrakeTitleId = Number(item?.handBrakeTitleId);
        const durationSecondsRaw = Number(item?.durationSeconds ?? item?.duration ?? 0);
        const durationSeconds = Number.isFinite(durationSecondsRaw) && durationSecondsRaw > 0
          ? Math.trunc(durationSecondsRaw)
          : 0;
        const durationLabelRaw = String(item?.durationLabel || '').trim();
        const durationLabel = durationLabelRaw || formatDurationClock(durationSeconds);
        return {
          playlistId,
          playlistFile,
          titleId: Number.isFinite(Number(item?.titleId)) ? Number(item.titleId) : null,
          durationSeconds,
          durationLabel: durationLabel || null,
          score: Number.isFinite(score) ? score : null,
          evaluationLabel: item?.evaluationLabel || null,
          segmentCommand: item?.segmentCommand
            || `strings BDMV/PLAYLIST/${playlistId}.mpls | grep m2ts`,
          segmentFiles: Array.isArray(item?.segmentFiles) ? item.segmentFiles : [],
          sequenceCoherence: Number.isFinite(sequenceCoherence) ? sequenceCoherence : null,
          recommended: Boolean(item?.recommended),
          handBrakeTitleId: Number.isFinite(handBrakeTitleId) && handBrakeTitleId > 0
            ? Math.trunc(handBrakeTitleId)
            : null,
          audioSummary: item?.audioSummary || null,
          audioTrackPreview: Array.isArray(item?.audioTrackPreview) ? item.audioTrackPreview : []
        };
      })
      .filter(Boolean);

    const dedup = [];
    const seen = new Set();
    for (const row of normalized) {
      if (seen.has(row.playlistId)) {
        continue;
      }
      seen.add(row.playlistId);
      dedup.push(row);
    }
    return dedup;
  }, [playlistAnalysis, pipeline?.context?.playlistCandidates]);

  const handBrakeTitleDecisionRequired = state === 'WAITING_FOR_USER_DECISION'
    && Boolean(pipeline?.context?.handBrakeTitleDecisionRequired);

  const waitingHandBrakeTitleRows = useMemo(() => {
    const raw = Array.isArray(pipeline?.context?.handBrakeTitleCandidates)
      ? pipeline.context.handBrakeTitleCandidates
      : [];
    return raw
      .map((item) => {
        const titleId = Number(item?.handBrakeTitleId);
        if (!Number.isFinite(titleId) || titleId <= 0) return null;
        const durationSeconds = Number(item?.durationSeconds || 0);
        const minutes = Math.floor(durationSeconds / 60);
        const seconds = durationSeconds % 60;
        const durationLabel = `${minutes}:${String(seconds).padStart(2, '0')} min`;
        return {
          handBrakeTitleId: Math.trunc(titleId),
          durationSeconds,
          durationLabel,
          durationMinutes: Number(item?.durationMinutes || 0),
          audioTrackCount: Number(item?.audioTrackCount || 0),
          subtitleTrackCount: Number(item?.subtitleTrackCount || 0),
          sizeBytes: Number(item?.sizeBytes || 0)
        };
      })
      .filter(Boolean)
      .sort((a, b) => a.handBrakeTitleId - b.handBrakeTitleId);
  }, [pipeline?.context?.handBrakeTitleCandidates]);

  useEffect(() => {
    if (!handBrakeTitleDecisionRequired) {
      setSelectedHandBrakeTitleIdsState([]);
      return;
    }
    const contextSelectedIds = Array.isArray(pipeline?.context?.selectedHandBrakeTitleIds)
      ? pipeline.context.selectedHandBrakeTitleIds
      : [];
    const normalizedContextIds = contextSelectedIds
      .map((value) => Number(value))
      .filter((value) => Number.isFinite(value) && value > 0)
      .map((value) => Math.trunc(value));
    if (normalizedContextIds.length > 0) {
      setSelectedHandBrakeTitleIdsState(Array.from(new Set(normalizedContextIds)));
      return;
    }
    const singleContextId = Number(pipeline?.context?.selectedHandBrakeTitleId || 0);
    if (Number.isFinite(singleContextId) && singleContextId > 0) {
      setSelectedHandBrakeTitleIdsState([Math.trunc(singleContextId)]);
      return;
    }
    const best = waitingHandBrakeTitleRows[0]?.handBrakeTitleId || null;
    setSelectedHandBrakeTitleIdsState(best ? [best] : []);
  }, [
    handBrakeTitleDecisionRequired,
    retryJobId,
    waitingHandBrakeTitleRows,
    pipeline?.context?.selectedHandBrakeTitleId,
    pipeline?.context?.selectedHandBrakeTitleIds
  ]);

  useEffect(() => {
    if (state !== 'WAITING_FOR_USER_DECISION') {
      setSelectedPlaylistId(null);
      return;
    }

    const current = normalizePlaylistId(pipeline?.context?.selectedPlaylist);
    if (current) {
      setSelectedPlaylistId(current);
      return;
    }

    const recommendedFromRows = waitingPlaylistRows.find((item) => item.recommended)?.playlistId || null;
    const recommendedFromAnalysis = normalizePlaylistId(playlistAnalysis?.recommendation?.playlistId);
    const fallback = waitingPlaylistRows[0]?.playlistId || null;
    setSelectedPlaylistId(recommendedFromRows || recommendedFromAnalysis || fallback);
  }, [
    state,
    retryJobId,
    waitingPlaylistRows,
    playlistAnalysis?.recommendation?.playlistId,
    pipeline?.context?.selectedPlaylist
  ]);

  useEffect(() => {
    if (!running || !retryJobId) {
      setLiveJobLog('');
      return undefined;
    }
    let cancelled = false;
    const refresh = async () => {
      try {
        const response = await api.getJob(retryJobId, { includeLiveLog: true, lite: true });
        if (!cancelled) {
          setLiveJobLog(response?.job?.log || '');
        }
      } catch (_err) {
        // ignore transient polling errors
      }
    };
    void refresh();
    const interval = setInterval(refresh, 2500);
    return () => {
      cancelled = true;
      clearInterval(interval);
    };
  }, [running, retryJobId]);

  const playlistDecisionRequiredBeforeStart = state === 'WAITING_FOR_USER_DECISION' && !handBrakeTitleDecisionRequired;
  const isSeriesDvdReview = useMemo(() => {
    if (jobMediaProfile !== 'dvd') {
      return false;
    }
    const seasonNumberRaw = selectedMetadata?.seasonNumber ?? null;
    const seasonNumber = Number(String(seasonNumberRaw ?? '').replace(',', '.'));
    const hasSeasonNumber = Number.isFinite(seasonNumber) && seasonNumber > 0;
    const hasEpisodeList = Array.isArray(selectedMetadata?.episodes) && selectedMetadata.episodes.length > 0;
    const metadataProvider = String(selectedMetadata?.metadataProvider || '').trim().toLowerCase();
    return hasSeasonNumber || hasEpisodeList || metadataProvider === 'tmdb' || metadataProvider === 'themoviedb';
  }, [jobMediaProfile, selectedMetadata?.seasonNumber, selectedMetadata?.episodes, selectedMetadata?.metadataProvider]);
  useEffect(() => {
    let cancelled = false;

    const loadContainerUsedEpisodes = async () => {
      if (!isSeriesDvdReview || !retryJobId) {
        if (!cancelled) {
          setContainerUsedEpisodeKeys([]);
        }
        return;
      }

      try {
        const currentResponse = await api.getJob(retryJobId, { lite: true, forceRefresh: true });
        if (cancelled) {
          return;
        }
        const currentJob = currentResponse?.job && typeof currentResponse.job === 'object'
          ? currentResponse.job
          : null;
        const currentJobId = normalizeJobId(currentJob?.id || retryJobId);
        const normalizedCurrentKind = String(currentJob?.job_kind || '').trim().toLowerCase();
        const containerJobId = normalizedCurrentKind === 'dvd_series_container'
          ? normalizeJobId(currentJob?.id)
          : normalizeJobId(currentJob?.parent_job_id);

        if (!containerJobId) {
          if (!cancelled) {
            setContainerUsedEpisodeKeys([]);
          }
          return;
        }

        const containerResponse = await api.getJob(containerJobId, { lite: true, forceRefresh: true });
        if (cancelled) {
          return;
        }
        const containerJob = containerResponse?.job && typeof containerResponse.job === 'object'
          ? containerResponse.job
          : null;
        const children = Array.isArray(containerJob?.children) ? containerJob.children : [];
        const usedKeys = new Set();

        for (const child of children) {
          const childId = normalizeJobId(child?.id);
          if (currentJobId && childId && currentJobId === childId) {
            continue;
          }
          if (isSeriesBatchEpisodePlan(child?.encodePlan)) {
            continue;
          }

          const refs = collectEpisodeReferencesFromPlan(child?.encodePlan);
          for (const ref of refs) {
            const key = buildEpisodeReferenceKey(ref);
            if (key) {
              usedKeys.add(key);
            }
          }

          const batchEpisodes = Array.isArray(child?.handbrakeInfo?.seriesBatch?.episodes)
            ? child.handbrakeInfo.seriesBatch.episodes
            : [];
          for (const episode of batchEpisodes) {
            const key = buildEpisodeReferenceKey({
              episodeId: episode?.episodeId,
              episodeNumber: episode?.episodeNumber,
              seasonNumber: episode?.seasonNumber
            });
            if (key) {
              usedKeys.add(key);
            }
          }
        }

        if (!cancelled) {
          setContainerUsedEpisodeKeys(Array.from(usedKeys));
        }
      } catch (_error) {
        if (!cancelled) {
          setContainerUsedEpisodeKeys([]);
        }
      }
    };

    void loadContainerUsedEpisodes();
    return () => {
      cancelled = true;
    };
  }, [
    isSeriesDvdReview,
    retryJobId,
    mediaInfoReview?.generatedAt,
    mediaInfoReview?.reviewConfirmedAt,
    selectedMetadata?.tmdbId,
    selectedMetadata?.seasonNumber
  ]);
  const seriesBatchContext = useMemo(() => {
    const raw = pipeline?.context?.seriesBatch;
    if (!raw || typeof raw !== 'object') {
      return null;
    }
    const rawChildren = Array.isArray(raw?.children) ? raw.children : [];
    const children = rawChildren.map((child, index) => {
      const status = normalizeSeriesBatchStatus(child?.status);
      return {
        jobId: normalizeJobId(child?.jobId),
        title: String(child?.title || '').trim() || `Episode #${index + 1}`,
        status,
        progress: normalizeSeriesBatchProgress(child?.progress, status === 'FINISHED' ? 100 : 0),
        eta: child?.eta ?? null,
        episodeIndex: normalizePositiveInt(child?.episodeIndex),
        titleId: normalizeTitleId(child?.titleId)
      };
    });
    const totalCountRaw = Number(raw?.totalCount || 0);
    const totalCount = Number.isFinite(totalCountRaw) && totalCountRaw > 0
      ? Math.trunc(totalCountRaw)
      : children.length;
    const finishedCountRaw = Number(raw?.finishedCount || 0);
    const cancelledCountRaw = Number(raw?.cancelledCount || 0);
    const errorCountRaw = Number(raw?.errorCount || 0);
    const runningCountRaw = Number(raw?.runningCount || 0);
    const finishedCount = Number.isFinite(finishedCountRaw) ? Math.max(0, Math.trunc(finishedCountRaw)) : 0;
    const cancelledCount = Number.isFinite(cancelledCountRaw) ? Math.max(0, Math.trunc(cancelledCountRaw)) : 0;
    const errorCount = Number.isFinite(errorCountRaw) ? Math.max(0, Math.trunc(errorCountRaw)) : 0;
    const runningCount = Number.isFinite(runningCountRaw) ? Math.max(0, Math.trunc(runningCountRaw)) : 0;
    const aggregateProgress = totalCount > 0
      ? (
        children.reduce((sum, child) => {
          const status = normalizeSeriesBatchStatus(child?.status);
          if (status === 'FINISHED') {
            return sum + 100;
          }
          return sum + normalizeSeriesBatchProgress(child?.progress, 0);
        }, 0) / totalCount
      )
      : 0;
    const overallProgress = normalizeSeriesBatchProgress(raw?.overallProgress, aggregateProgress);
    return {
      totalCount,
      finishedCount,
      cancelledCount,
      errorCount,
      runningCount,
      overallProgress,
      children
    };
  }, [pipeline?.context?.seriesBatch]);
  const hasSeriesBatchProgress = Boolean(
    seriesBatchContext
    && (seriesBatchContext.totalCount > 0 || seriesBatchContext.children.length > 0)
  );
  const runningSeriesEpisode = hasSeriesBatchProgress
    ? (seriesBatchContext.children.find((child) => normalizeSeriesBatchStatus(child?.status) === 'RUNNING') || null)
    : null;
  const primaryProgressValue = hasSeriesBatchProgress
    ? normalizeSeriesBatchProgress(seriesBatchContext?.overallProgress, progress)
    : progress;
  const primaryEta = hasSeriesBatchProgress
    ? (runningSeriesEpisode?.eta || pipeline?.eta || null)
    : (pipeline?.eta || null);
  const primaryStatusText = hasSeriesBatchProgress
    ? (
      `Serien-Encoding ${primaryProgressValue.toFixed(2)}%`
      + `${runningSeriesEpisode?.title ? ` - ${runningSeriesEpisode.title}` : ''}`
    )
    : (pipeline?.statusText || 'Bereit');
  const isSeriesBatchParentReview = Boolean(
    mediaInfoReview?.seriesBatchParent
    || pipeline?.context?.mediaInfoReview?.seriesBatchParent
    || hasSeriesBatchProgress
  );
  const commandOutputPath = useMemo(() => {
    if (jobMediaProfile === 'converter') {
      const reviewWithOutput = mediaInfoReview
        ? { ...mediaInfoReview, outputFormat: selectedOutputFormat }
        : { outputFormat: selectedOutputFormat };
      return buildConverterOutputPathPreview(settingsMap, reviewWithOutput, selectedMetadata, retryJobId);
    }
    if (isSeriesDvdReview) {
      return null;
    }
    return buildOutputPathPreview(settingsMap, jobMediaProfile, selectedMetadata, retryJobId);
  }, [settingsMap, jobMediaProfile, mediaInfoReview, selectedMetadata, retryJobId, selectedOutputFormat, isSeriesDvdReview]);
  const commandOutputPathByTitle = useMemo(() => {
    if (!isSeriesDvdReview) {
      return {};
    }
    const reviewTitles = Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [];
    const map = {};
    for (const title of reviewTitles) {
      const titleId = normalizeTitleId(title?.id);
      if (!titleId) {
        continue;
      }
      const assignment = episodeAssignmentsByTitle?.[titleId]
        || episodeAssignmentsByTitle?.[String(titleId)]
        || null;
      const path = buildDvdSeriesEpisodeOutputPathPreview(
        settingsMap,
        selectedMetadata || {},
        title,
        assignment,
        retryJobId
      );
      if (path) {
        map[titleId] = path;
      }
    }
    return map;
  }, [isSeriesDvdReview, mediaInfoReview?.titles, episodeAssignmentsByTitle, settingsMap, selectedMetadata, retryJobId]);
  const presetDisplayValue = useMemo(() => {
    const preset = String(mediaInfoReview?.selectors?.preset || '').trim();
    if (!preset) {
      return '';
    }
    return presetDisplayMap[preset] || preset;
  }, [mediaInfoReview?.selectors?.preset, presetDisplayMap]);
  const converterUserPresetOptions = useMemo(() => {
    if (!showConverterConfig) {
      return [];
    }
    const userOptions = (Array.isArray(effectiveUserPresets) ? effectiveUserPresets : [])
      .map((preset) => ({
        label: String(preset?.name || '').trim(),
        value: Number(preset?.id)
      }))
      .filter((option) => option.label && Number.isFinite(option.value));
    return [{ label: 'Kein User-Preset', value: null }, ...userOptions];
  }, [showConverterConfig, effectiveUserPresets]);
  const converterHandBrakePresetOptions = useMemo(() => {
    if (!showConverterConfig) {
      return [];
    }
    const list = Array.isArray(handBrakePresetOptions) ? handBrakePresetOptions : [];
    const hbOptions = list
      .map((option) => {
        const label = String(option?.label || '').trim();
        if (!label) {
          return null;
        }
        const disabled = Boolean(option?.disabled);
        const lowerLabel = label.toLowerCase();
        if (lowerLabel.includes('handbrake cli') || lowerLabel.includes('cli default')) {
          return null;
        }
        if (!disabled && (lowerLabel.includes('kein preset') || lowerLabel.includes('cli-parameter'))) {
          return null;
        }
        const value = disabled
          ? String(option?.value || label)
          : String(option?.value || '').trim();
        if (!disabled && !value) {
          return null;
        }
        return {
          label,
          value,
          disabled
        };
      })
      .filter(Boolean);
    return [{ label: 'Kein HandBrake-Preset', value: '' }, ...hbOptions];
  }, [showConverterConfig, handBrakePresetOptions]);
  const buildSelectedTrackSelectionForCurrentTitle = () => {
    const reviewTitles = Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [];
    const titlesById = new Map(
      reviewTitles
        .map((title) => [normalizeTitleId(title?.id), title])
        .filter((entry) => entry[0] !== null)
    );
    const fallbackSelectedIds = normalizeTitleIdList([
      ...selectedEncodeTitleIds,
      selectedEncodeTitleId,
      mediaInfoReview?.encodeInputTitleId,
      ...reviewTitles.filter((title) => Boolean(title?.selectedForEncode)).map((title) => title?.id)
    ]);
    const selectedTitleIdsInReviewOrder = reviewTitles
      .map((title) => normalizeTitleId(title?.id))
      .filter((id) => id !== null && fallbackSelectedIds.includes(id));
    const effectiveSelectedTitleIds = selectedTitleIdsInReviewOrder.length > 0
      ? selectedTitleIdsInReviewOrder
      : fallbackSelectedIds;
    const preferredPrimaryTitleId = normalizeTitleId(selectedEncodeTitleId)
      || normalizeTitleId(mediaInfoReview?.encodeInputTitleId)
      || null;
    const encodeTitleId = effectiveSelectedTitleIds.includes(preferredPrimaryTitleId)
      ? preferredPrimaryTitleId
      : (effectiveSelectedTitleIds[0] || null);
    const selectedTrackSelection = {};
    for (const titleId of effectiveSelectedTitleIds) {
      const selectionEntry = trackSelectionByTitle?.[titleId] || trackSelectionByTitle?.[String(titleId)] || null;
      const fallbackSelection = defaultTrackSelectionForTitle(mediaInfoReview, titleId);
      const effectiveSelection = selectionEntry || fallbackSelection;
      const encodeTitle = titlesById.get(titleId) || null;
      const blockedSubtitleTrackIds = new Set(
        (Array.isArray(encodeTitle?.subtitleTracks) ? encodeTitle.subtitleTracks : [])
          .filter((track) => isBurnedSubtitleTrack(track) || isDuplicateSubtitleTrack(track))
          .map((track) => normalizeTrackId(track?.id))
          .filter((id) => id !== null)
          .map((id) => String(id))
      );
      const availableSubtitleLanguages = new Set(
        (Array.isArray(encodeTitle?.subtitleTracks) ? encodeTitle.subtitleTracks : [])
          .filter((track) => !isBurnedSubtitleTrack(track) && !isDuplicateSubtitleTrack(track))
          .map((track) => normalizeSubtitleLanguage(track?.language || track?.languageLabel || 'und'))
      );
      const normalizedVariantSelection = normalizeSubtitleVariantSelectionMap(
        effectiveSelection?.subtitleVariantSelection || {}
      );
      const filteredVariantSelection = {};
      for (const [language, value] of Object.entries(normalizedVariantSelection)) {
        if (!availableSubtitleLanguages.has(language)) {
          continue;
        }
        filteredVariantSelection[language] = {
          full: Boolean(value?.full),
          forced: Boolean(value?.forced)
        };
      }
      const normalizedLanguageOrder = normalizeSubtitleLanguageOrder(effectiveSelection?.subtitleLanguageOrder || [])
        .filter((language) => availableSubtitleLanguages.has(language));
      const hasExplicitVariantSelection = Object.keys(filteredVariantSelection).length > 0;
      const filteredSubtitleTrackIds = normalizeTrackIdList(effectiveSelection?.subtitleTrackIds || [])
        .filter((id) => !blockedSubtitleTrackIds.has(String(id)));
      selectedTrackSelection[titleId] = {
        audioTrackIds: normalizeTrackIdList(effectiveSelection?.audioTrackIds || []),
        subtitleTrackIds: hasExplicitVariantSelection ? [] : filteredSubtitleTrackIds,
        subtitleVariantSelection: filteredVariantSelection,
        subtitleLanguageOrder: normalizedLanguageOrder
      };
    }
    const episodeAssignments = {};
    for (const titleId of effectiveSelectedTitleIds) {
      const assignment = episodeAssignmentsByTitle?.[titleId]
        || episodeAssignmentsByTitle?.[String(titleId)]
        || null;
      const title = titlesById.get(titleId) || null;
      const episodeId = Number(assignment?.episodeId || title?.episodeId || 0);
      const episodeNumber = Number(assignment?.episodeNumber || title?.episodeNumber || 0);
      const seasonNumber = Number(assignment?.seasonNumber || title?.seasonNumber || 0);
      const episodeTitle = String(assignment?.episodeTitle || title?.episodeTitle || '').trim();
      if (!episodeTitle && !(episodeId > 0) && !(episodeNumber > 0) && !(seasonNumber > 0)) {
        continue;
      }
      episodeAssignments[titleId] = {
        episodeId: Number.isFinite(episodeId) && episodeId > 0 ? Math.trunc(episodeId) : null,
        episodeNumber: Number.isFinite(episodeNumber) && episodeNumber > 0 ? Math.trunc(episodeNumber) : null,
        seasonNumber: Number.isFinite(seasonNumber) && seasonNumber > 0 ? Math.trunc(seasonNumber) : null,
        episodeTitle: episodeTitle || null
      };
    }
    const selectedPostScriptIds = postEncodeItems.filter((i) => i.type === 'script').map((i) => i.id);
    const selectedPreScriptIds = preEncodeItems.filter((i) => i.type === 'script').map((i) => i.id);
    const selectedPostChainIds = postEncodeItems.filter((i) => i.type === 'chain').map((i) => i.id);
    const selectedPreChainIds = preEncodeItems.filter((i) => i.type === 'chain').map((i) => i.id);
    return {
      encodeTitleId,
      selectedEncodeTitleIds: effectiveSelectedTitleIds,
      selectedTrackSelection: Object.keys(selectedTrackSelection).length > 0 ? selectedTrackSelection : null,
      episodeAssignments: Object.keys(episodeAssignments).length > 0 ? episodeAssignments : null,
      selectedPostScriptIds,
      selectedPreScriptIds,
      selectedPostChainIds,
      selectedPreChainIds,
      selectedHandBrakePreset: trimmedHandBrakePreset
    };
  };

  return (
    <Card title="Pipeline-Status" subTitle="Live-Zustand und Fortschritt">
      <div className="status-row">
        <Tag value={stateLabel} severity={getStatusSeverity(state)} />
        <span>{primaryStatusText}</span>
      </div>

      {running && (
        <div className="progress-wrap">
          <ProgressBar
            value={primaryProgressValue}
            showValue
            displayValueTemplate={(value) => formatPercentTwoDecimals(value)}
          />
          <small>
            {primaryEta
              ? (hasSeriesBatchProgress ? `ETA aktuelle Episode ${primaryEta}` : `ETA ${primaryEta}`)
              : 'ETA unbekannt'}
          </small>
        </div>
      )}

      {state === 'FINISHED' && (
        <div className="progress-wrap">
          <ProgressBar value={100} showValue />
        </div>
      )}

      {hasSeriesBatchProgress ? (
        <div className="series-batch-progress-wrap">
          <div className="series-batch-head">
            <strong>Serien-Encode Fortschritt</strong>
            <Tag
              value={`${seriesBatchContext.finishedCount}/${seriesBatchContext.totalCount || seriesBatchContext.children.length} fertig`}
              severity={seriesBatchContext.errorCount > 0 ? 'danger' : (seriesBatchContext.cancelledCount > 0 ? 'warning' : 'success')}
            />
          </div>
          <small>
            {`Laufend: ${seriesBatchContext.runningCount} | Fehler: ${seriesBatchContext.errorCount} | Abgebrochen: ${seriesBatchContext.cancelledCount}`}
          </small>
          <div className="series-batch-episodes">
            {seriesBatchContext.children.map((child, index) => (
              <div
                key={`series-batch-episode-${child.titleId || child.episodeIndex || index}`}
                className="series-batch-episode-row"
              >
                <div className="series-batch-episode-head">
                  <span className="series-batch-episode-title">{child.title}</span>
                  <Tag value={getStatusLabel(child.status)} severity={getSeriesBatchTagSeverity(child.status)} />
                </div>
                <ProgressBar value={child.progress} showValue={false} style={{ height: '5px' }} />
                <small>{child.eta ? `${Math.round(child.progress)}% | ETA ${child.eta}` : `${Math.round(child.progress)}%`}</small>
              </div>
            ))}
          </div>
        </div>
      ) : null}

      <div className="actions-row">
        {queueLocked ? (
          <Button
            label="Aus Queue löschen"
            icon="pi pi-times"
            severity="danger"
            outlined
            onClick={() => onRemoveFromQueue?.(retryJobId)}
            loading={busy}
            disabled={typeof onRemoveFromQueue !== 'function'}
          />
        ) : (
          <>
            {(state === 'DISC_DETECTED' || state === 'IDLE') && (
              <Button
                label="Analyse starten"
                icon="pi pi-search"
                onClick={onAnalyze}
                loading={busy}
              />
            )}

            {(state === 'METADATA_SELECTION' || state === 'WAITING_FOR_USER_DECISION')
              && retryJobId
              && typeof onOpenMetadata === 'function'
              && !(jobMediaProfile === 'converter' && !selectedMetadata?.imdbId) ? (
              <Button
                label="Metadaten öffnen"
                icon="pi pi-list"
                severity="info"
                onClick={() => onOpenMetadata?.(retryJobId)}
                loading={busy}
              />
            ) : null}

            {state === 'METADATA_SELECTION' && jobMediaProfile === 'converter' && retryJobId ? (
              <Button
                label="Review starten"
                icon="pi pi-play"
                severity="info"
                onClick={() => onStart(retryJobId)}
                loading={busy}
              />
            ) : null}

            {!running
              && state !== 'METADATA_SELECTION'
              && state !== 'WAITING_FOR_USER_DECISION'
              && state !== 'IDLE'
              && state !== 'DISC_DETECTED'
              && retryJobId
              && typeof onReassignOmdb === 'function'
              && !(jobMediaProfile === 'converter' && state === 'READY_TO_START' && !selectedMetadata?.imdbId) ? (
              <Button
                label="OMDb neu zuordnen"
                icon="pi pi-search"
                severity="secondary"
                size="small"
                onClick={() => onReassignOmdb?.(retryJobId)}
                loading={busy}
              />
            ) : null}

            {state === 'READY_TO_START' && retryJobId ? (
              <Button
                label={jobMediaProfile === 'converter' ? 'Review starten' : 'Job starten'}
                icon="pi pi-play"
                severity={jobMediaProfile === 'converter' ? 'info' : 'success'}
                onClick={() => onStart(retryJobId)}
                loading={busy}
              />
            ) : null}

            {playlistDecisionRequiredBeforeStart && retryJobId && (
              <Button
                label="Playlist übernehmen"
                icon="pi pi-check"
                severity="warning"
                outlined
                onClick={() => onSelectPlaylist?.(retryJobId, selectedPlaylistId)}
                loading={busy}
                disabled={!normalizePlaylistId(selectedPlaylistId)}
              />
            )}

            {handBrakeTitleDecisionRequired && retryJobId && (
              <Button
                label="DVD Titel bestätigen"
                icon="pi pi-check"
                severity="warning"
                outlined
                onClick={() => onSelectHandBrakeTitle?.(retryJobId, selectedHandBrakeTitleIdsState)}
                loading={busy}
                disabled={selectedHandBrakeTitleIdsState.length === 0}
              />
            )}

            {state === 'READY_TO_ENCODE' && retryJobId ? (
              <Button
                label={isPreRipReview ? 'Backup + Encoding starten' : 'Encoding starten'}
                icon="pi pi-play"
                severity="success"
                onClick={async () => {
                  const {
                    encodeTitleId,
                    selectedEncodeTitleIds: selectedEncodeTitleIdsForPayload,
                    selectedTrackSelection,
                    episodeAssignments,
                    selectedPostScriptIds,
                    selectedPreScriptIds,
                    selectedPostChainIds,
                    selectedPreChainIds,
                    selectedHandBrakePreset
                  } = buildSelectedTrackSelectionForCurrentTitle();
                  const startPayload = {
                    ensureConfirmed: true,
                    selectedEncodeTitleId: encodeTitleId,
                    selectedEncodeTitleIds: selectedEncodeTitleIdsForPayload,
                    selectedTrackSelection,
                    episodeAssignments,
                    selectedPostEncodeScriptIds: selectedPostScriptIds,
                    selectedPreEncodeScriptIds: selectedPreScriptIds,
                    selectedPostEncodeChainIds: selectedPostChainIds,
                    selectedPreEncodeChainIds: selectedPreChainIds,
                    selectedHandBrakePreset
                  };
                  startPayload.selectedUserPresetId = selectedUserPresetId || null;
                  await onStart(retryJobId, startPayload);
                }}
                loading={busy}
                disabled={!canStartReadyJob || !canConfirmReview}
              />
            ) : null}

            {running && (
              <Button
                label="Abbrechen"
                icon="pi pi-stop"
                severity="danger"
                onClick={() => onCancel?.(retryJobId, state)}
                loading={busy}
              />
            )}

            {canRestartReviewFromRaw ? (
              <Button
                label="Review neu starten"
                icon="pi pi-refresh"
                severity="info"
                outlined
                onClick={() => onRestartReview?.(retryJobId)}
                loading={busy}
                disabled={!retryJobId}
              />
            ) : null}

            {canRestartEncodeFromLastSettings ? (
              isSeriesBatchParentReview ? (
                <>
                  <Button
                    label="Alle neu starten"
                    icon="pi pi-play"
                    severity="success"
                    onClick={() => onRestartEncode?.(retryJobId, { restartMode: 'all' })}
                    loading={busy}
                    disabled={!retryJobId}
                  />
                  <Button
                    label="Neu starten ab Abbruch"
                    icon="pi pi-forward"
                    severity="warning"
                    outlined
                    onClick={() => onRestartEncode?.(retryJobId, { restartMode: 'from_abort' })}
                    loading={busy}
                    disabled={!retryJobId}
                  />
                </>
              ) : (
                <Button
                  label="Encode neu starten"
                  icon="pi pi-play"
                  severity="success"
                  onClick={() => onRestartEncode?.(retryJobId, { restartMode: 'all' })}
                  loading={busy}
                  disabled={!retryJobId}
                />
              )
            ) : null}

            {(state === 'ERROR' || state === 'CANCELLED') && retryJobId && (
              <Button
                label="Retry Rippen"
                icon="pi pi-refresh"
                severity="warning"
                onClick={() => onRetry(retryJobId)}
                loading={busy}
              />
            )}

            {(state === 'ERROR' || state === 'CANCELLED') ? (
              <Button
                label="Disk-Analyse neu starten"
                icon="pi pi-search"
                severity="secondary"
                onClick={onReanalyze || onAnalyze}
                loading={busy}
              />
            ) : null}
          </>
        )}
        {retryJobId && typeof onDeleteJob === 'function' ? (
          <Button
            label="Job löschen"
            icon="pi pi-trash"
            severity="danger"
            outlined
            onClick={() => onDeleteJob?.(retryJobId)}
            loading={busy}
          />
        ) : null}
      </div>

      {running ? (
        <div className="live-log-block">
          <h4>Aktueller Job-Log</h4>
          <pre className="log-box">{liveJobLog || 'Noch keine Log-Ausgabe vorhanden.'}</pre>
        </div>
      ) : null}

      {playlistDecisionRequiredBeforeStart && !queueLocked ? (
        <div className="playlist-decision-block">
          <h3>Playlist-Auswahl erforderlich</h3>
          <small>
            Metadaten sind abgeschlossen. Vor Start muss ein Titel/Playlist manuell per Checkbox gewählt werden.
          </small>
          {waitingPlaylistRows.length > 0 ? (
            <div className="playlist-decision-list">
              {waitingPlaylistRows.map((row) => (
                <div key={row.playlistId} className="playlist-decision-item">
                  <label className="readonly-check-row">
                    <input
                      type="checkbox"
                      checked={normalizePlaylistId(selectedPlaylistId) === row.playlistId}
                      disabled={queueLocked}
                      onChange={() => {
                        const next = normalizePlaylistId(selectedPlaylistId) === row.playlistId ? null : row.playlistId;
                        setSelectedPlaylistId(next);
                      }}
                    />
                    <span>
                      {row.playlistFile}
                      {row.titleId !== null ? ` | Titel #${row.titleId}` : ''}
                      {row.durationLabel ? ` | Dauer ${row.durationLabel}` : ''}
                      {row.score !== null ? ` | Score ${row.score}` : ''}
                      {row.recommended ? ' | empfohlen' : ''}
                    </span>
                  </label>
                  {row.evaluationLabel ? <small className="track-action-note">{row.evaluationLabel}</small> : null}
                  {row.sequenceCoherence !== null ? (
                    <small className="track-action-note">Sequenz-Kohärenz: {row.sequenceCoherence.toFixed(3)}</small>
                  ) : null}
                  {row.handBrakeTitleId !== null ? (
                    <small className="track-action-note">HandBrake Titel: -t {row.handBrakeTitleId}</small>
                  ) : null}
                  {row.audioSummary ? (
                    <small className="track-action-note">Audio: {row.audioSummary}</small>
                  ) : null}
                  {row.segmentCommand ? <small className="track-action-note">Info: {row.segmentCommand}</small> : null}
                  {Array.isArray(row.audioTrackPreview) && row.audioTrackPreview.length > 0 ? (
                    <details className="playlist-segment-toggle">
                      <summary>Audio-Spuren anzeigen ({row.audioTrackPreview.length})</summary>
                      <pre className="playlist-segment-output">{row.audioTrackPreview.join('\n')}</pre>
                    </details>
                  ) : null}
                  {Array.isArray(row.segmentFiles) && row.segmentFiles.length > 0 ? (
                    <details className="playlist-segment-toggle">
                      <summary>Segment-Dateien anzeigen ({row.segmentFiles.length})</summary>
                      <pre className="playlist-segment-output">{row.segmentFiles.join('\n')}</pre>
                    </details>
                  ) : (
                    <small className="track-action-note">Keine Segmentliste aus TINFO:26 verfügbar.</small>
                  )}
                </div>
              ))}
            </div>
          ) : (
            <small>Keine Kandidaten gefunden. Bitte Analyse erneut ausführen.</small>
          )}
        </div>
      ) : null}

      {handBrakeTitleDecisionRequired && !queueLocked ? (
        <div className="playlist-decision-block">
          <h3>DVD Titel-Auswahl erforderlich</h3>
          <small>
            Mehrere DVD-Titel gefunden, die die Mindestlänge erfüllen. Bitte einen oder mehrere Episoden-Titel auswählen.
          </small>
          {waitingHandBrakeTitleRows.length > 0 ? (
            <div className="playlist-decision-list">
              {waitingHandBrakeTitleRows.map((row) => (
                <div key={row.handBrakeTitleId} className="playlist-decision-item">
                  <label className="readonly-check-row">
                    <input
                      type="checkbox"
                      checked={selectedHandBrakeTitleIdsState.includes(row.handBrakeTitleId)}
                      disabled={queueLocked}
                      onChange={() => {
                        setSelectedHandBrakeTitleIdsState((prev) => {
                          const list = Array.isArray(prev) ? prev : [];
                          if (list.includes(row.handBrakeTitleId)) {
                            return list.filter((value) => value !== row.handBrakeTitleId);
                          }
                          return [...list, row.handBrakeTitleId];
                        });
                      }}
                    />
                    <span>
                      {`Titel -t ${row.handBrakeTitleId}`}
                      {row.durationLabel ? ` | ${row.durationLabel}` : ''}
                      {row.audioTrackCount > 0 ? ` | Audio: ${row.audioTrackCount}` : ''}
                      {row.subtitleTrackCount > 0 ? ` | Untertitel: ${row.subtitleTrackCount}` : ''}
                    </span>
                  </label>
                </div>
              ))}
            </div>
          ) : (
            <small>Keine Kandidaten verfügbar. Bitte Analyse erneut ausführen.</small>
          )}
        </div>
      ) : null}

      {selectedMetadata && !isDvdTitleSelectionRequired ? (
        <div className="pipeline-meta-inline">
          {selectedMetadata.poster ? (
            <img
              src={selectedMetadata.poster}
              alt={selectedMetadata.title || 'Poster'}
              className={jobMediaProfile === 'cd' || jobMediaProfile === 'audiobook' ? 'poster-large-audio' : 'poster-large'}
            />
          ) : (
            <div className={`${jobMediaProfile === 'cd' || jobMediaProfile === 'audiobook' ? 'poster-large-audio' : 'poster-large'} poster-fallback`}>Kein {jobMediaProfile === 'cd' || jobMediaProfile === 'audiobook' ? 'Cover' : 'Poster'}</div>
          )}
          <div className="device-meta">
            <div>
              <strong>Titel:</strong> {selectedMetadata.title || '-'}
            </div>
            <div>
              <strong>Jahr:</strong> {selectedMetadata.year || '-'}
            </div>
            <div>
              <strong>IMDb:</strong> {selectedMetadata.imdbId || '-'}
            </div>
            <div>
              <strong>Status:</strong> {stateLabel}
            </div>
          </div>
        </div>
      ) : null}

      {(state === 'READY_TO_ENCODE' || state === 'MEDIAINFO_CHECK' || mediaInfoReview) ? (
        <div className="mediainfo-review-block">
          {!isDvdTitleSelectionRequired ? <h3>Titel-/Spurprüfung</h3> : null}
          {state === 'READY_TO_ENCODE' && !queueLocked && !isDvdTitleSelectionRequired ? (
            <small>
              {isPreRipReview
                ? 'Spurauswahl kann direkt übernommen werden. Beim Klick auf "Backup + Encoding starten" wird automatisch bestätigt und gestartet.'
                : 'Spurauswahl kann direkt übernommen werden. Beim Klick auf "Encoding starten" wird automatisch bestätigt und gestartet.'}
              {reviewPlaylistDecisionRequired ? ' Bitte den korrekten Titel per Checkbox auswählen.' : ''}
            </small>
          ) : null}
          {showConverterConfig ? (
            <div style={{ marginTop: '0.75rem', marginBottom: '0.75rem', padding: '0.75rem', border: '1px solid var(--surface-border, #e0e0e0)', borderRadius: '6px', background: 'var(--surface-ground, #f8f8f8)' }}>
              <div style={{ display: 'grid', gap: '0.75rem', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))' }}>
                <div>
                  <label style={{ display: 'block', marginBottom: '0.35rem', fontWeight: 600 }}>Ausgabeformat</label>
                  <Dropdown
                    value={selectedOutputFormat}
                    options={CONVERTER_VIDEO_OUTPUT_FORMATS}
                    onChange={(e) => setSelectedOutputFormat(e.value)}
                    style={{ width: '100%' }}
                    disabled={!allowConverterConfigEdit}
                  />
                </div>
                <div>
                  <label style={{ display: 'block', marginBottom: '0.35rem', fontWeight: 600 }}>User-Preset</label>
                  <Dropdown
                    value={selectedUserPresetId}
                    options={converterUserPresetOptions}
                    optionLabel="label"
                    optionValue="value"
                    onChange={(e) => {
                      const rawValue = e?.value && typeof e.value === 'object' ? e.value.value : e?.value;
                      const nextValue = rawValue === null || rawValue === undefined || String(rawValue).trim() === ''
                        ? null
                        : Number(rawValue);
                      const normalized = Number.isFinite(nextValue) && nextValue > 0 ? Math.trunc(nextValue) : null;
                      setSelectedUserPresetId(normalized);
                      if (normalized) {
                        setSelectedHandBrakePreset('');
                      }
                    }}
                    style={{ width: '100%' }}
                    placeholder="User-Preset auswählen …"
                    showClear
                    disabled={!allowConverterConfigEdit || Boolean(trimmedHandBrakePreset) || converterUserPresetOptions.length <= 1}
                  />
                  {converterUserPresetOptions.length <= 1 ? (
                    <small style={{ color: 'var(--text-color-secondary)' }}>Keine User-Presets vorhanden.</small>
                  ) : null}
                </div>
                <div>
                  <label style={{ display: 'block', marginBottom: '0.35rem', fontWeight: 600 }}>HandBrake-Preset</label>
                  <Dropdown
                    value={trimmedHandBrakePreset}
                    options={converterHandBrakePresetOptions}
                    optionLabel="label"
                    optionValue="value"
                    onChange={(e) => {
                      const rawValue = e?.value && typeof e.value === 'object' ? e.value.value : e?.value;
                      const next = String(rawValue || '').trim();
                      setSelectedHandBrakePreset(next);
                      if (next) {
                        setSelectedUserPresetId(null);
                      }
                    }}
                    style={{ width: '100%' }}
                    placeholder="HandBrake-Preset auswählen …"
                    filter
                    optionDisabled="disabled"
                    showClear
                    disabled={!allowConverterConfigEdit || Boolean(selectedUserPresetId) || converterHandBrakePresetOptions.length <= 1}
                  />
                  {converterHandBrakePresetOptions.length <= 1 ? (
                    <small style={{ color: 'var(--text-color-secondary)' }}>Keine HandBrake-Presets geladen.</small>
                  ) : null}
                </div>
              </div>
              {allowConverterConfigEdit && !hasSelectedPreset ? (
                <small style={{ display: 'block', marginTop: '0.5rem', color: 'var(--red-600)' }}>
                  Bitte wähle ein User- oder HandBrake-Preset aus, bevor das Encoding starten kann.
                </small>
              ) : null}
              {allowConverterConfigEdit ? (
                converterConfigError ? (
                  <small style={{ display: 'block', marginTop: '0.5rem', color: 'var(--red-600)' }}>
                    {converterConfigError}
                  </small>
                ) : (
                  <small style={{ display: 'block', marginTop: '0.5rem', color: 'var(--text-color-secondary)' }}>
                    {converterConfigSaving ? 'Speichere Konfiguration …' : 'Konfiguration gespeichert'}
                  </small>
                )
              ) : null}
            </div>
          ) : null}
          <MediaInfoReviewPanel
            review={mediaInfoReview}
            presetDisplayValue={presetDisplayValue}
            commandOutputPath={commandOutputPath}
            commandOutputPathByTitle={commandOutputPathByTitle}
            selectedEncodeTitleId={normalizeTitleId(selectedEncodeTitleId)}
            selectedEncodeTitleIds={normalizeTitleIdList(selectedEncodeTitleIds)}
            allowTitleSelection={(state === 'READY_TO_ENCODE' || state === 'WAITING_FOR_USER_DECISION') && !queueLocked}
            compactTitleSelection={isDvdTitleSelectionRequired}
            onSelectEncodeTitle={(titleId) => {
              const normalizedTitleId = normalizeTitleId(titleId);
              setSelectedEncodeTitleId(normalizedTitleId);
              if (!normalizedTitleId) {
                return;
              }
              setSelectedEncodeTitleIds((prev) => normalizeTitleIdList([...(Array.isArray(prev) ? prev : []), normalizedTitleId]));
            }}
            onToggleEncodeTitle={(titleId, checked) => {
              const normalizedTitleId = normalizeTitleId(titleId);
              if (!normalizedTitleId) {
                return;
              }
              setSelectedEncodeTitleIds((prev) => {
                const current = normalizeTitleIdList(prev);
                if (checked) {
                  const next = normalizeTitleIdList([...current, normalizedTitleId]);
                  setSelectedEncodeTitleId(normalizedTitleId);
                  return next;
                }
                const next = current.filter((id) => id !== normalizedTitleId);
                setSelectedEncodeTitleId((prevPrimary) => {
                  const normalizedPrimary = normalizeTitleId(prevPrimary);
                  if (normalizedPrimary === normalizedTitleId) {
                    return next[0] || null;
                  }
                  return normalizedPrimary;
                });
                return next;
              });
            }}
            allowTrackSelection={state === 'READY_TO_ENCODE' && !queueLocked}
            trackSelectionByTitle={trackSelectionByTitle}
            onTrackSelectionChange={(titleId, trackType, trackId, checked) => {
              const normalizedTitleId = normalizeTitleId(titleId);
              const normalizedTrackId = normalizeTrackId(trackId);
              if (!normalizedTitleId || normalizedTrackId === null) {
                return;
              }

              setTrackSelectionByTitle((prev) => {
                const current = prev?.[normalizedTitleId] || prev?.[String(normalizedTitleId)] || {
                  audioTrackIds: [],
                  subtitleTrackIds: [],
                  subtitleVariantSelection: {},
                  subtitleLanguageOrder: []
                };
                const key = trackType === 'subtitle' ? 'subtitleTrackIds' : 'audioTrackIds';
                const existing = normalizeTrackIdList(current?.[key] || []);
                const next = checked
                  ? normalizeTrackIdList([...existing, normalizedTrackId])
                  : existing.filter((id) => id !== normalizedTrackId);

                return {
                  ...prev,
                  [normalizedTitleId]: {
                    ...current,
                    [key]: next
                  }
                };
              });
            }}
            onSubtitleVariantSelectionChange={(titleId, language, variant, checked) => {
              const normalizedTitleId = normalizeTitleId(titleId);
              const normalizedLanguage = normalizeSubtitleLanguage(language);
              const normalizedVariant = String(variant || '').trim().toLowerCase() === 'forced' ? 'forced' : 'full';
              if (!normalizedTitleId || !normalizedLanguage) {
                return;
              }

              setTrackSelectionByTitle((prev) => {
                const current = prev?.[normalizedTitleId] || prev?.[String(normalizedTitleId)] || {
                  audioTrackIds: [],
                  subtitleTrackIds: [],
                  subtitleVariantSelection: {},
                  subtitleLanguageOrder: []
                };
                const currentMap = normalizeSubtitleVariantSelectionMap(current?.subtitleVariantSelection || {});
                const nextMap = { ...currentMap };
                const currentEntry = nextMap[normalizedLanguage] || { full: false, forced: false };
                nextMap[normalizedLanguage] = {
                  ...currentEntry,
                  [normalizedVariant]: Boolean(checked)
                };
                const existingOrder = normalizeSubtitleLanguageOrder(current?.subtitleLanguageOrder || []);
                const fallbackOrder = normalizeSubtitleLanguageOrder(
                  defaultTrackSelectionForTitle(mediaInfoReview, normalizedTitleId)?.subtitleLanguageOrder || []
                );
                let nextOrder = existingOrder.length > 0
                  ? existingOrder
                  : fallbackOrder;
                if (nextOrder.length === 0) {
                  nextOrder = [normalizedLanguage];
                } else if (!nextOrder.includes(normalizedLanguage) && fallbackOrder.includes(normalizedLanguage)) {
                  nextOrder = fallbackOrder;
                } else if (!nextOrder.includes(normalizedLanguage)) {
                  nextOrder = [...nextOrder, normalizedLanguage];
                }

                return {
                  ...prev,
                  [normalizedTitleId]: {
                    ...current,
                    subtitleVariantSelection: nextMap,
                    subtitleLanguageOrder: nextOrder
                  }
                };
              });
            }}
            selectedMetadataEpisodes={selectedMetadataEpisodes}
            selectedEpisodeFillStart={selectedEpisodeFillStart}
            onEpisodeFillStartChange={(episodeRef) => {
              const normalizedRef = String(episodeRef || '').trim();
              setSelectedEpisodeFillStart(normalizedRef || null);
              if (normalizedRef) {
                applyEpisodeFillFrom(normalizedRef);
              }
            }}
            episodeAssignmentsByTitle={episodeAssignmentsByTitle}
            onEpisodeAssignmentChange={(titleId, patch = {}) => {
              const normalizedTitleId = normalizeTitleId(titleId);
              if (!normalizedTitleId || !patch || typeof patch !== 'object') {
                return;
              }
              const toNumberOrNull = (value) => {
                const parsed = Number(value);
                if (!Number.isFinite(parsed) || parsed <= 0) {
                  return null;
                }
                return Math.trunc(parsed);
              };
              setEpisodeAssignmentsByTitle((prev) => {
                const currentMap = prev && typeof prev === 'object' ? prev : {};
                const current = currentMap?.[normalizedTitleId] || currentMap?.[String(normalizedTitleId)] || {};
                const nextTitle = String(
                  patch.episodeTitle !== undefined
                    ? patch.episodeTitle
                    : current.episodeTitle || ''
                ).trim();
                const next = {
                  episodeId: patch.episodeId !== undefined
                    ? toNumberOrNull(patch.episodeId)
                    : toNumberOrNull(current.episodeId),
                  episodeNumber: patch.episodeNumber !== undefined
                    ? toNumberOrNull(patch.episodeNumber)
                    : toNumberOrNull(current.episodeNumber),
                  seasonNumber: patch.seasonNumber !== undefined
                    ? toNumberOrNull(patch.seasonNumber)
                    : toNumberOrNull(current.seasonNumber),
                  episodeTitle: nextTitle || null
                };
                return {
                  ...currentMap,
                  [normalizedTitleId]: next
                };
              });
            }}
            availableScripts={scriptCatalog}
            availableChains={chainCatalog}
            preEncodeItems={preEncodeItems}
            postEncodeItems={postEncodeItems}
            userPresets={effectiveUserPresets}
            selectedUserPresetId={selectedUserPresetId}
            onUserPresetChange={showConverterConfig ? null : (presetId) => setSelectedUserPresetId(presetId)}
            selectedHandBrakePreset={selectedHandBrakePreset}
            allowEncodeItemSelection={state === 'READY_TO_ENCODE' && !queueLocked}
            onAddPreEncodeItem={(itemType) => {
              setPreEncodeItems((prev) => {
                const current = Array.isArray(prev) ? prev : [];
                if (itemType === 'chain') {
                  const selectedSet = new Set(
                    current
                      .filter((item) => item?.type === 'chain')
                      .map((item) => normalizeChainId(item?.id))
                      .filter((id) => id !== null)
                      .map((id) => String(id))
                  );
                  const nextCandidate = (Array.isArray(chainCatalog) ? chainCatalog : [])
                    .map((item) => normalizeChainId(item?.id))
                    .find((id) => id !== null && !selectedSet.has(String(id)));
                  if (nextCandidate === undefined || nextCandidate === null) {
                    return current;
                  }
                  return [...current, { type: 'chain', id: nextCandidate }];
                }
                const selectedSet = new Set(
                  current
                    .filter((item) => item?.type === 'script')
                    .map((item) => normalizeScriptId(item?.id))
                    .filter((id) => id !== null)
                    .map((id) => String(id))
                );
                const nextCandidate = (Array.isArray(scriptCatalog) ? scriptCatalog : [])
                  .map((item) => normalizeScriptId(item?.id))
                  .find((id) => id !== null && !selectedSet.has(String(id)));
                if (nextCandidate === undefined || nextCandidate === null) {
                  return current;
                }
                return [...current, { type: 'script', id: nextCandidate }];
              });
            }}
            onChangePreEncodeItem={(rowIndex, itemType, nextId) => {
              setPreEncodeItems((prev) => {
                const current = Array.isArray(prev) ? prev : [];
                const index = Number(rowIndex);
                if (!Number.isInteger(index) || index < 0 || index >= current.length) {
                  return current;
                }
                const type = itemType === 'chain' ? 'chain' : 'script';
                if (type === 'chain') {
                  const normalizedId = normalizeChainId(nextId);
                  if (normalizedId === null) {
                    return current;
                  }
                  const duplicate = current.some((item, idx) =>
                    idx !== index
                    && item?.type === 'chain'
                    && String(normalizeChainId(item?.id)) === String(normalizedId)
                  );
                  if (duplicate) {
                    return current;
                  }
                  const next = [...current];
                  next[index] = { type: 'chain', id: normalizedId };
                  return next;
                }
                const normalizedId = normalizeScriptId(nextId);
                if (normalizedId === null) {
                  return current;
                }
                const duplicate = current.some((item, idx) =>
                  idx !== index
                  && item?.type === 'script'
                  && String(normalizeScriptId(item?.id)) === String(normalizedId)
                );
                if (duplicate) {
                  return current;
                }
                const next = [...current];
                next[index] = { type: 'script', id: normalizedId };
                return next;
              });
            }}
            onRemovePreEncodeItem={(rowIndex) => {
              setPreEncodeItems((prev) => {
                const current = Array.isArray(prev) ? prev : [];
                const index = Number(rowIndex);
                if (!Number.isInteger(index) || index < 0 || index >= current.length) {
                  return current;
                }
                return current.filter((_, idx) => idx !== index);
              });
            }}
            onReorderPreEncodeItem={(fromIndex, toIndex) => {
              setPreEncodeItems((prev) => {
                const current = Array.isArray(prev) ? prev : [];
                const from = Number(fromIndex);
                const to = Number(toIndex);
                if (!Number.isInteger(from) || !Number.isInteger(to)) {
                  return current;
                }
                if (from < 0 || to < 0 || from >= current.length || to >= current.length || from === to) {
                  return current;
                }
                const next = [...current];
                const [moved] = next.splice(from, 1);
                next.splice(to, 0, moved);
                return next;
              });
            }}
            onAddPostEncodeItem={(itemType) => {
              setPostEncodeItems((prev) => {
                const current = Array.isArray(prev) ? prev : [];
                if (itemType === 'chain') {
                  const selectedSet = new Set(
                    current
                      .filter((item) => item?.type === 'chain')
                      .map((item) => normalizeChainId(item?.id))
                      .filter((id) => id !== null)
                      .map((id) => String(id))
                  );
                  const nextCandidate = (Array.isArray(chainCatalog) ? chainCatalog : [])
                    .map((item) => normalizeChainId(item?.id))
                    .find((id) => id !== null && !selectedSet.has(String(id)));
                  if (nextCandidate === undefined || nextCandidate === null) {
                    return current;
                  }
                  return [...current, { type: 'chain', id: nextCandidate }];
                }
                const selectedSet = new Set(
                  current
                    .filter((item) => item?.type === 'script')
                    .map((item) => normalizeScriptId(item?.id))
                    .filter((id) => id !== null)
                    .map((id) => String(id))
                );
                const nextCandidate = (Array.isArray(scriptCatalog) ? scriptCatalog : [])
                  .map((item) => normalizeScriptId(item?.id))
                  .find((id) => id !== null && !selectedSet.has(String(id)));
                if (nextCandidate === undefined || nextCandidate === null) {
                  return current;
                }
                return [...current, { type: 'script', id: nextCandidate }];
              });
            }}
            onChangePostEncodeItem={(rowIndex, itemType, nextId) => {
              setPostEncodeItems((prev) => {
                const current = Array.isArray(prev) ? prev : [];
                const index = Number(rowIndex);
                if (!Number.isInteger(index) || index < 0 || index >= current.length) {
                  return current;
                }
                const type = itemType === 'chain' ? 'chain' : 'script';
                if (type === 'chain') {
                  const normalizedId = normalizeChainId(nextId);
                  if (normalizedId === null) {
                    return current;
                  }
                  const duplicate = current.some((item, idx) =>
                    idx !== index
                    && item?.type === 'chain'
                    && String(normalizeChainId(item?.id)) === String(normalizedId)
                  );
                  if (duplicate) {
                    return current;
                  }
                  const next = [...current];
                  next[index] = { type: 'chain', id: normalizedId };
                  return next;
                }
                const normalizedId = normalizeScriptId(nextId);
                if (normalizedId === null) {
                  return current;
                }
                const duplicate = current.some((item, idx) =>
                  idx !== index
                  && item?.type === 'script'
                  && String(normalizeScriptId(item?.id)) === String(normalizedId)
                );
                if (duplicate) {
                  return current;
                }
                const next = [...current];
                next[index] = { type: 'script', id: normalizedId };
                return next;
              });
            }}
            onRemovePostEncodeItem={(rowIndex) => {
              setPostEncodeItems((prev) => {
                const current = Array.isArray(prev) ? prev : [];
                const index = Number(rowIndex);
                if (!Number.isInteger(index) || index < 0 || index >= current.length) {
                  return current;
                }
                return current.filter((_, idx) => idx !== index);
              });
            }}
            onReorderPostEncodeItem={(fromIndex, toIndex) => {
              setPostEncodeItems((prev) => {
                const current = Array.isArray(prev) ? prev : [];
                const from = Number(fromIndex);
                const to = Number(toIndex);
                if (!Number.isInteger(from) || !Number.isInteger(to)) {
                  return current;
                }
                if (from < 0 || to < 0 || from >= current.length || to >= current.length || from === to) {
                  return current;
                }
                const next = [...current];
                const [moved] = next.splice(from, 1);
                next.splice(to, 0, moved);
                return next;
              });
            }}
          />
        </div>
      ) : null}
    </Card>
  );
}
