import { useEffect, useRef, useState } from 'react';
import { Navigate, Outlet, Routes, Route, useLocation, useNavigate } from 'react-router-dom';
import { Button } from 'primereact/button';
import { ProgressBar } from 'primereact/progressbar';
import { Tag } from 'primereact/tag';
import { Toast } from 'primereact/toast';
import { ConfirmDialog } from 'primereact/confirmdialog';
import { api } from './api/client';
import { useWebSocket } from './hooks/useWebSocket';
import RipperPage from './pages/RipperPage';
import SettingsPage from './pages/SettingsPage';
import HistoryPage from './pages/HistoryPage';
import DatabasePage from './pages/DatabasePage';
import DownloadsPage from './pages/DownloadsPage';
import ConverterPage from './pages/ConverterPage';
import JobsInboxPage from './pages/JobsInboxPage';
import AudiobooksPage from './pages/AudiobooksPage';

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

function clampPercent(value) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) {
    return 0;
  }
  return Math.max(0, Math.min(100, parsed));
}

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

function isRunningStage(value) {
  const normalized = normalizeStage(value);
  return normalized === 'ANALYZING'
    || normalized === 'RIPPING'
    || normalized === 'MEDIAINFO_CHECK'
    || normalized === 'ENCODING'
    || normalized === 'CD_ANALYZING'
    || normalized === 'CD_RIPPING'
    || normalized === 'CD_ENCODING';
}

function isTerminalStage(value) {
  const normalized = normalizeStage(value);
  return normalized === 'FINISHED' || normalized === 'ERROR' || normalized === 'CANCELLED';
}

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 createInitialAudiobookUploadState() {
  return {
    phase: 'idle',
    fileName: null,
    loadedBytes: 0,
    totalBytes: 0,
    progressPercent: 0,
    statusText: null,
    errorMessage: null,
    jobId: null,
    startedAt: null,
    finishedAt: null
  };
}

function getAudiobookUploadTagMeta(phase) {
  const normalized = String(phase || '').trim().toLowerCase();
  if (normalized === 'uploading') {
    return { label: 'Upload läuft', severity: 'warning' };
  }
  if (normalized === 'processing') {
    return { label: 'Server verarbeitet', severity: 'info' };
  }
  if (normalized === 'completed') {
    return { label: 'Bereit', severity: 'success' };
  }
  if (normalized === 'error') {
    return { label: 'Fehler', severity: 'danger' };
  }
  return { label: 'Inaktiv', severity: 'secondary' };
}

function getDownloadIndicatorMeta(summary) {
  const activeCount = Number(summary?.activeCount || 0);
  const failedCount = Number(summary?.failedCount || 0);
  const totalCount = Number(summary?.totalCount || 0);

  if (activeCount > 0) {
    return {
      icon: 'pi pi-spinner pi-spin',
      label: activeCount === 1 ? '1 ZIP aktiv' : `${activeCount} ZIPs aktiv`,
      className: 'zip-status-indicator-active'
    };
  }
  if (totalCount > 0) {
    return {
      icon: 'pi pi-check',
      label: failedCount > 0 ? 'ZIP-Jobs beendet' : 'ZIPs fertig',
      className: 'zip-status-indicator-ready'
    };
  }
  return {
    icon: 'pi pi-download',
    label: 'ZIPs',
    className: 'zip-status-indicator-idle'
  };
}

function App() {
  const appVersion = __APP_VERSION__;
  const [pipeline, setPipeline] = useState({ state: 'IDLE', progress: 0, context: {} });
  const [hardwareMonitoring, setHardwareMonitoring] = useState(null);
  const [lastDiscEvent, setLastDiscEvent] = useState(null);
  const [expertMode, setExpertMode] = useState(false);
  const [audiobookUpload, setAudiobookUpload] = useState(() => createInitialAudiobookUploadState());
  const [ripperJobsRefreshToken, setRipperJobsRefreshToken] = useState(0);
  const [historyJobsRefreshToken, setHistoryJobsRefreshToken] = useState(0);
  const [downloadsRefreshToken, setDownloadsRefreshToken] = useState(0);
  const [downloadSummary, setDownloadSummary] = useState(null);
  const [pendingRipperJobId, setPendingRipperJobId] = useState(null);
  const location = useLocation();
  const navigate = useNavigate();
  const globalToastRef = useRef(null);
  const prevCdDrivesRef = useRef({});

  // When a virtual CD drive is removed (CD encode/rip finished or failed),
  // or when a CD drive transitions away from an active job, force both
  // Ripper and History to re-fetch so jobs leave the live list reliably.
  useEffect(() => {
    const current = pipeline?.cdDrives || {};
    const prev = prevCdDrivesRef.current;
    const normalizeState = (value) => String(value || '').trim().toUpperCase();
    const ACTIVE_CD_STATES = new Set(['CD_ANALYZING', 'CD_METADATA_SELECTION', 'CD_READY_TO_RIP', 'CD_RIPPING', 'CD_ENCODING']);
    const TERMINAL_CD_STATES = new Set(['FINISHED', 'ERROR', 'CANCELLED']);
    let shouldRefresh = false;

    for (const [devicePath, previousDrive] of Object.entries(prev)) {
      const previousJobId = normalizeJobId(previousDrive?.jobId);
      if (!previousJobId) {
        continue;
      }
      const previousState = normalizeState(previousDrive?.state);
      const currentDrive = current?.[devicePath] || null;
      const currentJobId = normalizeJobId(currentDrive?.jobId);
      const currentState = normalizeState(currentDrive?.state);
      const driveRemoved = !currentDrive;
      const jobChanged = currentJobId !== previousJobId;
      const movedToTerminal = currentJobId === previousJobId
        && TERMINAL_CD_STATES.has(currentState)
        && currentState !== previousState;
      const leftActiveLifecycle = ACTIVE_CD_STATES.has(previousState)
        && (driveRemoved || jobChanged || !ACTIVE_CD_STATES.has(currentState));
      if (movedToTerminal || leftActiveLifecycle) {
        shouldRefresh = true;
        break;
      }
    }

    if (shouldRefresh) {
      setRipperJobsRefreshToken((t) => t + 1);
      setHistoryJobsRefreshToken((t) => t + 1);
    }
    prevCdDrivesRef.current = current;
  }, [pipeline?.cdDrives]);

  const refreshPipeline = async () => {
    const response = await api.getPipelineState();
    setPipeline(response.pipeline);
    setHardwareMonitoring(response?.hardwareMonitoring || null);
    return response;
  };

  const clearAudiobookUpload = () => {
    setAudiobookUpload(createInitialAudiobookUploadState());
  };

  const handleAudiobookUpload = async (file, payload = {}) => {
    if (!file) {
      throw new Error('Bitte zuerst eine AAX-Datei auswählen.');
    }

    const fallbackTotalBytes = Number.isFinite(Number(file.size)) && Number(file.size) > 0
      ? Number(file.size)
      : 0;

    setAudiobookUpload({
      phase: 'uploading',
      fileName: String(file.name || '').trim() || 'upload.aax',
      loadedBytes: 0,
      totalBytes: fallbackTotalBytes,
      progressPercent: 0,
      statusText: 'AAX-Datei wird hochgeladen ...',
      errorMessage: null,
      jobId: null,
      startedAt: new Date().toISOString(),
      finishedAt: null
    });

    try {
      const response = await api.uploadAudiobook(file, payload, {
        onProgress: ({ loaded, total, percent }) => {
          const nextLoaded = Number.isFinite(Number(loaded)) && Number(loaded) >= 0
            ? Number(loaded)
            : 0;
          const nextTotal = Number.isFinite(Number(total)) && Number(total) > 0
            ? Number(total)
            : fallbackTotalBytes;
          const nextPercent = Number.isFinite(Number(percent))
            ? clampPercent(Number(percent))
            : (nextTotal > 0 ? clampPercent((nextLoaded / nextTotal) * 100) : 0);
          const transferComplete = nextTotal > 0 && nextLoaded >= nextTotal;

          setAudiobookUpload((prev) => ({
            ...prev,
            phase: transferComplete ? 'processing' : 'uploading',
            loadedBytes: nextLoaded,
            totalBytes: nextTotal,
            progressPercent: nextPercent,
            statusText: transferComplete
              ? 'Upload abgeschlossen, AAX wird serverseitig verarbeitet ...'
              : 'AAX-Datei wird hochgeladen ...'
          }));
        }
      });

      const uploadedJobId = normalizeJobId(response?.result?.jobId);
      await refreshPipeline().catch(() => null);
      setRipperJobsRefreshToken((prev) => prev + 1);
      setHistoryJobsRefreshToken((prev) => prev + 1);
      if (uploadedJobId) {
        setPendingRipperJobId(uploadedJobId);
      }

      setAudiobookUpload((prev) => ({
        ...prev,
        phase: 'completed',
        loadedBytes: prev.totalBytes || prev.loadedBytes || fallbackTotalBytes,
        totalBytes: prev.totalBytes || fallbackTotalBytes,
        progressPercent: 100,
        statusText: uploadedJobId
          ? `Upload abgeschlossen. Job #${uploadedJobId} ist bereit fuer den naechsten Schritt.`
          : 'Upload abgeschlossen.',
        errorMessage: null,
        jobId: uploadedJobId,
        finishedAt: new Date().toISOString()
      }));

      return response;
    } catch (error) {
      setAudiobookUpload((prev) => ({
        ...prev,
        phase: 'error',
        errorMessage: error?.message || 'Upload fehlgeschlagen.',
        statusText: error?.message || 'Upload fehlgeschlagen.',
        finishedAt: new Date().toISOString()
      }));
      throw error;
    }
  };

  const handleRipperJobFocusConsumed = (jobId) => {
    const normalizedJobId = normalizeJobId(jobId);
    if (!normalizedJobId) {
      return;
    }
    setPendingRipperJobId((prev) => (
      normalizeJobId(prev) === normalizedJobId ? null : prev
    ));
  };

  useEffect(() => {
    refreshPipeline().catch(() => null);
    api.getDownloadsSummary()
      .then((response) => {
        setDownloadSummary(response?.summary || null);
      })
      .catch(() => null);
    api.getSettings()
      .then((response) => {
        const allSettings = (response?.categories || []).flatMap((c) => c.settings || []);
        const val = allSettings.find((s) => s.key === 'ui_expert_mode')?.value;
        setExpertMode(val === 'true' || val === true);
      })
      .catch(() => null);
  }, []);

  useWebSocket({
    onMessage: (message) => {
      if (message.type === 'PIPELINE_STATE_CHANGED') {
        setPipeline(message.payload);
      }

      if (message.type === 'PIPELINE_PROGRESS') {
        const payload = message.payload;
        const progressJobId = payload?.activeJobId;
        const contextPatch = payload?.contextPatch && typeof payload.contextPatch === 'object'
          ? payload.contextPatch
          : null;
        setPipeline((prev) => {
          const next = { ...prev };
          const normalizedProgressJobId = normalizeJobId(progressJobId);
          const progressStage = normalizeStage(payload?.state);
          const isCdProgressStage = progressStage === 'CD_ANALYZING'
            || progressStage === 'CD_RIPPING'
            || progressStage === 'CD_ENCODING';
          const incomingIsRunning = isRunningStage(progressStage);
          const incomingIsTerminal = isTerminalStage(progressStage);
          const prevActiveJobId = normalizeJobId(prev?.activeJobId || prev?.context?.jobId);
          const prevJobProgress = normalizedProgressJobId
            ? (prev?.jobProgress?.[normalizedProgressJobId] || null)
            : null;
          const prevJobProgressState = normalizeStage(prevJobProgress?.state);
          const prevJobProgressIsRunning = isRunningStage(prevJobProgressState);
          const prevJobProgressIsTerminal = isTerminalStage(prevJobProgressState);
          const matchingCdDriveEntry = normalizedProgressJobId
            ? Object.values(prev?.cdDrives || {}).find((driveState) => normalizeJobId(driveState?.jobId) === normalizedProgressJobId)
            : null;
          const matchingCdDriveState = normalizeStage(matchingCdDriveEntry?.state);
          const matchingCdDriveIsRunning = isRunningStage(matchingCdDriveState);
          const matchingCdDriveIsTerminal = isTerminalStage(matchingCdDriveEntry?.state);
          const previousGlobalStage = normalizeStage(prev?.state);
          const previousGlobalIsRunning = isRunningStage(previousGlobalStage);
          const previousGlobalIsTerminal = isTerminalStage(previousGlobalStage);
          const hasKnownBinding = Boolean(
            normalizedProgressJobId
            && (
              prevActiveJobId === normalizedProgressJobId
              || prevJobProgress
              || matchingCdDriveEntry
            )
          );
          const regressesStableJobState = Boolean(
            normalizedProgressJobId
            && incomingIsRunning
            && (
              (prevJobProgressState && !prevJobProgressIsRunning && !prevJobProgressIsTerminal)
              || (matchingCdDriveState && !matchingCdDriveIsRunning && !matchingCdDriveIsTerminal)
              || (
                prevActiveJobId === normalizedProgressJobId
                && previousGlobalStage
                && !previousGlobalIsRunning
                && !previousGlobalIsTerminal
              )
            )
          );

          // Ignore late/stale progress packets that arrive after a terminal state
          // or regress a job from an interactive/ready state back into an
          // earlier running stage because an older progress packet arrived late.
          if (
            normalizedProgressJobId
            && !incomingIsTerminal
            && (
              prevJobProgressIsTerminal
              || matchingCdDriveIsTerminal
              || !hasKnownBinding
              || regressesStableJobState
            )
          ) {
            return prev;
          }

          if (progressJobId != null) {
            const previousJobProgress = prev?.jobProgress?.[progressJobId] || {};
            const previousJobStage = normalizeStage(previousJobProgress?.state);
            const keepPreviousJobStage = isTerminalStage(previousJobStage) && !incomingIsTerminal;
            const mergedJobContext = contextPatch
              ? {
                ...(previousJobProgress?.context && typeof previousJobProgress.context === 'object'
                  ? previousJobProgress.context
                  : {}),
                ...contextPatch
              }
              : (previousJobProgress?.context && typeof previousJobProgress.context === 'object'
                  ? previousJobProgress.context
                  : undefined);
            next.jobProgress = {
              ...(prev?.jobProgress || {}),
              [progressJobId]: {
                ...previousJobProgress,
                state: keepPreviousJobStage ? previousJobProgress.state : payload.state,
                progress: keepPreviousJobStage ? previousJobProgress.progress : payload.progress,
                eta: keepPreviousJobStage ? previousJobProgress.eta : payload.eta,
                statusText: keepPreviousJobStage ? previousJobProgress.statusText : payload.statusText,
                ...(mergedJobContext !== undefined ? { context: mergedJobContext } : {})
              }
            };
          }
          if (progressJobId === prev?.activeJobId || progressJobId == null) {
            const keepPreviousGlobalStage = isTerminalStage(previousGlobalStage) && !incomingIsTerminal;
            next.state = keepPreviousGlobalStage ? prev?.state : (payload.state ?? prev?.state);
            next.progress = keepPreviousGlobalStage ? prev?.progress : (payload.progress ?? prev?.progress);
            next.eta = keepPreviousGlobalStage ? prev?.eta : (payload.eta ?? prev?.eta);
            next.statusText = keepPreviousGlobalStage ? prev?.statusText : (payload.statusText ?? prev?.statusText);
            if (contextPatch) {
              next.context = {
                ...(prev?.context && typeof prev.context === 'object' ? prev.context : {}),
                ...contextPatch
              };
            }
          }

          // Keep per-drive CD progress in sync with live progress events.
          // Backend sends frequent PIPELINE_PROGRESS updates, while cdDrives
          // snapshots are only broadcast on state transitions.
          if (isCdProgressStage && prev?.cdDrives && typeof prev.cdDrives === 'object') {
            const cdDrivesEntries = Object.entries(prev.cdDrives);
            const nextCdDrives = { ...prev.cdDrives };
            const patchDrive = (driveState) => {
              const currentDriveStage = normalizeStage(driveState?.state);
              if (isTerminalStage(currentDriveStage) && !incomingIsTerminal) {
                return driveState;
              }
              const mergedContext = contextPatch
                ? {
                  ...(driveState?.context && typeof driveState.context === 'object' ? driveState.context : {}),
                  ...contextPatch
                }
                : driveState?.context;
              return {
                ...driveState,
                state: payload?.state ?? driveState?.state,
                progress: payload?.progress ?? driveState?.progress,
                eta: payload?.eta ?? driveState?.eta,
                statusText: payload?.statusText ?? driveState?.statusText,
                ...(mergedContext !== undefined ? { context: mergedContext } : {})
              };
            };

            let updated = false;
            if (normalizedProgressJobId) {
              for (const [drivePath, driveState] of cdDrivesEntries) {
                if (normalizeJobId(driveState?.jobId) === normalizedProgressJobId) {
                  nextCdDrives[drivePath] = patchDrive(driveState);
                  updated = true;
                }
              }
            }

            if (!updated) {
              const activeCdEntries = cdDrivesEntries.filter(([, driveState]) => {
                const driveStage = String(driveState?.state || '').trim().toUpperCase();
                return driveStage === 'CD_ANALYZING'
                  || driveStage === 'CD_RIPPING'
                  || driveStage === 'CD_ENCODING';
              });
              if (activeCdEntries.length === 1) {
                const [drivePath, driveState] = activeCdEntries[0];
                nextCdDrives[drivePath] = patchDrive(driveState);
                updated = true;
              }
            }

            if (updated) {
              next.cdDrives = nextCdDrives;
            }
          }

          return next;
        });
      }

      if (message.type === 'PIPELINE_QUEUE_CHANGED') {
        setPipeline((prev) => ({
          ...(prev || {}),
          queue: message.payload || null
        }));
        setRipperJobsRefreshToken((prev) => prev + 1);
        setHistoryJobsRefreshToken((prev) => prev + 1);
      }

      if (message.type === 'DISC_DETECTED') {
        setLastDiscEvent(message.payload?.device || null);
        refreshPipeline().catch(() => null);
      }

      if (message.type === 'DISC_REMOVED') {
        setLastDiscEvent(null);
      }

      if (message.type === 'HARDWARE_MONITOR_UPDATE') {
        setHardwareMonitoring(message.payload || null);
      }

      if (message.type === 'SETTINGS_UPDATED') {
        const setting = message.payload;
        if (setting?.key === 'ui_expert_mode') {
          const val = setting?.value;
          setExpertMode(val === 'true' || val === true);
        }
      }

      if (message.type === 'SETTINGS_BULK_UPDATED') {
        const keys = message.payload?.keys || [];
        if (keys.includes('ui_expert_mode')) {
          api.getSettings({ forceRefresh: true })
            .then((response) => {
              const allSettings = (response?.categories || []).flatMap((c) => c.settings || []);
              const val = allSettings.find((s) => s.key === 'ui_expert_mode')?.value;
              setExpertMode(val === 'true' || val === true);
            })
            .catch(() => null);
        }
      }

      if (message.type === 'DOWNLOADS_UPDATED') {
        const summary = message.payload?.summary && typeof message.payload.summary === 'object'
          ? message.payload.summary
          : null;
        const reason = String(message.payload?.reason || '').trim().toLowerCase();
        const item = message.payload?.item && typeof message.payload.item === 'object'
          ? message.payload.item
          : null;

        if (summary) {
          setDownloadSummary(summary);
        }
        setDownloadsRefreshToken((prev) => prev + 1);

        if (reason === 'ready' && item) {
          globalToastRef.current?.show({
            severity: 'success',
            summary: 'ZIP fertig',
            detail: `${item.archiveName || 'ZIP-Datei'} steht jetzt auf der Downloads-Seite bereit.`,
            life: 4500
          });
        }

        if (reason === 'failed' && item) {
          globalToastRef.current?.show({
            severity: 'error',
            summary: 'ZIP fehlgeschlagen',
            detail: item.errorMessage || `${item.archiveName || 'ZIP-Datei'} konnte nicht erstellt werden.`,
            life: 5000
          });
        }
      }
    }
  });

  const nav = [
    { label: 'Jobs', path: '/jobs' },
    { label: 'Ripper', path: '/ripper' },
    { label: 'Converter', path: '/converter' },
    { label: 'Audiobooks', path: '/audiobooks' },
    { label: 'Settings', path: '/settings' },
    { label: 'Historie', path: '/history' },
    { label: 'Downloads', path: '/downloads' },
    ...(expertMode ? [{ label: 'Database', path: '/database' }] : [])
  ];
  const uploadPhase = String(audiobookUpload?.phase || 'idle').trim().toLowerCase();
  const showAudiobookUploadBanner = uploadPhase !== 'idle';
  const uploadProgress = clampPercent(audiobookUpload?.progressPercent);
  const uploadTagMeta = getAudiobookUploadTagMeta(uploadPhase);
  const uploadLoadedBytes = Number(audiobookUpload?.loadedBytes || 0);
  const uploadTotalBytes = Number(audiobookUpload?.totalBytes || 0);
  const uploadBytesLabel = uploadTotalBytes > 0
    ? `${formatBytes(uploadLoadedBytes)} / ${formatBytes(uploadTotalBytes)}`
    : (uploadLoadedBytes > 0 ? `${formatBytes(uploadLoadedBytes)} hochgeladen` : null);
  const canDismissUploadBanner = uploadPhase === 'completed' || uploadPhase === 'error';
  const hasUploadedJob = Boolean(normalizeJobId(audiobookUpload?.jobId));
  const isAudiobooksRoute = location.pathname === '/audiobooks';
  const downloadIndicator = getDownloadIndicatorMeta(downloadSummary);
  const isNavActive = (path) => {
    if (path === '/ripper') {
      return location.pathname === '/' || location.pathname === '/ripper';
    }
    return location.pathname === path;
  };

  return (
    <div className="app-shell">
      <Toast ref={globalToastRef} position="top-right" />
      <ConfirmDialog />

      <header className="app-header">
        <div className="brand-block">
          <img src="/logo.png" alt="Ripster Logo" className="brand-logo" />
          <div className="brand-copy">
            <h1>Ripster</h1>
            <div className="brand-meta">
              <p>Disc Ripping Control Center</p>
              <span className="app-version" aria-label={`Version ${appVersion}`}>
                v{appVersion}
              </span>
            </div>
          </div>
        </div>
        <div className="nav-buttons">
          {nav.map((item) => (
            <Button
              key={item.path}
              label={item.label}
              onClick={() => navigate(item.path)}
              className={isNavActive(item.path) ? 'nav-btn nav-btn-active' : 'nav-btn'}
              outlined={!isNavActive(item.path)}
            />
          ))}
        </div>
      </header>

      {showAudiobookUploadBanner ? (
        <section className={`app-upload-banner phase-${uploadPhase}`}>
          <div className="app-upload-banner-copy">
            <div className="app-upload-banner-head">
              <strong>Audiobook Upload</strong>
              <Tag value={uploadTagMeta.label} severity={uploadTagMeta.severity} />
            </div>
            <small>{audiobookUpload?.statusText || 'Upload aktiv.'}</small>
            {audiobookUpload?.fileName ? <small>Datei: {audiobookUpload.fileName}</small> : null}
          </div>

          <div
            className="app-upload-banner-progress"
            aria-label={`Audiobook Upload ${Math.trunc(uploadProgress)} Prozent`}
          >
            <ProgressBar value={uploadProgress} showValue={false} />
            <small>
              {uploadPhase === 'processing'
                ? `100% | ${uploadBytesLabel || 'Upload abgeschlossen'}`
                : uploadBytesLabel
                  ? `${Math.trunc(uploadProgress)}% | ${uploadBytesLabel}`
                  : `${Math.trunc(uploadProgress)}%`}
            </small>
          </div>

          <div className="app-upload-banner-actions">
            {hasUploadedJob && !isAudiobooksRoute ? (
              <Button
                label="Zu Audiobooks"
                icon="pi pi-arrow-right"
                severity="secondary"
                outlined
                onClick={() => {
                  navigate('/audiobooks');
                }}
              />
            ) : null}
            {canDismissUploadBanner ? (
              <Button
                icon="pi pi-times"
                rounded
                text
                severity="secondary"
                aria-label="Upload-Hinweis schliessen"
                onClick={clearAudiobookUpload}
              />
            ) : null}
          </div>
        </section>
      ) : null}

      <main className="app-main">
        <Routes>
          <Route
            path="/"
            element={
              <RipperPage
                pipeline={pipeline}
                hardwareMonitoring={hardwareMonitoring}
                lastDiscEvent={lastDiscEvent}
                refreshPipeline={refreshPipeline}
                jobsRefreshToken={ripperJobsRefreshToken}
                pendingExpandedJobId={pendingRipperJobId}
                onPendingExpandedJobHandled={handleRipperJobFocusConsumed}
                downloadSummary={downloadSummary}
              >
                <Outlet />
              </RipperPage>
            }
          >
            <Route index element={<Navigate to="jobs" replace />} />
            <Route path="ripper" element={null} />
            <Route path="settings" element={<SettingsPage />} />
            <Route path="history" element={<HistoryPage refreshToken={historyJobsRefreshToken} />} />
            <Route path="downloads" element={<DownloadsPage refreshToken={downloadsRefreshToken} />} />
            <Route path="database" element={<DatabasePage />} />
            <Route path="jobs" element={<JobsInboxPage />} />
            <Route path="converter" element={<ConverterPage />} />
            <Route
              path="audiobooks"
              element={
                <AudiobooksPage
                  audiobookUpload={audiobookUpload}
                  onAudiobookUpload={handleAudiobookUpload}
                />
              }
            />
          </Route>
        </Routes>
      </main>
    </div>
  );
}

export default App;
