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';
import { confirmModal } from '../utils/confirmModal';

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 hasSeriesMetadataShape(source = null) {
  const metadata = source && typeof source === 'object' ? source : {};
  const seasonNumber = normalizePositiveInt(metadata?.seasonNumber ?? metadata?.season ?? null);
  const episodes = Array.isArray(metadata?.episodes) ? metadata.episodes : [];
  const metadataProvider = String(metadata?.metadataProvider || '').trim().toLowerCase();
  const workflowKind = String(metadata?.workflowKind || metadata?.kind || '').trim().toLowerCase();
  return Boolean(
    seasonNumber
    || episodes.length > 0
    || metadataProvider === 'tmdb'
    || metadataProvider === 'themoviedb'
    || workflowKind === 'series'
    || workflowKind === 'season'
  );
}

function hasSeriesEpisodeAssignmentHints(titles = []) {
  const reviewTitles = Array.isArray(titles) ? titles : [];
  return reviewTitles.some((title) => {
    const seasonNumber = normalizePositiveInt(title?.seasonNumber ?? title?.season ?? null);
    const episodeNumber = normalizePositiveInt(title?.episodeNumber ?? title?.number ?? null);
    const episodeId = normalizePositiveInt(title?.episodeId ?? null);
    const episodeTitle = String(title?.episodeTitle || '').trim();
    const episodeTitleStart = String(title?.episodeTitleStart || '').trim();
    return Boolean(seasonNumber || episodeNumber || episodeId || episodeTitle || episodeTitleStart);
  });
}

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 collectEpisodeReferenceKeys(entry = {}) {
  const normalized = normalizeEpisodeReference(entry);
  if (!normalized) {
    return [];
  }
  const keys = [];
  if (normalized.episodeId) {
    keys.push(`id:${normalized.episodeId}`);
  }
  if (normalized.seasonNumber && normalized.episodeNumber) {
    keys.push(`se:${normalized.seasonNumber}:${normalized.episodeNumber}`);
  }
  if (normalized.episodeNumber) {
    keys.push(`ep:${normalized.episodeNumber}`);
  }
  return Array.from(new Set(keys));
}

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 seasonNumber = normalizePositiveInt(assignment?.seasonNumber ?? assignment?.season ?? null);
    const episodeStart = normalizePositiveInt(
      assignment?.episodeNumberStart
      ?? assignment?.episodeNoStart
      ?? assignment?.episodeNumber
      ?? assignment?.number
      ?? null
    );
    const episodeEnd = normalizePositiveInt(
      assignment?.episodeNumberEnd
      ?? assignment?.episodeNoEnd
      ?? null
    );
    if (episodeStart && episodeEnd && episodeEnd > episodeStart && (episodeEnd - episodeStart) <= 64) {
      for (let number = episodeStart; number <= episodeEnd; number += 1) {
        refs.push({
          episodeId: null,
          episodeNumber: number,
          seasonNumber
        });
      }
    }
  }

  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 normalizeSeriesTitleForOutputPath(value, fallback = 'series') {
  const raw = String(value || '').trim();
  if (!raw) {
    return String(fallback || 'series').trim() || 'series';
  }
  const stripped = raw
    .replace(/\s*-\s*S\d{1,2}E\d{1,3}(?:-\d{1,3})?\b.*$/i, '')
    .trim();
  if (stripped) {
    return stripped;
  }
  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 medianPositiveInt(values = []) {
  const nums = (Array.isArray(values) ? values : [])
    .map((value) => Number(value))
    .filter((value) => Number.isFinite(value) && value > 0)
    .sort((left, right) => left - right);
  if (nums.length === 0) {
    return 0;
  }
  const middle = Math.floor(nums.length / 2);
  if (nums.length % 2 === 1) {
    return nums[middle];
  }
  return (nums[middle - 1] + nums[middle]) / 2;
}

function formatTemplateTwoDigit(value) {
  const numeric = Number(value);
  if (!Number.isFinite(numeric)) {
    return String(value || '').trim() || '00';
  }
  if (Number.isInteger(numeric)) {
    return String(Math.trunc(numeric)).padStart(2, '0');
  }
  return String(value || '').trim() || String(numeric);
}

function resolveEpisodeAssignmentRange(assignment = null, title = null, fallbackEpisodeNumber = 1, options = {}) {
  const assignmentSource = assignment && typeof assignment === 'object' ? assignment : {};
  const titleSource = title && typeof title === 'object' ? title : {};
  const allowTitleEpisodeNumber = options?.allowTitleEpisodeNumber !== false;
  const fallbackNumber = normalizePositiveInt(fallbackEpisodeNumber) || 1;
  const episodeStart = normalizePositiveInt(
    assignmentSource?.episodeNumberStart
    ?? assignmentSource?.episodeNoStart
    ?? assignmentSource?.episodeNumber
    ?? (allowTitleEpisodeNumber ? titleSource?.episodeNumber : null)
    ?? fallbackNumber
  ) || fallbackNumber;
  const explicitEpisodeEnd = normalizePositiveInt(
    assignmentSource?.episodeNumberEnd
    ?? assignmentSource?.episodeNoEnd
    ?? null
  );
  const explicitSpan = normalizePositiveInt(
    assignmentSource?.episodeSpan
    ?? assignmentSource?.episodeCount
    ?? null
  );

  let episodeEnd = explicitEpisodeEnd;
  if (!episodeEnd && explicitSpan && explicitSpan > 1) {
    episodeEnd = episodeStart + explicitSpan - 1;
  }
  if (episodeEnd && episodeEnd < episodeStart) {
    episodeEnd = episodeStart;
  }
  const normalizedEnd = episodeEnd || episodeStart;
  const episodeSpan = Math.max(1, normalizedEnd - episodeStart + 1);
  return {
    start: episodeStart,
    end: normalizedEnd,
    span: episodeSpan,
    isMulti: normalizedEnd > episodeStart
  };
}

function hasExplicitEpisodeAssignmentRange(assignment = null) {
  const source = assignment && typeof assignment === 'object' ? assignment : null;
  if (!source) {
    return false;
  }
  const hasStart = normalizePositiveInt(
    source?.episodeNumberStart
    ?? source?.episodeNoStart
    ?? null
  ) !== null;
  const hasEnd = normalizePositiveInt(
    source?.episodeNumberEnd
    ?? source?.episodeNoEnd
    ?? null
  ) !== null;
  const hasSpan = normalizePositiveInt(
    source?.episodeSpan
    ?? source?.episodeCount
    ?? null
  ) !== null;
  const hasEpisodeNumber = normalizePositiveInt(source?.episodeNumber ?? null) !== null;
  const hasEpisodeIdStart = normalizePositiveInt(source?.episodeIdStart ?? null) !== null;
  const hasRangeToken = String(source?.episodeRange || '').trim().length > 0;
  return Boolean(hasStart || hasEnd || hasSpan || hasEpisodeNumber || hasEpisodeIdStart || hasRangeToken);
}

function buildEpisodeRangeToken(start, end) {
  const startToken = formatTemplateTwoDigit(start);
  const endToken = formatTemplateTwoDigit(end);
  if (Number(end) > Number(start)) {
    return `${startToken}-${endToken}`;
  }
  return startToken;
}

function buildEpisodePartsToken(episodeRange = null) {
  const start = normalizePositiveInt(episodeRange?.start) || 1;
  const end = normalizePositiveInt(episodeRange?.end) || start;
  const span = Math.max(1, end - start + 1);
  if (span <= 1) {
    return '1';
  }
  if (span === 2) {
    return '1+2';
  }
  return `1-${span}`;
}

function normalizeEpisodeTitleTokenForCompare(value) {
  return String(value || '')
    .toLowerCase()
    .normalize('NFD')
    .replace(/\p{M}+/gu, '')
    .replace(/[^a-z0-9]+/g, '')
    .trim();
}

function findCommonEpisodeTitlePrefix(values = []) {
  const rows = (Array.isArray(values) ? values : [])
    .map((value) => String(value || '').trim())
    .filter(Boolean);
  if (rows.length === 0) {
    return '';
  }
  if (rows.length === 1) {
    return rows[0];
  }
  const tokenRows = rows.map((value) => value.split(/\s+/).filter(Boolean));
  const firstTokens = tokenRows[0];
  if (firstTokens.length === 0) {
    return '';
  }
  let prefixLength = 0;
  for (let index = 0; index < firstTokens.length; index += 1) {
    const tokenKey = normalizeEpisodeTitleTokenForCompare(firstTokens[index]);
    if (!tokenKey) {
      break;
    }
    const matchesAll = tokenRows.slice(1).every((tokens) => {
      const current = tokens[index];
      return normalizeEpisodeTitleTokenForCompare(current) === tokenKey;
    });
    if (!matchesAll) {
      break;
    }
    prefixLength += 1;
  }
  if (prefixLength <= 0) {
    return '';
  }
  return firstTokens
    .slice(0, prefixLength)
    .join(' ')
    .replace(/[-–—:.,\s]+$/g, '')
    .trim();
}

function stripEpisodePartSuffix(value) {
  const source = String(value || '').replace(/\s+/g, ' ').trim();
  if (!source) {
    return '';
  }
  // Output cleanup for multi-episode labels:
  // remove "Teil/Part" markers and pure numeric/roman suffixes in
  // parentheses like "(1)" / "(2)".
  const suffixPattern = /(?:\s*[-–—:.,]?\s*)(?:(?:\(?\s*(?:teil|part|pt)\s*(?:\d{1,2}|[ivxlcdm]{1,6})(?:\s*(?:\/|von|of)\s*(?:\d{1,2}|[ivxlcdm]{1,6}))?\s*\)?)|\(\s*(?:\d{1,2}|[ivxlcdm]{1,6})\s*\))\s*$/i;
  let normalized = source;
  let changed = false;
  for (let i = 0; i < 2; i += 1) {
    const next = normalized.replace(suffixPattern, '').replace(/[-–—:.,\s]+$/g, '').trim();
    if (!next || next === normalized) {
      break;
    }
    normalized = next;
    changed = true;
  }
  return changed ? (normalized || source) : source;
}

function normalizeEpisodeTitleForOutput(value, episodeRange = null) {
  const source = String(value || '').replace(/\s+/g, ' ').trim();
  if (!source) {
    return '';
  }
  if (!Boolean(episodeRange?.isMulti)) {
    return source;
  }
  const strippedSource = stripEpisodePartSuffix(source) || source;
  const rawSegments = source
    .split(/\s+(?:\+|\/|\||&)\s+/)
    .map((part) => String(part || '').trim())
    .filter(Boolean);
  if (rawSegments.length < 2) {
    return strippedSource;
  }
  const cleanedSegments = rawSegments
    .map((part) => stripEpisodePartSuffix(part) || part)
    .map((part) => String(part || '').replace(/[-–—:.,\s]+$/g, '').trim())
    .filter(Boolean);
  if (cleanedSegments.length === 0) {
    return strippedSource;
  }
  const uniqueSegments = [];
  const seen = new Set();
  for (const segment of cleanedSegments) {
    const key = normalizeEpisodeTitleTokenForCompare(segment);
    if (!key || seen.has(key)) {
      continue;
    }
    seen.add(key);
    uniqueSegments.push(segment);
  }
  if (uniqueSegments.length === 1) {
    return uniqueSegments[0];
  }
  const allSegmentsLookLikePartTitles = rawSegments.every((segment) => /\b(?:teil|part|pt)\b/i.test(segment));
  if (allSegmentsLookLikePartTitles) {
    const commonPrefix = findCommonEpisodeTitlePrefix(cleanedSegments);
    if (commonPrefix && commonPrefix.length >= 6) {
      return commonPrefix;
    }
    return uniqueSegments[0] || cleanedSegments[0] || strippedSource;
  }
  return strippedSource;
}

function resolveTitleDurationSeconds(title = null) {
  const fromSeconds = Number(title?.durationSeconds || 0);
  if (Number.isFinite(fromSeconds) && fromSeconds > 0) {
    return fromSeconds;
  }
  const fromMinutes = Number(title?.durationMinutes || 0);
  if (Number.isFinite(fromMinutes) && fromMinutes > 0) {
    return fromMinutes * 60;
  }
  return 0;
}

function estimateSeriesEpisodeSpanForTitle(title = null, allTitles = []) {
  const titleDuration = resolveTitleDurationSeconds(title);
  if (!Number.isFinite(titleDuration) || titleDuration <= 0) {
    return 1;
  }
  const durations = (Array.isArray(allTitles) ? allTitles : [])
    .map((item) => resolveTitleDurationSeconds(item))
    .filter((value) => Number.isFinite(value) && value > 0);
  if (durations.length === 0) {
    return 1;
  }
  const medianDuration = medianPositiveInt(durations);
  if (!medianDuration) {
    return 1;
  }
  const baselinePool = durations.filter((value) => value <= medianDuration * 1.35);
  const baselineDuration = medianPositiveInt(baselinePool.length > 0 ? baselinePool : durations) || medianDuration;
  if (!baselineDuration) {
    return 1;
  }
  const ratio = titleDuration / baselineDuration;
  const roundedSpan = Math.round(ratio);
  if (!Number.isFinite(roundedSpan) || roundedSpan < 2 || roundedSpan > 4) {
    return 1;
  }
  const minRatio = roundedSpan === 2 ? 1.55 : roundedSpan * 0.72;
  const maxRatio = roundedSpan * 1.32;
  if (ratio < minRatio || ratio > maxRatio) {
    return 1;
  }
  return roundedSpan;
}

function resolveEffectiveEpisodeRangeForTitle(assignment = null, title = null, allTitles = [], fallbackEpisodeNumber = 1) {
  const baseRange = resolveEpisodeAssignmentRange(assignment, title, fallbackEpisodeNumber);
  if (baseRange.isMulti) {
    return baseRange;
  }
  if (hasExplicitEpisodeAssignmentRange(assignment)) {
    return baseRange;
  }
  const spanHint = estimateSeriesEpisodeSpanForTitle(title, allTitles);
  if (spanHint <= 1) {
    return baseRange;
  }
  return {
    start: baseRange.start,
    end: baseRange.start + spanHint - 1,
    span: spanHint,
    isMulti: true
  };
}

function resolveSeriesTitleOrderForPreview(allTitles = [], selectedTitleIds = [], targetTitleId = null) {
  const explicitSelectedIds = normalizeTitleIdList(selectedTitleIds);
  const fallbackSelectedIds = explicitSelectedIds.length > 0
    ? explicitSelectedIds
    : normalizeTitleIdList(
      (Array.isArray(allTitles) ? allTitles : [])
        .filter((item) => Boolean(item?.selectedForEncode || item?.encodeInput))
        .map((item) => item?.id)
    );
  let ordered = fallbackSelectedIds;
  const normalizedTarget = normalizeTitleId(targetTitleId);
  if (normalizedTarget && !ordered.some((id) => Number(id) === Number(normalizedTarget))) {
    ordered = [...ordered, Number(normalizedTarget)];
  }
  if (ordered.length === 0 && normalizedTarget) {
    ordered = [Number(normalizedTarget)];
  }
  return ordered;
}

function resolveEpisodeRangeForTitleFromAssignments({
  assignmentByTitle = {},
  allTitles = [],
  selectedTitleIds = [],
  targetTitle = null,
  fallbackEpisodeNumber = 1
}) {
  const titles = Array.isArray(allTitles) ? allTitles : [];
  const titlesById = new Map();
  for (const item of titles) {
    const id = normalizeTitleId(item?.id);
    if (!id) {
      continue;
    }
    titlesById.set(Number(id), item);
  }
  const targetTitleId = normalizeTitleId(targetTitle?.id || null);
  const orderedTitleIds = resolveSeriesTitleOrderForPreview(titles, selectedTitleIds, targetTitleId);
  const fallbackStart = normalizePositiveInt(fallbackEpisodeNumber) || 1;
  let cursor = fallbackStart;
  let targetRange = null;

  for (const rawId of orderedTitleIds) {
    const currentId = normalizeTitleId(rawId);
    if (!currentId) {
      continue;
    }
    const assignment = assignmentByTitle?.[currentId] || assignmentByTitle?.[String(currentId)] || null;
    const currentTitle = titlesById.get(Number(currentId)) || null;
    const hasExplicitRange = hasExplicitEpisodeAssignmentRange(assignment);
    const range = resolveEpisodeAssignmentRange(
      assignment,
      currentTitle,
      cursor,
      { allowTitleEpisodeNumber: hasExplicitRange }
    );
    const normalizedEnd = normalizePositiveInt(range?.end);
    if (normalizedEnd !== null) {
      cursor = normalizedEnd + 1;
    }
    if (targetTitleId && Number(currentId) === Number(targetTitleId)) {
      targetRange = range;
      break;
    }
  }

  if (!targetRange) {
    const assignment = targetTitleId
      ? (assignmentByTitle?.[targetTitleId] || assignmentByTitle?.[String(targetTitleId)] || null)
      : null;
    const hasExplicitRange = hasExplicitEpisodeAssignmentRange(assignment);
    targetRange = resolveEpisodeAssignmentRange(
      assignment,
      targetTitle,
      cursor,
      { allowTitleEpisodeNumber: hasExplicitRange }
    );
  }

  return targetRange;
}

function templateUsesLeadingZeroForToken(template, token) {
  const source = String(template || '').trim();
  const normalizedToken = String(token || '').trim();
  if (!source || !normalizedToken) {
    return false;
  }
  const pattern = new RegExp(`\\{\\s*0\\s*:\\s*${normalizedToken}\\s*\\}`, 'i');
  return pattern.test(source);
}

function formatEpisodeNumberByTemplate(rawValue, template, token = 'episodeNr') {
  const numeric = Number(rawValue);
  if (!Number.isFinite(numeric) || numeric <= 0) {
    return String(rawValue || '').trim() || '0';
  }
  if (!Number.isInteger(numeric)) {
    return String(numeric);
  }
  const normalized = String(Math.trunc(numeric));
  if (templateUsesLeadingZeroForToken(template, token)) {
    return normalized.padStart(2, '0');
  }
  return normalized;
}

function buildFallbackEpisodeTitleForOutput(episodeRange = null, template = '') {
  const start = normalizePositiveInt(episodeRange?.start) || 1;
  const end = normalizePositiveInt(episodeRange?.end) || start;
  if (end > start) {
    const startToken = formatEpisodeNumberByTemplate(start, template, 'episodeNr');
    const endToken = formatEpisodeNumberByTemplate(end, template, 'episodeNr');
    return `Folge ${startToken}-${endToken}`;
  }
  const token = formatEpisodeNumberByTemplate(start, template, 'episodeNr');
  return `Folge ${token}`;
}

function buildDvdSeriesEpisodeOutputPathPreview(
  settings,
  mediaProfile,
  metadata,
  title,
  assignment = null,
  fallbackJobId = null,
  allTitles = [],
  assignmentByTitle = {},
  selectedTitleIds = []
) {
  const normalizedSeriesMediaProfile = String(mediaProfile || '').trim().toLowerCase() === 'bluray'
    ? 'bluray'
    : 'dvd';
  const seriesRootDir = String(
    (normalizedSeriesMediaProfile === 'bluray'
      ? (settings?.series_dir_bluray || settings?.series_dir_dvd || settings?.movie_dir_bluray || settings?.movie_dir_dvd)
      : (settings?.series_dir_dvd || settings?.series_dir_bluray || settings?.movie_dir_dvd || settings?.movie_dir_bluray))
    || settings?.movie_dir
    || ''
  ).trim();
  if (!seriesRootDir) {
    return null;
  }

  const seasonNumber = normalizePositiveInt(
    assignment?.seasonNumber ?? metadata?.seasonNumber ?? title?.seasonNumber ?? 1
  ) || 1;
  const normalizedSelectedIds = normalizeTitleIdList(selectedTitleIds);
  const targetTitleId = normalizeTitleId(title?.id);
  const fallbackEpisodeNumber = targetTitleId && normalizedSelectedIds.length > 0
    ? (normalizedSelectedIds.findIndex((id) => Number(id) === Number(targetTitleId)) + 1)
    : 1;
  const inferredRange = resolveEpisodeRangeForTitleFromAssignments({
    assignmentByTitle,
    allTitles,
    selectedTitleIds: normalizedSelectedIds,
    targetTitle: title,
    fallbackEpisodeNumber: fallbackEpisodeNumber > 0 ? fallbackEpisodeNumber : 1
  });
  const episodeRange = resolveEffectiveEpisodeRangeForTitle(
    assignment || {},
    title,
    allTitles,
    inferredRange?.start || (fallbackEpisodeNumber > 0 ? fallbackEpisodeNumber : 1)
  );
  const effectiveEpisodeRange = episodeRange;
  const episodeNumber = effectiveEpisodeRange.start;
  const episodeRangeToken = buildEpisodeRangeToken(effectiveEpisodeRange.start, effectiveEpisodeRange.end);
  const partsToken = buildEpisodePartsToken(effectiveEpisodeRange);
  const discNumber = normalizePositiveInt(metadata?.discNumber ?? assignment?.discNumber ?? null);
  const seriesTitle = normalizeSeriesTitleForOutputPath(
    metadata?.seriesTitle
      || metadata?.title
      || (fallbackJobId ? `job-${fallbackJobId}` : 'series'),
    fallbackJobId ? `job-${fallbackJobId}` : 'series'
  );
  const year = String(metadata?.year || new Date().getFullYear()).trim();
  const language = normalizeSeriesLanguage(settings?.dvd_series_language || metadata?.language || null);

  const singleTemplateRaw = String(
    (normalizedSeriesMediaProfile === 'bluray'
      ? settings?.output_template_bluray_series_episode
      : settings?.output_template_dvd_series_episode)
    || settings?.output_template_dvd_series_episode
    || settings?.output_template_bluray_series_episode
    || '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeNr} - {episodeTitle}'
  ).trim();
  const multiTemplateRaw = String(
    (normalizedSeriesMediaProfile === 'bluray'
      ? settings?.output_template_bluray_series_multi_episode
      : settings?.output_template_dvd_series_multi_episode)
    || settings?.output_template_dvd_series_multi_episode
    || settings?.output_template_bluray_series_multi_episode
    || '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})'
  ).trim();
  const singleTemplate = singleTemplateRaw || '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeNr} - {episodeTitle}';
  const multiTemplate = multiTemplateRaw || '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})';
  const template = effectiveEpisodeRange.isMulti ? multiTemplate : singleTemplate;
  const fallbackEpisodeTitle = buildFallbackEpisodeTitleForOutput(effectiveEpisodeRange, template);
  const episodeTitleRaw = String(
    assignment?.episodeTitle
    || title?.episodeTitle
    || fallbackEpisodeTitle
  ).trim();
  const rendered = renderTemplate(template, {
    seriesTitle,
    seasonNr: seasonNumber,
    episodeNr: episodeNumber,
    episodeNoStart: effectiveEpisodeRange.start,
    episodeNoEnd: effectiveEpisodeRange.end,
    episodeNumberStart: effectiveEpisodeRange.start,
    episodeNumberEnd: effectiveEpisodeRange.end,
    episodeRange: episodeRangeToken,
    episodeSpan: effectiveEpisodeRange.span,
    parts: partsToken,
    episodeTitle: normalizeEpisodeTitleForOutput(
      episodeTitleRaw || fallbackEpisodeTitle,
      effectiveEpisodeRange
    ) || fallbackEpisodeTitle,
    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(
    (normalizedSeriesMediaProfile === 'bluray'
      ? settings?.output_extension_bluray
      : settings?.output_extension_dvd)
    || settings?.output_extension_dvd
    || settings?.output_extension_bluray
    || 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,
  onSubmitRawDecision,
  onCancel,
  onRetry,
  onDeleteJob,
  onInputsChanged,
  jobRow,
  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 orphanSource = String(
    jobRow?.makemkvInfo?.source
    || pipeline?.context?.source
    || pipeline?.context?.importSource
    || ''
  ).trim().toLowerCase();
  const isOrphanImport = orphanSource === 'orphan_raw_import';
  const isOrphanCancelled = isOrphanImport && (state === 'ERROR' || state === 'CANCELLED');
  const ripCompleted = Boolean(
    Number(jobRow?.rip_successful || jobRow?.ripSuccessful || pipeline?.context?.ripSuccessful || 0) === 1
    || jobRow?.raw_path
    || pipeline?.context?.rawPath
    || jobRow?.makemkvInfo?.rawPath
    || String(jobRow?.makemkvInfo?.status || '').trim().toUpperCase() === 'SUCCESS'
  );
  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 discTypeLabel = jobMediaProfile === 'bluray' ? 'Blu-ray' : 'DVD';
  const isDiscTitleSelectionRequired = Boolean(
    (jobMediaProfile === 'dvd' || jobMediaProfile === 'bluray')
    && (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 [containerSeasonEpisodes, setContainerSeasonEpisodes] = 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 metadataEpisodes = Array.isArray(selectedMetadata?.episodes) && selectedMetadata.episodes.length > 0
      ? selectedMetadata.episodes
      : (Array.isArray(containerSeasonEpisodes) ? containerSeasonEpisodes : []);
    const rows = Array.isArray(metadataEpisodes) ? metadataEpisodes : [];
    const normalizedRows = rows
      .map((item) => {
        const episodeId = Number(item?.id ?? item?.episodeId ?? 0);
        const episodeIdStart = Number(item?.episodeIdStart ?? item?.idStart ?? item?.id ?? item?.episodeId ?? 0);
        const episodeIdEnd = Number(item?.episodeIdEnd ?? item?.idEnd ?? 0);
        const episodeNumberStart = Number(
          item?.episodeNumberStart
          ?? item?.numberStart
          ?? item?.episodeNoStart
          ?? item?.number
          ?? item?.episodeNumber
          ?? 0
        );
        const episodeNumberEnd = Number(
          item?.episodeNumberEnd
          ?? item?.numberEnd
          ?? item?.episodeNoEnd
          ?? 0
        );
        const episodeSpan = Number(item?.episodeSpan ?? item?.episodeCount ?? 0);
        const episodeNumber = Number(item?.number ?? item?.episodeNumber ?? episodeNumberStart ?? 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,
          episodeIdStart: Number.isFinite(episodeIdStart) && episodeIdStart > 0 ? Math.trunc(episodeIdStart) : null,
          episodeIdEnd: Number.isFinite(episodeIdEnd) && episodeIdEnd > 0 ? Math.trunc(episodeIdEnd) : null,
          episodeNumber: Number.isFinite(episodeNumber) && episodeNumber > 0 ? Math.trunc(episodeNumber) : null,
          episodeNumberStart: Number.isFinite(episodeNumberStart) && episodeNumberStart > 0 ? Math.trunc(episodeNumberStart) : null,
          episodeNumberEnd: Number.isFinite(episodeNumberEnd) && episodeNumberEnd > 0 ? Math.trunc(episodeNumberEnd) : null,
          episodeSpan: Number.isFinite(episodeSpan) && episodeSpan > 0 ? Math.trunc(episodeSpan) : 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 keys = collectEpisodeReferenceKeys(episode);
      if (keys.length === 0) {
        return true;
      }
      return keys.every((key) => !usedKeySet.has(key));
    });
  }, [selectedMetadata?.episodes, containerSeasonEpisodes, 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 isSeriesVideoByMetadata = Boolean(
      (jobMediaProfile === 'dvd' || jobMediaProfile === 'bluray')
      && (
        hasSeriesMetadataShape(selectedMetadata)
        || hasSeriesMetadataShape(mediaInfoReview?.selectedMetadata)
        || hasSeriesEpisodeAssignmentHints(reviewTitles)
        || Boolean(mediaInfoReview?.seriesBatchParent)
      )
    );
    const defaultAllTitles = Boolean(
      (
        mediaInfoReview?.titleSelectionRequired
        || mediaInfoReview?.handBrakeTitleDecisionRequired
        || (isSeriesVideoByMetadata && reviewTitles.length > 1)
      )
      && 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 = {};
    const allReviewTitles = Array.isArray(reviewTitles) ? reviewTitles : [];
    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 rawEpisodeTitle = String(title?.episodeTitle || '').trim();
      const titleEpisodeRange = resolveEffectiveEpisodeRangeForTitle(
        title,
        title,
        allReviewTitles,
        titleId
      );
      const episodeTitle = rawEpisodeTitle
        ? normalizeEpisodeTitleForOutput(rawEpisodeTitle, titleEpisodeRange)
        : '';
      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,
    selectedMetadata?.seasonNumber,
    selectedMetadata?.episodes,
    selectedMetadata?.metadataProvider,
    jobMediaProfile,
    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 resolveEpisodeFillContext = () => {
    const episodeRows = Array.isArray(selectedMetadataEpisodes) ? selectedMetadataEpisodes : [];
    const reviewTitles = Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [];
    const selectedTitleIds = normalizeTitleIdList([
      ...normalizeTitleIdList(selectedEncodeTitleIds),
      ...(Array.isArray(mediaInfoReview?.selectedTitleIds) ? mediaInfoReview.selectedTitleIds : []),
      ...reviewTitles.filter((title) => Boolean(title?.selectedForEncode)).map((title) => title?.id)
    ]);
    const orderedTitles = reviewTitles
      .map((title) => ({
        id: normalizeTitleId(title?.id),
        title
      }))
      .filter((row) => row.id !== null);
    const orderedSelectedTitleRows = selectedTitleIds.length > 0
      ? orderedTitles.filter((row) => selectedTitleIds.includes(row.id))
      : [];
    const fallbackSelectedTitleRows = orderedSelectedTitleRows.length > 0
      ? orderedSelectedTitleRows
      : orderedTitles.filter((row) => selectedTitleIds.includes(row.id));
    return {
      episodeRows,
      reviewTitles,
      selectedTitleIds,
      fallbackSelectedTitleRows
    };
  };

  const buildGroupedEpisodeUnits = (episodeRows = [], selectedTitleRows = [], reviewTitles = []) => {
    const rows = Array.isArray(episodeRows) ? episodeRows : [];
    const selectedRows = Array.isArray(selectedTitleRows) ? selectedTitleRows : [];
    const allReviewTitles = Array.isArray(reviewTitles) ? reviewTitles : [];
    const normalizeEpisodeStart = (episode) => normalizePositiveInt(
      episode?.episodeNumberStart
      ?? episode?.episodeNoStart
      ?? episode?.episodeNumber
      ?? null
    );
    const normalizeEpisodeEnd = (episode, fallbackStart = null) => {
      const start = normalizePositiveInt(fallbackStart);
      const explicitEnd = normalizePositiveInt(
        episode?.episodeNumberEnd
        ?? episode?.episodeNoEnd
        ?? null
      );
      if (explicitEnd && (!start || explicitEnd >= start)) {
        return explicitEnd;
      }
      const explicitSpan = normalizePositiveInt(
        episode?.episodeSpan
        ?? episode?.episodeCount
        ?? null
      );
      if (start && explicitSpan && explicitSpan > 1) {
        return start + explicitSpan - 1;
      }
      return start;
    };
    const titleAnchorsByEpisodeIndex = new Map();
    if (rows.length > 0) {
      let episodeAnchorCursor = 0;
      for (const selectedTitleRow of selectedRows) {
        if (episodeAnchorCursor >= rows.length) {
          break;
        }
        const explicitSpan = normalizePositiveInt(
          selectedTitleRow?.title?.episodeSpan
          ?? selectedTitleRow?.title?.episodeCount
          ?? null
        );
        const estimatedSpan = estimateSeriesEpisodeSpanForTitle(selectedTitleRow?.title, allReviewTitles);
        const span = Math.max(1, explicitSpan || 1, estimatedSpan || 1);
        const safeSpan = Math.max(1, Math.min(span, rows.length - episodeAnchorCursor));
        titleAnchorsByEpisodeIndex.set(episodeAnchorCursor, { span: safeSpan });
        episodeAnchorCursor += safeSpan;
      }
    }

    const groupedEpisodeUnits = [];
    let sourceEpisodeIndex = 0;
    while (sourceEpisodeIndex < rows.length) {
      const startRow = rows[sourceEpisodeIndex];
      const rangeStart = normalizeEpisodeStart(startRow)
        ?? normalizePositiveInt(startRow?.episodeNumber)
        ?? null;
      const explicitRangeEnd = normalizeEpisodeEnd(startRow, rangeStart);
      const explicitRangeSpan = (
        rangeStart
        && explicitRangeEnd
        && explicitRangeEnd >= rangeStart
      )
        ? Math.max(1, Math.trunc(explicitRangeEnd - rangeStart + 1))
        : 1;

      if (explicitRangeSpan > 1) {
        groupedEpisodeUnits.push({
          startRow,
          endRow: startRow,
          rangeStart,
          rangeEnd: explicitRangeEnd || rangeStart,
          consumedRows: 1
        });
        sourceEpisodeIndex += 1;
        continue;
      }

      const anchor = titleAnchorsByEpisodeIndex.get(sourceEpisodeIndex) || null;
      const anchorSpan = normalizePositiveInt(anchor?.span) || 1;
      if (anchorSpan > 1) {
        const endIndex = Math.min(rows.length - 1, sourceEpisodeIndex + anchorSpan - 1);
        const endRow = rows[endIndex] || startRow;
        const endRowStart = normalizeEpisodeStart(endRow)
          ?? normalizePositiveInt(endRow?.episodeNumber)
          ?? null;
        const rangeEnd = (
          rangeStart
          && endRowStart
          && endRowStart >= rangeStart
        )
          ? endRowStart
          : (rangeStart ? (rangeStart + anchorSpan - 1) : endRowStart);
        groupedEpisodeUnits.push({
          startRow,
          endRow,
          rangeStart,
          rangeEnd,
          consumedRows: anchorSpan
        });
        sourceEpisodeIndex += anchorSpan;
        continue;
      }

      groupedEpisodeUnits.push({
        startRow,
        endRow: startRow,
        rangeStart,
        rangeEnd: rangeStart,
        consumedRows: 1
      });
      sourceEpisodeIndex += 1;
    }

    return groupedEpisodeUnits;
  };

  const applyEpisodeFillFrom = (startEpisodeRef = null) => {
    const { episodeRows, reviewTitles, selectedTitleIds, fallbackSelectedTitleRows } = resolveEpisodeFillContext();
    if (episodeRows.length === 0 || selectedTitleIds.length === 0) {
      return;
    }
    if (fallbackSelectedTitleRows.length === 0) {
      return;
    }
    const rawFillRef = String(startEpisodeRef || '').trim();
    const normalizedRefMatch = rawFillRef.match(/^fill:([^:]+):title:\d+$/i);
    const normalizedRef = normalizedRefMatch && normalizedRefMatch[1]
      ? String(normalizedRefMatch[1]).trim()
      : rawFillRef;

    const groupedEpisodeUnits = buildGroupedEpisodeUnits(
      episodeRows,
      fallbackSelectedTitleRows,
      reviewTitles
    );

    let startUnitIndex = 0;
    if (normalizedRef) {
      const foundUnitIndex = groupedEpisodeUnits.findIndex((unit) =>
        String(unit?.startRow?.episodeIdStart || '') === normalizedRef
        || String(unit?.startRow?.episodeId || '') === normalizedRef
        || String(unit?.startRow?.episodeNumberStart || '') === normalizedRef
        || String(unit?.startRow?.episodeNumber || '') === normalizedRef
      );
      if (foundUnitIndex >= 0) {
        startUnitIndex = foundUnitIndex;
      }
    }

    const nextAssignments = {};
    let unitCursor = startUnitIndex;

    fallbackSelectedTitleRows.forEach((row) => {
      const titleId = row.id;
      const unit = groupedEpisodeUnits[unitCursor];
      if (!unit || !titleId) {
        return;
      }
      const startRow = unit.startRow || null;
      const endRow = unit.endRow || startRow;
      const rangeStart = normalizePositiveInt(unit?.rangeStart)
        ?? normalizeEpisodeStart(startRow)
        ?? normalizePositiveInt(startRow?.episodeNumber)
        ?? null;
      let rangeEnd = normalizePositiveInt(unit?.rangeEnd)
        ?? normalizeEpisodeStart(endRow)
        ?? normalizePositiveInt(endRow?.episodeNumber)
        ?? rangeStart;
      if (rangeStart && rangeEnd && rangeEnd < rangeStart) {
        rangeEnd = rangeStart;
      }
      const normalizedSpan = (
        rangeStart
        && rangeEnd
        && rangeEnd >= rangeStart
      )
        ? Math.max(1, Math.trunc(rangeEnd - rangeStart + 1))
        : Math.max(1, normalizePositiveInt(unit?.consumedRows) || 1);
      const rangeToken = buildEpisodeRangeToken(
        rangeStart,
        rangeEnd
      );
      const assignmentRange = {
        start: rangeStart,
        end: rangeEnd,
        span: normalizedSpan,
        isMulti: normalizedSpan > 1
      };
      const episodeTitleRaw = String(startRow?.episodeTitle || '').trim();
      const endEpisodeTitleRaw = String(endRow?.episodeTitle || '').trim();
      const titleSource = episodeTitleRaw || endEpisodeTitleRaw;
      const mergedTitle = titleSource
        ? normalizeEpisodeTitleForOutput(titleSource, assignmentRange)
        : null;
      const episodeIdStart = normalizePositiveInt(
        startRow?.episodeIdStart
        ?? startRow?.episodeId
        ?? null
      );
      const endRowEpisodeId = normalizePositiveInt(
        endRow?.episodeIdStart
        ?? endRow?.episodeId
        ?? null
      );
      const explicitEpisodeIdEnd = normalizePositiveInt(startRow?.episodeIdEnd ?? null);
      const episodeIdEnd = endRowEpisodeId || explicitEpisodeIdEnd || episodeIdStart || null;
      nextAssignments[titleId] = {
        episodeId: episodeIdStart,
        episodeNumber: rangeStart,
        seasonNumber: startRow?.seasonNumber ?? null,
        episodeTitle: mergedTitle,
        episodeIdStart: episodeIdStart,
        episodeIdEnd,
        episodeNumberStart: rangeStart,
        episodeNumberEnd: rangeEnd,
        episodeSpan: normalizedSpan,
        episodeRange: rangeToken,
        episodeTitleStart: episodeTitleRaw || null,
        episodeTitleEnd: endEpisodeTitleRaw || episodeTitleRaw || null
      };
      unitCursor += 1;
    });
    setEpisodeAssignmentsByTitle((prev) => ({
      ...(prev && typeof prev === 'object' ? prev : {}),
      ...nextAssignments
    }));
  };

  const episodeFillOptions = useMemo(() => {
    const { episodeRows, reviewTitles, fallbackSelectedTitleRows } = resolveEpisodeFillContext();
    if (!Array.isArray(episodeRows) || episodeRows.length === 0) {
      return [];
    }
    const groupedUnits = buildGroupedEpisodeUnits(episodeRows, fallbackSelectedTitleRows, reviewTitles);
    return groupedUnits
      .map((unit) => {
        const startRow = unit?.startRow || null;
        const value = startRow?.episodeIdStart
          || startRow?.episodeId
          || startRow?.episodeNumberStart
          || startRow?.episodeNumber
          || null;
        if (!value) {
          return null;
        }
        return {
          value: String(value),
          startRef: String(value)
        };
      })
      .filter(Boolean);
  }, [selectedMetadataEpisodes, selectedEncodeTitleIds, mediaInfoReview?.titles, mediaInfoReview?.selectedTitleIds]);

  useEffect(() => {
    if (!Array.isArray(selectedMetadataEpisodes) || selectedMetadataEpisodes.length === 0) {
      return;
    }
    if (!Array.isArray(episodeFillOptions) || episodeFillOptions.length === 0) {
      return;
    }
    const normalizedSelection = String(selectedEpisodeFillStart || '').trim();
    const hasValidSelection = Boolean(
      normalizedSelection
      && episodeFillOptions.some((option) => String(option?.value || '').trim() === normalizedSelection)
    );
    const defaultStartRef = hasValidSelection
      ? normalizedSelection
      : (episodeFillOptions[0]?.value || episodeFillOptions[0]?.startRef || null);
    if (!defaultStartRef) {
      return;
    }
    if (!hasValidSelection) {
      setSelectedEpisodeFillStart(String(defaultStartRef));
      // Keep dropdown + episodentitel in sync when options change (e.g. Disc 2
      // after already used episodes were loaded from the container).
      applyEpisodeFillFrom(defaultStartRef);
      return;
    }

    const reviewTitles = Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [];
    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 hasEpisodeAssignmentPayload = (assignment) => {
      if (!assignment || typeof assignment !== 'object') {
        return false;
      }
      const episodeId = Number(assignment?.episodeId || assignment?.episodeIdStart || 0);
      const episodeNumber = Number(assignment?.episodeNumber || assignment?.episodeNumberStart || 0);
      const seasonNumber = Number(assignment?.seasonNumber || 0);
      const episodeTitle = String(assignment?.episodeTitle || '').trim();
      return Boolean(episodeTitle || episodeId > 0 || episodeNumber > 0 || seasonNumber > 0);
    };

    const hasAnyEpisodeAssignment = effectiveSelectedTitleIds.some((titleId) => {
      const assignment = episodeAssignmentsByTitle?.[titleId]
        || episodeAssignmentsByTitle?.[String(titleId)]
        || null;
      return hasEpisodeAssignmentPayload(assignment);
    });
    const hasCompleteEpisodeAssignments = effectiveSelectedTitleIds.length > 0
      && effectiveSelectedTitleIds.every((titleId) => {
        const assignment = episodeAssignmentsByTitle?.[titleId]
          || episodeAssignmentsByTitle?.[String(titleId)]
          || null;
        return hasEpisodeAssignmentPayload(assignment);
      });

    if (hasCompleteEpisodeAssignments) {
      return;
    }
    if (hasAnyEpisodeAssignment && normalizedSelection) {
      return;
    }
    applyEpisodeFillFrom(defaultStartRef);
  }, [
    selectedMetadataEpisodes,
    selectedEncodeTitleIds,
    selectedEncodeTitleId,
    mediaInfoReview?.titles,
    mediaInfoReview?.encodeInputTitleId,
    mediaInfoReview?.generatedAt,
    episodeAssignmentsByTitle,
    episodeFillOptions,
    selectedEpisodeFillStart
  ]);

  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 canCancelReview = Boolean(
    !running
    && retryJobId
    && (state === 'READY_TO_ENCODE' || state === 'WAITING_FOR_USER_DECISION')
  );
  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 handBrakeDecisionMode = String(pipeline?.context?.handBrakeDecisionMode || '').trim().toLowerCase();
  const isSeriesPlayAllDecisionMode = handBrakeDecisionMode === 'series_playall_or_double';
  const handBrakeDecisionSummary = pipeline?.context?.handBrakeDecisionSummary
    && typeof pipeline.context.handBrakeDecisionSummary === 'object'
    ? pipeline.context.handBrakeDecisionSummary
    : null;
  const ambiguousLongestTitleId = normalizeTitleId(handBrakeDecisionSummary?.longestCandidateTitleId);
  const handBrakeDecisionCandidateTitleIds = useMemo(() => normalizeTitleIdList(
    (Array.isArray(pipeline?.context?.handBrakeTitleCandidates)
      ? pipeline.context.handBrakeTitleCandidates
      : []
    ).map((item) => item?.handBrakeTitleId)
  ), [pipeline?.context?.handBrakeTitleCandidates]);
  const handBrakeDecisionSelectableTitleIds = useMemo(() => {
    const explicit = normalizeTitleIdList(pipeline?.context?.handBrakeDecisionSelectableTitleIds);
    if (explicit.length > 0) {
      return explicit;
    }
    if (isSeriesPlayAllDecisionMode && ambiguousLongestTitleId) {
      return [ambiguousLongestTitleId];
    }
    return handBrakeDecisionCandidateTitleIds;
  }, [
    isSeriesPlayAllDecisionMode,
    ambiguousLongestTitleId,
    handBrakeDecisionCandidateTitleIds,
    pipeline?.context?.handBrakeDecisionSelectableTitleIds
  ]);
  const handBrakeDecisionSelectableTitleIdSet = useMemo(
    () => new Set(handBrakeDecisionSelectableTitleIds.map((id) => Number(id))),
    [handBrakeDecisionSelectableTitleIds]
  );

  const waitingHandBrakeTitleRows = useMemo(() => {
    const rawCandidates = Array.isArray(pipeline?.context?.handBrakeTitleCandidates)
      ? pipeline.context.handBrakeTitleCandidates
      : [];
    const rawAllTitles = isSeriesPlayAllDecisionMode && Array.isArray(pipeline?.context?.handBrakeAllTitles)
      ? pipeline.context.handBrakeAllTitles
      : [];
    const sourceRows = rawAllTitles.length > 0 ? rawAllTitles : rawCandidates;
    const candidateIdSet = new Set(
      normalizeTitleIdList(rawCandidates.map((item) => item?.handBrakeTitleId)).map((id) => Number(id))
    );
    return sourceRows
      .map((item) => {
        const titleId = normalizeTitleId(item?.handBrakeTitleId);
        if (!titleId) {
          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),
          decisionCandidate: candidateIdSet.has(Number(titleId))
        };
      })
      .filter(Boolean)
      .sort((a, b) => a.handBrakeTitleId - b.handBrakeTitleId);
  }, [
    isSeriesPlayAllDecisionMode,
    pipeline?.context?.handBrakeAllTitles,
    pipeline?.context?.handBrakeTitleCandidates
  ]);
  const visibleWaitingHandBrakeTitleRows = useMemo(() => {
    if (!isSeriesPlayAllDecisionMode) {
      return waitingHandBrakeTitleRows;
    }
    return waitingHandBrakeTitleRows.filter((row) => row.decisionCandidate);
  }, [isSeriesPlayAllDecisionMode, waitingHandBrakeTitleRows]);

  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;
    }
    if (isSeriesPlayAllDecisionMode) {
      const lockedDefaultIds = waitingHandBrakeTitleRows
        .filter((row) => row.decisionCandidate && !handBrakeDecisionSelectableTitleIdSet.has(Number(row.handBrakeTitleId)))
        .map((row) => row.handBrakeTitleId);
      if (lockedDefaultIds.length > 0) {
        setSelectedHandBrakeTitleIdsState(normalizeTitleIdList(lockedDefaultIds));
        return;
      }
    }
    const firstSelectable = waitingHandBrakeTitleRows.find((row) => (
      !isSeriesPlayAllDecisionMode || handBrakeDecisionSelectableTitleIdSet.has(Number(row.handBrakeTitleId))
    ))?.handBrakeTitleId || null;
    setSelectedHandBrakeTitleIdsState(firstSelectable ? [firstSelectable] : []);
  }, [
    handBrakeTitleDecisionRequired,
    isSeriesPlayAllDecisionMode,
    retryJobId,
    handBrakeDecisionSelectableTitleIdSet,
    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
  ]);

  const seriesBatchRunningChildJobIdHint = useMemo(() => {
    const seriesBatch = pipeline?.context?.seriesBatch;
    if (!seriesBatch || typeof seriesBatch !== 'object') {
      return null;
    }
    const children = Array.isArray(seriesBatch?.children) ? seriesBatch.children : [];
    const runningChild = children.find((child) => normalizeSeriesBatchStatus(child?.status) === 'RUNNING') || null;
    return normalizeJobId(runningChild?.jobId);
  }, [pipeline?.context?.seriesBatch]);

  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) {
          const liveJob = response?.job && typeof response.job === 'object'
            ? response.job
            : null;
          const childRows = Array.isArray(liveJob?.children) ? liveJob.children : [];
          const liveHintChildId = normalizeJobId(seriesBatchRunningChildJobIdHint);
          const isSeriesBatchParentRun = Boolean(
            liveHintChildId
            || liveJob?.encodePlan?.seriesBatchParent
            || String(liveJob?.handbrakeInfo?.mode || '').trim().toLowerCase() === 'series_batch'
          );

          if (isSeriesBatchParentRun) {
            const activeChildStates = new Set(['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'RUNNING']);
            let activeChild = liveHintChildId
              ? (childRows.find((child) => normalizeJobId(child?.id) === liveHintChildId) || null)
              : null;
            if (activeChild) {
              const hintChildStatus = String(activeChild?.status || activeChild?.last_state || '').trim().toUpperCase();
              if (!activeChildStates.has(hintChildStatus)) {
                activeChild = null;
              }
            }
            if (!activeChild) {
              activeChild = childRows.find((child) => (
                activeChildStates.has(String(child?.status || child?.last_state || '').trim().toUpperCase())
              )) || null;
            }
            setLiveJobLog(String(activeChild?.log || '').trim());
            return;
          }

          setLiveJobLog(String(liveJob?.log || '').trim());
        }
      } catch (_err) {
        // ignore transient polling errors
      }
    };
    void refresh();
    const interval = setInterval(refresh, 2500);
    return () => {
      cancelled = true;
      clearInterval(interval);
    };
  }, [running, retryJobId, seriesBatchRunningChildJobIdHint]);

  const manualDecisionState = pipeline?.context?.manualDecisionState || null;
  const rawDecisionRequiredBeforeStart = state === 'WAITING_FOR_USER_DECISION' && manualDecisionState === 'waiting_for_raw_decision';
  const playlistDecisionRequiredBeforeStart = state === 'WAITING_FOR_USER_DECISION' && !handBrakeTitleDecisionRequired && !rawDecisionRequiredBeforeStart;
  const isSeriesDvdReview = useMemo(() => {
    if (jobMediaProfile !== 'dvd' && jobMediaProfile !== 'bluray') {
      return false;
    }
    return Boolean(
      hasSeriesMetadataShape(selectedMetadata)
      || hasSeriesMetadataShape(mediaInfoReview?.selectedMetadata)
      || hasSeriesEpisodeAssignmentHints(mediaInfoReview?.titles)
      || Boolean(mediaInfoReview?.seriesBatchParent)
    );
  }, [
    jobMediaProfile,
    selectedMetadata?.seasonNumber,
    selectedMetadata?.episodes,
    selectedMetadata?.metadataProvider,
    selectedMetadata?.workflowKind,
    mediaInfoReview?.selectedMetadata?.seasonNumber,
    mediaInfoReview?.selectedMetadata?.episodes,
    mediaInfoReview?.selectedMetadata?.metadataProvider,
    mediaInfoReview?.selectedMetadata?.workflowKind,
    mediaInfoReview?.titles,
    mediaInfoReview?.seriesBatchParent
  ]);
  useEffect(() => {
    let cancelled = false;

    const loadContainerUsedEpisodes = async () => {
      if (!isSeriesDvdReview || !retryJobId) {
        if (!cancelled) {
          setContainerUsedEpisodeKeys([]);
          setContainerSeasonEpisodes([]);
        }
        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([]);
            setContainerSeasonEpisodes([]);
          }
          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();
        const extractEpisodesFromJob = (jobRow) => {
          const mkInfo = jobRow?.makemkvInfo && typeof jobRow.makemkvInfo === 'object'
            ? jobRow.makemkvInfo
            : {};
          const analyzeContext = mkInfo?.analyzeContext && typeof mkInfo.analyzeContext === 'object'
            ? mkInfo.analyzeContext
            : {};
          const selectedFromAnalyze = analyzeContext?.selectedMetadata && typeof analyzeContext.selectedMetadata === 'object'
            ? analyzeContext.selectedMetadata
            : {};
          const selectedFromRoot = mkInfo?.selectedMetadata && typeof mkInfo.selectedMetadata === 'object'
            ? mkInfo.selectedMetadata
            : {};
          const episodes = Array.isArray(selectedFromAnalyze?.episodes)
            ? selectedFromAnalyze.episodes
            : (Array.isArray(selectedFromRoot?.episodes) ? selectedFromRoot.episodes : []);
          return Array.isArray(episodes) ? episodes : [];
        };
        let fallbackEpisodes = extractEpisodesFromJob(containerJob);

        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 keys = collectEpisodeReferenceKeys(ref);
            for (const key of keys) {
              usedKeys.add(key);
            }
          }

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

          if ((!Array.isArray(fallbackEpisodes) || fallbackEpisodes.length === 0)) {
            const childEpisodes = extractEpisodesFromJob(child);
            if (Array.isArray(childEpisodes) && childEpisodes.length > 0) {
              fallbackEpisodes = childEpisodes;
            }
          }
        }

        if (!cancelled) {
          setContainerUsedEpisodeKeys(Array.from(usedKeys));
          setContainerSeasonEpisodes(Array.isArray(fallbackEpisodes) ? fallbackEpisodes : []);
        }
      } catch (_error) {
        if (!cancelled) {
          setContainerUsedEpisodeKeys([]);
          setContainerSeasonEpisodes([]);
        }
      }
    };

    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,
        jobMediaProfile,
        selectedMetadata || {},
        title,
        assignment,
        retryJobId,
        Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [],
        episodeAssignmentsByTitle,
        selectedEncodeTitleIds
      );
      if (path) {
        map[titleId] = path;
      }
    }
    return map;
  }, [
    isSeriesDvdReview,
    mediaInfoReview?.titles,
    episodeAssignmentsByTitle,
    settingsMap,
    jobMediaProfile,
    selectedMetadata,
    retryJobId,
    selectedEncodeTitleIds
  ]);
  const multiEpisodeReviewWarnings = useMemo(() => {
    if (!isSeriesDvdReview) {
      return [];
    }
    const reviewTitles = Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [];
    if (reviewTitles.length === 0) {
      return [];
    }
    return reviewTitles
      .map((title) => {
        const titleId = normalizeTitleId(title?.id);
        if (!titleId) {
          return null;
        }
        const assignment = episodeAssignmentsByTitle?.[titleId]
          || episodeAssignmentsByTitle?.[String(titleId)]
          || null;
        const episodeRange = resolveEffectiveEpisodeRangeForTitle(
          assignment,
          title,
          reviewTitles,
          titleId
        );
        if (!episodeRange.isMulti) {
          return null;
        }
        const episodeRangeToken = buildEpisodeRangeToken(episodeRange.start, episodeRange.end);
        const partsToken = buildEpisodePartsToken(episodeRange);
        const rawTitle = String(
          assignment?.episodeTitle
          || title?.episodeTitle
          || title?.fileName
          || `Episode ${episodeRangeToken}`
        ).trim() || `Episode ${episodeRangeToken}`;
        const normalizedTitle = normalizeEpisodeTitleForOutput(rawTitle, episodeRange);
        const normalizedByHeuristic = normalizedTitle !== rawTitle;
        const listTitleRaw = String(title?.episodeTitle || '').trim();
        const normalizedListTitle = listTitleRaw
          ? normalizeEpisodeTitleForOutput(listTitleRaw, episodeRange)
          : '';
        const mismatchWithTitleList = Boolean(
          normalizedListTitle
          && normalizeEpisodeTitleTokenForCompare(normalizedTitle) !== normalizeEpisodeTitleTokenForCompare(normalizedListTitle)
        );
        return {
          titleId,
          episodeRangeToken,
          partsToken,
          rawTitle,
          normalizedTitle,
          normalizedByHeuristic,
          normalizedListTitle: normalizedListTitle || null,
          mismatchWithTitleList
        };
      })
      .filter(Boolean);
  }, [isSeriesDvdReview, mediaInfoReview?.titles, episodeAssignmentsByTitle]);
  const episodeRangeConflictWarnings = useMemo(() => {
    if (!isSeriesDvdReview) {
      return [];
    }
    const reviewTitles = Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [];
    if (reviewTitles.length === 0) {
      return [];
    }
    const metadataRows = (Array.isArray(selectedMetadataEpisodes) ? selectedMetadataEpisodes : [])
      .map((episode) => {
        const episodeNumber = normalizePositiveInt(
          episode?.episodeNumberStart
          ?? episode?.episodeNoStart
          ?? episode?.episodeNumber
          ?? episode?.number
          ?? null
        );
        const episodeTitle = String(
          episode?.episodeTitle
          || episode?.name
          || episode?.title
          || ''
        ).trim();
        return {
          episodeNumber,
          episodeTitle
        };
      })
      .filter((row) => row.episodeNumber);
    const titleAnchorSuggestionById = new Map();
    if (metadataRows.length > 0) {
      let cursor = 0;
      for (const title of reviewTitles) {
        const titleId = normalizeTitleId(title?.id);
        if (!titleId) {
          continue;
        }
        if (cursor >= metadataRows.length) {
          break;
        }
        const explicitSpan = normalizePositiveInt(
          title?.episodeSpan
          ?? title?.episodeCount
          ?? null
        );
        const estimatedSpan = estimateSeriesEpisodeSpanForTitle(title, reviewTitles);
        const span = Math.max(1, explicitSpan || 1, estimatedSpan || 1);
        const safeSpan = Math.max(1, Math.min(span, metadataRows.length - cursor));
        const spanRows = metadataRows.slice(cursor, cursor + safeSpan);
        const startNumber = normalizePositiveInt(spanRows[0]?.episodeNumber);
        const endNumber = normalizePositiveInt(spanRows[spanRows.length - 1]?.episodeNumber) || startNumber;
        const isMulti = Boolean(startNumber && endNumber && endNumber > startNumber);
        const sourceTitle = spanRows
          .map((row) => String(row?.episodeTitle || '').trim())
          .filter(Boolean)
          .join(' + ');
        const normalizedTitle = sourceTitle
          ? normalizeEpisodeTitleForOutput(
            sourceTitle,
            {
              start: startNumber || 1,
              end: endNumber || startNumber || 1,
              span: safeSpan,
              isMulti
            }
          )
          : '';
        titleAnchorSuggestionById.set(titleId, {
          suggestedTitle: normalizedTitle || '(ohne Titel)',
          isMulti
        });
        cursor += safeSpan;
      }
    }
    return reviewTitles
      .map((title) => {
        const titleId = normalizeTitleId(title?.id);
        if (!titleId) {
          return null;
        }
        const assignment = episodeAssignmentsByTitle?.[titleId]
          || episodeAssignmentsByTitle?.[String(titleId)]
          || null;
        if (!hasExplicitEpisodeAssignmentRange(assignment)) {
          return null;
        }
        const explicitRange = resolveEpisodeAssignmentRange(assignment, title, titleId);
        if (!explicitRange || explicitRange.isMulti) {
          return null;
        }
        const spanHint = estimateSeriesEpisodeSpanForTitle(title, reviewTitles);
        if (!Number.isFinite(spanHint) || spanHint <= 1) {
          return null;
        }
        const effectiveSpan = Math.max(1, Math.trunc(spanHint));
        const suggestedRange = {
          start: explicitRange.start,
          end: explicitRange.start + effectiveSpan - 1,
          span: effectiveSpan,
          isMulti: effectiveSpan > 1
        };
        const suggestedTitle = String(titleAnchorSuggestionById.get(titleId)?.suggestedTitle || '').trim();
        return {
          titleId,
          episodeTitle: String(
            assignment?.episodeTitle
            || title?.episodeTitle
            || title?.fileName
            || ''
          ).trim() || '(ohne Titel)',
          suggestedTitle: suggestedTitle || normalizeEpisodeTitleForOutput(
            String(assignment?.episodeTitle || title?.episodeTitle || '').trim() || '(ohne Titel)',
            suggestedRange
          )
        };
      })
      .filter(Boolean);
  }, [isSeriesDvdReview, mediaInfoReview?.titles, episodeAssignmentsByTitle, selectedMetadataEpisodes]);
  const episodeAssignmentWarningLines = useMemo(() => (
    episodeRangeConflictWarnings
      .map((item) => {
        const currentTitle = String(item?.episodeTitle || '').trim() || '(ohne Titel)';
        const suggestionTitle = String(item?.suggestedTitle || '').trim() || '(ohne Titel)';
        return `Titel ${item.titleId}: ${currentTitle} --> Vorschlag: ${suggestionTitle} (Heuristik vermutet Doppelfolge)`;
      })
      .filter(Boolean)
  ), [episodeRangeConflictWarnings]);
  const hasMultiEpisodeInJob = multiEpisodeReviewWarnings.length > 0 || episodeRangeConflictWarnings.length > 0;
  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
        || assignment?.episodeIdStart
        || title?.episodeId
        || 0
      );
      const episodeNumber = Number(
        assignment?.episodeNumber
        || assignment?.episodeNumberStart
        || title?.episodeNumber
        || 0
      );
      const seasonNumber = Number(assignment?.seasonNumber || title?.seasonNumber || 0);
      const episodeIdStart = Number(
        assignment?.episodeIdStart
        || assignment?.episodeId
        || 0
      );
      const episodeIdEnd = Number(
        assignment?.episodeIdEnd
        || 0
      );
      const episodeNumberStart = Number(
        assignment?.episodeNumberStart
        || assignment?.episodeNoStart
        || assignment?.episodeNumber
        || title?.episodeNumber
        || 0
      );
      const episodeNumberEnd = Number(
        assignment?.episodeNumberEnd
        || assignment?.episodeNoEnd
        || 0
      );
      const episodeSpanRaw = Number(assignment?.episodeSpan || assignment?.episodeCount || 0);
      const rawEpisodeTitle = String(assignment?.episodeTitle || title?.episodeTitle || '').trim();
      const normalizedEpisodeStart = Number.isFinite(episodeNumberStart) && episodeNumberStart > 0
        ? Math.trunc(episodeNumberStart)
        : (Number.isFinite(episodeNumber) && episodeNumber > 0 ? Math.trunc(episodeNumber) : null);
      const normalizedEpisodeEnd = Number.isFinite(episodeNumberEnd) && episodeNumberEnd > 0
        ? Math.trunc(episodeNumberEnd)
        : normalizedEpisodeStart;
      const normalizedEpisodeSpan = Number.isFinite(episodeSpanRaw) && episodeSpanRaw > 0
        ? Math.trunc(episodeSpanRaw)
        : (
          normalizedEpisodeStart && normalizedEpisodeEnd && normalizedEpisodeEnd >= normalizedEpisodeStart
            ? (normalizedEpisodeEnd - normalizedEpisodeStart + 1)
            : 1
        );
      const normalizedEpisodeRange = {
        start: normalizedEpisodeStart || 1,
        end: normalizedEpisodeEnd || normalizedEpisodeStart || 1,
        span: normalizedEpisodeSpan,
        isMulti: normalizedEpisodeSpan > 1
      };
      const normalizedEpisodeTitle = rawEpisodeTitle
        ? normalizeEpisodeTitleForOutput(rawEpisodeTitle, normalizedEpisodeRange)
        : '';
      if (!normalizedEpisodeTitle && !(episodeId > 0) && !(episodeNumber > 0) && !(seasonNumber > 0)) {
        continue;
      }
      const normalizedRangeToken = normalizedEpisodeStart
        ? buildEpisodeRangeToken(normalizedEpisodeStart, normalizedEpisodeEnd || normalizedEpisodeStart)
        : null;
      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: normalizedEpisodeTitle || null,
        episodeIdStart: Number.isFinite(episodeIdStart) && episodeIdStart > 0 ? Math.trunc(episodeIdStart) : null,
        episodeIdEnd: Number.isFinite(episodeIdEnd) && episodeIdEnd > 0 ? Math.trunc(episodeIdEnd) : null,
        episodeNumberStart: normalizedEpisodeStart,
        episodeNumberEnd: normalizedEpisodeEnd || normalizedEpisodeStart,
        episodeSpan: normalizedEpisodeSpan,
        episodeRange: normalizedRangeToken,
        episodeTitleStart: String(assignment?.episodeTitleStart || '').trim() || null,
        episodeTitleEnd: String(assignment?.episodeTitleEnd || '').trim() || 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' || isOrphanCancelled)
              && 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'
              && !isOrphanCancelled
              && !(jobMediaProfile === 'converter' && state === 'READY_TO_START' && !selectedMetadata?.imdbId) ? (
              <Button
                label="Metadaten neu zuweisen"
                icon="pi pi-search"
                severity="info"
                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={isSeriesPlayAllDecisionMode ? 'PlayAll/Doppelfolge bestätigen' : `${discTypeLabel} 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 () => {
                  if (hasMultiEpisodeInJob) {
                    const introText = 'Im Job wurden Hinweise zur Episoden-Zuordnung erkannt. Bitte Episodentitel und Episodenbereich prüfen, bevor das Encoding startet.';
                    const rows = episodeAssignmentWarningLines;
                    const modalMessage = rows.length > 0
                      ? `${introText}\n\n${rows.join('\n')}`
                      : introText;
                    const confirmed = await confirmModal({
                      header: 'Episoden-Zuordnung prüfen',
                      message: modalMessage,
                      acceptLabel: 'Encoding starten',
                      rejectLabel: 'Abbrechen',
                      icon: 'pi pi-exclamation-triangle',
                      style: { width: '34rem', maxWidth: '92vw' },
                      breakpoints: {
                        '960px': '92vw',
                        '640px': '95vw'
                      }
                    });
                    if (!confirmed) {
                      return;
                    }
                  }
                  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 || canCancelReview) && (
              <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 && !isOrphanCancelled && !ripCompleted && (
              <Button
                label="Retry Rippen"
                icon="pi pi-refresh"
                severity="warning"
                onClick={() => onRetry(retryJobId)}
                loading={busy}
              />
            )}

            {(state === 'ERROR' || state === 'CANCELLED') && !isOrphanCancelled && !ripCompleted ? (
              <Button
                label="Disk-Analyse neu starten"
                icon="pi pi-search"
                severity="secondary"
                onClick={onReanalyze || onAnalyze}
                loading={busy}
              />
            ) : null}
          </>
        )}
        {!running && 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}

      {rawDecisionRequiredBeforeStart && !queueLocked ? (
        <div className="playlist-decision-block">
          <h3>RAW Entscheidung erforderlich</h3>
          <small>
            Vorhandenes Raw zur Disk gefunden. Wie soll weiter verfahren werden?
          </small>
          <div className="actions-row mt-3">
            <Button
              label="mit RAW weiter machen"
              icon="pi pi-check"
              severity="success"
              onClick={() => onSubmitRawDecision?.(retryJobId, 'continue')}
              loading={busy}
            />
            <Button
              label="RAW löschen und Rip erneut starten"
              icon="pi pi-trash"
              severity="danger"
              outlined
              onClick={() => onSubmitRawDecision?.(retryJobId, 'delete')}
              loading={busy}
            />
            <Button
              label="abbrechen"
              icon="pi pi-times"
              severity="secondary"
              outlined
              onClick={() => onSubmitRawDecision?.(retryJobId, 'cancel')}
              loading={busy}
            />
          </div>
        </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>{isSeriesPlayAllDecisionMode ? 'Serien-Grenzfall: PlayAll oder Doppelfolge?' : `${discTypeLabel} Titelauswahl erforderlich`}</h3>
          <small>
            {isSeriesPlayAllDecisionMode
              ? 'Min. ein Titel passt zeitlich zur Summe der kürzeren Episoden. Bitte durch Auswahl entscheiden, ob PlayAll Multi-Episode (nicht anhaken) oder Doppelfolge (anhaken).'
              : `Mehrere ${discTypeLabel}-Titel gefunden, die die Mindestlänge erfüllen. Bitte einen oder mehrere Episodentitel auswählen.`}
          </small>
          {visibleWaitingHandBrakeTitleRows.length > 0 ? (
            <div className="playlist-decision-list">
              {visibleWaitingHandBrakeTitleRows.map((row) => {
                const isDecisionSelectable = !isSeriesPlayAllDecisionMode
                  || handBrakeDecisionSelectableTitleIdSet.has(Number(row.handBrakeTitleId));
                const isAmbiguousDecisionTitle = isSeriesPlayAllDecisionMode
                  && isDecisionSelectable
                  && (
                    Number(row.handBrakeTitleId) === Number(ambiguousLongestTitleId)
                    || handBrakeDecisionSelectableTitleIdSet.has(Number(row.handBrakeTitleId))
                  );
                const isRecognizedLocked = isSeriesPlayAllDecisionMode
                  && row.decisionCandidate
                  && !isDecisionSelectable;
                const isOutOfDecisionScope = isSeriesPlayAllDecisionMode
                  && !row.decisionCandidate;
                return (
                  <div key={row.handBrakeTitleId} className="playlist-decision-item">
                    <label className="readonly-check-row">
                      <input
                        type="checkbox"
                        checked={selectedHandBrakeTitleIdsState.includes(row.handBrakeTitleId)}
                        disabled={queueLocked || !isDecisionSelectable}
                        onChange={() => {
                          if (!isDecisionSelectable) {
                            return;
                          }
                          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>
                        {isAmbiguousDecisionTitle ? <strong>{`Titel -t ${row.handBrakeTitleId}`}</strong> : `Titel -t ${row.handBrakeTitleId}`}
                        {isAmbiguousDecisionTitle ? ' | Grenzfalltitel (PlayAll/Doppelfolge)' : ''}
                        {isRecognizedLocked ? ' | automatisch erkannt (fix)' : ''}
                        {isOutOfDecisionScope ? ' | nicht entscheidungsrelevant' : ''}
                        {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 && !isDiscTitleSelectionRequired ? (
        <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) && !(handBrakeTitleDecisionRequired && isSeriesPlayAllDecisionMode) ? (
        <div className="mediainfo-review-block">
          {!isDiscTitleSelectionRequired ? <h3>Titel-/Spurprüfung</h3> : null}
          {state === 'READY_TO_ENCODE' && !queueLocked && !isDiscTitleSelectionRequired ? (
            <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}
          {isSeriesDvdReview && hasMultiEpisodeInJob ? (
            <div className="playlist-decision-block">
              <h3>Hinweis Episoden-Zuordnung</h3>
              <small>
                Im Job wurden Hinweise zur Episoden-Zuordnung erkannt. Bitte Episodentitel und Episodenbereich prüfen, bevor das Encoding startet.
              </small>
              {episodeAssignmentWarningLines.map((line, index) => (
                <small key={`episode-assignment-warning-${index}`}>{line}</small>
              ))}
            </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={isDiscTitleSelectionRequired}
            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}
            allowEpisodeAssignments={isSeriesDvdReview}
            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 = {
                  ...current,
                  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
                };
                if (next.episodeNumber !== null && next.episodeNumberStart == null) {
                  next.episodeNumberStart = next.episodeNumber;
                }
                if (
                  next.episodeNumberStart !== null
                  && next.episodeNumberEnd !== null
                  && Number(next.episodeNumberEnd) < Number(next.episodeNumberStart)
                ) {
                  next.episodeNumberEnd = next.episodeNumberStart;
                }
                if (next.episodeNumberStart !== null && next.episodeNumberEnd == null) {
                  next.episodeNumberEnd = next.episodeNumberStart;
                }
                if (next.episodeNumberStart !== null) {
                  const start = Number(next.episodeNumberStart);
                  const end = Number(next.episodeNumberEnd ?? start);
                  if (Number.isFinite(start) && start > 0 && Number.isFinite(end) && end > 0) {
                    next.episodeSpan = Math.max(1, Math.trunc(end - start + 1));
                    next.episodeRange = buildEpisodeRangeToken(start, end);
                  }
                }
                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>
  );
}
