import { useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Toast } from 'primereact/toast';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { Tag } from 'primereact/tag';
import { ProgressBar } from 'primereact/progressbar';
import { Dialog } from 'primereact/dialog';
import { FileUpload } from 'primereact/fileupload';
import { InputNumber } from 'primereact/inputnumber';
import { InputText } from 'primereact/inputtext';
import { api } from '../api/client';
import { useWebSocket } from '../hooks/useWebSocket';
import PipelineStatusCard from '../components/PipelineStatusCard';
import MetadataSelectionDialog from '../components/MetadataSelectionDialog';
import CdMetadataDialog from '../components/CdMetadataDialog';
import CdRipConfigPanel from '../components/CdRipConfigPanel';
import AudiobookConfigPanel from '../components/AudiobookConfigPanel';
import blurayIndicatorIcon from '../assets/media-bluray.svg';
import discIndicatorIcon from '../assets/media-disc.svg';
import otherIndicatorIcon from '../assets/media-other.svg';
import audiobookIndicatorIcon from '../assets/media-audiobook.svg';
import { getStatusLabel, getStatusSeverity, normalizeStatus } from '../utils/statusPresentation';

const processingStates = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING'];
const driveActiveStates = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'CD_ANALYZING', 'CD_RIPPING'];
const dashboardStatuses = new Set([
  'ANALYZING',
  'METADATA_SELECTION',
  'WAITING_FOR_USER_DECISION',
  'READY_TO_START',
  'MEDIAINFO_CHECK',
  'READY_TO_ENCODE',
  'RIPPING',
  'ENCODING',
  'CANCELLED',
  'ERROR',
  'CD_METADATA_SELECTION',
  'CD_READY_TO_RIP',
  'CD_ANALYZING',
  'CD_RIPPING',
  'CD_ENCODING'
]);

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

function formatPercent(value, digits = 1) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) {
    return 'n/a';
  }
  return `${parsed.toFixed(digits)}%`;
}

function formatTemperature(value) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) {
    return 'n/a';
  }
  return `${parsed.toFixed(1)}°C`;
}

function formatBytes(value) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed < 0) {
    return 'n/a';
  }
  if (parsed === 0) {
    return '0 B';
  }
  const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
  let unitIndex = 0;
  let current = parsed;
  while (current >= 1024 && unitIndex < units.length - 1) {
    current /= 1024;
    unitIndex += 1;
  }
  const digits = unitIndex <= 1 ? 0 : 2;
  return `${current.toFixed(digits)} ${units[unitIndex]}`;
}

function formatUpdatedAt(value) {
  if (!value) {
    return '-';
  }
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) {
    return '-';
  }
  return date.toLocaleString('de-DE');
}

function formatDurationMs(value) {
  const ms = Number(value);
  if (!Number.isFinite(ms) || ms < 0) {
    return '-';
  }
  if (ms < 1000) {
    return `${Math.round(ms)} ms`;
  }
  const seconds = Math.round(ms / 1000);
  if (seconds < 60) {
    return `${seconds}s`;
  }
  const minutes = Math.floor(seconds / 60);
  const restSeconds = seconds % 60;
  return `${minutes}m ${restSeconds}s`;
}

function normalizeRuntimeActivitiesPayload(rawPayload) {
  const payload = rawPayload && typeof rawPayload === 'object' ? rawPayload : {};
  const normalizeItem = (item) => {
    const source = item && typeof item === 'object' ? item : {};
    const parsedId = Number(source.id);
    const id = Number.isFinite(parsedId) && parsedId > 0 ? Math.trunc(parsedId) : null;
    return {
      id,
      type: String(source.type || '').trim().toLowerCase() || 'task',
      name: String(source.name || '').trim() || '-',
      status: String(source.status || '').trim().toLowerCase() || 'running',
      outcome: String(source.outcome || '').trim().toLowerCase() || null,
      source: String(source.source || '').trim() || null,
      message: String(source.message || '').trim() || null,
      errorMessage: String(source.errorMessage || '').trim() || null,
      currentStep: String(source.currentStep || '').trim() || null,
      currentScriptName: String(source.currentScriptName || '').trim() || null,
      output: source.output != null ? String(source.output) : null,
      stdout: source.stdout != null ? String(source.stdout) : null,
      stderr: source.stderr != null ? String(source.stderr) : null,
      outputTruncated: Boolean(source.outputTruncated),
      stdoutTruncated: Boolean(source.stdoutTruncated),
      stderrTruncated: Boolean(source.stderrTruncated),
      exitCode: Number.isFinite(Number(source.exitCode)) ? Number(source.exitCode) : null,
      startedAt: source.startedAt || null,
      finishedAt: source.finishedAt || null,
      durationMs: Number.isFinite(Number(source.durationMs)) ? Number(source.durationMs) : null,
      jobId: Number.isFinite(Number(source.jobId)) && Number(source.jobId) > 0 ? Math.trunc(Number(source.jobId)) : null,
      cronJobId: Number.isFinite(Number(source.cronJobId)) && Number(source.cronJobId) > 0 ? Math.trunc(Number(source.cronJobId)) : null,
      canCancel: Boolean(source.canCancel),
      canNextStep: Boolean(source.canNextStep)
    };
  };
  const active = (Array.isArray(payload.active) ? payload.active : []).map(normalizeItem);
  const recent = (Array.isArray(payload.recent) ? payload.recent : []).map(normalizeItem);
  return {
    active,
    recent,
    updatedAt: payload.updatedAt || null
  };
}

function runtimeTypeLabel(type) {
  const normalized = String(type || '').trim().toLowerCase();
  if (normalized === 'script') return 'Skript';
  if (normalized === 'chain') return 'Kette';
  if (normalized === 'cron') return 'Cronjob';
  return normalized || 'Task';
}

function runtimeStatusMeta(status) {
  const normalized = String(status || '').trim().toLowerCase();
  if (normalized === 'running') return { label: 'Läuft', severity: 'warning' };
  if (normalized === 'success') return { label: 'Abgeschlossen', severity: 'success' };
  if (normalized === 'error') return { label: 'Fehler', severity: 'danger' };
  return { label: normalized || '-', severity: 'secondary' };
}

function runtimeOutcomeMeta(outcome, status) {
  const normalized = String(outcome || '').trim().toLowerCase();
  if (normalized === 'success') return { label: 'Erfolg', severity: 'success' };
  if (normalized === 'error') return { label: 'Fehler', severity: 'danger' };
  if (normalized === 'cancelled') return { label: 'Abgebrochen', severity: 'warning' };
  if (normalized === 'skipped') return { label: 'Übersprungen', severity: 'info' };
  return runtimeStatusMeta(status);
}

function hasRuntimeOutputDetails(item) {
  if (!item || typeof item !== 'object') {
    return false;
  }
  const hasRelevantExitCode = Number.isFinite(Number(item.exitCode)) && Number(item.exitCode) !== 0;
  return Boolean(
    String(item.errorMessage || '').trim()
    || String(item.output || '').trim()
    || String(item.stdout || '').trim()
    || String(item.stderr || '').trim()
    || hasRelevantExitCode
  );
}

function hasRuntimeLogContent(item) {
  if (!item || typeof item !== 'object') {
    return false;
  }
  return Boolean(
    String(item.output || '').trim()
    || String(item.stdout || '').trim()
    || String(item.stderr || '').trim()
  );
}

function RuntimeActivityDetails({
  item,
  summary,
  emptyLabel = 'Noch keine Log-Ausgabe vorhanden.'
}) {
  const hasLogs = hasRuntimeLogContent(item);
  const hasOutput = Boolean(String(item?.output || '').trim());
  const hasStdout = Boolean(String(item?.stdout || '').trim());
  const hasStderr = Boolean(String(item?.stderr || '').trim());

  return (
    <details className="runtime-activity-details">
      <summary>{summary}</summary>
      {!hasLogs ? <small>{emptyLabel}</small> : null}
      {hasOutput ? (
        <div className="runtime-activity-details-block">
          <small><strong>Ausgabe:</strong>{item?.outputTruncated ? ' (gekürzt)' : ''}</small>
          <pre>{item.output}</pre>
        </div>
      ) : null}
      {hasStdout ? (
        <div className="runtime-activity-details-block">
          <small><strong>stdout:</strong>{item?.stdoutTruncated ? ' (gekürzt)' : ''}</small>
          <pre>{item.stdout}</pre>
        </div>
      ) : null}
      {hasStderr ? (
        <div className="runtime-activity-details-block">
          <small><strong>stderr:</strong>{item?.stderrTruncated ? ' (gekürzt)' : ''}</small>
          <pre>{item.stderr}</pre>
        </div>
      ) : null}
    </details>
  );
}

function normalizeHardwareMonitoringPayload(rawPayload) {
  const payload = rawPayload && typeof rawPayload === 'object' ? rawPayload : {};
  return {
    enabled: Boolean(payload.enabled),
    intervalMs: Number(payload.intervalMs || 0),
    updatedAt: payload.updatedAt || null,
    sample: payload.sample && typeof payload.sample === 'object' ? payload.sample : null,
    error: payload.error ? String(payload.error) : null
  };
}

function getStorageUsageTone(usagePercent) {
  const value = Number(usagePercent);
  if (!Number.isFinite(value)) {
    return 'unknown';
  }
  if (value >= 95) {
    return 'critical';
  }
  if (value >= 85) {
    return 'high';
  }
  if (value >= 70) {
    return 'warn';
  }
  return 'ok';
}

function normalizeQueue(queue) {
  const payload = queue && typeof queue === 'object' ? queue : {};
  const runningJobs = Array.isArray(payload.runningJobs) ? payload.runningJobs : [];
  const queuedJobs = Array.isArray(payload.queuedJobs) ? payload.queuedJobs : [];
  return {
    maxParallelJobs: Number(payload.maxParallelJobs || 1),
    maxParallelCdEncodes: Number(payload.maxParallelCdEncodes || 2),
    maxTotalEncodes: Number(payload.maxTotalEncodes || 3),
    cdBypassesQueue: Boolean(payload.cdBypassesQueue),
    runningCount: Number(payload.runningCount || runningJobs.length || 0),
    runningCdCount: Number(payload.runningCdCount || 0),
    runningJobs,
    queuedJobs,
    queuedCount: Number(payload.queuedCount || queuedJobs.length || 0),
    updatedAt: payload.updatedAt || null
  };
}

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

function showQueuedToast(toastRef, actionLabel, result) {
  if (!toastRef?.current) {
    return;
  }
  const queuePosition = Number(result?.queuePosition || 0);
  const positionText = queuePosition > 0 ? `Position ${queuePosition}` : 'in der Warteschlange';
  toastRef.current.show({
    severity: 'info',
    summary: `${actionLabel} in Queue`,
    detail: `${actionLabel} wurde ${positionText} eingeplant.`,
    life: 3200
  });
}

function reorderQueuedItems(items, draggedEntryId, targetEntryId) {
  const list = Array.isArray(items) ? items : [];
  const from = list.findIndex((item) => Number(item?.entryId) === Number(draggedEntryId));
  const to = list.findIndex((item) => Number(item?.entryId) === Number(targetEntryId));
  if (from < 0 || to < 0 || from === to) {
    return list;
  }
  const next = [...list];
  const [moved] = next.splice(from, 1);
  next.splice(to, 0, moved);
  return next.map((item, index) => ({
    ...item,
    position: index + 1
  }));
}

function queueEntryIcon(type) {
  if (type === 'script') return 'pi pi-code';
  if (type === 'chain') return 'pi pi-link';
  if (type === 'wait') return 'pi pi-clock';
  return 'pi pi-box';
}

function queueEntryLabel(item) {
  if (item.type === 'script') return `Skript: ${item.title}`;
  if (item.type === 'chain') return `Kette: ${item.title}`;
  if (item.type === 'wait') return `Warten: ${item.waitSeconds}s`;
  return item.title || `Job #${item.jobId}`;
}

function normalizeQueueNameList(values) {
  const list = Array.isArray(values) ? values : [];
  const seen = new Set();
  const output = [];
  for (const item of list) {
    const name = String(item || '').trim();
    if (!name) {
      continue;
    }
    const key = name.toLowerCase();
    if (seen.has(key)) {
      continue;
    }
    seen.add(key);
    output.push(name);
  }
  return output;
}

function normalizeQueueScriptSummary(item) {
  const source = item?.scriptSummary && typeof item.scriptSummary === 'object' ? item.scriptSummary : {};
  return {
    preScripts: normalizeQueueNameList(source.preScripts),
    postScripts: normalizeQueueNameList(source.postScripts),
    preChains: normalizeQueueNameList(source.preChains),
    postChains: normalizeQueueNameList(source.postChains)
  };
}

function hasQueueScriptSummary(item) {
  const summary = normalizeQueueScriptSummary(item);
  return summary.preScripts.length > 0
    || summary.postScripts.length > 0
    || summary.preChains.length > 0
    || summary.postChains.length > 0;
}

function QueueJobScriptSummary({ item }) {
  const summary = normalizeQueueScriptSummary(item);
  const groups = [
    { key: 'pre-scripts', icon: 'pi pi-code', label: 'Pre-Encode Skripte', values: summary.preScripts },
    { key: 'post-scripts', icon: 'pi pi-code', label: 'Post-Encode Skripte', values: summary.postScripts },
    { key: 'pre-chains', icon: 'pi pi-link', label: 'Pre-Encode Ketten', values: summary.preChains },
    { key: 'post-chains', icon: 'pi pi-link', label: 'Post-Encode Ketten', values: summary.postChains }
  ].filter((group) => group.values.length > 0);

  if (groups.length === 0) {
    return null;
  }

  return (
    <div className="queue-job-script-details">
      {groups.map((group) => {
        const text = group.values.join(' | ');
        return (
          <div key={group.key} className="queue-job-script-group">
            <strong><i className={group.icon} /> {group.label}</strong>
            <small title={text}>{text}</small>
          </div>
        );
      })}
    </div>
  );
}

function getAnalyzeContext(job) {
  return job?.makemkvInfo?.analyzeContext && typeof job.makemkvInfo.analyzeContext === 'object'
    ? job.makemkvInfo.analyzeContext
    : {};
}

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

function mediaIndicatorMeta(job) {
  const mediaType = resolveMediaType(job);
  if (mediaType === 'bluray') {
    return { mediaType, src: blurayIndicatorIcon, alt: 'Blu-ray', title: 'Blu-ray' };
  }
  if (mediaType === 'dvd') {
    return { mediaType, src: discIndicatorIcon, alt: 'DVD', title: 'DVD' };
  }
  if (mediaType === 'cd') {
    return { mediaType, src: otherIndicatorIcon, alt: 'Audio CD', title: 'Audio CD' };
  }
  if (mediaType === 'audiobook') {
    return { mediaType, src: audiobookIndicatorIcon, alt: 'Audiobook', title: 'Audiobook' };
  }
  return { mediaType, src: otherIndicatorIcon, alt: 'Sonstiges Medium', title: 'Sonstiges Medium' };
}

function JobStepChecks({ backupSuccess, encodeSuccess }) {
  const hasAny = Boolean(backupSuccess || encodeSuccess);
  if (!hasAny) {
    return null;
  }
  return (
    <div className="job-step-checks">
      {backupSuccess ? (
        <span className="job-step-inline-ok" title="Backup/Rip erfolgreich">
          <i className="pi pi-check-circle" aria-hidden="true" />
          <span>Backup</span>
        </span>
      ) : null}
      {encodeSuccess ? (
        <span className="job-step-inline-ok" title="Encode erfolgreich">
          <i className="pi pi-check-circle" aria-hidden="true" />
          <span>Encode</span>
        </span>
      ) : null}
    </div>
  );
}

function buildPipelineFromJob(job, currentPipeline, currentPipelineJobId) {
  const jobId = normalizeJobId(job?.id);
  const isCurrentSessionJob = Boolean(
    jobId
    && currentPipelineJobId
    && jobId === currentPipelineJobId
    && String(currentPipeline?.state || '').trim().toUpperCase() !== 'IDLE'
  );

  const encodePlan = job?.encodePlan && typeof job.encodePlan === 'object' ? job.encodePlan : null;
  const makemkvInfo = job?.makemkvInfo && typeof job.makemkvInfo === 'object' ? job.makemkvInfo : {};
  const resolvedMediaType = resolveMediaType(job);
  const analyzeContext = getAnalyzeContext(job);
  const normalizePlanIdList = (values) => {
    const list = Array.isArray(values) ? values : [];
    const seen = new Set();
    const output = [];
    for (const value of list) {
      const parsed = Number(value);
      if (!Number.isFinite(parsed) || parsed <= 0) {
        continue;
      }
      const id = Math.trunc(parsed);
      const key = String(id);
      if (seen.has(key)) {
        continue;
      }
      seen.add(key);
      output.push(id);
    }
    return output;
  };
  const buildNamedSelection = (ids, entries, fallbackLabel) => {
    const source = Array.isArray(entries) ? entries : [];
    const namesById = new Map(
      source
        .map((entry) => {
          const id = Number(entry?.id ?? entry?.scriptId ?? entry?.chainId);
          const normalized = Number.isFinite(id) && id > 0 ? Math.trunc(id) : null;
          const name = String(entry?.name || entry?.scriptName || entry?.chainName || '').trim();
          if (!normalized) {
            return null;
          }
          return [normalized, name || null];
        })
        .filter(Boolean)
    );
    return ids.map((id) => ({
      id,
      name: namesById.get(id) || `${fallbackLabel} #${id}`
    }));
  };
  const planPreScriptIds = normalizePlanIdList([
    ...(Array.isArray(encodePlan?.preEncodeScriptIds) ? encodePlan.preEncodeScriptIds : []),
    ...(Array.isArray(encodePlan?.preEncodeScripts) ? encodePlan.preEncodeScripts.map((entry) => entry?.id ?? entry?.scriptId) : [])
  ]);
  const planPostScriptIds = normalizePlanIdList([
    ...(Array.isArray(encodePlan?.postEncodeScriptIds) ? encodePlan.postEncodeScriptIds : []),
    ...(Array.isArray(encodePlan?.postEncodeScripts) ? encodePlan.postEncodeScripts.map((entry) => entry?.id ?? entry?.scriptId) : [])
  ]);
  const planPreChainIds = normalizePlanIdList([
    ...(Array.isArray(encodePlan?.preEncodeChainIds) ? encodePlan.preEncodeChainIds : []),
    ...(Array.isArray(encodePlan?.preEncodeChains) ? encodePlan.preEncodeChains.map((entry) => entry?.id ?? entry?.chainId) : [])
  ]);
  const planPostChainIds = normalizePlanIdList([
    ...(Array.isArray(encodePlan?.postEncodeChainIds) ? encodePlan.postEncodeChainIds : []),
    ...(Array.isArray(encodePlan?.postEncodeChains) ? encodePlan.postEncodeChains.map((entry) => entry?.id ?? entry?.chainId) : [])
  ]);
  const cdRipConfig = encodePlan && typeof encodePlan === 'object'
    ? {
      format: String(encodePlan?.format || '').trim().toLowerCase() || null,
      formatOptions: encodePlan?.formatOptions && typeof encodePlan.formatOptions === 'object'
        ? encodePlan.formatOptions
        : {},
      selectedTracks: Array.isArray(encodePlan?.selectedTracks)
        ? encodePlan.selectedTracks
          .map((value) => Number(value))
          .filter((value) => Number.isFinite(value) && value > 0)
          .map((value) => Math.trunc(value))
        : [],
      preEncodeScriptIds: planPreScriptIds,
      postEncodeScriptIds: planPostScriptIds,
      preEncodeChainIds: planPreChainIds,
      postEncodeChainIds: planPostChainIds,
      preEncodeScripts: buildNamedSelection(planPreScriptIds, encodePlan?.preEncodeScripts, 'Skript'),
      postEncodeScripts: buildNamedSelection(planPostScriptIds, encodePlan?.postEncodeScripts, 'Skript'),
      preEncodeChains: buildNamedSelection(planPreChainIds, encodePlan?.preEncodeChains, 'Kette'),
      postEncodeChains: buildNamedSelection(planPostChainIds, encodePlan?.postEncodeChains, 'Kette'),
      outputTemplate: String(encodePlan?.outputTemplate || '').trim() || null
    }
    : null;
  const cdTracks = Array.isArray(makemkvInfo?.tracks)
    ? makemkvInfo.tracks
      .map((track) => {
        const position = Number(track?.position);
        if (!Number.isFinite(position) || position <= 0) {
          return null;
        }
        return {
          ...track,
          position: Math.trunc(position),
          selected: track?.selected !== false
        };
      })
      .filter(Boolean)
    : [];
  const cdSelectedMeta = makemkvInfo?.selectedMetadata && typeof makemkvInfo.selectedMetadata === 'object'
    ? makemkvInfo.selectedMetadata
    : {};
  const fallbackCdArtist = cdTracks
    .map((track) => String(track?.artist || '').trim())
    .find(Boolean) || null;
  const resolvedCdMbId = String(
    cdSelectedMeta?.mbId
    || cdSelectedMeta?.musicBrainzId
    || cdSelectedMeta?.musicbrainzId
    || cdSelectedMeta?.mbid
    || ''
  ).trim() || null;
  const resolvedCdCoverUrl = String(
    cdSelectedMeta?.coverUrl
    || cdSelectedMeta?.poster
    || cdSelectedMeta?.posterUrl
    || job?.poster_url
    || ''
  ).trim() || null;
  const cdparanoiaCmd = String(makemkvInfo?.cdparanoiaCmd || 'cdparanoia').trim() || 'cdparanoia';
  const devicePath = String(job?.disc_device || '').trim() || null;
  const firstConfiguredTrack = Array.isArray(encodePlan?.selectedTracks) && encodePlan.selectedTracks.length > 0
    ? Number(encodePlan.selectedTracks[0])
    : null;
  const fallbackTrack = cdTracks[0]?.position ? Number(cdTracks[0].position) : null;
  const previewTrackPos = Number.isFinite(firstConfiguredTrack) && firstConfiguredTrack > 0
    ? Math.trunc(firstConfiguredTrack)
    : (Number.isFinite(fallbackTrack) && fallbackTrack > 0 ? Math.trunc(fallbackTrack) : null);
  const previewWavPath = previewTrackPos && job?.raw_path
    ? `${job.raw_path}/track${String(previewTrackPos).padStart(2, '0')}.cdda.wav`
    : '<temp>/trackNN.cdda.wav';
  const cdparanoiaCommandPreview = `${cdparanoiaCmd} -d ${devicePath || '<device>'} ${previewTrackPos || '<trackNr>'} ${previewWavPath}`;
  const audiobookSelectedMeta = {
    ...(makemkvInfo?.selectedMetadata && typeof makemkvInfo.selectedMetadata === 'object'
      ? makemkvInfo.selectedMetadata
      : {}),
    ...(encodePlan?.metadata && typeof encodePlan.metadata === 'object'
      ? encodePlan.metadata
      : {})
  };
  const selectedMetadata = resolvedMediaType === 'audiobook'
    ? {
      title: audiobookSelectedMeta?.title || job?.title || job?.detected_title || null,
      author: audiobookSelectedMeta?.author || audiobookSelectedMeta?.artist || null,
      narrator: audiobookSelectedMeta?.narrator || null,
      description: audiobookSelectedMeta?.description || null,
      series: audiobookSelectedMeta?.series || null,
      part: audiobookSelectedMeta?.part || null,
      year: audiobookSelectedMeta?.year ?? job?.year ?? null,
      chapters: Array.isArray(audiobookSelectedMeta?.chapters) ? audiobookSelectedMeta.chapters : [],
      durationMs: audiobookSelectedMeta?.durationMs || 0,
      poster: audiobookSelectedMeta?.poster || job?.poster_url || null
    }
    : {
      title: cdSelectedMeta?.title || job?.title || job?.detected_title || null,
      artist: cdSelectedMeta?.artist || fallbackCdArtist || null,
      year: cdSelectedMeta?.year ?? job?.year ?? null,
      mbId: resolvedCdMbId,
      coverUrl: resolvedCdCoverUrl,
      imdbId: job?.imdb_id || null,
      poster: job?.poster_url || resolvedCdCoverUrl || null
    };
  const mode = String(encodePlan?.mode || 'rip').trim().toLowerCase();
  const isPreRip = mode === 'pre_rip' || Boolean(encodePlan?.preRip);
  const inputPath = isPreRip
    ? null
    : (job?.encode_input_path || encodePlan?.encodeInputPath || null);
  const reviewConfirmed = Boolean(Number(job?.encode_review_confirmed || 0) || encodePlan?.reviewConfirmed);
  const hasEncodableTitle = isPreRip
    ? Boolean(encodePlan?.encodeInputTitleId)
    : Boolean(inputPath || job?.raw_path);
  const jobStatus = String(job?.status || job?.last_state || 'IDLE').trim().toUpperCase() || 'IDLE';
  const lastState = String(job?.last_state || '').trim().toUpperCase();
  const errorText = String(job?.error_message || '').trim().toUpperCase();
  const hasOutputPath = Boolean(String(job?.output_path || '').trim());
  const hasEncodePlan = Boolean(encodePlan && Array.isArray(encodePlan?.titles) && encodePlan.titles.length > 0);
  const looksLikeCancelledEncode = (jobStatus === 'ERROR' || jobStatus === 'CANCELLED') && (
    (errorText.includes('ABGEBROCHEN') || errorText.includes('CANCELLED'))
    && (hasOutputPath || Boolean(job?.encode_input_path) || Boolean(job?.handbrakeInfo))
  );
  const looksLikeEncodingError = (jobStatus === 'ERROR' || jobStatus === 'CANCELLED') && (
    errorText.includes('ENCODING')
    || errorText.includes('HANDBRAKE')
    || lastState === 'ENCODING'
    || Boolean(job?.handbrakeInfo)
    || looksLikeCancelledEncode
  );
  const canRestartEncodeFromLastSettings = Boolean(
    hasEncodePlan
    && reviewConfirmed
    && hasEncodableTitle
    && (
      jobStatus === 'READY_TO_ENCODE'
      || jobStatus === 'ENCODING'
      || jobStatus === 'CANCELLED'
      || looksLikeEncodingError
    )
  );
  const canRestartReviewFromRaw = Boolean(
    job?.raw_path
    && !processingStates.includes(jobStatus)
    && resolvedMediaType !== 'audiobook'
  );
  const computedContext = {
    jobId,
    rawPath: job?.raw_path || null,
    outputPath: job?.output_path || null,
    detectedTitle: job?.detected_title || null,
    mediaProfile: resolvedMediaType,
    lastState,
    devicePath,
    cdparanoiaCmd,
    cdparanoiaCommandPreview,
    cdRipConfig,
    tracks: cdTracks,
    inputPath,
    hasEncodableTitle,
    reviewConfirmed,
    mode,
    sourceJobId: encodePlan?.sourceJobId || null,
    selectedMetadata,
    audiobookConfig: resolvedMediaType === 'audiobook'
      ? {
        format: String(encodePlan?.format || '').trim().toLowerCase() || 'mp3',
        formatOptions: encodePlan?.formatOptions && typeof encodePlan.formatOptions === 'object'
          ? encodePlan.formatOptions
          : {}
      }
      : null,
    mediaInfoReview: encodePlan,
    playlistAnalysis: analyzeContext.playlistAnalysis || null,
    playlistDecisionRequired: Boolean(analyzeContext.playlistDecisionRequired),
    playlistCandidates: Array.isArray(analyzeContext?.playlistAnalysis?.evaluatedCandidates)
      ? analyzeContext.playlistAnalysis.evaluatedCandidates
      : [],
    selectedPlaylist: analyzeContext.selectedPlaylist || null,
    selectedTitleId: analyzeContext.selectedTitleId ?? null,
    omdbCandidates: [],
    canRestartEncodeFromLastSettings,
    canRestartReviewFromRaw
  };

  if (isCurrentSessionJob) {
    const existingContext = currentPipeline?.context && typeof currentPipeline.context === 'object'
      ? currentPipeline.context
      : {};
    return {
      ...currentPipeline,
      context: {
        ...computedContext,
        ...existingContext,
        rawPath: existingContext.rawPath || computedContext.rawPath,
        outputPath: existingContext.outputPath || computedContext.outputPath,
        tracks: (Array.isArray(existingContext.tracks) && existingContext.tracks.length > 0)
          ? existingContext.tracks
          : computedContext.tracks,
        selectedMetadata: existingContext.selectedMetadata || computedContext.selectedMetadata,
        canRestartEncodeFromLastSettings:
          existingContext.canRestartEncodeFromLastSettings ?? computedContext.canRestartEncodeFromLastSettings,
        canRestartReviewFromRaw:
          existingContext.canRestartReviewFromRaw ?? computedContext.canRestartReviewFromRaw
      }
    };
  }

  // Use live per-job progress from the backend if available (concurrent jobs).
  const liveJobProgress = currentPipeline?.jobProgress && jobId
    ? (currentPipeline.jobProgress[jobId] || null)
    : null;
  const liveContext = liveJobProgress?.context && typeof liveJobProgress.context === 'object'
    ? liveJobProgress.context
    : null;
  const mergedContext = liveContext
    ? {
      ...computedContext,
      ...liveContext,
      tracks: (Array.isArray(liveContext.tracks) && liveContext.tracks.length > 0)
        ? liveContext.tracks
        : computedContext.tracks,
      selectedMetadata: liveContext.selectedMetadata || computedContext.selectedMetadata,
      cdRipConfig: liveContext.cdRipConfig || computedContext.cdRipConfig
    }
    : computedContext;

  return {
    state: liveJobProgress?.state || jobStatus,
    activeJobId: jobId,
    progress: liveJobProgress != null ? Number(liveJobProgress.progress ?? 0) : 0,
    eta: liveJobProgress?.eta || null,
    statusText: liveJobProgress?.statusText || job?.error_message || null,
    context: mergedContext
  };
}

export default function DashboardPage({
  pipeline,
  hardwareMonitoring,
  lastDiscEvent,
  refreshPipeline,
  audiobookUpload,
  onAudiobookUpload,
  jobsRefreshToken,
  pendingExpandedJobId,
  onPendingExpandedJobHandled
}) {
  const navigate = useNavigate();
  const [busy, setBusy] = useState(false);
  const [busyJobIds, setBusyJobIds] = useState(() => new Set());
  const setJobBusy = (jobId, isBusy) => {
    setBusyJobIds((prev) => {
      const next = new Set(prev);
      if (isBusy) {
        next.add(jobId);
      } else {
        next.delete(jobId);
      }
      return next;
    });
  };
  const [metadataDialogVisible, setMetadataDialogVisible] = useState(false);
  const [metadataDialogContext, setMetadataDialogContext] = useState(null);
  const [metadataDialogReassignMode, setMetadataDialogReassignMode] = useState(false);
  const [duplicateJobDialog, setDuplicateJobDialog] = useState({ visible: false, existingJob: null, pendingPayload: null });
  const [cdMetadataDialogVisible, setCdMetadataDialogVisible] = useState(false);
  const [cdMetadataDialogContext, setCdMetadataDialogContext] = useState(null);
  const [cdRipPanelJobId, setCdRipPanelJobId] = useState(null);
  const [cancelCleanupDialog, setCancelCleanupDialog] = useState({
    visible: false,
    jobId: null,
    target: null,
    path: null
  });
  const [cancelCleanupBusy, setCancelCleanupBusy] = useState(false);
  const [queueState, setQueueState] = useState(() => normalizeQueue(pipeline?.queue));
  const [queueReorderBusy, setQueueReorderBusy] = useState(false);
  const [draggingQueueEntryId, setDraggingQueueEntryId] = useState(null);
  const [insertQueueDialog, setInsertQueueDialog] = useState({ visible: false, afterEntryId: null });
  const [liveJobLog, setLiveJobLog] = useState('');
  const [runtimeActivities, setRuntimeActivities] = useState(() => normalizeRuntimeActivitiesPayload(null));
  const [runtimeLoading, setRuntimeLoading] = useState(false);
  const [runtimeActionBusyKeys, setRuntimeActionBusyKeys] = useState(() => new Set());
  const [runtimeRecentClearing, setRuntimeRecentClearing] = useState(false);
  const [jobsLoading, setJobsLoading] = useState(false);
  const [dashboardJobs, setDashboardJobs] = useState([]);
  const [expandedJobId, setExpandedJobId] = useState(undefined);
  const [audiobookUploadFile, setAudiobookUploadFile] = useState(null);
  const [activationBytesDialog, setActivationBytesDialog] = useState({ visible: false, checksum: null, jobId: null });
  const [activationBytesInput, setActivationBytesInput] = useState('');
  const [activationBytesBusy, setActivationBytesBusy] = useState(false);
  const [pendingActivationJobIds, setPendingActivationJobIds] = useState(() => new Set());
  const [cpuCoresExpanded, setCpuCoresExpanded] = useState(false);
  const [expandedQueueScriptKeys, setExpandedQueueScriptKeys] = useState(() => new Set());
  const [queueCatalog, setQueueCatalog] = useState({ scripts: [], chains: [] });
  const [insertWaitSeconds, setInsertWaitSeconds] = useState(30);
  const toastRef = useRef(null);
  const audiobookFileUploadRef = useRef(null);
  const [audiobookUploadStatusVisible, setAudiobookUploadStatusVisible] = useState(false);

  const state = String(pipeline?.state || 'IDLE').trim().toUpperCase();
  const currentPipelineJobId = normalizeJobId(pipeline?.activeJobId || pipeline?.context?.jobId);
  const isProcessing = processingStates.includes(state);
  const monitoringState = useMemo(
    () => normalizeHardwareMonitoringPayload(hardwareMonitoring),
    [hardwareMonitoring]
  );
  const monitoringSample = monitoringState.sample;
  const cpuMetrics = monitoringSample?.cpu || null;
  const memoryMetrics = monitoringSample?.memory || null;
  const gpuMetrics = monitoringSample?.gpu || null;
  const storageMetrics = Array.isArray(monitoringSample?.storage) ? monitoringSample.storage : [];
  const storageGroups = useMemo(() => {
    // Phase 1: group by mountPoint
    const phase1 = [];
    const mountMap = new Map();
    for (const entry of storageMetrics) {
      const groupKey = entry?.mountPoint || `__no_mount_${entry?.key}`;
      if (!mountMap.has(groupKey)) {
        const group = { mountPoint: entry?.mountPoint || null, entries: [], representative: entry };
        mountMap.set(groupKey, group);
        phase1.push(group);
      }
      mountMap.get(groupKey).entries.push(entry);
    }

    // Phase 2: merge groups with identical disk signature (same physical disk)
    const merged = [];
    const diskSigMap = new Map();
    for (const group of phase1) {
      const totalBytes = Number(group.representative?.totalBytes);
      const freeBytes = Number(group.representative?.freeBytes);
      if (Number.isFinite(totalBytes) && totalBytes > 0 && Number.isFinite(freeBytes)) {
        const sig = `${totalBytes}:${freeBytes}`;
        if (diskSigMap.has(sig)) {
          diskSigMap.get(sig).entries.push(...group.entries);
        } else {
          diskSigMap.set(sig, group);
          merged.push(group);
        }
      } else {
        merged.push(group);
      }
    }
    return merged;
  }, [storageMetrics]);
  const cpuPerCoreMetrics = Array.isArray(cpuMetrics?.perCore) ? cpuMetrics.perCore : [];
  const gpuDevices = Array.isArray(gpuMetrics?.devices) ? gpuMetrics.devices : [];
  const audiobookUploadPhase = String(audiobookUpload?.phase || 'idle').trim().toLowerCase();
  const audiobookUploadBusy = audiobookUploadPhase === 'uploading' || audiobookUploadPhase === 'processing';
  const audiobookUploadProgress = Number.isFinite(Number(audiobookUpload?.progressPercent))
    ? Math.max(0, Math.min(100, Number(audiobookUpload.progressPercent)))
    : 0;
  const audiobookUploadLoadedBytes = Number(audiobookUpload?.loadedBytes || 0);
  const audiobookUploadTotalBytes = Number(audiobookUpload?.totalBytes || 0);
  const audiobookUploadFileName = String(audiobookUpload?.fileName || '').trim()
    || String(audiobookUploadFile?.name || '').trim()
    || null;
  const audiobookUploadStatusTone = audiobookUploadPhase === 'error'
    ? 'danger'
    : audiobookUploadPhase === 'completed'
      ? 'success'
      : audiobookUploadPhase === 'processing'
        ? 'info'
        : audiobookUploadPhase === 'uploading'
          ? 'warning'
          : 'secondary';
  const audiobookUploadStatusLabel = audiobookUploadPhase === 'uploading'
    ? 'Upload läuft'
    : audiobookUploadPhase === 'processing'
      ? 'Server verarbeitet'
      : audiobookUploadPhase === 'completed'
        ? 'Bereit'
        : audiobookUploadPhase === 'error'
          ? 'Fehler'
          : 'Inaktiv';

  const loadDashboardJobs = async () => {
    setJobsLoading(true);
    try {
      const [jobsResponse, queueResponse] = await Promise.allSettled([
        api.getJobs({
          statuses: Array.from(dashboardStatuses),
          limit: 160,
          lite: true
        }),
        api.getPipelineQueue()
      ]);
      const allJobs = jobsResponse.status === 'fulfilled'
        ? (Array.isArray(jobsResponse.value?.jobs) ? jobsResponse.value.jobs : [])
        : [];
      if (queueResponse.status === 'fulfilled') {
        setQueueState(normalizeQueue(queueResponse.value?.queue));
      }
      const next = allJobs
        .filter((job) => dashboardStatuses.has(String(job?.status || '').trim().toUpperCase()))
        .sort((a, b) => Number(b?.id || 0) - Number(a?.id || 0));

      if (currentPipelineJobId && !next.some((job) => normalizeJobId(job?.id) === currentPipelineJobId)) {
        try {
          const active = await api.getJob(currentPipelineJobId, { lite: true });
          if (active?.job) {
            next.unshift(active.job);
          }
        } catch (_error) {
          // ignore; dashboard still shows available rows
        }
      }

      const seen = new Set();
      const deduped = [];
      for (const job of next) {
        const id = normalizeJobId(job?.id);
        if (!id || seen.has(String(id))) {
          continue;
        }
        seen.add(String(id));
        deduped.push(job);
      }

      setDashboardJobs(deduped);

      // Prüfen ob Audiobook-Jobs auf Activation Bytes warten
      try {
        const { pending } = await api.getPendingActivation();
        if (Array.isArray(pending) && pending.length > 0) {
          setPendingActivationJobIds(new Set(pending.map((p) => p.jobId)));
          // Modal automatisch öffnen wenn noch nicht sichtbar
          setActivationBytesDialog((prev) => {
            if (prev.visible) return prev;
            const first = pending[0];
            setActivationBytesInput('');
            return { visible: true, checksum: first.checksum, jobId: first.jobId };
          });
        } else {
          setPendingActivationJobIds(new Set());
        }
      } catch (_err) {
        // ignorieren
      }
    } catch (_error) {
      setDashboardJobs([]);
    } finally {
      setJobsLoading(false);
    }
  };

  useEffect(() => {
    if (!metadataDialogVisible) {
      return;
    }
    if (metadataDialogContext?.jobId) {
      return;
    }
    if (pipeline?.state !== 'METADATA_SELECTION' && pipeline?.state !== 'WAITING_FOR_USER_DECISION') {
      setMetadataDialogVisible(false);
    }
  }, [pipeline?.state, metadataDialogVisible, metadataDialogContext?.jobId]);

  // Auto-open CD metadata dialog when pipeline enters CD_METADATA_SELECTION
  useEffect(() => {
    const currentState = String(pipeline?.state || '').trim().toUpperCase();
    if (currentState === 'CD_METADATA_SELECTION') {
      const ctx = pipeline?.context && typeof pipeline.context === 'object' ? pipeline.context : null;
      if (ctx?.jobId && !cdMetadataDialogVisible) {
        setCdMetadataDialogContext(ctx);
        setCdMetadataDialogVisible(true);
      }
    }
    if (currentState === 'CD_READY_TO_RIP') {
      const ctx = pipeline?.context && typeof pipeline.context === 'object' ? pipeline.context : null;
      if (ctx?.jobId) {
        setCdRipPanelJobId(ctx.jobId);
      }
    }
  }, [pipeline?.state, pipeline?.context?.jobId]);

  useEffect(() => {
    setQueueState(normalizeQueue(pipeline?.queue));
  }, [pipeline?.queue]);

  useEffect(() => {
    void loadDashboardJobs();
  }, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, jobsRefreshToken]);

  useEffect(() => {
    const requestedJobId = normalizeJobId(pendingExpandedJobId);
    if (!requestedJobId) {
      return;
    }
    const hasRequestedJob = dashboardJobs.some((job) => normalizeJobId(job?.id) === requestedJobId);
    if (!hasRequestedJob) {
      return;
    }
    setExpandedJobId(requestedJobId);
    onPendingExpandedJobHandled?.(requestedJobId);
  }, [pendingExpandedJobId, dashboardJobs, onPendingExpandedJobHandled]);

  useEffect(() => {
    let cancelled = false;
    const load = async (silent = false) => {
      try {
        const response = await api.getRuntimeActivities();
        if (!cancelled) {
          setRuntimeActivities(normalizeRuntimeActivitiesPayload(response));
          if (!silent) {
            setRuntimeLoading(false);
          }
        }
      } catch (_error) {
        if (!cancelled && !silent) {
          setRuntimeActivities(normalizeRuntimeActivitiesPayload(null));
          setRuntimeLoading(false);
        }
      }
    };
    setRuntimeLoading(true);
    void load(false);
    const interval = setInterval(() => {
      void load(true);
    }, 10000);
    return () => {
      cancelled = true;
      clearInterval(interval);
    };
  }, []);

  useWebSocket({
    onMessage: (message) => {
      if (message?.type !== 'RUNTIME_ACTIVITY_CHANGED') {
        return;
      }
      setRuntimeActivities(normalizeRuntimeActivitiesPayload(message.payload));
      setRuntimeLoading(false);
    }
  });

  useEffect(() => {
    const normalizedExpanded = normalizeJobId(expandedJobId);
    const hasExpanded = dashboardJobs.some((job) => normalizeJobId(job?.id) === normalizedExpanded);
    if (hasExpanded) {
      return;
    }

    // Respect explicit user collapse.
    if (expandedJobId === null) {
      return;
    }

    if (currentPipelineJobId && dashboardJobs.some((job) => normalizeJobId(job?.id) === currentPipelineJobId)) {
      setExpandedJobId(currentPipelineJobId);
      return;
    }
    setExpandedJobId(normalizeJobId(dashboardJobs[0]?.id));
  }, [dashboardJobs, expandedJobId, currentPipelineJobId]);

  useEffect(() => {
    if (audiobookUploadPhase === 'idle') {
      setAudiobookUploadStatusVisible(false);
      return undefined;
    }
    setAudiobookUploadStatusVisible(true);
    if (audiobookUploadPhase === 'completed') {
      const timer = setTimeout(() => setAudiobookUploadStatusVisible(false), 5000);
      return () => clearTimeout(timer);
    }
    return undefined;
  }, [audiobookUploadPhase]);

  useEffect(() => {
    if (!currentPipelineJobId || !isProcessing) {
      setLiveJobLog('');
      return undefined;
    }

    let cancelled = false;
    const refreshLiveLog = async () => {
      try {
        const response = await api.getJob(currentPipelineJobId, { includeLiveLog: true, lite: true });
        if (!cancelled) {
          setLiveJobLog(response?.job?.log || '');
        }
      } catch (_error) {
        // ignore transient polling errors to avoid noisy toasts while background polling
      }
    };

    void refreshLiveLog();
    const interval = setInterval(refreshLiveLog, 2500);
    return () => {
      cancelled = true;
      clearInterval(interval);
    };
  }, [currentPipelineJobId, isProcessing]);

  const pipelineByJobId = useMemo(() => {
    const map = new Map();
    for (const job of dashboardJobs) {
      const id = normalizeJobId(job?.id);
      if (!id) {
        continue;
      }
      map.set(id, buildPipelineFromJob(job, pipeline, currentPipelineJobId));
    }
    return map;
  }, [dashboardJobs, pipeline, currentPipelineJobId]);

  const buildMetadataContextForJob = (jobId) => {
    const normalizedJobId = normalizeJobId(jobId);
    if (!normalizedJobId) {
      return null;
    }
    const job = dashboardJobs.find((item) => normalizeJobId(item?.id) === normalizedJobId) || null;
    const pipelineForJob = pipelineByJobId.get(normalizedJobId) || null;
    const context = pipelineForJob?.context && typeof pipelineForJob.context === 'object'
      ? pipelineForJob.context
      : {};
    const selectedMetadata = context.selectedMetadata && typeof context.selectedMetadata === 'object'
      ? context.selectedMetadata
      : {
        title: job?.title || job?.detected_title || context?.detectedTitle || '',
        year: job?.year || null,
        imdbId: job?.imdb_id || null,
        poster: job?.poster_url || null
      };
    return {
      ...context,
      jobId: normalizedJobId,
      detectedTitle: context?.detectedTitle || job?.detected_title || selectedMetadata?.title || '',
      selectedMetadata,
      omdbCandidates: Array.isArray(context?.omdbCandidates) ? context.omdbCandidates : []
    };
  };

  const defaultMetadataDialogContext = useMemo(() => {
    const currentState = String(pipeline?.state || '').trim().toUpperCase();
    const currentContext = pipeline?.context && typeof pipeline.context === 'object'
      ? pipeline.context
      : null;
    const currentContextJobId = normalizeJobId(currentContext?.jobId);
    if (
      (currentState === 'METADATA_SELECTION' || currentState === 'WAITING_FOR_USER_DECISION')
      && currentContextJobId
    ) {
      return {
        ...currentContext,
        jobId: currentContextJobId,
        selectedMetadata: currentContext?.selectedMetadata || {
          title: currentContext?.detectedTitle || '',
          year: null,
          imdbId: null,
          poster: null
        },
        omdbCandidates: Array.isArray(currentContext?.omdbCandidates) ? currentContext.omdbCandidates : []
      };
    }

    const pendingJob = dashboardJobs.find((job) => {
      const normalized = normalizeStatus(job?.status);
      return normalized === 'METADATA_SELECTION' || normalized === 'WAITING_FOR_USER_DECISION';
    });
    if (!pendingJob) {
      return null;
    }
    return buildMetadataContextForJob(pendingJob.id);
  }, [pipeline, dashboardJobs, pipelineByJobId]);

  const effectiveMetadataDialogContext = metadataDialogContext
    || defaultMetadataDialogContext
    || pipeline?.context
    || {};

  const showError = (error) => {
    toastRef.current?.show({
      severity: 'error',
      summary: 'Fehler',
      detail: error.message,
      life: 4500
    });
  };

  const handleOpenMetadataDialog = (jobId = null) => {
    const context = jobId ? buildMetadataContextForJob(jobId) : defaultMetadataDialogContext;
    if (!context?.jobId) {
      showError(new Error('Kein Job mit offener Metadaten-Auswahl gefunden.'));
      return;
    }
    setMetadataDialogReassignMode(false);
    setMetadataDialogContext(context);
    setMetadataDialogVisible(true);
  };

  const handleOpenReassignOmdbDialog = (jobId) => {
    const context = buildMetadataContextForJob(jobId);
    if (!context?.jobId) {
      showError(new Error('Job nicht gefunden.'));
      return;
    }
    setMetadataDialogReassignMode(true);
    setMetadataDialogContext(context);
    setMetadataDialogVisible(true);
  };

  const handleAnalyze = async () => {
    setBusy(true);
    try {
      const response = await api.analyzeDisc();
      await refreshPipeline();
      await loadDashboardJobs();
      const analyzedJobId = normalizeJobId(response?.result?.jobId);
      if (analyzedJobId && state === 'ENCODING') {
        setMetadataDialogContext({
          jobId: analyzedJobId,
          detectedTitle: response?.result?.detectedTitle || '',
          selectedMetadata: {
            title: response?.result?.detectedTitle || '',
            year: null,
            imdbId: null,
            poster: null
          },
          omdbCandidates: Array.isArray(response?.result?.omdbCandidates)
            ? response.result.omdbCandidates
            : []
        });
        setMetadataDialogVisible(true);
      }
    } catch (error) {
      showError(error);
    } finally {
      setBusy(false);
    }
  };

  const handleReanalyze = async () => {
    await handleAnalyze();
  };

  const handleRescan = async () => {
    setBusy(true);
    try {
      const response = await api.rescanDisc();
      const emitted = response?.result?.emitted || 'none';
      toastRef.current?.show({
        severity: emitted === 'discInserted' ? 'success' : 'info',
        summary: 'Laufwerk neu gelesen',
        detail:
          emitted === 'discInserted'
            ? 'Disk-Event wurde neu ausgelöst.'
            : 'Kein Medium erkannt.',
        life: 2800
      });
      await refreshPipeline();
      await loadDashboardJobs();
    } catch (error) {
      showError(error);
    } finally {
      setBusy(false);
    }
  };

  const handleCancel = async (jobId = null, jobState = null) => {
    const cancelledJobId = normalizeJobId(jobId) || currentPipelineJobId;
    const cancelledJob = dashboardJobs.find((item) => normalizeJobId(item?.id) === cancelledJobId) || null;
    const cancelledState = String(
      jobState
      || cancelledJob?.status
      || state
      || 'IDLE'
    ).trim().toUpperCase();

    if (cancelledJobId) setJobBusy(cancelledJobId, true);
    try {
      await api.cancelPipeline(cancelledJobId);
      await refreshPipeline();
      await loadDashboardJobs();
      let latestCancelledJob = null;
      const fetchLatestCancelledJob = async () => {
        if (!cancelledJobId) {
          return null;
        }
        try {
          const latestResponse = await api.getJob(cancelledJobId, { lite: true });
          return latestResponse?.job && typeof latestResponse.job === 'object'
            ? latestResponse.job
            : null;
        } catch (_error) {
          return null;
        }
      };
      latestCancelledJob = await fetchLatestCancelledJob();
      if (cancelledState === 'ENCODING') {
        const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
        for (let attempt = 0; attempt < 8; attempt += 1) {
          const latestStatus = String(
            latestCancelledJob?.status
            || latestCancelledJob?.last_state
            || ''
          ).trim().toUpperCase();
          if (latestStatus && latestStatus !== 'ENCODING') {
            break;
          }
          await wait(250);
          latestCancelledJob = await fetchLatestCancelledJob();
        }
      }
      const latestStatus = String(
        latestCancelledJob?.status
        || latestCancelledJob?.last_state
        || ''
      ).trim().toUpperCase();
      const autoPreparedForRestart = cancelledState === 'ENCODING' && latestStatus === 'READY_TO_ENCODE';
      if (cancelledState === 'ENCODING' && cancelledJobId && !autoPreparedForRestart) {
        setCancelCleanupDialog({
          visible: true,
          jobId: cancelledJobId,
          target: 'movie',
          path: latestCancelledJob?.output_path || cancelledJob?.output_path || null
        });
      } else if (cancelledState === 'RIPPING' && cancelledJobId) {
        setCancelCleanupDialog({
          visible: true,
          jobId: cancelledJobId,
          target: 'raw',
          path: latestCancelledJob?.raw_path || cancelledJob?.raw_path || null
        });
      }
    } catch (error) {
      showError(error);
    } finally {
      if (cancelledJobId) setJobBusy(cancelledJobId, false);
    }
  };

  const handleDeleteCancelledOutput = async () => {
    const jobId = normalizeJobId(cancelCleanupDialog?.jobId);
    const target = String(cancelCleanupDialog?.target || '').trim().toLowerCase();
    const effectiveTarget = target === 'raw' ? 'raw' : 'movie';
    if (!jobId) {
      setCancelCleanupDialog({ visible: false, jobId: null, target: null, path: null });
      return;
    }

    setCancelCleanupBusy(true);
    try {
      const response = await api.deleteJobFiles(jobId, effectiveTarget);
      const summary = response?.summary || {};
      const deletedFiles = effectiveTarget === 'raw'
        ? (summary.raw?.filesDeleted ?? 0)
        : (summary.movie?.filesDeleted ?? 0);
      const removedDirs = effectiveTarget === 'raw'
        ? (summary.raw?.dirsRemoved ?? 0)
        : (summary.movie?.dirsRemoved ?? 0);
      toastRef.current?.show({
        severity: 'success',
        summary: effectiveTarget === 'raw' ? 'RAW gelöscht' : 'Movie gelöscht',
        detail: `Entfernt: ${deletedFiles} Datei(en), ${removedDirs} Ordner.`,
        life: 4000
      });
      await loadDashboardJobs();
      await refreshPipeline();
      setCancelCleanupDialog({ visible: false, jobId: null, target: null, path: null });
    } catch (error) {
      showError(error);
    } finally {
      setCancelCleanupBusy(false);
    }
  };

  const handleStartJob = async (jobId, options = null) => {
    const normalizedJobId = normalizeJobId(jobId);
    if (!normalizedJobId) {
      return;
    }

    const startOptions = options && typeof options === 'object' ? options : {};
    const startJobRow = dashboardJobs.find((item) => normalizeJobId(item?.id) === normalizedJobId) || null;
    const mediaType = resolveMediaType(startJobRow);
    setJobBusy(normalizedJobId, true);
    try {
      if (startOptions.ensureConfirmed) {
        const confirmPayload = {
          selectedEncodeTitleId: startOptions.selectedEncodeTitleId ?? null,
          selectedTrackSelection: startOptions.selectedTrackSelection ?? null,
          skipPipelineStateUpdate: true
        };
        if (startOptions.selectedPostEncodeScriptIds !== undefined) {
          confirmPayload.selectedPostEncodeScriptIds = startOptions.selectedPostEncodeScriptIds;
        }
        if (startOptions.selectedPreEncodeScriptIds !== undefined) {
          confirmPayload.selectedPreEncodeScriptIds = startOptions.selectedPreEncodeScriptIds;
        }
        if (startOptions.selectedPostEncodeChainIds !== undefined) {
          confirmPayload.selectedPostEncodeChainIds = startOptions.selectedPostEncodeChainIds;
        }
        if (startOptions.selectedPreEncodeChainIds !== undefined) {
          confirmPayload.selectedPreEncodeChainIds = startOptions.selectedPreEncodeChainIds;
        }
        if (startOptions.selectedUserPresetId !== undefined) {
          confirmPayload.selectedUserPresetId = startOptions.selectedUserPresetId;
        }
        await api.confirmEncodeReview(normalizedJobId, confirmPayload);
      }
      const response = mediaType === 'audiobook'
        ? await api.startJob(normalizedJobId)
        : await api.startJob(normalizedJobId);
      const result = getQueueActionResult(response);
      await refreshPipeline();
      await loadDashboardJobs();
      if (result.queued) {
        showQueuedToast(toastRef, 'Start', result);
      } else {
        setExpandedJobId(normalizedJobId);
      }
    } catch (error) {
      showError(error);
    } finally {
      setJobBusy(normalizedJobId, false);
    }
  };

  const handleAudiobookUpload = async () => {
    if (!audiobookUploadFile) {
      showError(new Error('Bitte zuerst eine AAX-Datei auswählen.'));
      return;
    }
    try {
      const response = await onAudiobookUpload?.(audiobookUploadFile, { startImmediately: false });
      const result = response?.result || {};
      const uploadedJobId = normalizeJobId(result.jobId);
      if (result.needsActivationBytes && result.checksum) {
        setActivationBytesInput('');
        setActivationBytesDialog({ visible: true, checksum: result.checksum, jobId: uploadedJobId });
      } else if (uploadedJobId) {
        toastRef.current?.show({
          severity: 'success',
          summary: 'Audiobook importiert',
          detail: `Job #${uploadedJobId} wurde angelegt und wird geoeffnet.`,
          life: 3200
        });
      }
      setAudiobookUploadFile(null);
    } catch (error) {
      showError(error);
    }
  };

  const handleSaveActivationBytes = async () => {
    const { checksum, jobId } = activationBytesDialog;
    const bytes = activationBytesInput.trim().toLowerCase();
    setActivationBytesBusy(true);
    try {
      await api.saveActivationBytes(checksum, bytes);
      setPendingActivationJobIds(new Set());
      setActivationBytesDialog({ visible: false, checksum: null, jobId: null });
      toastRef.current?.show({
        severity: 'success',
        summary: 'Activation Bytes gespeichert',
        detail: jobId ? `Job #${jobId} kann jetzt gestartet werden.` : 'Bytes wurden lokal gespeichert.',
        life: 4000
      });
      await loadDashboardJobs();
    } catch (error) {
      showError(error);
    } finally {
      setActivationBytesBusy(false);
    }
  };

  const handleAudiobookStart = async (jobId, audiobookConfig) => {
    const normalizedJobId = normalizeJobId(jobId);
    if (!normalizedJobId) {
      return;
    }
    if (pendingActivationJobIds.has(normalizedJobId)) {
      setActivationBytesInput('');
      const pending = await api.getPendingActivation().catch(() => ({ pending: [] }));
      const entry = (pending?.pending || []).find((p) => p.jobId === normalizedJobId);
      if (entry) {
        setActivationBytesDialog({ visible: true, checksum: entry.checksum, jobId: normalizedJobId });
      }
      return;
    }
    setJobBusy(normalizedJobId, true);
    try {
      const response = await api.startAudiobook(normalizedJobId, audiobookConfig || {});
      const result = getQueueActionResult(response);
      await refreshPipeline();
      await loadDashboardJobs();
      if (result.queued) {
        showQueuedToast(toastRef, 'Audiobook', result);
      } else {
        setExpandedJobId(normalizedJobId);
      }
    } catch (error) {
      showError(error);
    } finally {
      setJobBusy(normalizedJobId, false);
    }
  };

  const handleConfirmReview = async (
    jobId,
    selectedEncodeTitleId = null,
    selectedTrackSelection = null,
    selectedPostEncodeScriptIds = undefined
  ) => {
    const normalizedJobId = normalizeJobId(jobId);
    if (normalizedJobId) setJobBusy(normalizedJobId, true);
    try {
      await api.confirmEncodeReview(jobId, {
        selectedEncodeTitleId,
        selectedTrackSelection,
        selectedPostEncodeScriptIds
      });
      await refreshPipeline();
      await loadDashboardJobs();
      setExpandedJobId(normalizedJobId);
    } catch (error) {
      showError(error);
    } finally {
      if (normalizedJobId) setJobBusy(normalizedJobId, false);
    }
  };

  const handleSelectPlaylist = async (jobId, selectedPlaylist = null) => {
    const normalizedJobId = normalizeJobId(jobId);
    if (normalizedJobId) setJobBusy(normalizedJobId, true);
    try {
      await api.selectMetadata({
        jobId,
        selectedPlaylist: selectedPlaylist || null
      });
      await refreshPipeline();
      await loadDashboardJobs();
      setExpandedJobId(normalizedJobId);
    } catch (error) {
      showError(error);
    } finally {
      if (normalizedJobId) setJobBusy(normalizedJobId, false);
    }
  };

  const handleSelectHandBrakeTitle = async (jobId, selectedHandBrakeTitleId = null) => {
    const normalizedJobId = normalizeJobId(jobId);
    if (normalizedJobId) setJobBusy(normalizedJobId, true);
    try {
      await api.selectMetadata({
        jobId,
        selectedHandBrakeTitleId: selectedHandBrakeTitleId || null
      });
      await refreshPipeline();
      await loadDashboardJobs();
      setExpandedJobId(normalizedJobId);
    } catch (error) {
      showError(error);
    } finally {
      if (normalizedJobId) setJobBusy(normalizedJobId, false);
    }
  };

  const handleRetry = async (jobId) => {
    const normalizedJobId = normalizeJobId(jobId);
    if (normalizedJobId) setJobBusy(normalizedJobId, true);
    try {
      const response = await api.retryJob(jobId);
      const result = getQueueActionResult(response);
      const retryJobId = normalizeJobId(result?.jobId) || normalizedJobId;
      await refreshPipeline();
      await loadDashboardJobs();
      if (result.queued) {
        showQueuedToast(toastRef, 'Retry', result);
      } else {
        setExpandedJobId(retryJobId);
      }
    } catch (error) {
      showError(error);
    } finally {
      if (normalizedJobId) setJobBusy(normalizedJobId, false);
    }
  };

  const handleRestartEncodeWithLastSettings = async (jobId) => {
    const job = dashboardJobs.find((item) => normalizeJobId(item?.id) === normalizeJobId(jobId)) || null;
    const title = job?.title || job?.detected_title || `Job #${jobId}`;
    if (job?.encodeSuccess) {
      const confirmed = window.confirm(
        `Encode für "${title}" ist bereits erfolgreich abgeschlossen. Wirklich erneut encodieren?\n` +
        'Es wird eine neue Datei mit Kollisionsprüfung angelegt.'
      );
      if (!confirmed) {
        return;
      }
    }

    const normalizedJobId = normalizeJobId(jobId);
    if (normalizedJobId) setJobBusy(normalizedJobId, true);
    try {
      const response = await api.restartEncodeWithLastSettings(jobId);
      const result = getQueueActionResult(response);
      const replacementJobId = normalizeJobId(result?.jobId) || normalizedJobId;
      await refreshPipeline();
      await loadDashboardJobs();
      if (result.queued) {
        showQueuedToast(toastRef, 'Encode-Neustart', result);
      } else {
        setExpandedJobId(replacementJobId);
      }
    } catch (error) {
      showError(error);
    } finally {
      if (normalizedJobId) setJobBusy(normalizedJobId, false);
    }
  };

  const handleRestartReviewFromRaw = async (jobId) => {
    const normalizedJobId = normalizeJobId(jobId);
    if (!normalizedJobId) {
      return;
    }

    setJobBusy(normalizedJobId, true);
    try {
      const response = await api.restartReviewFromRaw(normalizedJobId);
      const result = getQueueActionResult(response);
      const replacementJobId = normalizeJobId(result?.jobId) || normalizedJobId;
      await refreshPipeline();
      await loadDashboardJobs();
      setExpandedJobId(replacementJobId);
    } catch (error) {
      showError(error);
    } finally {
      setJobBusy(normalizedJobId, false);
    }
  };

  const handleQueueDragEnter = (targetEntryId) => {
    const targetId = Number(targetEntryId);
    const draggedId = Number(draggingQueueEntryId);
    if (!targetId || !draggedId || targetId === draggedId || queueReorderBusy) {
      return;
    }
    setQueueState((prev) => {
      const queuedJobs = reorderQueuedItems(prev?.queuedJobs || [], draggedId, targetId);
      return {
        ...normalizeQueue(prev),
        queuedJobs,
        queuedCount: queuedJobs.length
      };
    });
  };

  const handleQueueDrop = async () => {
    const draggedId = Number(draggingQueueEntryId);
    setDraggingQueueEntryId(null);
    if (!draggedId || queueReorderBusy) {
      return;
    }

    const orderedEntryIds = (Array.isArray(queueState?.queuedJobs) ? queueState.queuedJobs : [])
      .map((item) => Number(item?.entryId))
      .filter(Boolean);
    if (orderedEntryIds.length <= 1) {
      return;
    }

    setQueueReorderBusy(true);
    try {
      const response = await api.reorderPipelineQueue(orderedEntryIds);
      setQueueState(normalizeQueue(response?.queue));
    } catch (error) {
      showError(error);
      try {
        const latest = await api.getPipelineQueue();
        setQueueState(normalizeQueue(latest?.queue));
      } catch (_reloadError) {
        // ignore reload failures after reorder error
      }
    } finally {
      setQueueReorderBusy(false);
    }
  };

  const handleRemoveQueuedJob = async (jobId) => {
    const normalizedJobId = normalizeJobId(jobId);
    if (!normalizedJobId || queueReorderBusy) {
      return;
    }

    setQueueReorderBusy(true);
    setJobBusy(normalizedJobId, true);
    try {
      await api.cancelPipeline(normalizedJobId);
      const latest = await api.getPipelineQueue();
      setQueueState(normalizeQueue(latest?.queue));
    } catch (error) {
      showError(error);
    } finally {
      setQueueReorderBusy(false);
      setJobBusy(normalizedJobId, false);
    }
  };

  const handleRemoveQueueEntry = async (entryId) => {
    if (!entryId || queueReorderBusy) {
      return;
    }
    setQueueReorderBusy(true);
    try {
      const response = await api.removeQueueEntry(entryId);
      setQueueState(normalizeQueue(response?.queue));
    } catch (error) {
      showError(error);
    } finally {
      setQueueReorderBusy(false);
    }
  };

  const openInsertQueueDialog = async (afterEntryId) => {
    setInsertQueueDialog({ visible: true, afterEntryId: afterEntryId ?? null });
    try {
      const [scriptsRes, chainsRes] = await Promise.allSettled([api.getScripts(), api.getScriptChains()]);
      setQueueCatalog({
        scripts: scriptsRes.status === 'fulfilled' ? (Array.isArray(scriptsRes.value?.scripts) ? scriptsRes.value.scripts : []) : [],
        chains: chainsRes.status === 'fulfilled' ? (Array.isArray(chainsRes.value?.chains) ? chainsRes.value.chains : []) : []
      });
    } catch (_) { /* ignore */ }
  };

  const handleAddQueueEntry = async (type, params) => {
    const afterEntryId = insertQueueDialog.afterEntryId;
    setInsertQueueDialog({ visible: false, afterEntryId: null });
    try {
      const response = await api.addQueueEntry({ type, ...params, insertAfterEntryId: afterEntryId });
      setQueueState(normalizeQueue(response?.queue));
    } catch (error) {
      showError(error);
    }
  };

  const syncQueueFromServer = async () => {
    try {
      const latest = await api.getPipelineQueue();
      setQueueState(normalizeQueue(latest?.queue));
    } catch (_error) {
      // ignore sync failures
    }
  };

  const handleOmdbSearch = async (query) => {
    try {
      const response = await api.searchOmdb(query);
      return response.results || [];
    } catch (error) {
      showError(error);
      return [];
    }
  };

  const doSelectMetadata = async (payload) => {
    setBusy(true);
    try {
      if (metadataDialogReassignMode) {
        await api.assignJobOmdb(payload.jobId, payload);
      } else {
        await api.selectMetadata(payload);
      }
      await refreshPipeline();
      await loadDashboardJobs();
      setMetadataDialogVisible(false);
      setMetadataDialogContext(null);
      setMetadataDialogReassignMode(false);
    } catch (error) {
      showError(error);
    } finally {
      setBusy(false);
    }
  };

  const handleMetadataSubmit = async (payload) => {
    if (metadataDialogReassignMode) {
      await doSelectMetadata(payload);
      return;
    }

    // Duplikatprüfung: nur bei OMDB-Auswahl mit imdbId sinnvoll
    const searchTitle = payload.title || '';
    const searchImdbId = payload.imdbId || null;
    if (searchTitle) {
      try {
        const currentJobMediaProfile = String(effectiveMetadataDialogContext?.mediaProfile || '').trim().toLowerCase();
        const historyResponse = await api.getJobs({ search: searchTitle, limit: 50, lite: true });
        const historyJobs = Array.isArray(historyResponse?.jobs) ? historyResponse.jobs : [];
        const duplicate = historyJobs.find((job) => {
          if (normalizeJobId(job.id) === normalizeJobId(payload.jobId)) {
            return false; // aktueller Job selbst
          }
          // Gleicher Titel / imdbId?
          const titleMatch = searchImdbId
            ? (job.imdb_id && job.imdb_id === searchImdbId)
            : (String(job.title || '').toLowerCase() === searchTitle.toLowerCase());
          if (!titleMatch) {
            return false;
          }
          // Gleiches Medium? Verschiedene Medien (DVD vs. Bluray) → kein Duplikat
          if (!currentJobMediaProfile || currentJobMediaProfile === 'other') {
            return false;
          }
          const jobMediaType = resolveMediaType(job);
          return jobMediaType === currentJobMediaProfile;
        });

        if (duplicate) {
          setDuplicateJobDialog({ visible: true, existingJob: duplicate, pendingPayload: payload });
          return;
        }
      } catch (_error) {
        // Bei Fehler einfach fortfahren
      }
    }

    await doSelectMetadata(payload);
  };

  const handleMusicBrainzSearch = async (query) => {
    try {
      const response = await api.searchMusicBrainz(query);
      return response.results || [];
    } catch (error) {
      showError(error);
      return [];
    }
  };

  const handleMusicBrainzReleaseFetch = async (mbId) => {
    try {
      const response = await api.getMusicBrainzRelease(mbId);
      return response?.release || null;
    } catch (error) {
      showError(error);
      return null;
    }
  };

  const handleCdMetadataSubmit = async (payload) => {
    setBusy(true);
    try {
      await api.selectCdMetadata(payload);
      await refreshPipeline();
      await loadDashboardJobs();
      setCdMetadataDialogVisible(false);
      setCdMetadataDialogContext(null);
    } catch (error) {
      showError(error);
    } finally {
      setBusy(false);
    }
  };

  const handleCdRipStart = async (jobId, ripConfig) => {
    if (!jobId) {
      return;
    }
    const normalizedJobId = normalizeJobId(jobId);
    if (normalizedJobId) {
      setJobBusy(normalizedJobId, true);
    }
    try {
      const response = await api.startCdRip(jobId, ripConfig);
      const result = getQueueActionResult(response);
      if (result.queued) {
        showQueuedToast(toastRef, 'Audio CD', result);
      }
      const replacementJobId = normalizeJobId(result?.jobId) || normalizedJobId;
      await refreshPipeline();
      await loadDashboardJobs();
      if (replacementJobId) {
        setExpandedJobId(replacementJobId);
      }
    } catch (error) {
      showError(error);
    } finally {
      if (normalizedJobId) {
        setJobBusy(normalizedJobId, false);
      }
    }
  };

  const device = lastDiscEvent || pipeline?.context?.device;
  const isDriveActive = driveActiveStates.includes(state);
  const canRescan = !isDriveActive;
  const canReanalyze = !isDriveActive && (state === 'ENCODING' ? Boolean(device) : !processingStates.includes(state));
  const canOpenMetadataModal = Boolean(defaultMetadataDialogContext?.jobId);
  const queueRunningJobs = Array.isArray(queueState?.runningJobs) ? queueState.runningJobs : [];
  const queuedJobs = Array.isArray(queueState?.queuedJobs) ? queueState.queuedJobs : [];
  const canReorderQueue = queuedJobs.length > 1 && !queueReorderBusy;
  const buildRunningQueueScriptKey = (jobId) => `running-${normalizeJobId(jobId) || '-'}`;
  const buildQueuedQueueScriptKey = (entryId) => `queued-${Number(entryId) || '-'}`;
  const toggleQueueScriptDetails = (key) => {
    if (!key) {
      return;
    }
    setExpandedQueueScriptKeys((prev) => {
      const next = new Set(prev);
      if (next.has(key)) {
        next.delete(key);
      } else {
        next.add(key);
      }
      return next;
    });
  };
  useEffect(() => {
    const validKeys = new Set();
    for (const item of queueRunningJobs) {
      if (!hasQueueScriptSummary(item)) {
        continue;
      }
      validKeys.add(buildRunningQueueScriptKey(item?.jobId));
    }
    for (const item of queuedJobs) {
      if (String(item?.type || 'job') !== 'job' || !hasQueueScriptSummary(item)) {
        continue;
      }
      validKeys.add(buildQueuedQueueScriptKey(item?.entryId));
    }
    setExpandedQueueScriptKeys((prev) => {
      let changed = false;
      const next = new Set();
      for (const key of prev) {
        if (validKeys.has(key)) {
          next.add(key);
        } else {
          changed = true;
        }
      }
      return changed ? next : prev;
    });
  }, [queueRunningJobs, queuedJobs]);
  const queuedJobIdSet = useMemo(() => {
    const set = new Set();
    for (const item of queuedJobs) {
      const id = normalizeJobId(item?.jobId);
      if (id) {
        set.add(id);
      }
    }
    return set;
  }, [queuedJobs]);

  const setRuntimeActionBusy = (activityId, action, busyFlag) => {
    const key = `${Number(activityId) || 0}:${String(action || '')}`;
    setRuntimeActionBusyKeys((prev) => {
      const next = new Set(prev);
      if (busyFlag) {
        next.add(key);
      } else {
        next.delete(key);
      }
      return next;
    });
  };

  const isRuntimeActionBusy = (activityId, action) => runtimeActionBusyKeys.has(
    `${Number(activityId) || 0}:${String(action || '')}`
  );

  const handleRuntimeControl = async (item, action) => {
    const activityId = Number(item?.id);
    if (!Number.isFinite(activityId) || activityId <= 0) {
      return;
    }
    const normalizedAction = String(action || '').trim().toLowerCase();
    const actionLabel = normalizedAction === 'next-step' ? 'Nächster Schritt' : 'Abbrechen';
    setRuntimeActionBusy(activityId, normalizedAction, true);
    try {
      const response = normalizedAction === 'next-step'
        ? await api.requestRuntimeNextStep(activityId)
        : await api.cancelRuntimeActivity(activityId, { reason: 'Benutzerabbruch via Dashboard' });
      if (response?.snapshot) {
        setRuntimeActivities(normalizeRuntimeActivitiesPayload(response.snapshot));
      } else {
        const fresh = await api.getRuntimeActivities();
        setRuntimeActivities(normalizeRuntimeActivitiesPayload(fresh));
      }
      const accepted = response?.action?.accepted !== false;
      const actionMessage = String(response?.action?.message || '').trim();
      toastRef.current?.show({
        severity: accepted ? 'info' : 'warn',
        summary: actionLabel,
        detail: actionMessage || (accepted ? 'Aktion ausgelöst.' : 'Aktion aktuell nicht möglich.'),
        life: 2600
      });
    } catch (error) {
      toastRef.current?.show({
        severity: 'error',
        summary: actionLabel,
        detail: error?.message || 'Aktion fehlgeschlagen.',
        life: 3200
      });
    } finally {
      setRuntimeActionBusy(activityId, normalizedAction, false);
    }
  };

  const handleClearRuntimeRecent = async () => {
    if (runtimeRecentClearing || runtimeRecentItems.length === 0) {
      return;
    }
    setRuntimeRecentClearing(true);
    try {
      const response = await api.clearRuntimeRecentActivities();
      if (response?.snapshot) {
        setRuntimeActivities(normalizeRuntimeActivitiesPayload(response.snapshot));
      } else {
        const fresh = await api.getRuntimeActivities();
        setRuntimeActivities(normalizeRuntimeActivitiesPayload(fresh));
      }
      toastRef.current?.show({
        severity: 'success',
        summary: 'Abgeschlossene Liste',
        detail: `Einträge entfernt: ${Number(response?.removed || 0)}`,
        life: 2200
      });
    } catch (error) {
      toastRef.current?.show({
        severity: 'error',
        summary: 'Liste leeren',
        detail: error?.message || 'Leeren fehlgeschlagen.',
        life: 3200
      });
    } finally {
      setRuntimeRecentClearing(false);
    }
  };

  const runtimeActiveItems = Array.isArray(runtimeActivities?.active) ? runtimeActivities.active : [];
  const runtimeRecentItems = Array.isArray(runtimeActivities?.recent)
    ? runtimeActivities.recent.slice(0, 8)
    : [];

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

      <div className="dashboard-3col-grid">
        <div className="dashboard-col dashboard-col-left">
          <Card title="Audiobook Upload">
        <FileUpload
          ref={audiobookFileUploadRef}
          accept=".aax"
          maxFileSize={10737418240}
          customUpload
          uploadHandler={() => void handleAudiobookUpload()}
          disabled={audiobookUploadBusy}
          onSelect={(e) => setAudiobookUploadFile(e.files[0] || null)}
          onClear={() => setAudiobookUploadFile(null)}
          onRemove={() => setAudiobookUploadFile(null)}
          chooseOptions={{ icon: 'pi pi-images', iconOnly: true, className: 'p-button-rounded p-button-outlined' }}
          uploadOptions={{ icon: 'pi pi-cloud-upload', iconOnly: true, className: 'p-button-rounded p-button-outlined p-button-success' }}
          cancelOptions={{ icon: 'pi pi-times', iconOnly: true, className: 'p-button-rounded p-button-outlined p-button-danger' }}
          itemTemplate={(file, options) => (
            <div className="aax-file-item">
              <i className="pi pi-headphones aax-file-icon" />
              <div className="aax-file-info">
                <span className="aax-file-name" title={file.name}>{file.name}</span>
                <small>{options.formatSize}</small>
              </div>
              <Button
                icon="pi pi-times"
                text
                rounded
                severity="danger"
                size="small"
                onClick={options.onRemove}
                disabled={audiobookUploadBusy}
              />
            </div>
          )}
          emptyTemplate={() => (
            <div className="aax-drop-zone">
              <i className="pi pi-headphones aax-drop-icon" />
              <p>AAX-Datei hier ablegen</p>
              <small>oder oben „Auswählen" klicken</small>
            </div>
          )}
        />
        {audiobookUploadStatusVisible ? (
          <div className={`audiobook-upload-status tone-${audiobookUploadStatusTone}`}>
            <div className="audiobook-upload-status-head">
              <strong>{audiobookUploadStatusLabel}</strong>
              <Tag value={audiobookUploadStatusLabel} severity={audiobookUploadStatusTone} />
            </div>
            {audiobookUpload?.statusText ? <small>{audiobookUpload.statusText}</small> : null}
            {audiobookUploadFileName ? (
              <small className="audiobook-upload-file" title={audiobookUploadFileName}>
                Datei: {audiobookUploadFileName}
              </small>
            ) : null}
            <div
              className="dashboard-job-row-progress audiobook-upload-progress"
              aria-label={`Audiobook Upload ${Math.round(audiobookUploadProgress)} Prozent`}
            >
              <ProgressBar value={audiobookUploadProgress} showValue={false} />
              <small>
                {audiobookUploadPhase === 'processing'
                  ? '100% | Upload fertig, Job wird vorbereitet ...'
                  : audiobookUploadTotalBytes > 0
                    ? `${Math.round(audiobookUploadProgress)}% | ${formatBytes(audiobookUploadLoadedBytes)} / ${formatBytes(audiobookUploadTotalBytes)}`
                    : `${Math.round(audiobookUploadProgress)}%`}
              </small>
            </div>
          </div>
        ) : null}
      </Card>

          <Card title="Disk-Information">
        <div className="actions-row">
          <Button
            label="Laufwerk neu lesen"
            icon="pi pi-refresh"
            severity="secondary"
            onClick={handleRescan}
            loading={busy}
            disabled={!canRescan}
          />
          <Button
            label="Disk neu analysieren"
            icon="pi pi-search"
            severity="warning"
            onClick={handleReanalyze}
            loading={busy}
            disabled={!canReanalyze}
          />
          <Button
            label="Metadaten-Modal öffnen"
            icon="pi pi-list"
            onClick={() => handleOpenMetadataDialog()}
            disabled={!canOpenMetadataModal}
          />
        </div>
        {device ? (
          <div className="device-meta">
            <div>
              <strong>Pfad:</strong> {device.path || '-'}
            </div>
            <div>
              <strong>Modell:</strong> {device.model || '-'}
            </div>
            <div>
              <strong>Disk-Label:</strong> {device.discLabel || '-'}
            </div>
            <div>
              <strong>Laufwerks-Label:</strong> {device.label || '-'}
            </div>
            <div>
              <strong>Mount:</strong> {device.mountpoint || '-'}
            </div>
          </div>
        ) : (
          <p>Aktuell keine Disk erkannt.</p>
        )}
      </Card>

      <Card title="Freier Speicher">
        <div className="hardware-storage-list">
          {storageGroups.map((group) => {
            const rep = group.representative;
            const tone = getStorageUsageTone(rep?.usagePercent);
            const usagePercent = Number(rep?.usagePercent);
            const barValue = Number.isFinite(usagePercent)
              ? Math.max(0, Math.min(100, usagePercent))
              : 0;
            const hasError = group.entries.every((e) => e?.error);
            const groupKey = group.mountPoint || group.entries.map((e) => e?.key).join('-');
            const labels = group.entries.map((e) => e?.label || e?.key || 'Pfad').join(' · ');
            return (
              <div
                key={`storage-group-${groupKey}`}
                className={`hardware-storage-item compact${hasError ? ' has-error' : ''}`}
              >
                <small className="hardware-storage-group-label">{labels}</small>
                {hasError ? (
                  <small className="error-text">{rep?.error}</small>
                ) : (
                  <>
                    <div className="hardware-storage-bar-row">
                      <div className={`hardware-storage-bar tone-${tone}`}>
                        <ProgressBar value={barValue} showValue={false} />
                      </div>
                      <span className={`hardware-storage-percent tone-${tone}`}>
                        {formatPercent(rep?.usagePercent)}
                      </span>
                    </div>
                    <div className="hardware-storage-summary">
                      <small>Frei: {formatBytes(rep?.freeBytes)}</small>
                      <small>Gesamt: {formatBytes(rep?.totalBytes)}</small>
                    </div>
                  </>
                )}
              </div>
            );
          })}
        </div>
      </Card>
        </div>

        <div className="dashboard-col dashboard-col-center">
          <Card title="Job Übersicht" subTitle="Kompakte Liste; Klick auf Zeile öffnet die volle Job-Detailansicht mit passenden CTAs">
        {jobsLoading ? (
          <p>Jobs werden geladen ...</p>
        ) : dashboardJobs.length === 0 ? (
          <p>Keine relevanten Jobs im Dashboard (aktive/fortsetzbare Status).</p>
        ) : (
          <div className="dashboard-job-list">
            {dashboardJobs.map((job) => {
              const jobId = normalizeJobId(job?.id);
              if (!jobId) {
                return null;
              }
              const normalizedStatus = normalizeStatus(job?.status);
              const isQueued = queuedJobIdSet.has(jobId);
              const statusBadgeValue = getStatusLabel(job?.status, { queued: isQueued });
              const statusBadgeSeverity = getStatusSeverity(normalizedStatus, { queued: isQueued });
              const isExpanded = normalizeJobId(expandedJobId) === jobId;
              const isCurrentSession = currentPipelineJobId === jobId && state !== 'IDLE';
              const reviewConfirmed = Boolean(Number(job?.encode_review_confirmed || 0));
              const pipelineForJob = pipelineByJobId.get(jobId) || pipeline;
              const jobTitle = job?.title || job?.detected_title || `Job #${jobId}`;
              const mediaIndicator = mediaIndicatorMeta(job);
              const isResumable = (
                normalizedStatus === 'READY_TO_ENCODE'
                || (mediaIndicator.mediaType === 'audiobook' && normalizedStatus === 'READY_TO_START')
              ) && !isCurrentSession;
              const mediaProfile = String(pipelineForJob?.context?.mediaProfile || '').trim().toLowerCase();
              const jobState = String(pipelineForJob?.state || normalizedStatus).trim().toUpperCase();
              const pipelineStage = String(pipelineForJob?.context?.stage || '').trim().toUpperCase();
              const pipelineStatusText = String(pipelineForJob?.statusText || '').trim().toUpperCase();
              const isCdJob = jobState.startsWith('CD_')
                || pipelineStage.startsWith('CD_')
                || mediaProfile === 'cd'
                || mediaIndicator.mediaType === 'cd'
                || pipelineStatusText.includes('CD_');
              const isAudiobookJob = mediaProfile === 'audiobook'
                || mediaIndicator.mediaType === 'audiobook'
                || String(pipelineForJob?.context?.mode || '').trim().toLowerCase() === 'audiobook';
              const rawProgress = Number(pipelineForJob?.progress ?? 0);
              const clampedProgress = Number.isFinite(rawProgress)
                ? Math.max(0, Math.min(100, rawProgress))
                : 0;
              const progressLabel = `${Math.round(clampedProgress)}%`;
              const etaLabel = String(pipelineForJob?.eta || '').trim();

              const audiobookMeta = pipelineForJob?.context?.selectedMetadata && typeof pipelineForJob.context.selectedMetadata === 'object'
                ? pipelineForJob.context.selectedMetadata
                : {};
              const audiobookChapterCount = Array.isArray(audiobookMeta?.chapters) ? audiobookMeta.chapters.length : 0;

              if (isExpanded) {
                return (
                  <div key={jobId} className="dashboard-job-expanded">
                    <div className="dashboard-job-expanded-head">
                      {(job?.poster_url && job.poster_url !== 'N/A') ? (
                        <img src={job.poster_url} alt={jobTitle} className="poster-thumb" />
                      ) : (
                        <div className="poster-thumb dashboard-job-poster-fallback">{isAudiobookJob ? 'Kein Cover' : 'Kein Poster'}</div>
                      )}
                      <div className="dashboard-job-expanded-title">
                        <strong className="dashboard-job-title-line">
                          <img
                            src={mediaIndicator.src}
                            alt={mediaIndicator.alt}
                            title={mediaIndicator.title}
                            className="media-indicator-icon"
                          />
                          <span>#{jobId} | {jobTitle}</span>
                        </strong>
                        <div className="dashboard-job-badges">
                          <Tag value={statusBadgeValue} severity={statusBadgeSeverity} />
                          {isCurrentSession ? <Tag value="Aktive Session" severity="info" /> : null}
                          {isResumable ? <Tag value="Fortsetzbar" severity="success" /> : null}
                          {normalizedStatus === 'READY_TO_ENCODE'
                            ? <Tag value={reviewConfirmed ? 'Review bestätigt' : 'Review offen'} severity={reviewConfirmed ? 'success' : 'warning'} />
                            : null}
                          <JobStepChecks backupSuccess={Boolean(job?.backupSuccess)} encodeSuccess={Boolean(job?.encodeSuccess)} />
                        </div>
                      </div>
                      <Button
                        label="Einklappen"
                        icon="pi pi-angle-up"
                        severity="secondary"
                        outlined
                        onClick={() => setExpandedJobId(null)}
                        disabled={busyJobIds.has(jobId)}
                      />
                    </div>
                    {(() => {
                      if (isCdJob) {
                        return (
                          <>
                            {isCdJob ? (
                              <CdRipConfigPanel
                                pipeline={pipelineForJob}
                                onStart={(ripConfig) => handleCdRipStart(jobId, ripConfig)}
                                onCancel={() => handleCancel(jobId, jobState)}
                                onRetry={() => handleRetry(jobId)}
                                onOpenMetadata={() => {
                                  const ctx = pipelineForJob?.context && typeof pipelineForJob.context === 'object'
                                    ? pipelineForJob.context
                                    : pipeline?.context || {};
                                  setCdMetadataDialogContext({ ...ctx, jobId });
                                  setCdMetadataDialogVisible(true);
                                }}
                                busy={busyJobIds.has(jobId)}
                              />
                            ) : null}
                          </>
                        );
                      }
                      if (isAudiobookJob) {
                        const needsBytes = pendingActivationJobIds.has(jobId);
                        return (
                          <>
                            {needsBytes && (
                              <div style={{ padding: '0.75rem 1rem', marginBottom: '0.5rem', background: 'var(--yellow-100)', border: '1px solid var(--yellow-400)', borderRadius: '6px', color: 'var(--yellow-900)', fontSize: '0.875rem' }}>
                                <i className="pi pi-lock" style={{ marginRight: '0.5rem' }} />
                                <strong>Activation Bytes fehlen.</strong>{' '}
                                <button type="button" style={{ background: 'none', border: 'none', color: 'var(--primary-color)', cursor: 'pointer', textDecoration: 'underline', padding: 0 }} onClick={() => handleAudiobookStart(jobId, null)}>
                                  Jetzt eintragen
                                </button>
                              </div>
                            )}
                            <AudiobookConfigPanel
                              pipeline={pipelineForJob}
                              onStart={(config) => handleAudiobookStart(jobId, config)}
                              onCancel={() => handleCancel(jobId, jobState)}
                              onRetry={() => handleRetry(jobId)}
                              busy={busyJobIds.has(jobId) || needsBytes}
                            />
                          </>
                        );
                      }
                      return null;
                    })()}
                    {!isCdJob && !isAudiobookJob ? (
                    <PipelineStatusCard
                      pipeline={pipelineForJob}
                      onAnalyze={handleAnalyze}
                      onReanalyze={handleReanalyze}
                      onOpenMetadata={handleOpenMetadataDialog}
                      onReassignOmdb={handleOpenReassignOmdbDialog}
                      onStart={handleStartJob}
                      onRestartEncode={handleRestartEncodeWithLastSettings}
                      onRestartReview={handleRestartReviewFromRaw}
                      onConfirmReview={handleConfirmReview}
                      onSelectPlaylist={handleSelectPlaylist}
                      onSelectHandBrakeTitle={handleSelectHandBrakeTitle}
                      onCancel={handleCancel}
                      onRetry={handleRetry}
                      onRemoveFromQueue={handleRemoveQueuedJob}
                      isQueued={isQueued}
                      busy={busyJobIds.has(jobId)}
                      liveJobLog={isCurrentSession ? liveJobLog : ''}
                    />
                    ) : null}
                  </div>
                );
              }

              return (
                <button
                  key={jobId}
                  type="button"
                  className="dashboard-job-row"
                  onClick={() => setExpandedJobId(jobId)}
                >
                  {job?.poster_url && job.poster_url !== 'N/A' ? (
                    <img src={job.poster_url} alt={jobTitle} className="poster-thumb" />
                  ) : (
                    <div className="poster-thumb dashboard-job-poster-fallback">{isAudiobookJob ? 'Kein Cover' : 'Kein Poster'}</div>
                  )}
                  <div className="dashboard-job-row-content">
                    <div className="dashboard-job-row-main">
                      <strong className="dashboard-job-title-line">
                        <img
                          src={mediaIndicator.src}
                          alt={mediaIndicator.alt}
                          title={mediaIndicator.title}
                          className="media-indicator-icon"
                        />
                        <span>{jobTitle}</span>
                      </strong>
                      <small>
                        #{jobId}
                        {isAudiobookJob
                          ? (
                            `${audiobookMeta?.author ? ` | ${audiobookMeta.author}` : ''}`
                            + `${audiobookMeta?.narrator ? ` | ${audiobookMeta.narrator}` : ''}`
                            + `${audiobookChapterCount > 0 ? ` | ${audiobookChapterCount} Kapitel` : ''}`
                          )
                          : (
                            `${job?.year ? ` | ${job.year}` : ''}`
                            + `${job?.imdb_id ? ` | ${job.imdb_id}` : ''}`
                          )}
                      </small>
                    </div>
                    <div className="dashboard-job-badges">
                      <Tag value={statusBadgeValue} severity={statusBadgeSeverity} />
                      {isCurrentSession ? <Tag value="Aktive Session" severity="info" /> : null}
                      {isResumable ? <Tag value="Fortsetzbar" severity="success" /> : null}
                      {normalizedStatus === 'READY_TO_ENCODE'
                        ? <Tag value={reviewConfirmed ? 'Bestätigt' : 'Unbestätigt'} severity={reviewConfirmed ? 'success' : 'warning'} />
                        : null}
                      <JobStepChecks backupSuccess={Boolean(job?.backupSuccess)} encodeSuccess={Boolean(job?.encodeSuccess)} />
                    </div>
                    <div className="dashboard-job-row-progress" aria-label={`Job Fortschritt ${progressLabel}`}>
                      <ProgressBar value={clampedProgress} showValue={false} />
                      <small>{etaLabel ? `${progressLabel} | ETA ${etaLabel}` : progressLabel}</small>
                    </div>
                  </div>
                  <i className="pi pi-angle-down" aria-hidden="true" />
                </button>
              );
            })}
          </div>
        )}
      </Card>
        </div>

        <div className="dashboard-col dashboard-col-right">
          <Card title="Job Queue" subTitle="Elemente können per Drag-and-Drop umsortiert werden.">
        <div className="pipeline-queue-meta">
          <Tag value={`Film max.: ${queueState?.maxParallelJobs || 1}`} severity="info" />
          <Tag value={`CD max.: ${queueState?.maxParallelCdEncodes || 2}`} severity="info" />
          <Tag value={`Gesamt max.: ${queueState?.maxTotalEncodes || 3}`} severity="info" />
          {queueState?.cdBypassesQueue && <Tag value="CD bypass" severity="secondary" title="Audio CDs überspringen die Film-Queue-Reihenfolge" />}
          <Tag value={`Film laufend: ${queueState?.runningCount || 0}`} severity={(queueState?.runningCount || 0) > 0 ? 'warning' : 'success'} />
          <Tag value={`CD laufend: ${queueState?.runningCdCount || 0}`} severity={(queueState?.runningCdCount || 0) > 0 ? 'warning' : 'success'} />
          <Tag value={`Wartend: ${queueState?.queuedCount || 0}`} severity={queuedJobs.length > 0 ? 'warning' : 'success'} />
        </div>

        <div className="pipeline-queue-grid">
          <div className="pipeline-queue-col">
            <h4>Laufende Jobs</h4>
            {queueRunningJobs.length === 0 ? (
              <small>Keine laufenden Jobs.</small>
            ) : (
              queueRunningJobs.map((item) => {
                const hasScriptSummary = hasQueueScriptSummary(item);
                const detailKey = buildRunningQueueScriptKey(item?.jobId);
                const detailsExpanded = hasScriptSummary && expandedQueueScriptKeys.has(detailKey);
                return (
                  <div key={`running-${item.jobId}`} className="pipeline-queue-entry-wrap">
                    <div className="pipeline-queue-item running queue-job-item">
                      <div className="pipeline-queue-item-main">
                        <strong>
                          #{item.jobId} | {item.title || `Job #${item.jobId}`}
                          {item.hasScripts ? <i className="pi pi-code queue-job-tag" title="Skripte hinterlegt" /> : null}
                          {item.hasChains ? <i className="pi pi-link queue-job-tag" title="Skriptketten hinterlegt" /> : null}
                        </strong>
                        <small>{getStatusLabel(item.status)}</small>
                      </div>
                      {hasScriptSummary ? (
                        <button
                          type="button"
                          className="queue-job-expand-btn"
                          aria-label={detailsExpanded ? 'Skriptdetails ausblenden' : 'Skriptdetails einblenden'}
                          aria-expanded={detailsExpanded}
                          onClick={() => toggleQueueScriptDetails(detailKey)}
                        >
                          <i className={`pi ${detailsExpanded ? 'pi-angle-up' : 'pi-angle-down'}`} />
                        </button>
                      ) : null}
                    </div>
                    {detailsExpanded ? <QueueJobScriptSummary item={item} /> : null}
                  </div>
                );
              })
            )}
          </div>
          <div className="pipeline-queue-col">
            <div className="pipeline-queue-col-header">
              <h4>Warteschlange</h4>
              <button
                type="button"
                className="queue-add-entry-btn"
                title="Skript, Kette oder Wartezeit zur Queue hinzufügen"
                onClick={() => void openInsertQueueDialog(null)}
              >
                <i className="pi pi-plus" /> Hinzufügen
              </button>
            </div>
            {queuedJobs.length === 0 ? (
              <small className="queue-empty-hint">Queue ist leer.</small>
            ) : (
              <>
                {queuedJobs.map((item) => {
                  const entryId = Number(item?.entryId);
                  const isNonJob = item.type && item.type !== 'job';
                  const isDragging = Number(draggingQueueEntryId) === entryId;
                  const hasScriptSummary = !isNonJob && hasQueueScriptSummary(item);
                  const detailKey = buildQueuedQueueScriptKey(entryId);
                  const detailsExpanded = hasScriptSummary && expandedQueueScriptKeys.has(detailKey);
                  return (
                    <div key={`queued-entry-${entryId}`} className="pipeline-queue-entry-wrap">
                      <div
                        className={`pipeline-queue-item queued${isDragging ? ' dragging' : ''}${isNonJob ? ' non-job' : ''}`}
                        draggable={canReorderQueue}
                        onDragStart={() => setDraggingQueueEntryId(entryId)}
                        onDragEnter={() => handleQueueDragEnter(entryId)}
                        onDragOver={(event) => event.preventDefault()}
                        onDrop={(event) => {
                          event.preventDefault();
                          void handleQueueDrop();
                        }}
                        onDragEnd={() => {
                          setDraggingQueueEntryId(null);
                          void syncQueueFromServer();
                        }}
                      >
                        <span className={`pipeline-queue-drag-handle${canReorderQueue ? '' : ' disabled'}`} title="Reihenfolge ändern">
                          <i className="pi pi-bars" />
                        </span>
                        <i className={`pipeline-queue-type-icon ${queueEntryIcon(item.type)}`} title={item.type || 'job'} />
                        <div className="pipeline-queue-item-main">
                          {isNonJob ? (
                            <strong>{item.position || '-'}. {queueEntryLabel(item)}</strong>
                          ) : (
                            <>
                              <strong>
                                {item.position || '-'} | #{item.jobId} | {item.title || `Job #${item.jobId}`}
                                {item.hasScripts ? <i className="pi pi-code queue-job-tag" title="Skripte hinterlegt" /> : null}
                                {item.hasChains ? <i className="pi pi-link queue-job-tag" title="Skriptketten hinterlegt" /> : null}
                              </strong>
                              <small>{item.actionLabel || item.action || '-'} | {getStatusLabel(item.status)}</small>
                            </>
                          )}
                        </div>
                        <div className="pipeline-queue-item-actions">
                          {hasScriptSummary ? (
                            <button
                              type="button"
                              className="queue-job-expand-btn"
                              aria-label={detailsExpanded ? 'Skriptdetails ausblenden' : 'Skriptdetails einblenden'}
                              aria-expanded={detailsExpanded}
                              onClick={(event) => {
                                event.preventDefault();
                                event.stopPropagation();
                                toggleQueueScriptDetails(detailKey);
                              }}
                            >
                              <i className={`pi ${detailsExpanded ? 'pi-angle-up' : 'pi-angle-down'}`} />
                            </button>
                          ) : null}
                          <Button
                            icon="pi pi-times"
                            severity="danger"
                            text
                            rounded
                            size="small"
                            className="pipeline-queue-remove-btn"
                            disabled={queueReorderBusy}
                            onClick={(event) => {
                              event.preventDefault();
                              event.stopPropagation();
                              if (isNonJob) {
                                void handleRemoveQueueEntry(entryId);
                              } else {
                                void handleRemoveQueuedJob(item.jobId);
                              }
                            }}
                          />
                        </div>
                      </div>
                      {detailsExpanded ? <QueueJobScriptSummary item={item} /> : null}
                      <button
                        type="button"
                        className="queue-insert-btn"
                        title="Eintrag danach einfügen"
                        onClick={() => void openInsertQueueDialog(entryId)}
                      >
                        <i className="pi pi-plus" />
                      </button>
                    </div>
                  );
                })}
              </>
            )}
          </div>
        </div>
      </Card>

          <Card title="Skript- / Cron-Status">
        <div className="runtime-activity-meta pipeline-queue-meta">
          <Tag value={`Laufend: ${runtimeActiveItems.length}`} severity={runtimeActiveItems.length > 0 ? 'warning' : 'success'} />
          <Tag value={`Zuletzt: ${runtimeRecentItems.length}`} severity="info" />
          <Tag value={`Update: ${formatUpdatedAt(runtimeActivities?.updatedAt)}`} severity="secondary" />
          <Button
            type="button"
            icon="pi pi-trash"
            label="Liste leeren"
            severity="secondary"
            outlined
            size="small"
            onClick={() => {
              void handleClearRuntimeRecent();
            }}
            disabled={runtimeRecentItems.length === 0}
            loading={runtimeRecentClearing}
          />
        </div>

        {runtimeLoading && runtimeActiveItems.length === 0 && runtimeRecentItems.length === 0 ? (
          <p>Aktivitäten werden geladen ...</p>
        ) : (
          <div className="runtime-activity-grid pipeline-queue-grid">
            <div className="runtime-activity-col pipeline-queue-col">
              <h4>Aktiv</h4>
              {runtimeActiveItems.length === 0 ? (
                <small className="queue-empty-hint">Keine laufenden Skript-/Ketten-/Cron-Ausführungen.</small>
              ) : (
                <div className="runtime-activity-list">
                  {runtimeActiveItems.map((item, index) => {
                    const statusMeta = runtimeStatusMeta(item?.status);
                    const canCancel = Boolean(item?.canCancel);
                    const canNextStep = String(item?.type || '').trim().toLowerCase() === 'chain' && Boolean(item?.canNextStep);
                    const cancelBusy = isRuntimeActionBusy(item?.id, 'cancel');
                    const nextStepBusy = isRuntimeActionBusy(item?.id, 'next-step');
                    return (
                      <div key={`runtime-active-${item?.id || index}`} className="runtime-activity-item running">
                        <div className="runtime-activity-head">
                          <strong>{item?.name || '-'}</strong>
                          <div className="runtime-activity-tags">
                            <Tag value={runtimeTypeLabel(item?.type)} severity="info" />
                            <Tag value={statusMeta.label} severity={statusMeta.severity} />
                          </div>
                        </div>
                        <small>
                          Quelle: {item?.source || '-'}
                          {item?.jobId ? ` | Job #${item.jobId}` : ''}
                          {item?.cronJobId ? ` | Cron #${item.cronJobId}` : ''}
                        </small>
                        {item?.currentStep ? <small>Schritt: {item.currentStep}</small> : null}
                        {item?.currentScriptName ? <small>Laufendes Skript: {item.currentScriptName}</small> : null}
                        {item?.message ? <small>{item.message}</small> : null}
                        <RuntimeActivityDetails
                          item={item}
                          summary="Live-Ausgabe anzeigen"
                        />
                        <small>Gestartet: {formatUpdatedAt(item?.startedAt)}</small>
                        {canCancel || canNextStep ? (
                          <div className="runtime-activity-actions">
                            {canNextStep ? (
                              <Button
                                type="button"
                                icon="pi pi-step-forward"
                                label="Nächster Schritt"
                                outlined
                                severity="secondary"
                                size="small"
                                loading={nextStepBusy}
                                disabled={cancelBusy}
                                onClick={() => {
                                  void handleRuntimeControl(item, 'next-step');
                                }}
                              />
                            ) : null}
                            {canCancel ? (
                              <Button
                                type="button"
                                icon="pi pi-stop"
                                label="Abbrechen"
                                outlined
                                severity="danger"
                                size="small"
                                loading={cancelBusy}
                                disabled={nextStepBusy}
                                onClick={() => {
                                  void handleRuntimeControl(item, 'cancel');
                                }}
                              />
                            ) : null}
                          </div>
                        ) : null}
                      </div>
                    );
                  })}
                </div>
              )}
            </div>

            <div className="runtime-activity-col pipeline-queue-col">
              <h4>Zuletzt abgeschlossen</h4>
              {runtimeRecentItems.length === 0 ? (
                <small className="queue-empty-hint">Keine abgeschlossenen Einträge vorhanden.</small>
              ) : (
                <div className="runtime-activity-list">
                  {runtimeRecentItems.map((item, index) => {
                    const outcomeMeta = runtimeOutcomeMeta(item?.outcome, item?.status);
                    return (
                      <div key={`runtime-recent-${item?.id || index}`} className="runtime-activity-item done">
                        <div className="runtime-activity-head">
                          <strong>{item?.name || '-'}</strong>
                          <div className="runtime-activity-tags">
                            <Tag value={runtimeTypeLabel(item?.type)} severity="info" />
                            <Tag value={outcomeMeta.label} severity={outcomeMeta.severity} />
                          </div>
                        </div>
                        <small>
                          Quelle: {item?.source || '-'}
                          {item?.jobId ? ` | Job #${item.jobId}` : ''}
                          {item?.cronJobId ? ` | Cron #${item.cronJobId}` : ''}
                        </small>
                        {Number.isFinite(Number(item?.exitCode)) ? <small>Exit-Code: {item.exitCode}</small> : null}
                        {item?.message ? <small>{item.message}</small> : null}
                        {item?.errorMessage ? <small className="error-text">{item.errorMessage}</small> : null}
                        {hasRuntimeOutputDetails(item) ? (
                          <RuntimeActivityDetails
                            item={item}
                            summary="Details anzeigen"
                          />
                        ) : null}
                        <small>
                          Ende: {formatUpdatedAt(item?.finishedAt || item?.startedAt)}
                          {item?.durationMs != null ? ` | Dauer: ${formatDurationMs(item.durationMs)}` : ''}
                        </small>
                      </div>
                    );
                  })}
                </div>
              )}
            </div>
          </div>
        )}
      </Card>
        </div>
      </div>

      {monitoringState.enabled ? (
        <Card title="Hardware Monitoring">
          <div className="hardware-monitor-head">
            {!monitoringState.enabled ? (
              <small>Hardware-Monitoring ist deaktiviert.</small>
            ) : !monitoringSample ? (
              <small>Warte auf erste Messung ...</small>
            ) : (
              <div className="hardware-monitor-grid">
                <section className="hardware-monitor-block">
                  <h4>CPU</h4>
                  <div className="hardware-cpu-summary">
                    <div className="hardware-cpu-chip" title="CPU Gesamtauslastung">
                      <i className="pi pi-chart-line" />
                      <span>{formatPercent(cpuMetrics?.overallUsagePercent)}</span>
                    </div>
                    <div className="hardware-cpu-chip" title="CPU Gesamttemperatur">
                      <i className="pi pi-bolt" />
                      <span>{formatTemperature(cpuMetrics?.overallTemperatureC)}</span>
                    </div>
                    <div className="hardware-cpu-load-group">
                      <div className="hardware-cpu-chip" title="CPU Load Average">
                        <i className="pi pi-chart-bar" />
                        <span>{Array.isArray(cpuMetrics?.loadAverage) ? cpuMetrics.loadAverage.join(' / ') : '-'}</span>
                      </div>
                      {cpuPerCoreMetrics.length > 0 ? (
                        <button
                          type="button"
                          className="hardware-cpu-core-toggle-btn"
                          onClick={() => setCpuCoresExpanded((prev) => !prev)}
                          aria-label={cpuCoresExpanded ? 'CPU-Kerne ausblenden' : 'CPU-Kerne einblenden'}
                          aria-expanded={cpuCoresExpanded}
                        >
                          <i className={`pi ${cpuCoresExpanded ? 'pi-angle-up' : 'pi-angle-down'}`} />
                        </button>
                      ) : null}
                    </div>
                  </div>
                  {cpuPerCoreMetrics.length === 0 ? (
                    <small>Pro-Core-Daten sind noch nicht verfuegbar.</small>
                  ) : null}
                  {cpuPerCoreMetrics.length > 0 && cpuCoresExpanded ? (
                    <div className="hardware-core-grid compact">
                      {cpuPerCoreMetrics.map((core) => (
                        <div key={`core-${core.index}`} className="hardware-core-item compact">
                          <div className="hardware-core-title">C{core.index}</div>
                          <div className="hardware-core-metric" title="Auslastung">
                            <i className="pi pi-chart-line" />
                            <small>{formatPercent(core.usagePercent)}</small>
                          </div>
                          <div className="hardware-core-metric" title="Temperatur">
                            <i className="pi pi-bolt" />
                            <small>{formatTemperature(core.temperatureC)}</small>
                          </div>
                        </div>
                      ))}
                    </div>
                  ) : null}
                </section>

                <section className="hardware-monitor-block">
                  <h4>RAM</h4>
                  <div className="hardware-cpu-summary">
                    <div className="hardware-cpu-chip" title="RAM Auslastung">
                      <i className="pi pi-chart-pie" />
                      <span>{formatPercent(memoryMetrics?.usagePercent)}</span>
                    </div>
                    <div className="hardware-cpu-chip" title="RAM Belegt">
                      <i className="pi pi-arrow-up" />
                      <span>{formatBytes(memoryMetrics?.usedBytes)}</span>
                    </div>
                    <div className="hardware-cpu-chip" title="RAM Frei">
                      <i className="pi pi-arrow-down" />
                      <span>{formatBytes(memoryMetrics?.freeBytes)}</span>
                    </div>
                    <div className="hardware-cpu-chip" title="RAM Gesamt">
                      <i className="pi pi-database" />
                      <span>{formatBytes(memoryMetrics?.totalBytes)}</span>
                    </div>
                  </div>
                </section>

                <section className="hardware-monitor-block">
                  <h4>GPU</h4>
                  {!gpuMetrics?.available ? (
                    <small>{gpuMetrics?.message || 'Keine GPU-Metriken verfuegbar.'}</small>
                  ) : (
                    <div className="hardware-gpu-list">
                      {gpuDevices.map((gpu, index) => (
                        <div key={`gpu-${gpu?.index ?? index}`} className="hardware-gpu-item">
                          <strong>
                            GPU {gpu?.index ?? index}
                            {gpu?.name ? ` | ${gpu.name}` : ''}
                          </strong>
                          <span className="hardware-gpu-chip" title="GPU Load">
                            <i className="pi pi-chart-line" />{formatPercent(gpu?.utilizationPercent)}
                          </span>
                          <span className="hardware-gpu-chip" title="Speicher-Load">
                            <i className="pi pi-chart-pie" />{formatPercent(gpu?.memoryUtilizationPercent)}
                          </span>
                          <span className="hardware-gpu-chip" title="Temperatur">
                            <i className="pi pi-bolt" />{formatTemperature(gpu?.temperatureC)}
                          </span>
                          <span className="hardware-gpu-chip" title="VRAM">
                            <i className="pi pi-database" />{formatBytes(gpu?.memoryUsedBytes)} / {formatBytes(gpu?.memoryTotalBytes)}
                          </span>
                          <span className="hardware-gpu-chip" title="Power">
                            <i className="pi pi-flash" />
                            {Number.isFinite(Number(gpu?.powerDrawW)) ? `${gpu.powerDrawW} W` : 'n/a'}
                            {' / '}
                            {Number.isFinite(Number(gpu?.powerLimitW)) ? `${gpu.powerLimitW} W` : 'n/a'}
                          </span>
                        </div>
                      ))}
                    </div>
                  )}
                </section>
              </div>
            )}
          </div>
        </Card>
      ) : null}

      <MetadataSelectionDialog
        visible={metadataDialogVisible}
        context={effectiveMetadataDialogContext}
        onHide={() => {
          setMetadataDialogVisible(false);
          setMetadataDialogContext(null);
          setMetadataDialogReassignMode(false);
        }}
        onSubmit={handleMetadataSubmit}
        onSearch={handleOmdbSearch}
        busy={busy}
      />

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

      <Dialog
        header="Titel bereits in der Historie"
        visible={duplicateJobDialog.visible}
        onHide={() => setDuplicateJobDialog({ visible: false, existingJob: null, pendingPayload: null })}
        style={{ width: '30rem', maxWidth: '96vw' }}
        modal
      >
        <p>
          <strong>{duplicateJobDialog.existingJob?.title || duplicateJobDialog.pendingPayload?.title}</strong> ist bereits als Job #{duplicateJobDialog.existingJob?.id} in der Historie vorhanden.
        </p>
        <p>Neuen Job anlegen oder mit dem vorhandenen Eintrag weiterarbeiten?</p>
        <div className="dialog-actions">
          <Button
            label="Vorhandenen Job öffnen"
            icon="pi pi-history"
            onClick={() => {
              const jobId = duplicateJobDialog.existingJob?.id;
              setDuplicateJobDialog({ visible: false, existingJob: null, pendingPayload: null });
              navigate(`/history?open=${jobId}`);
            }}
          />
          <Button
            label="Neuen Job anlegen"
            severity="secondary"
            outlined
            onClick={async () => {
              const payload = duplicateJobDialog.pendingPayload;
              setDuplicateJobDialog({ visible: false, existingJob: null, pendingPayload: null });
              await doSelectMetadata(payload);
            }}
          />
        </div>
      </Dialog>

      <Dialog
        header={cancelCleanupDialog?.target === 'raw' ? 'Rip abgebrochen' : 'Encode abgebrochen'}
        visible={Boolean(cancelCleanupDialog.visible)}
        onHide={() => setCancelCleanupDialog({ visible: false, jobId: null, target: null, path: null })}
        style={{ width: '32rem', maxWidth: '96vw' }}
        modal
      >
        <p>
          {cancelCleanupDialog?.target === 'raw'
            ? 'Soll der bisher erzeugte RAW-Ordner gelöscht werden?'
            : 'Soll die bisher erzeugte Movie-Datei inklusive Job-Ordner im Ausgabeverzeichnis gelöscht werden?'}
        </p>
        {cancelCleanupDialog?.path ? (
          <small className="muted-inline">
            {cancelCleanupDialog?.target === 'raw' ? 'RAW-Pfad' : 'Output-Pfad'}: {cancelCleanupDialog.path}
          </small>
        ) : null}
        <div className="dialog-actions">
          <Button
            label="Behalten"
            severity="secondary"
            outlined
            onClick={() => setCancelCleanupDialog({ visible: false, jobId: null, target: null, path: null })}
            disabled={cancelCleanupBusy}
          />
          <Button
            label={cancelCleanupDialog?.target === 'raw' ? 'RAW löschen' : 'Movie löschen'}
            icon="pi pi-trash"
            severity="danger"
            onClick={handleDeleteCancelledOutput}
            loading={cancelCleanupBusy}
          />
        </div>
      </Dialog>

      <Dialog
        header="Queue-Eintrag einfügen"
        visible={insertQueueDialog.visible}
        onHide={() => setInsertQueueDialog({ visible: false, afterEntryId: null })}
        style={{ width: '28rem', maxWidth: '96vw' }}
        modal
      >
        <div className="queue-insert-dialog-body">
          <p className="queue-insert-dialog-hint">
            {insertQueueDialog.afterEntryId
              ? 'Eintrag wird nach dem ausgewählten Element eingefügt.'
              : 'Eintrag wird am Ende der Queue eingefügt.'}
          </p>

          {queueCatalog.scripts.length > 0 ? (
            <div className="queue-insert-section">
              <strong><i className="pi pi-code" /> Skript</strong>
              <div className="queue-insert-options">
                {queueCatalog.scripts.map((script) => (
                  <button
                    key={`qi-script-${script.id}`}
                    type="button"
                    className="queue-insert-option"
                    onClick={() => void handleAddQueueEntry('script', { scriptId: script.id })}
                  >
                    {script.name}
                  </button>
                ))}
              </div>
            </div>
          ) : null}

          {queueCatalog.chains.length > 0 ? (
            <div className="queue-insert-section">
              <strong><i className="pi pi-link" /> Skriptkette</strong>
              <div className="queue-insert-options">
                {queueCatalog.chains.map((chain) => (
                  <button
                    key={`qi-chain-${chain.id}`}
                    type="button"
                    className="queue-insert-option"
                    onClick={() => void handleAddQueueEntry('chain', { chainId: chain.id })}
                  >
                    {chain.name}
                  </button>
                ))}
              </div>
            </div>
          ) : null}

          <div className="queue-insert-section">
            <strong><i className="pi pi-clock" /> Warten</strong>
            <div className="queue-insert-wait-row">
              <InputNumber
                value={insertWaitSeconds}
                onValueChange={(e) => setInsertWaitSeconds(e.value ?? 30)}
                min={1}
                max={3600}
                suffix="s"
                style={{ width: '7rem' }}
              />
              <Button
                label="Einfügen"
                icon="pi pi-check"
                onClick={() => void handleAddQueueEntry('wait', { waitSeconds: insertWaitSeconds })}
              />
            </div>
          </div>

          {queueCatalog.scripts.length === 0 && queueCatalog.chains.length === 0 ? (
            <small className="muted-inline">Keine Skripte oder Ketten konfiguriert. In den Settings anlegen.</small>
          ) : null}
        </div>
      </Dialog>

      <Dialog
        header="Audible Activation Bytes eintragen"
        visible={activationBytesDialog.visible}
        onHide={() => setActivationBytesDialog({ visible: false, checksum: null, jobId: null })}
        style={{ width: '36rem', maxWidth: '96vw' }}
        modal
        footer={(
          <div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
            <Button label="Abbrechen" severity="secondary" outlined onClick={() => setActivationBytesDialog({ visible: false, checksum: null, jobId: null })} disabled={activationBytesBusy} />
            <Button label="Speichern" icon="pi pi-check" onClick={() => void handleSaveActivationBytes()} loading={activationBytesBusy} disabled={activationBytesInput.trim().length !== 8} />
          </div>
        )}
      >
        <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
          <div>
            <label style={{ display: 'block', marginBottom: '0.375rem', fontWeight: 600 }}>Checksum (aus der AAX-Datei)</label>
            <div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
              <InputText
                value={activationBytesDialog.checksum || ''}
                readOnly
                style={{ flex: 1, fontFamily: 'monospace', fontSize: '0.85rem' }}
              />
              <Button
                icon="pi pi-copy"
                severity="secondary"
                outlined
                tooltip="Kopieren"
                onClick={() => navigator.clipboard.writeText(activationBytesDialog.checksum || '')}
              />
            </div>
          </div>
          <div>
            <label style={{ display: 'block', marginBottom: '0.375rem', fontWeight: 600 }}>Activation Bytes (8 Hex-Zeichen)</label>
            <InputText
              value={activationBytesInput}
              onChange={(e) => setActivationBytesInput(e.target.value)}
              placeholder="z.B. 1a2b3c4d"
              style={{ width: '100%', fontFamily: 'monospace' }}
              maxLength={8}
            />
          </div>
          <div style={{ background: 'var(--surface-100)', borderRadius: '6px', padding: '0.875rem', fontSize: '0.875rem', lineHeight: 1.6 }}>
            <strong>So bekommst du die Activation Bytes:</strong>
            <ol style={{ margin: '0.5rem 0 0 1.25rem', padding: 0 }}>
              <li>Öffne <strong>audible-tools.kamsker.at</strong></li>
              <li>Aktiviere den <strong>Experten-Modus</strong></li>
              <li>Gib die obige Checksum ein</li>
              <li>Kopiere die zurückgegebenen Activation Bytes hier rein</li>
            </ol>
            <p style={{ margin: '0.5rem 0 0', color: 'var(--text-color-secondary)' }}>
              Die Bytes werden lokal gespeichert und für alle weiteren AAX-Dateien desselben Accounts wiederverwendet.
            </p>
          </div>
        </div>
      </Dialog>
    </div>
  );
}
