const { getDb } = require('../db/database');
const logger = require('./logger').child('HISTORY');
const fs = require('fs');
const path = require('path');
const { defaultConverterRawDir } = require('../config');
const settingsService = require('./settingsService');
const omdbService = require('./omdbService');
const tmdbService = require('./tmdbService');
const cdRipService = require('./cdRipService');
const { getJobLogDir } = require('./logPathService');
const thumbnailService = require('./thumbnailService');

function parseJsonSafe(raw, fallback = null) {
  if (!raw) {
    return fallback;
  }

  try {
    return JSON.parse(raw);
  } catch (error) {
    return fallback;
  }
}

const PROCESS_LOG_TAIL_MAX_BYTES = 1024 * 1024;
const processLogStreams = new Map();
const PROFILE_PATH_SUFFIXES = ['bluray', 'dvd', 'cd', 'audiobook', 'converter', 'other'];
const RAW_INCOMPLETE_PREFIX = 'Incomplete_';
const RAW_RIP_COMPLETE_PREFIX = 'Rip_Complete_';
const PROCESS_LOG_FILE_PATTERN = /^job-(\d+)\.process\.log$/i;
const CD_OUTPUT_AUDIO_EXTENSIONS = new Set(['.flac', '.wav', '.mp3', '.opus', '.ogg']);
const CD_RAW_TRACK_FILE_PATTERN = /^track(\d{1,3})\.cdda\.wav$/i;

function inspectDirectory(dirPath) {
  if (!dirPath) {
    return {
      path: null,
      exists: false,
      isDirectory: false,
      isEmpty: null,
      entryCount: null
    };
  }

  try {
    const stat = fs.statSync(dirPath);
    if (!stat.isDirectory()) {
      return {
        path: dirPath,
        exists: true,
        isDirectory: false,
        isEmpty: null,
        entryCount: null
      };
    }

    // Fast path: only determine whether directory is empty, avoid loading all entries.
    let firstEntry = null;
    let openError = null;
    try {
      const dir = fs.opendirSync(dirPath);
      try {
        firstEntry = dir.readSync();
      } finally {
        dir.closeSync();
      }
    } catch (error) {
      openError = error;
    }
    if (openError) {
      const entries = fs.readdirSync(dirPath);
      return {
        path: dirPath,
        exists: true,
        isDirectory: true,
        isEmpty: entries.length === 0,
        entryCount: entries.length
      };
    }
    return {
      path: dirPath,
      exists: true,
      isDirectory: true,
      isEmpty: !firstEntry,
      entryCount: firstEntry ? null : 0
    };
  } catch (error) {
    return {
      path: dirPath,
      exists: false,
      isDirectory: false,
      isEmpty: null,
      entryCount: null
    };
  }
}

function inspectOutputFile(filePath) {
  if (!filePath) {
    return {
      path: null,
      exists: false,
      isFile: false,
      sizeBytes: null
    };
  }

  try {
    const stat = fs.statSync(filePath);
    return {
      path: filePath,
      exists: true,
      isFile: stat.isFile(),
      sizeBytes: stat.size
    };
  } catch (error) {
    return {
      path: filePath,
      exists: false,
      isFile: false,
      sizeBytes: null
    };
  }
}

function parseInfoFromValue(value, fallback = null) {
  if (!value) {
    return fallback;
  }
  if (typeof value === 'object') {
    return value;
  }
  return parseJsonSafe(value, fallback);
}

function hasBlurayStructure(rawPath) {
  const basePath = String(rawPath || '').trim();
  if (!basePath) {
    return false;
  }

  const bdmvPath = path.join(basePath, 'BDMV');
  const streamPath = path.join(bdmvPath, 'STREAM');

  try {
    if (fs.existsSync(streamPath)) {
      const streamStat = fs.statSync(streamPath);
      if (streamStat.isDirectory()) {
        return true;
      }
    }
  } catch (_error) {
    // ignore fs errors and continue with fallback checks
  }

  try {
    if (fs.existsSync(bdmvPath)) {
      const bdmvStat = fs.statSync(bdmvPath);
      if (bdmvStat.isDirectory()) {
        return true;
      }
    }
  } catch (_error) {
    // ignore fs errors
  }

  return false;
}

function hasCdStructure(rawPath) {
  const basePath = String(rawPath || '').trim();
  if (!basePath) {
    return false;
  }

  try {
    if (!fs.existsSync(basePath)) {
      return false;
    }
    const stat = fs.statSync(basePath);
    if (!stat.isDirectory()) {
      return false;
    }
    const entries = fs.readdirSync(basePath);
    const audioExtensions = new Set(['.flac', '.wav', '.mp3', '.opus', '.ogg', '.aiff', '.aif']);
    return entries.some((entry) => audioExtensions.has(path.extname(entry).toLowerCase()));
  } catch (_error) {
    return false;
  }
}

function hasAudiobookStructure(rawPath) {
  const basePath = String(rawPath || '').trim();
  if (!basePath) {
    return false;
  }

  try {
    if (!fs.existsSync(basePath)) {
      return false;
    }
    const stat = fs.statSync(basePath);
    if (stat.isFile()) {
      return path.extname(basePath).toLowerCase() === '.aax';
    }
    if (!stat.isDirectory()) {
      return false;
    }
    const entries = fs.readdirSync(basePath);
    return entries.some((entry) => path.extname(entry).toLowerCase() === '.aax');
  } catch (_error) {
    return false;
  }
}

function hasNestedMediaStructure(rawPath, detector, maxDepth = 2) {
  const startPath = String(rawPath || '').trim();
  if (!startPath || typeof detector !== 'function') {
    return false;
  }

  const normalizedMaxDepth = Number.isFinite(Number(maxDepth))
    ? Math.max(0, Math.trunc(Number(maxDepth)))
    : 0;
  const queue = [{ currentPath: startPath, depth: 0 }];
  const visited = new Set();

  while (queue.length > 0) {
    const next = queue.shift();
    const currentPath = String(next?.currentPath || '').trim();
    const depth = Number(next?.depth || 0);
    if (!currentPath) {
      continue;
    }
    const normalizedPath = normalizeComparablePath(currentPath);
    if (!normalizedPath || visited.has(normalizedPath)) {
      continue;
    }
    visited.add(normalizedPath);

    if (detector(currentPath)) {
      return true;
    }
    if (depth >= normalizedMaxDepth) {
      continue;
    }

    try {
      const entries = fs.readdirSync(currentPath, { withFileTypes: true });
      for (const entry of entries) {
        if (!entry?.isDirectory?.() || isHiddenDirectoryName(entry.name)) {
          continue;
        }
        queue.push({
          currentPath: path.join(currentPath, entry.name),
          depth: depth + 1
        });
      }
    } catch (_error) {
      // ignore fs errors while traversing nested structures
    }
  }

  return false;
}

function detectOrphanMediaType(rawPath, options = {}) {
  if (hasBlurayStructure(rawPath)) {
    return 'bluray';
  }
  if (hasDvdStructure(rawPath)) {
    return 'dvd';
  }
  // Some RAW folders contain one or two nested disc directories (e.g. ".../Rip_Complete_XYZ/.../VIDEO_TS").
  // Inspect nested folders as fallback so these imports are not classified as "other".
  if (hasNestedMediaStructure(rawPath, hasBlurayStructure, 2)) {
    return 'bluray';
  }
  if (hasNestedMediaStructure(rawPath, hasDvdStructure, 2)) {
    return 'dvd';
  }
  if (hasCdStructure(rawPath)) {
    return 'cd';
  }
  if (hasAudiobookStructure(rawPath)) {
    return 'audiobook';
  }
  if (options?.seriesRawPathHint) {
    return 'dvd';
  }
  return 'other';
}

function hasDvdStructure(rawPath) {
  const basePath = String(rawPath || '').trim();
  if (!basePath) {
    return false;
  }

  const videoTsPath = path.join(basePath, 'VIDEO_TS');
  try {
    if (fs.existsSync(videoTsPath)) {
      const stat = fs.statSync(videoTsPath);
      if (stat.isDirectory()) {
        return true;
      }
    }
  } catch (_error) {
    // ignore fs errors
  }

  try {
    if (fs.existsSync(basePath)) {
      const stat = fs.statSync(basePath);
      if (stat.isDirectory()) {
        const entries = fs.readdirSync(basePath);
        if (entries.some((entry) => /^vts_\d{2}_\d\.(ifo|vob|bup)$/i.test(entry) || /^video_ts\.(ifo|vob|bup)$/i.test(entry))) {
          return true;
        }
      } else if (stat.isFile()) {
        return /(^|\/)video_ts\/.+\.(ifo|vob|bup)$/i.test(basePath) || /\.(ifo|vob|bup)$/i.test(basePath);
      }
    }
  } catch (_error) {
    // ignore fs errors and fallback to path checks
  }

  if (/(^|\/)video_ts(\/|$)/i.test(basePath)) {
    return true;
  }

  return false;
}

function normalizeMediaTypeValue(value) {
  const raw = String(value || '').trim().toLowerCase();
  if (!raw) {
    return null;
  }
  if (
    raw === 'bluray'
    || raw === 'blu-ray'
    || raw === 'blu_ray'
    || raw === 'bd'
    || raw === 'bdmv'
    || raw === 'bdrom'
    || raw === 'bd-rom'
    || raw === 'bd-r'
    || raw === 'bd-re'
  ) {
    return 'bluray';
  }
  if (
    raw === 'dvd'
    || raw === 'dvdvideo'
    || raw === 'dvd-video'
    || raw === 'dvdrom'
    || raw === 'dvd-rom'
    || raw === 'video_ts'
    || raw === 'iso9660'
  ) {
    return 'dvd';
  }
  if (raw === 'cd' || raw === 'audio_cd') {
    return 'cd';
  }
  if (raw === 'audiobook' || raw === 'audio_book' || raw === 'audio book' || raw === 'book') {
    return 'audiobook';
  }
  return null;
}

function normalizeJobKindValue(value) {
  const raw = String(value || '').trim().toLowerCase();
  if (!raw) {
    return null;
  }
  if (raw === 'audiobook' || raw === 'cd' || raw === 'dvd' || raw === 'bluray') {
    return raw;
  }
  if (raw === 'converter_audio' || raw === 'converter_video' || raw === 'converter_iso') {
    return raw;
  }
  return null;
}

function inferMediaTypeFromJobKind(value) {
  const normalized = normalizeJobKindValue(value);
  if (normalized === 'converter_audio' || normalized === 'converter_video' || normalized === 'converter_iso') {
    return 'converter';
  }
  if (normalized === 'audiobook' || normalized === 'cd' || normalized === 'dvd' || normalized === 'bluray') {
    return normalized;
  }
  const legacy = String(value || '').trim().toLowerCase();
  if (legacy === 'converter') {
    return 'converter';
  }
  return null;
}

function inferMediaType(job, makemkvInfo, mediainfoInfo, encodePlan, handbrakeInfo = null) {
  const mkInfo = parseInfoFromValue(makemkvInfo, null);
  const miInfo = parseInfoFromValue(mediainfoInfo, null);
  const plan = parseInfoFromValue(encodePlan, null);
  const hbInfo = parseInfoFromValue(handbrakeInfo, null);
  const rawPath = String(job?.raw_path || '').trim();
  const encodeInputPath = String(job?.encode_input_path || plan?.encodeInputPath || '').trim();
  const profileHint = normalizeMediaTypeValue(
    plan?.mediaProfile
    || mkInfo?.analyzeContext?.mediaProfile
    || mkInfo?.mediaProfile
    || miInfo?.mediaProfile
    || job?.media_type
    || job?.mediaType
  );

  if (profileHint === 'bluray' || profileHint === 'dvd' || profileHint === 'cd' || profileHint === 'audiobook') {
    return profileHint;
  }

  const profileFromJobKind = inferMediaTypeFromJobKind(
    job?.job_kind
    || plan?.jobKind
    || mkInfo?.jobKind
    || mkInfo?.analyzeContext?.jobKind
    || miInfo?.jobKind
    || hbInfo?.jobKind
  );
  if (profileFromJobKind) {
    return profileFromJobKind;
  }

  // Converter jobs: detected via media_type field (plan.mediaProfile = 'converter')
  const rawMediaType = normalizeMediaTypeValue(job?.media_type);
  const rawMediaTypeLegacy = String(job?.media_type || '').trim().toLowerCase();
  const rawPlanProfile = String(plan?.mediaProfile || '').trim().toLowerCase();
  const converterMediaTypeHint = String(plan?.converterMediaType || '').trim().toLowerCase();
  const hasConverterPathHint = (value) => {
    const normalized = String(value || '')
      .trim()
      .replace(/\\/g, '/')
      .toLowerCase();
    if (!normalized) {
      return false;
    }
    return /(^|\/)converter(\/|$)/.test(normalized);
  };
  if (
    rawPlanProfile === 'converter'
    || rawMediaType === 'converter'
    || rawMediaTypeLegacy === 'converter'
    || converterMediaTypeHint === 'video'
    || converterMediaTypeHint === 'audio'
    || converterMediaTypeHint === 'iso'
    || hasConverterPathHint(rawPath)
    || hasConverterPathHint(encodeInputPath)
    || hasConverterPathHint(plan?.inputPath)
  ) {
    return 'converter';
  }

  const statusCandidates = [
    job?.status,
    job?.last_state,
    mkInfo?.lastState
  ];
  if (statusCandidates.some((value) => String(value || '').trim().toUpperCase().startsWith('CD_'))) {
    return 'cd';
  }

  const planFormat = String(plan?.format || '').trim().toLowerCase();
  const hasCdTracksInPlan = Array.isArray(plan?.selectedTracks) && plan.selectedTracks.length > 0;
  if (hasCdTracksInPlan && ['flac', 'wav', 'mp3', 'opus', 'ogg'].includes(planFormat)) {
    return 'cd';
  }
  if (String(hbInfo?.mode || '').trim().toLowerCase() === 'cd_rip') {
    return 'cd';
  }
  if (Array.isArray(mkInfo?.tracks) && mkInfo.tracks.length > 0) {
    return 'cd';
  }
  if (hasAudiobookStructure(rawPath) || hasAudiobookStructure(encodeInputPath)) {
    return 'audiobook';
  }
  if (['audiobook_encode', 'audiobook_encode_split'].includes(String(hbInfo?.mode || '').trim().toLowerCase())) {
    return 'audiobook';
  }
  if (String(plan?.mode || '').trim().toLowerCase() === 'audiobook') {
    return 'audiobook';
  }

  if (hasBlurayStructure(rawPath)) {
    return 'bluray';
  }
  if (hasDvdStructure(rawPath)) {
    return 'dvd';
  }

  const mkSource = String(mkInfo?.source || '').trim().toLowerCase();
  const mkRipMode = String(mkInfo?.ripMode || mkInfo?.rip_mode || '').trim().toLowerCase();
  if (Boolean(mkInfo?.analyzeContext?.playlistAnalysis)) {
    return 'bluray';
  }
  if (mkRipMode === 'backup' || mkSource.includes('backup') || mkSource.includes('raw_backup')) {
    if (hasDvdStructure(rawPath) || hasDvdStructure(encodeInputPath)) {
      return 'dvd';
    }
    if (hasBlurayStructure(rawPath) || hasBlurayStructure(encodeInputPath)) {
      return 'bluray';
    }
  }

  const planMode = String(plan?.mode || '').trim().toLowerCase();
  if (planMode === 'pre_rip' || Boolean(plan?.preRip)) {
    return 'bluray';
  }

  const mediainfoSource = String(miInfo?.source || '').trim().toLowerCase();
  if (Number(miInfo?.handbrakeTitleId) > 0) {
    return 'bluray';
  }
  if (mediainfoSource.includes('raw_backup')) {
    if (hasDvdStructure(rawPath) || hasDvdStructure(encodeInputPath)) {
      return 'dvd';
    }
    if (hasBlurayStructure(rawPath) || hasBlurayStructure(encodeInputPath)) {
      return 'bluray';
    }
  }

  if (
    /(^|\/)bdmv(\/|$)/i.test(rawPath)
    || /(^|\/)bdmv(\/|$)/i.test(encodeInputPath)
    || /\.m2ts(\.|$)/i.test(encodeInputPath)
  ) {
    return 'bluray';
  }
  if (
    /(^|\/)video_ts(\/|$)/i.test(rawPath)
    || /(^|\/)video_ts(\/|$)/i.test(encodeInputPath)
    || /\.(ifo|vob|bup)(\.|$)/i.test(encodeInputPath)
  ) {
    return 'dvd';
  }

  return profileHint || 'other';
}

function toProcessLogPath(jobId) {
  const normalizedId = Number(jobId);
  if (!Number.isFinite(normalizedId) || normalizedId <= 0) {
    return null;
  }
  return path.join(getJobLogDir(), `job-${Math.trunc(normalizedId)}.process.log`);
}

function hasProcessLogFile(jobId) {
  const filePath = toProcessLogPath(jobId);
  return Boolean(filePath && fs.existsSync(filePath));
}

function toProcessLogStreamKey(jobId) {
  const normalizedId = Number(jobId);
  if (!Number.isFinite(normalizedId) || normalizedId <= 0) {
    return null;
  }
  return String(Math.trunc(normalizedId));
}

function resolveEffectiveRawPath(storedPath, rawDir, extraDirs = []) {
  const stored = String(storedPath || '').trim();
  if (!stored) return stored;
  const baseFolderName = path.basename(stored);
  if (!baseFolderName) return stored;
  const folderNames = [baseFolderName];
  const stripped = stripRawFolderStatePrefix(baseFolderName);
  if (stripped) {
    folderNames.push(applyRawFolderPrefix(stripped, RAW_RIP_COMPLETE_PREFIX));
    folderNames.push(applyRawFolderPrefix(stripped, RAW_INCOMPLETE_PREFIX));
  }

  const candidates = [];
  const seen = new Set();
  const pushCandidate = (candidatePath) => {
    const normalized = String(candidatePath || '').trim();
    if (!normalized) {
      return;
    }
    const comparable = normalizeComparablePath(normalized);
    if (!comparable || seen.has(comparable)) {
      return;
    }
    seen.add(comparable);
    candidates.push(normalized);
  };

  pushCandidate(stored);
  const storedParent = path.dirname(stored);
  for (const name of folderNames) {
    if (!name) continue;
    if (storedParent && storedParent !== '.') {
      pushCandidate(path.join(storedParent, name));
    }
    if (rawDir) {
      pushCandidate(path.join(String(rawDir).trim(), name));
    }
    for (const extraDir of Array.isArray(extraDirs) ? extraDirs : []) {
      pushCandidate(path.join(String(extraDir || '').trim(), name));
    }
  }

  for (const candidate of candidates) {
    try {
      if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
        return candidate;
      }
    } catch (_error) {
      // ignore fs errors and continue with fallbacks
    }
  }

  return rawDir ? path.join(String(rawDir).trim(), baseFolderName) : stored;
}

function resolveEffectiveOutputPath(storedPath, movieDir) {
  const stored = String(storedPath || '').trim();
  if (!stored || !movieDir) return stored;
  // output_path structure: {movie_dir}/{folderName}/{fileName}
  const fileName = path.basename(stored);
  const folderName = path.basename(path.dirname(stored));
  if (!fileName || !folderName || folderName === '.') return stored;
  return path.join(String(movieDir).trim(), folderName, fileName);
}

function getConfiguredMediaPathList(settings = {}, baseKey) {
  const source = settings && typeof settings === 'object' ? settings : {};
  const candidates = [source[baseKey], ...PROFILE_PATH_SUFFIXES.map((suffix) => source[`${baseKey}_${suffix}`])];
  const unique = [];
  const seen = new Set();

  for (const candidate of candidates) {
    const rawPath = String(candidate || '').trim();
    if (!rawPath) {
      continue;
    }
    const normalized = normalizeComparablePath(rawPath);
    if (!normalized || seen.has(normalized)) {
      continue;
    }
    seen.add(normalized);
    unique.push(normalized);
  }

  return unique;
}

function getSeriesRawPathCandidates(settings = {}) {
  const source = settings && typeof settings === 'object' ? settings : {};
  const unique = [];
  const seen = new Set();
  const pushPath = (candidate) => {
    const rawPath = String(candidate || '').trim();
    if (!rawPath) {
      return;
    }
    const normalized = normalizeComparablePath(rawPath);
    if (!normalized || seen.has(normalized)) {
      return;
    }
    seen.add(normalized);
    unique.push(normalized);
  };

  for (const candidate of getConfiguredMediaPathList(source, 'series_raw_dir')) {
    pushPath(candidate);
  }
  pushPath(source.raw_dir_dvd_series);

  const dvdEffective = settingsService.resolveEffectiveToolSettings(source, 'dvd') || {};
  pushPath(dvdEffective.series_raw_dir);

  return unique;
}

function isLikelySeriesRawPath(rawPath, settings = {}) {
  const normalizedRawPath = normalizeComparablePath(rawPath);
  if (!normalizedRawPath) {
    return false;
  }

  const seriesRoots = getSeriesRawPathCandidates(settings);
  if (seriesRoots.some((candidate) => isPathInside(candidate, normalizedRawPath))) {
    return true;
  }

  // Fallback for common default layouts when series raw dir is not explicitly configured.
  return /(^|\/)raw\/dvd\/series(\/|$)/i.test(normalizedRawPath);
}

function getOrphanRawScanPathList(settings = {}) {
  const source = settings && typeof settings === 'object' ? settings : {};
  const unique = [];
  const seen = new Set();
  const pushPath = (candidate) => {
    const rawPath = String(candidate || '').trim();
    if (!rawPath) {
      return;
    }
    const normalized = normalizeComparablePath(rawPath);
    if (!normalized || seen.has(normalized)) {
      return;
    }
    seen.add(normalized);
    unique.push(normalized);
  };

  // Explicitly configured RAW roots (legacy + profiled keys)
  for (const candidate of getConfiguredMediaPathList(source, 'raw_dir')) {
    pushPath(candidate);
  }
  for (const candidate of getConfiguredMediaPathList(source, 'series_raw_dir')) {
    pushPath(candidate);
  }
  pushPath(source.raw_dir_dvd_series);
  pushPath(source.converter_raw_dir);

  // Effective settings roots per profile (covers fallback behavior in settings service)
  for (const profile of ['bluray', 'dvd', 'cd', 'audiobook']) {
    const effective = settingsService.resolveEffectiveToolSettings(source, profile) || {};
    pushPath(effective.raw_dir);
    if (profile === 'dvd') {
      pushPath(effective.series_raw_dir);
    }
  }

  // Hard defaults (even when no explicit setting exists yet)
  pushPath(settingsService.DEFAULT_RAW_DIR);
  pushPath(settingsService.DEFAULT_AUDIOBOOK_RAW_DIR);
  pushPath(defaultConverterRawDir);

  return unique;
}

function isDirectoryLikeOutput(mediaType, encodePlan = null, handbrakeInfo = null) {
  if (mediaType === 'cd') {
    return true;
  }
  if (mediaType !== 'audiobook') {
    return false;
  }
  const hbMode = String(handbrakeInfo?.mode || '').trim().toLowerCase();
  if (hbMode === 'audiobook_encode_split') {
    return true;
  }
  const format = String(encodePlan?.format || '').trim().toLowerCase();
  return Boolean(format && format !== 'm4b');
}

function resolveEffectiveStoragePathsForJob(settings = null, job = {}, parsed = {}) {
  const mkInfo = parsed?.makemkvInfo || parseJsonSafe(job?.makemkv_info_json, null);
  const miInfo = parsed?.mediainfoInfo || parseJsonSafe(job?.mediainfo_info_json, null);
  const plan = parsed?.encodePlan || parseJsonSafe(job?.encode_plan_json, null);
  const handbrakeInfo = parsed?.handbrakeInfo || parseJsonSafe(job?.handbrake_info_json, null);
  const mediaType = inferMediaType(job, mkInfo, miInfo, plan, handbrakeInfo);
  const directoryLikeOutput = isDirectoryLikeOutput(mediaType, plan, handbrakeInfo);
  const selectedMetadata = mkInfo?.analyzeContext?.selectedMetadata || mkInfo?.selectedMetadata || {};
  const metadataKind = String(selectedMetadata?.metadataKind || '').trim().toLowerCase();
  const normalizedJobKind = String(job?.job_kind || '').trim().toLowerCase();
  const hasSeriesJobKind = normalizedJobKind === 'dvd_series_child' || normalizedJobKind === 'dvd_series_container';
  const hasSeriesParent = normalizeJobIdValue(job?.parent_job_id) !== null;
  const hasSeriesRawPathHint = isLikelySeriesRawPath(job?.raw_path, settings || {});
  const hasSeriesMetadataHint = hasSeriesMetadataSignals(
    selectedMetadata,
    mkInfo?.analyzeContext && typeof mkInfo.analyzeContext === 'object' ? mkInfo.analyzeContext : {}
  );
  const seriesSignals = (
    Number(selectedMetadata?.tmdbId || 0) > 0
    || Number(selectedMetadata?.seasonNumber || 0) > 0
    || (Array.isArray(selectedMetadata?.episodes) && selectedMetadata.episodes.length > 0)
    || Number(selectedMetadata?.episodeCount || 0) > 0
    || ['series', 'season', 'tv', 'tv_series', 'tv-season', 'tv_season'].includes(metadataKind)
  );
  const isSeriesDvd = mediaType === 'dvd' && (
    seriesSignals
    || hasSeriesMetadataHint
    || hasSeriesJobKind
    || hasSeriesParent
    || hasSeriesRawPathHint
  );

  // Converter jobs use their own storage dirs — do not remap output path
  if (mediaType === 'converter') {
    const s = settings || {};
    const converterMediaType = String(plan?.converterMediaType || '').trim().toLowerCase();
    const converterMovieDir = converterMediaType === 'audio'
      ? (String(s.converter_audio_dir || '').trim() || String(s.converter_movie_dir || '').trim())
      : String(s.converter_movie_dir || '').trim();
    const converterRawDir = String(s.converter_raw_dir || '').trim();
    return {
      mediaType: 'converter',
      directoryLikeOutput: false,
      rawDir: converterRawDir,
      movieDir: converterMovieDir,
      effectiveRawPath: job?.raw_path || null,
      effectiveOutputPath: job?.output_path || null,
      makemkvInfo: mkInfo,
      mediainfoInfo: miInfo,
      handbrakeInfo,
      encodePlan: plan
    };
  }

  const effectiveSettings = settingsService.resolveEffectiveToolSettings(settings || {}, mediaType);
  const seriesRawDir = String(effectiveSettings?.series_raw_dir || '').trim();
  const seriesMovieDir = String(effectiveSettings?.series_dir || '').trim();
  const defaultRawDir = String(effectiveSettings?.raw_dir || '').trim();
  let rawDir = isSeriesDvd ? (seriesRawDir || defaultRawDir) : defaultRawDir;
  if (isSeriesDvd) {
    const normalizedStoredRawPath = normalizeComparablePath(job?.raw_path);
    if (normalizedStoredRawPath) {
      const seriesRawCandidates = getSeriesRawPathCandidates(settings || {});
      const matchedSeriesRawDir = seriesRawCandidates.find((candidate) => isPathInside(candidate, normalizedStoredRawPath));
      if (matchedSeriesRawDir) {
        rawDir = matchedSeriesRawDir;
      }
    }
  }
  const configuredMovieDir = isSeriesDvd
    ? (seriesMovieDir || String(effectiveSettings?.movie_dir || '').trim())
    : String(effectiveSettings?.movie_dir || '').trim();
  const movieDir = configuredMovieDir || rawDir;
  const rawLookupDirsBase = isSeriesDvd
    ? getSeriesRawPathCandidates(settings || {})
    : getConfiguredMediaPathList(settings || {}, 'raw_dir');
  const rawLookupDirs = rawLookupDirsBase
    .filter((candidate) => normalizeComparablePath(candidate) !== normalizeComparablePath(rawDir));
  const effectiveRawPath = job?.raw_path
    ? resolveEffectiveRawPath(job.raw_path, rawDir, rawLookupDirs)
    : (job?.raw_path || null);
  const effectiveOutputPath = (!directoryLikeOutput && configuredMovieDir && job?.output_path && !isSeriesDvd)
    ? resolveEffectiveOutputPath(job.output_path, configuredMovieDir)
    : (job?.output_path || null);

  return {
    mediaType,
    directoryLikeOutput,
    rawDir,
    movieDir,
    effectiveRawPath,
    effectiveOutputPath,
    makemkvInfo: mkInfo,
    mediainfoInfo: miInfo,
    handbrakeInfo,
    encodePlan: plan
  };
}

function buildUnknownDirectoryStatus(dirPath = null) {
  return {
    path: dirPath || null,
    exists: null,
    isDirectory: null,
    isEmpty: null,
    entryCount: null
  };
}

function buildUnknownFileStatus(filePath = null) {
  return {
    path: filePath || null,
    exists: null,
    isFile: null,
    sizeBytes: null
  };
}

function enrichJobRow(job, settings = null, options = {}) {
  const includeFsChecks = options?.includeFsChecks !== false;
  const omdbInfo = parseJsonSafe(job.omdb_json, null);
  const resolvedPaths = resolveEffectiveStoragePathsForJob(settings, job);
  const handbrakeInfo = resolvedPaths.handbrakeInfo;
  const directoryLikeOutput = Boolean(resolvedPaths.directoryLikeOutput);
  const outputStatus = includeFsChecks
    ? (directoryLikeOutput
      ? inspectDirectory(resolvedPaths.effectiveOutputPath)
      : inspectOutputFile(resolvedPaths.effectiveOutputPath))
    : (directoryLikeOutput
      ? buildUnknownDirectoryStatus(resolvedPaths.effectiveOutputPath)
      : buildUnknownFileStatus(resolvedPaths.effectiveOutputPath));
  const rawStatus = includeFsChecks
    ? inspectDirectory(resolvedPaths.effectiveRawPath)
    : buildUnknownDirectoryStatus(resolvedPaths.effectiveRawPath);
  const movieDirPath = resolvedPaths.effectiveOutputPath ? path.dirname(resolvedPaths.effectiveOutputPath) : null;
  const movieDirStatus = includeFsChecks
    ? inspectDirectory(movieDirPath)
    : buildUnknownDirectoryStatus(movieDirPath);
  const makemkvInfo = resolvedPaths.makemkvInfo;
  const mediainfoInfo = resolvedPaths.mediainfoInfo;
  const encodePlan = resolvedPaths.encodePlan;
  const mediaType = resolvedPaths.mediaType;
  const normalizedJobStatus = String(job?.status || '').trim().toUpperCase();
  const ripSuccessful = Number(job?.rip_successful || 0) === 1
    || String(makemkvInfo?.status || '').trim().toUpperCase() === 'SUCCESS';
  const backupSuccess = ripSuccessful;
  const finishedWithOutput = normalizedJobStatus === 'FINISHED'
    && (includeFsChecks ? Boolean(outputStatus?.exists) : true);
  const seriesBatchSummary = handbrakeInfo?.seriesBatch && typeof handbrakeInfo.seriesBatch === 'object'
    ? handbrakeInfo.seriesBatch
    : null;
  const seriesBatchMode = String(handbrakeInfo?.mode || '').trim().toLowerCase() === 'series_batch';
  const seriesBatchTotal = Number(seriesBatchSummary?.totalCount || 0);
  const seriesBatchFinished = Number(seriesBatchSummary?.finishedCount || 0);
  const seriesBatchErrors = Number(seriesBatchSummary?.errorCount || 0);
  const seriesBatchCancelled = Number(seriesBatchSummary?.cancelledCount || 0);
  const seriesBatchEncodeSuccess = seriesBatchMode
    && seriesBatchTotal > 0
    && seriesBatchFinished >= seriesBatchTotal
    && seriesBatchErrors <= 0
    && seriesBatchCancelled <= 0;
  const encodeSuccess = directoryLikeOutput
    ? finishedWithOutput
    : (
      seriesBatchEncodeSuccess
      || (mediaType === 'converter'
        ? finishedWithOutput
        : String(handbrakeInfo?.status || '').trim().toUpperCase() === 'SUCCESS')
    );

  return {
    ...job,
    raw_path: resolvedPaths.effectiveRawPath,
    output_path: resolvedPaths.effectiveOutputPath,
    makemkvInfo,
    handbrakeInfo,
    mediainfoInfo,
    omdbInfo,
    encodePlan,
    mediaType,
    ripSuccessful,
    backupSuccess,
    encodeSuccess,
    rawStatus,
    outputStatus,
    movieDirStatus
  };
}

function resolveSafe(inputPath) {
  return path.resolve(String(inputPath || ''));
}

function isPathInside(basePath, candidatePath) {
  if (!basePath || !candidatePath) {
    return false;
  }

  const base = resolveSafe(basePath);
  const candidate = resolveSafe(candidatePath);
  return candidate === base || candidate.startsWith(`${base}${path.sep}`);
}

function normalizeComparablePath(inputPath) {
  return resolveSafe(String(inputPath || '')).replace(/[\\/]+$/, '');
}

function escapeRegExp(input) {
  return String(input || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function parseNumberedFolderName(folderName) {
  const raw = String(folderName || '').trim();
  if (!raw) {
    return { baseName: '', suffixNumber: 0, numbered: false };
  }
  const match = raw.match(/^(.*)_([0-9]+)$/);
  if (!match) {
    return { baseName: raw, suffixNumber: 0, numbered: false };
  }
  const baseName = String(match[1] || '').trim() || raw;
  const suffixNumber = Number(match[2] || 0);
  return {
    baseName,
    suffixNumber: Number.isFinite(suffixNumber) && suffixNumber > 0 ? Math.trunc(suffixNumber) : 0,
    numbered: true
  };
}

function inferSiblingOutputFolders(outputPath) {
  const normalizedOutputPath = normalizeComparablePath(outputPath);
  if (!normalizedOutputPath) {
    return [];
  }

  let outputStat = null;
  try {
    outputStat = fs.statSync(normalizedOutputPath);
  } catch (_error) {
    return [];
  }
  if (!outputStat?.isDirectory?.()) {
    return [];
  }

  const parentDir = path.dirname(normalizedOutputPath);
  const folderName = path.basename(normalizedOutputPath);
  const parsedName = parseNumberedFolderName(folderName);
  const baseName = parsedName.baseName || folderName;
  if (!baseName) {
    return [normalizedOutputPath];
  }

  const siblingRegex = new RegExp(`^${escapeRegExp(baseName)}(?:_(\\d+))?$`);
  let entries = [];
  try {
    entries = fs.readdirSync(parentDir, { withFileTypes: true });
  } catch (_error) {
    return [normalizedOutputPath];
  }

  const candidates = [];
  for (const entry of entries) {
    if (!entry?.isDirectory?.()) {
      continue;
    }
    const name = String(entry.name || '').trim();
    if (!name) {
      continue;
    }
    const match = name.match(siblingRegex);
    if (!match) {
      continue;
    }
    const suffixNumber = match[1] ? Number(match[1]) : 0;
    const fullPath = normalizeComparablePath(path.join(parentDir, name));
    candidates.push({
      path: fullPath,
      suffixNumber: Number.isFinite(suffixNumber) && suffixNumber > 0 ? Math.trunc(suffixNumber) : 0
    });
  }

  if (candidates.length === 0) {
    return [normalizedOutputPath];
  }

  candidates.sort((left, right) => {
    if (left.suffixNumber !== right.suffixNumber) {
      return left.suffixNumber - right.suffixNumber;
    }
    return String(left.path || '').localeCompare(String(right.path || ''), 'de-DE');
  });
  return candidates.map((item) => item.path);
}

function compareOutputFolderPaths(leftPath, rightPath) {
  const leftNormalized = normalizeComparablePath(leftPath);
  const rightNormalized = normalizeComparablePath(rightPath);
  const leftParent = normalizeComparablePath(path.dirname(leftNormalized));
  const rightParent = normalizeComparablePath(path.dirname(rightNormalized));
  const parentCompare = leftParent.localeCompare(rightParent, 'de-DE');
  if (parentCompare !== 0) {
    return parentCompare;
  }

  const leftName = String(path.basename(leftNormalized) || '').trim();
  const rightName = String(path.basename(rightNormalized) || '').trim();
  const leftParsed = parseNumberedFolderName(leftName);
  const rightParsed = parseNumberedFolderName(rightName);
  const baseCompare = String(leftParsed.baseName || '').localeCompare(String(rightParsed.baseName || ''), 'de-DE');
  if (baseCompare !== 0) {
    return baseCompare;
  }

  const leftSuffix = Number(leftParsed?.suffixNumber || 0);
  const rightSuffix = Number(rightParsed?.suffixNumber || 0);
  if (leftSuffix !== rightSuffix) {
    return leftSuffix - rightSuffix;
  }

  return leftNormalized.localeCompare(rightNormalized, 'de-DE');
}

function stripRawFolderStatePrefix(folderName) {
  const rawName = String(folderName || '').trim();
  if (!rawName) {
    return '';
  }
  return rawName
    .replace(new RegExp(`^${RAW_INCOMPLETE_PREFIX}`, 'i'), '')
    .replace(new RegExp(`^${RAW_RIP_COMPLETE_PREFIX}`, 'i'), '')
    .trim();
}

function stripRawFolderJobSuffix(folderName) {
  const rawName = String(folderName || '').trim();
  if (!rawName) {
    return '';
  }
  return rawName.replace(/(?:\s*-\s*RAW\s*-\s*job-\d+\s*)+$/i, '').trim();
}

function applyRawFolderPrefix(folderName, prefix = '') {
  const normalized = stripRawFolderStatePrefix(folderName);
  if (!normalized) {
    return normalized;
  }
  const safePrefix = String(prefix || '').trim();
  return safePrefix ? `${safePrefix}${normalized}` : normalized;
}

function parseRawFolderMetadata(folderName) {
  const rawName = String(folderName || '').trim();
  const normalizedRawName = stripRawFolderStatePrefix(rawName);
  const folderJobIdMatches = Array.from(normalizedRawName.matchAll(/-\s*RAW\s*-\s*job-(\d+)/ig));
  const folderJobId = folderJobIdMatches.length > 0
    ? Number(folderJobIdMatches[folderJobIdMatches.length - 1][1])
    : null;
  let working = stripRawFolderJobSuffix(normalizedRawName);

  const imdbMatch = working.match(/\[(tt\d{6,12})\]/i);
  const imdbId = imdbMatch ? String(imdbMatch[1] || '').toLowerCase() : null;
  if (imdbMatch) {
    working = working.replace(imdbMatch[0], '').trim();
  }

  const yearMatch = working.match(/\((19|20)\d{2}\)/);
  const year = yearMatch ? Number(String(yearMatch[0]).replace(/[()]/g, '')) : null;
  if (yearMatch) {
    working = working.replace(yearMatch[0], '').trim();
  }

  const title = working.replace(/\s{2,}/g, ' ').trim() || null;

  return {
    title,
    year: Number.isFinite(year) ? year : null,
    imdbId,
    folderJobId: Number.isFinite(folderJobId) ? Math.trunc(folderJobId) : null
  };
}

function normalizePositiveNumberOrNull(value) {
  const raw = String(value ?? '').trim();
  if (!raw) {
    return null;
  }
  const parsed = Number(raw.replace(',', '.'));
  if (!Number.isFinite(parsed) || parsed <= 0) {
    return null;
  }
  return parsed;
}

function normalizePositiveIntegerOrNull(value) {
  const parsed = normalizePositiveNumberOrNull(value);
  if (parsed === null) {
    return null;
  }
  return Math.trunc(parsed);
}

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

function isSeriesBatchEpisodeSubJobRow(row = null) {
  const source = row && typeof row === 'object' ? row : {};
  const plan = source?.encodePlan && typeof source.encodePlan === 'object'
    ? source.encodePlan
    : parseJsonSafe(source?.encode_plan_json, null);
  return isSeriesBatchEpisodeSubJobPlan(plan);
}

function extractSeasonNumberFromText(value) {
  const text = String(value || '').trim();
  if (!text) {
    return null;
  }

  const directPattern = text.match(/(?:staffel|season|serie|series|s)\s*[-_.:]?\s*(\d{1,3}(?:[.,]\d+)?)/i);
  if (directPattern?.[1]) {
    return normalizePositiveNumberOrNull(directPattern[1]);
  }

  const tokenPattern = text.match(/(?:^|[\s._-])s(\d{1,3}(?:[.,]\d+)?)(?:$|[\s._-])/i);
  if (tokenPattern?.[1]) {
    return normalizePositiveNumberOrNull(tokenPattern[1]);
  }

  return null;
}

function extractDiscNumberFromText(value) {
  const text = String(value || '').trim();
  if (!text) {
    return null;
  }

  const directPattern = text.match(/(?:disc|disk|dvd|cd)\s*[-_.:]?\s*(\d{1,2})/i);
  if (directPattern?.[1]) {
    return normalizePositiveIntegerOrNull(directPattern[1]);
  }

  const tokenPattern = text.match(/(?:^|[\s._-])d(?:isc)?[-_.:]?\s*(\d{1,2})(?:$|[\s._-])/i);
  if (tokenPattern?.[1]) {
    return normalizePositiveIntegerOrNull(tokenPattern[1]);
  }

  return null;
}

function extractSelectedMetadataFromMakemkvInfo(makemkvInfo = {}) {
  const source = makemkvInfo && typeof makemkvInfo === 'object' ? makemkvInfo : {};
  const analyzeContext = source.analyzeContext && typeof source.analyzeContext === 'object'
    ? source.analyzeContext
    : {};
  const analyzeSelected = analyzeContext.selectedMetadata && typeof analyzeContext.selectedMetadata === 'object'
    ? analyzeContext.selectedMetadata
    : {};
  const topLevelSelected = source.selectedMetadata && typeof source.selectedMetadata === 'object'
    ? source.selectedMetadata
    : {};
  const merged = {
    ...analyzeSelected,
    ...topLevelSelected
  };

  if (!merged.metadataProvider && analyzeContext.metadataProvider) {
    merged.metadataProvider = analyzeContext.metadataProvider;
  }
  if (!merged.metadataKind && analyzeContext.metadataKind) {
    merged.metadataKind = analyzeContext.metadataKind;
  }

  return merged;
}

function hasSeriesMetadataSignals(selectedMetadata = {}, analyzeContext = {}) {
  const metadata = selectedMetadata && typeof selectedMetadata === 'object' ? selectedMetadata : {};
  const analyze = analyzeContext && typeof analyzeContext === 'object' ? analyzeContext : {};
  const metadataKind = String(metadata.metadataKind || analyze.metadataKind || '').trim().toLowerCase();
  const seasonNumber = normalizePositiveNumberOrNull(
    metadata.seasonNumber ?? analyze?.seriesLookupHint?.seasonNumber ?? null
  );
  const seasonName = String(metadata.seasonName || '').trim();
  const episodeCount = Number(metadata.episodeCount || 0) || 0;
  const hasEpisodeList = Array.isArray(metadata.episodes) && metadata.episodes.length > 0;
  const tmdbId = normalizePositiveIntegerOrNull(
    metadata.tmdbId ?? analyze.tmdbId ?? metadata.providerId ?? analyze.providerId ?? null
  );
  const provider = String(
    metadata.metadataProvider
    || analyze.metadataProvider
    || ''
  ).trim().toLowerCase();
  const providerIsTmdb = provider === 'tmdb' || provider === 'themoviedb';
  const seriesKind = ['series', 'season', 'tv', 'tv_series', 'tv-season', 'tv_season'].includes(metadataKind);

  if (seriesKind || seasonNumber !== null || Boolean(seasonName) || episodeCount > 0 || hasEpisodeList || tmdbId !== null) {
    return true;
  }
  return providerIsTmdb && Boolean(String(analyze?.seriesLookupHint?.query || '').trim());
}

function buildSeriesImportHints({
  folderName = '',
  rawPath = '',
  metadata = {},
  sourceSelectedMetadata = {},
  sourceAnalyzeContext = {}
} = {}) {
  const safeMetadata = metadata && typeof metadata === 'object' ? metadata : {};
  const selected = sourceSelectedMetadata && typeof sourceSelectedMetadata === 'object'
    ? sourceSelectedMetadata
    : {};
  const analyze = sourceAnalyzeContext && typeof sourceAnalyzeContext === 'object'
    ? sourceAnalyzeContext
    : {};
  const titleSource = String(
    selected.title
    || selected.seriesTitle
    || safeMetadata.title
    || ''
  ).trim();
  const combinedText = [folderName, path.basename(String(rawPath || '').trim())]
    .filter(Boolean)
    .join(' ');
  const seasonNumber = normalizePositiveNumberOrNull(
    selected.seasonNumber
    ?? analyze?.seriesLookupHint?.seasonNumber
    ?? extractSeasonNumberFromText(combinedText)
  );
  const discNumber = normalizePositiveIntegerOrNull(
    selected.discNumber
    ?? analyze?.seriesLookupHint?.discNumber
    ?? extractDiscNumberFromText(combinedText)
  );

  const seriesLookupHint = {
    query: titleSource || null,
    seasonNumber,
    discNumber
  };
  const metadataHasSeriesSignals = hasSeriesMetadataSignals(selected, analyze);
  if (!metadataHasSeriesSignals && !seriesLookupHint.query && seriesLookupHint.seasonNumber === null && seriesLookupHint.discNumber === null) {
    return null;
  }

  return {
    selectedMetadata: {
      ...selected,
      metadataProvider: String(selected.metadataProvider || analyze.metadataProvider || 'tmdb').trim().toLowerCase() || 'tmdb',
      metadataKind: String(selected.metadataKind || analyze.metadataKind || 'series').trim().toLowerCase() || 'series',
      title: titleSource || null,
      seasonNumber,
      discNumber
    },
    analyzeContextPatch: {
      metadataProvider: String(
        analyze.metadataProvider
        || selected.metadataProvider
        || 'tmdb'
      ).trim().toLowerCase() || 'tmdb',
      metadataKind: String(
        analyze.metadataKind
        || selected.metadataKind
        || 'series'
      ).trim().toLowerCase() || 'series',
      seriesLookupHint: {
        ...(analyze.seriesLookupHint && typeof analyze.seriesLookupHint === 'object'
          ? analyze.seriesLookupHint
          : {}),
        ...seriesLookupHint
      },
      seriesAnalysis: {
        ...(analyze.seriesAnalysis && typeof analyze.seriesAnalysis === 'object'
          ? analyze.seriesAnalysis
          : {}),
        summary: {
          ...(analyze?.seriesAnalysis?.summary && typeof analyze.seriesAnalysis.summary === 'object'
            ? analyze.seriesAnalysis.summary
            : {}),
          seriesLike: true,
          confidence: analyze?.seriesAnalysis?.summary?.confidence || 'medium'
        }
      }
    }
  };
}

function buildRawPathForJobId(rawPath, jobId) {
  const normalizedJobId = Number(jobId);
  if (!Number.isFinite(normalizedJobId) || normalizedJobId <= 0) {
    return rawPath;
  }

  const absRawPath = normalizeComparablePath(rawPath);
  const folderName = path.basename(absRawPath);
  const statePrefix = /^Rip_Complete_/i.test(folderName)
    ? RAW_RIP_COMPLETE_PREFIX
    : /^Incomplete_/i.test(folderName)
      ? RAW_INCOMPLETE_PREFIX
      : '';
  const stripped = stripRawFolderJobSuffix(stripRawFolderStatePrefix(folderName));
  if (!stripped) {
    return absRawPath;
  }
  const withJobId = `${statePrefix}${stripped} - RAW - job-${Math.trunc(normalizedJobId)}`;
  return path.join(path.dirname(absRawPath), withJobId);
}

function normalizeCdFormat(value) {
  const raw = String(value || '').trim().toLowerCase();
  return cdRipService.SUPPORTED_FORMATS.has(raw) ? raw : null;
}

function normalizeAudioPathToken(value) {
  let raw = String(value || '').trim();
  if (!raw) {
    return null;
  }

  // Unwrap common wrappers like "(...)" or quotes around the full token.
  let changed = true;
  while (changed && raw.length > 0) {
    changed = false;
    if (
      (raw.startsWith('"') && raw.endsWith('"'))
      || (raw.startsWith("'") && raw.endsWith("'"))
      || (raw.startsWith('`') && raw.endsWith('`'))
      || (raw.startsWith('(') && raw.endsWith(')'))
    ) {
      raw = raw.slice(1, -1).trim();
      changed = true;
    }
  }

  // Strip leading quotes and trailing punctuation (but keep balanced ')').
  raw = raw
    .replace(/^["'`]+/, '')
    .replace(/[,"'`;]+$/g, '')
    .trim();

  // Remove only dangling unmatched ')' at the end (e.g. ".../path)")
  // while preserving valid directory names like "... (2007)".
  let openCount = (raw.match(/\(/g) || []).length;
  let closeCount = (raw.match(/\)/g) || []).length;
  while (closeCount > openCount && raw.endsWith(')')) {
    raw = raw.slice(0, -1).trimEnd();
    closeCount -= 1;
  }

  if (!raw.startsWith('/')) {
    return null;
  }
  return raw;
}

function resolveExistingAudioPathCandidate(candidate) {
  const normalized = normalizeAudioPathToken(candidate) || String(candidate || '').trim() || null;
  if (!normalized || !normalized.startsWith('/')) {
    return null;
  }

  try {
    if (fs.existsSync(normalized)) {
      return normalized;
    }
  } catch (_error) {
    // ignore fs errors while probing possible output paths
  }

  const openCount = (normalized.match(/\(/g) || []).length;
  const closeCount = (normalized.match(/\)/g) || []).length;
  if (openCount <= closeCount) {
    return null;
  }

  let repaired = normalized;
  for (let missing = closeCount; missing < openCount; missing += 1) {
    repaired = `${repaired})`;
    try {
      if (fs.existsSync(repaired)) {
        return repaired;
      }
    } catch (_error) {
      // ignore fs errors while probing repaired variants
    }
  }

  return null;
}

function extractAbsolutePathTokens(text) {
  const source = String(text || '');
  const tokens = [];
  const seen = new Set();
  const pushToken = (value) => {
    const normalized = normalizeAudioPathToken(value);
    if (!normalized || seen.has(normalized)) {
      return;
    }
    seen.add(normalized);
    tokens.push(normalized);
  };

  let match;
  const quotedPattern = /'([^']+)'|"([^"]+)"/g;
  while ((match = quotedPattern.exec(source)) !== null) {
    pushToken(match[1] || match[2] || '');
  }

  const sanitized = source.replace(/'[^']*'|"[^"]*"/g, ' ');
  const barePattern = /(^|\s)(\/[^\s]+)/g;
  while ((match = barePattern.exec(sanitized)) !== null) {
    pushToken(match[2] || '');
  }

  return tokens;
}

function parseProcessLogTimestamp(line) {
  const match = String(line || '').match(/^\[([^\]]+)\]/);
  if (!match) {
    return null;
  }
  const parsed = Date.parse(match[1]);
  return Number.isFinite(parsed) ? parsed : null;
}

function readProcessLogFileLines(filePath) {
  try {
    const raw = fs.readFileSync(filePath, 'utf-8');
    return String(raw || '')
      .split(/\r\n|\n|\r/)
      .filter((line) => line.length > 0);
  } catch (_error) {
    return [];
  }
}

function listJobProcessLogFiles() {
  const logDir = getJobLogDir();
  try {
    if (!fs.existsSync(logDir) || !fs.statSync(logDir).isDirectory()) {
      return [];
    }
    return fs.readdirSync(logDir, { withFileTypes: true })
      .filter((entry) => entry.isFile() && PROCESS_LOG_FILE_PATTERN.test(entry.name))
      .map((entry) => {
        const match = entry.name.match(PROCESS_LOG_FILE_PATTERN);
        const jobId = match ? Number(match[1]) : null;
        return {
          fileName: entry.name,
          filePath: path.join(logDir, entry.name),
          jobId: Number.isFinite(jobId) ? Math.trunc(jobId) : null
        };
      });
  } catch (_error) {
    return [];
  }
}

function pickPreferredExistingPath(candidates = []) {
  const normalizedCandidates = [];
  const seen = new Set();

  for (const candidate of Array.isArray(candidates) ? candidates : []) {
    const normalized = normalizeAudioPathToken(candidate) || String(candidate || '').trim() || null;
    if (!normalized || seen.has(normalized)) {
      continue;
    }
    seen.add(normalized);
    normalizedCandidates.push(normalized);
  }

  for (let index = normalizedCandidates.length - 1; index >= 0; index -= 1) {
    const candidate = normalizedCandidates[index];
    const existingCandidate = resolveExistingAudioPathCandidate(candidate);
    if (existingCandidate) {
      return existingCandidate;
    }
  }

  return normalizedCandidates[normalizedCandidates.length - 1] || null;
}

function collectAudioFilesRecursively(rootPath, options = {}) {
  const maxDepth = Number.isFinite(Number(options.maxDepth)) ? Math.max(0, Math.trunc(Number(options.maxDepth))) : 6;
  const maxFiles = Number.isFinite(Number(options.maxFiles)) ? Math.max(1, Math.trunc(Number(options.maxFiles))) : 2000;
  const result = [];
  const normalizedRoot = String(rootPath || '').trim();
  if (!normalizedRoot || !fs.existsSync(normalizedRoot)) {
    return result;
  }

  const pushFile = (filePath) => {
    if (result.length >= maxFiles) {
      return;
    }
    const ext = path.extname(filePath).toLowerCase();
    if (CD_OUTPUT_AUDIO_EXTENSIONS.has(ext)) {
      result.push(filePath);
    }
  };

  try {
    const stat = fs.statSync(normalizedRoot);
    if (stat.isFile()) {
      pushFile(normalizedRoot);
      return result;
    }
  } catch (_error) {
    return result;
  }

  const queue = [{ dirPath: normalizedRoot, depth: 0 }];
  while (queue.length > 0 && result.length < maxFiles) {
    const current = queue.shift();
    if (!current) {
      continue;
    }
    let entries = [];
    try {
      entries = fs.readdirSync(current.dirPath, { withFileTypes: true });
    } catch (_error) {
      continue;
    }

    entries.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }));
    for (const entry of entries) {
      const absPath = path.join(current.dirPath, entry.name);
      if (entry.isFile()) {
        pushFile(absPath);
      } else if (entry.isDirectory() && current.depth < maxDepth) {
        queue.push({ dirPath: absPath, depth: current.depth + 1 });
      }
      if (result.length >= maxFiles) {
        break;
      }
    }
  }

  return result;
}

function parseCdOutputTrackFromPath(filePath) {
  const ext = path.extname(String(filePath || '')).toLowerCase();
  if (!CD_OUTPUT_AUDIO_EXTENSIONS.has(ext)) {
    return null;
  }

  const baseName = path.basename(filePath, ext).replace(/\s+/g, ' ').trim();
  if (!baseName) {
    return null;
  }

  const numberMatch = baseName.match(/^(\d{1,3})(.*)$/);
  const parsedPosition = numberMatch ? Number(numberMatch[1]) : null;
  const position = Number.isFinite(parsedPosition) && parsedPosition > 0 ? Math.trunc(parsedPosition) : null;
  let remainder = numberMatch ? String(numberMatch[2] || '').replace(/^[-._\s]+/, '').trim() : baseName;
  let artist = null;
  let title = remainder || baseName;

  const separatorIndex = remainder.indexOf(' - ');
  if (separatorIndex > 0) {
    artist = remainder.slice(0, separatorIndex).trim() || null;
    title = remainder.slice(separatorIndex + 3).trim() || remainder;
  }

  return {
    position,
    artist,
    title: title || baseName,
    format: normalizeCdFormat(ext.replace(/^\./, '')),
    filePath
  };
}

function assignMissingTrackPositions(entries = []) {
  const used = new Set(
    (Array.isArray(entries) ? entries : [])
      .map((entry) => Number(entry?.position))
      .filter((value) => Number.isFinite(value) && value > 0)
      .map((value) => Math.trunc(value))
  );

  let nextPosition = 1;
  return (Array.isArray(entries) ? entries : []).map((entry) => {
    const current = Number(entry?.position);
    if (Number.isFinite(current) && current > 0) {
      return {
        ...entry,
        position: Math.trunc(current)
      };
    }

    while (used.has(nextPosition)) {
      nextPosition += 1;
    }
    const assigned = nextPosition;
    used.add(assigned);
    nextPosition += 1;
    return {
      ...entry,
      position: assigned
    };
  });
}

function findMostCommonString(values = []) {
  const counts = new Map();
  for (const value of Array.isArray(values) ? values : []) {
    const normalized = String(value || '').trim();
    if (!normalized) {
      continue;
    }
    counts.set(normalized, (counts.get(normalized) || 0) + 1);
  }

  let bestValue = null;
  let bestCount = 0;
  for (const [value, count] of counts.entries()) {
    if (count > bestCount) {
      bestValue = value;
      bestCount = count;
    }
  }
  return bestValue;
}

function normalizeComparableLabel(value) {
  return String(value || '')
    .normalize('NFKD')
    .replace(/[^\x00-\x7F]+/g, '')
    .replace(/[^A-Za-z0-9]+/g, ' ')
    .trim()
    .toLowerCase();
}

function parseCdFolderMetadataFromOutputDir(outputDir) {
  const normalized = String(outputDir || '').trim();
  if (!normalized) {
    return {
      artist: null,
      album: null,
      year: null
    };
  }

  let working = path.basename(normalized).replace(/\s+/g, ' ').trim();
  if (!working || working === '.' || working === path.sep) {
    return {
      artist: null,
      album: null,
      year: null
    };
  }

  const yearMatch = working.match(/\((19|20)\d{2}\)\s*$/);
  const year = yearMatch ? Number(String(yearMatch[0]).replace(/[()]/g, '')) : null;
  if (yearMatch) {
    working = working.slice(0, working.length - yearMatch[0].length).trim();
  }

  const separatorIndex = working.indexOf(' - ');
  const artist = separatorIndex > 0 ? working.slice(0, separatorIndex).trim() || null : null;
  const album = separatorIndex > 0
    ? working.slice(separatorIndex + 3).trim() || null
    : (working || null);

  return {
    artist,
    album,
    year: Number.isFinite(year) ? year : null
  };
}

function directoryContainsAudioFiles(dirPath) {
  try {
    if (!dirPath || !fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
      return false;
    }
    const entries = fs.readdirSync(dirPath, { withFileTypes: true });
    for (const entry of entries) {
      const absPath = path.join(dirPath, entry.name);
      if (entry.isFile()) {
        if (CD_OUTPUT_AUDIO_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
          return true;
        }
      } else if (entry.isDirectory()) {
        const nestedEntries = fs.readdirSync(absPath, { withFileTypes: true });
        if (nestedEntries.some((nested) => nested.isFile() && CD_OUTPUT_AUDIO_EXTENSIONS.has(path.extname(nested.name).toLowerCase()))) {
          return true;
        }
      }
    }
  } catch (_error) {
    return false;
  }
  return false;
}

function findCdOutputDirByMetadata(options = {}) {
  const settings = options.settings && typeof options.settings === 'object' ? options.settings : {};
  const baseMetadata = options.baseMetadata && typeof options.baseMetadata === 'object'
    ? options.baseMetadata
    : {};
  const outputTemplate = String(options.outputTemplate || settings.cd_output_template || cdRipService.DEFAULT_CD_OUTPUT_TEMPLATE).trim()
    || cdRipService.DEFAULT_CD_OUTPUT_TEMPLATE;
  const effectiveSettings = settingsService.resolveEffectiveToolSettings(settings, 'cd');
  const outputBaseDir = String(effectiveSettings?.movie_dir || effectiveSettings?.raw_dir || '').trim() || null;
  if (!outputBaseDir) {
    return null;
  }

  const metadataVariants = [];
  const pushMetadataVariant = (candidate) => {
    if (!candidate || typeof candidate !== 'object') {
      return;
    }
    const title = String(candidate.title || candidate.album || '').trim() || null;
    const artist = String(candidate.artist || '').trim() || null;
    const yearRaw = Number(candidate.year);
    const year = Number.isFinite(yearRaw) && yearRaw > 0 ? Math.trunc(yearRaw) : null;
    if (!title && !artist && !year) {
      return;
    }
    const key = JSON.stringify({ title, artist, year });
    if (metadataVariants.some((entry) => entry.key === key)) {
      return;
    }
    metadataVariants.push({
      key,
      meta: {
        title,
        album: title,
        artist,
        year
      }
    });
  };

  pushMetadataVariant(baseMetadata);
  pushMetadataVariant({
    title: baseMetadata.album || baseMetadata.title || null,
    artist: null,
    year: baseMetadata.year || null
  });

  for (const variant of metadataVariants) {
    try {
      if (!variant.meta.title) {
        continue;
      }
      const candidatePath = cdRipService.buildOutputDir(variant.meta, outputBaseDir, outputTemplate);
      if (candidatePath && fs.existsSync(candidatePath) && directoryContainsAudioFiles(candidatePath)) {
        return candidatePath;
      }
    } catch (_error) {
      // ignore template/render errors and continue with directory scan fallback
    }
  }

  try {
    if (!fs.existsSync(outputBaseDir) || !fs.statSync(outputBaseDir).isDirectory()) {
      return null;
    }
    const targetAlbum = normalizeComparableLabel(baseMetadata.album || baseMetadata.title || '');
    const targetArtist = normalizeComparableLabel(baseMetadata.artist || '');
    const targetYearRaw = Number(baseMetadata.year);
    const targetYear = Number.isFinite(targetYearRaw) && targetYearRaw > 0 ? Math.trunc(targetYearRaw) : null;
    let best = null;
    let bestScore = -1;
    const entries = fs.readdirSync(outputBaseDir, { withFileTypes: true });
    for (const entry of entries) {
      if (!entry.isDirectory()) {
        continue;
      }
      const candidatePath = path.join(outputBaseDir, entry.name);
      if (!directoryContainsAudioFiles(candidatePath)) {
        continue;
      }
      const parsed = parseCdFolderMetadataFromOutputDir(candidatePath);
      const candidateAlbum = normalizeComparableLabel(parsed.album || '');
      const candidateArtist = normalizeComparableLabel(parsed.artist || '');
      const candidateYearRaw = Number(parsed.year);
      const candidateYear = Number.isFinite(candidateYearRaw) && candidateYearRaw > 0 ? Math.trunc(candidateYearRaw) : null;

      let score = 0;
      if (targetAlbum && candidateAlbum === targetAlbum) {
        score += 10;
      } else if (targetAlbum && candidateAlbum.includes(targetAlbum)) {
        score += 6;
      } else if (targetAlbum && targetAlbum.includes(candidateAlbum) && candidateAlbum) {
        score += 4;
      }
      if (targetYear && candidateYear === targetYear) {
        score += 5;
      }
      if (targetArtist && candidateArtist === targetArtist) {
        score += 3;
      }
      if (score > bestScore) {
        best = candidatePath;
        bestScore = score;
      }
    }
    return bestScore > 0 ? best : null;
  } catch (_error) {
    return null;
  }
}

function extractCommandToken(commandLine) {
  const raw = String(commandLine || '').trim();
  if (!raw) {
    return null;
  }
  const match = raw.match(/^'([^']+)'|"([^"]+)"|(\S+)/);
  return String(match?.[1] || match?.[2] || match?.[3] || '').trim() || null;
}

function findRelatedJobLogSources(options = {}) {
  const relatedJobIds = Array.isArray(options.relatedJobIds)
    ? options.relatedJobIds
      .map((value) => normalizeJobIdValue(value))
      .filter(Boolean)
    : [];
  const excludeJobIds = new Set(
    (Array.isArray(options.excludeJobIds) ? options.excludeJobIds : [])
      .map((value) => normalizeJobIdValue(value))
      .filter(Boolean)
  );
  const rawPathCandidates = Array.isArray(options.rawPathCandidates)
    ? options.rawPathCandidates
      .map((value) => String(value || '').trim())
      .filter(Boolean)
    : [];
  const pathNeedles = Array.from(new Set(
    rawPathCandidates
      .map((value) => normalizeComparablePath(value))
      .filter(Boolean)
  ));

  const collected = new Map();
  const addSource = (filePath, matchedBy) => {
    const normalizedFilePath = String(filePath || '').trim();
    if (!normalizedFilePath || collected.has(normalizedFilePath)) {
      return;
    }
    const lines = readProcessLogFileLines(normalizedFilePath);
    if (lines.length === 0) {
      return;
    }
    const fileName = path.basename(normalizedFilePath);
    const match = fileName.match(PROCESS_LOG_FILE_PATTERN);
    const parsedJobId = match ? Number(match[1]) : null;
    const jobId = Number.isFinite(parsedJobId) ? Math.trunc(parsedJobId) : null;
    if (jobId && excludeJobIds.has(jobId)) {
      return;
    }
    const firstTimestampMs = lines.reduce((found, line) => {
      if (found !== null) {
        return found;
      }
      return parseProcessLogTimestamp(line);
    }, null);
    collected.set(normalizedFilePath, {
      filePath: normalizedFilePath,
      fileName,
      jobId,
      matchedBy: matchedBy ? [matchedBy] : [],
      lines,
      firstTimestampMs
    });
  };

  for (const relatedJobId of relatedJobIds) {
    if (excludeJobIds.has(relatedJobId)) {
      continue;
    }
    const directPath = toProcessLogPath(relatedJobId);
    if (directPath && fs.existsSync(directPath)) {
      addSource(directPath, `jobId:${relatedJobId}`);
    }
  }

  if (pathNeedles.length > 0) {
    for (const entry of listJobProcessLogFiles()) {
      if (!entry?.filePath || collected.has(entry.filePath)) {
        continue;
      }
      if (entry.jobId && excludeJobIds.has(entry.jobId)) {
        continue;
      }

      let rawText = '';
      try {
        rawText = fs.readFileSync(entry.filePath, 'utf-8');
      } catch (_error) {
        continue;
      }

      const matchedNeedle = pathNeedles.find((needle) => rawText.includes(needle));
      if (matchedNeedle) {
        addSource(entry.filePath, `path:${matchedNeedle}`);
      }
    }
  }

  return Array.from(collected.values())
    .sort((a, b) => {
      if (a.firstTimestampMs !== null && b.firstTimestampMs !== null && a.firstTimestampMs !== b.firstTimestampMs) {
        return a.firstTimestampMs - b.firstTimestampMs;
      }
      if (a.jobId && b.jobId && a.jobId !== b.jobId) {
        return a.jobId - b.jobId;
      }
      return a.fileName.localeCompare(b.fileName, undefined, { numeric: true, sensitivity: 'base' });
    });
}

function recoverCdJobArtifactsForImport(options = {}) {
  const currentRawPath = String(options.currentRawPath || '').trim() || null;
  const settings = options.settings && typeof options.settings === 'object' ? options.settings : {};
  const baseMetadata = options.baseMetadata && typeof options.baseMetadata === 'object'
    ? options.baseMetadata
    : {};
  const rawPathCandidates = Array.isArray(options.rawPathCandidates)
    ? options.rawPathCandidates
    : [];
  const relatedJobIds = Array.isArray(options.relatedJobIds)
    ? options.relatedJobIds
    : [];
  const excludeJobIds = Array.isArray(options.excludeJobIds)
    ? options.excludeJobIds
    : [];
  const logSources = findRelatedJobLogSources({
    rawPathCandidates: [currentRawPath, ...rawPathCandidates],
    relatedJobIds,
    excludeJobIds
  });
  const logLines = logSources.flatMap((source) => source.lines || []);

  let formatFromLogs = null;
  let selectedTracksFromLogs = [];
  let selectedTracksLogSaysAll = false;
  let cdparanoiaCmd = null;
  const explicitOutputDirs = [];
  const outputFilePathsFromLogs = [];
  const seenOutputFilePath = new Set();

  for (const line of logLines) {
    const message = String(line || '').replace(/^\[[^\]]+\]\s+\[[^\]]+\]\s*/, '');
    if (!message) {
      continue;
    }

    const startMatch = message.match(/CD-Rip gestartet:\s*Format=([a-z0-9]+)\s*,\s*Tracks=(.+)$/i);
    if (startMatch) {
      formatFromLogs = normalizeCdFormat(startMatch[1]) || formatFromLogs;
      const selectedTrackText = String(startMatch[2] || '').trim();
      if (/^alle$/i.test(selectedTrackText)) {
        selectedTracksLogSaysAll = true;
        selectedTracksFromLogs = [];
      } else {
        selectedTracksFromLogs = selectedTrackText
          .split(',')
          .map((value) => Number(value))
          .filter((value) => Number.isFinite(value) && value > 0)
          .map((value) => Math.trunc(value));
      }
    }

    const completionMatch = message.match(/CD-Rip abgeschlossen\.\s*Ausgabe:\s*(.+)$/i);
    if (completionMatch) {
      const outputDir = normalizeAudioPathToken(completionMatch[1]) || String(completionMatch[1] || '').trim();
      if (outputDir) {
        explicitOutputDirs.push(outputDir);
      }
    }

    if (!cdparanoiaCmd) {
      const ripMatch = message.match(/Promptkette \[Rip \d+\/\d+]:\s*(.+)$/i);
      if (ripMatch) {
        cdparanoiaCmd = extractCommandToken(ripMatch[1]);
      }
    }

    for (const token of extractAbsolutePathTokens(message)) {
      const ext = path.extname(token).toLowerCase();
      if (!CD_OUTPUT_AUDIO_EXTENSIONS.has(ext)) {
        continue;
      }
      if (CD_RAW_TRACK_FILE_PATTERN.test(path.basename(token))) {
        continue;
      }
      if (!seenOutputFilePath.has(token)) {
        seenOutputFilePath.add(token);
        outputFilePathsFromLogs.push(token);
      }
    }
  }

  const outputPath = pickPreferredExistingPath([
    ...(options.fallbackOutputPath ? [options.fallbackOutputPath] : []),
    ...explicitOutputDirs,
    ...outputFilePathsFromLogs.map((filePath) => path.dirname(filePath))
  ]);

  const outputAudioFiles = outputPath ? collectAudioFilesRecursively(outputPath) : [];
  const parsedOutputTracks = assignMissingTrackPositions(
    (outputAudioFiles.length > 0 ? outputAudioFiles : outputFilePathsFromLogs)
      .map((filePath) => parseCdOutputTrackFromPath(filePath))
      .filter(Boolean)
  );

  const rawTracks = [];
  if (currentRawPath && fs.existsSync(currentRawPath)) {
    try {
      const rawEntries = fs.readdirSync(currentRawPath, { withFileTypes: true });
      for (const entry of rawEntries) {
        if (!entry.isFile()) {
          continue;
        }
        const match = entry.name.match(CD_RAW_TRACK_FILE_PATTERN);
        if (!match) {
          continue;
        }
        const parsedPosition = Number(match[1]);
        if (!Number.isFinite(parsedPosition) || parsedPosition <= 0) {
          continue;
        }
        rawTracks.push({
          position: Math.trunc(parsedPosition),
          title: `Track ${Math.trunc(parsedPosition)}`,
          artist: null
        });
      }
    } catch (_error) {
      // ignore raw dir read errors during best-effort import recovery
    }
  }

  const folderMetadata = parseRawFolderMetadata(path.basename(currentRawPath || rawPathCandidates[0] || ''));
  const outputFolderMetadata = parseCdFolderMetadataFromOutputDir(outputPath);
  const selectedTrackSet = new Set(
    selectedTracksFromLogs
      .map((value) => Number(value))
      .filter((value) => Number.isFinite(value) && value > 0)
      .map((value) => Math.trunc(value))
  );
  const trackPositionSet = new Set();
  for (const track of rawTracks) {
    trackPositionSet.add(track.position);
  }
  for (const track of parsedOutputTracks) {
    if (Number.isFinite(Number(track?.position)) && Number(track.position) > 0) {
      trackPositionSet.add(Math.trunc(Number(track.position)));
    }
  }
  for (const position of selectedTrackSet) {
    trackPositionSet.add(position);
  }

  const trackPositions = Array.from(trackPositionSet).sort((a, b) => a - b);
  const outputTrackByPosition = new Map(
    parsedOutputTracks
      .filter((track) => Number.isFinite(Number(track?.position)) && Number(track.position) > 0)
      .map((track) => [Math.trunc(Number(track.position)), track])
  );
  const rawTrackByPosition = new Map(
    rawTracks
      .filter((track) => Number.isFinite(Number(track?.position)) && Number(track.position) > 0)
      .map((track) => [Math.trunc(Number(track.position)), track])
  );

  const tracks = trackPositions.map((position) => {
    const outputTrack = outputTrackByPosition.get(position) || null;
    const rawTrack = rawTrackByPosition.get(position) || null;
    return {
      position,
      title: String(outputTrack?.title || rawTrack?.title || `Track ${position}`).trim() || `Track ${position}`,
      artist: String(outputTrack?.artist || rawTrack?.artist || '').trim() || null,
      selected: selectedTrackSet.size > 0 ? selectedTrackSet.has(position) : true
    };
  });

  const selectedTracks = selectedTracksLogSaysAll || selectedTrackSet.size === 0
    ? trackPositions
    : trackPositions.filter((position) => selectedTrackSet.has(position));
  const artistHints = [
    ...parsedOutputTracks.map((track) => track.artist),
    ...tracks.map((track) => track.artist),
    baseMetadata.artist,
    outputFolderMetadata.artist
  ].filter(Boolean);
  const formatHints = [
    formatFromLogs,
    ...parsedOutputTracks.map((track) => track.format)
  ].filter(Boolean);
  const inferredArtist = findMostCommonString(artistHints);
  const inferredFormat = findMostCommonString(formatHints);
  const normalizedYear = Number(baseMetadata.year);
  const year = Number.isFinite(normalizedYear) && normalizedYear > 0
    ? Math.trunc(normalizedYear)
    : (outputFolderMetadata.year || folderMetadata.year || null);
  const title = String(
    baseMetadata.title
    || baseMetadata.album
    || outputFolderMetadata.album
    || folderMetadata.title
    || ''
  ).trim() || null;
  const artist = String(baseMetadata.artist || inferredArtist || '').trim() || null;
  const selectedMetadata = {
    title,
    album: title,
    artist,
    year
  };
  const outputTemplate = String(settings.cd_output_template || cdRipService.DEFAULT_CD_OUTPUT_TEMPLATE).trim()
    || cdRipService.DEFAULT_CD_OUTPUT_TEMPLATE;
  const outputPathFromMetadata = findCdOutputDirByMetadata({
    settings,
    baseMetadata: selectedMetadata,
    outputTemplate
  });
  const resolvedOutputPath = outputPath || outputPathFromMetadata || null;

  return {
    logSources,
    outputPath: resolvedOutputPath,
    outputPathExists: Boolean(resolvedOutputPath && fs.existsSync(resolvedOutputPath)),
    format: normalizeCdFormat(inferredFormat),
    tracks,
    selectedTracks,
    selectedMetadata,
    cdparanoiaCmd: String(cdparanoiaCmd || 'cdparanoia').trim() || 'cdparanoia',
    outputTemplate,
    importedLogFiles: logSources.map((source) => ({
      fileName: source.fileName,
      jobId: source.jobId,
      matchedBy: Array.isArray(source.matchedBy) ? source.matchedBy : [],
      lineCount: Array.isArray(source.lines) ? source.lines.length : 0
    }))
  };
}

function deleteFilesRecursively(rootPath, keepRoot = true) {
  const result = {
    filesDeleted: 0,
    dirsRemoved: 0
  };

  const visit = (current, isRoot = false) => {
    if (!fs.existsSync(current)) {
      return;
    }

    const stat = fs.lstatSync(current);
    if (stat.isDirectory()) {
      const entries = fs.readdirSync(current, { withFileTypes: true });
      for (const entry of entries) {
        const abs = path.join(current, entry.name);
        if (entry.isDirectory()) {
          visit(abs, false);
        } else {
          fs.unlinkSync(abs);
          result.filesDeleted += 1;
        }
      }

      const remaining = fs.readdirSync(current);
      if (remaining.length === 0 && (!isRoot || !keepRoot)) {
        fs.rmdirSync(current);
        result.dirsRemoved += 1;
      }
      return;
    }

    fs.unlinkSync(current);
    result.filesDeleted += 1;
  };

  visit(rootPath, true);
  return result;
}

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

function normalizeArchiveTarget(value) {
  const raw = String(value || '').trim().toLowerCase();
  if (raw === 'raw') {
    return 'raw';
  }
  if (raw === 'output' || raw === 'movie' || raw === 'encode') {
    return 'output';
  }
  return null;
}

function sanitizeArchiveNamePart(value, fallback = 'job') {
  const normalized = String(value || '')
    .normalize('NFKD')
    .replace(/[^\x00-\x7F]+/g, '');
  const safe = normalized
    .replace(/[^A-Za-z0-9._-]+/g, '_')
    .replace(/_+/g, '_')
    .replace(/^[_-]+|[_-]+$/g, '')
    .slice(0, 80);
  return safe || fallback;
}

function buildJobArchiveName(job, target) {
  const jobId = normalizeJobIdValue(job?.id) || 'unknown';
  const titlePart = sanitizeArchiveNamePart(job?.title || job?.detected_title || '', 'job');
  const targetPart = target === 'raw' ? 'raw' : 'encode';
  return `job-${jobId}-${titlePart}-${targetPart}.zip`;
}

function parseSourceJobIdFromPlan(encodePlanRaw) {
  const plan = parseInfoFromValue(encodePlanRaw, null);
  const sourceJobId = normalizeJobIdValue(plan?.sourceJobId);
  return sourceJobId || null;
}

function parseRetryLinkedJobIdsFromLogLines(lines = []) {
  const jobIds = new Set();
  const list = Array.isArray(lines) ? lines : [];
  for (const line of list) {
    const text = String(line || '');
    if (!text) {
      continue;
    }
    if (!/retry/i.test(text)) {
      continue;
    }
    const regex = /job\s*#(\d+)/ig;
    let match = regex.exec(text);
    while (match) {
      const id = normalizeJobIdValue(match?.[1]);
      if (id) {
        jobIds.add(id);
      }
      match = regex.exec(text);
    }
  }
  return Array.from(jobIds);
}

function normalizeLineageReason(value) {
  const normalized = String(value || '').trim();
  return normalized || null;
}

function inspectDeletionPath(targetPath) {
  const normalized = normalizeComparablePath(targetPath);
  if (!normalized) {
    return {
      path: null,
      exists: false,
      isDirectory: false,
      isFile: false
    };
  }
  try {
    const stat = fs.lstatSync(normalized);
    return {
      path: normalized,
      exists: true,
      isDirectory: stat.isDirectory(),
      isFile: stat.isFile()
    };
  } catch (_error) {
    return {
      path: normalized,
      exists: false,
      isDirectory: false,
      isFile: false
    };
  }
}

function buildJobDisplayTitle(job = null) {
  if (!job || typeof job !== 'object') {
    return '-';
  }
  return String(job.title || job.detected_title || `Job #${job.id || '-'}`).trim() || '-';
}

function isHiddenDirectoryName(value) {
  return String(value || '').trim().startsWith('.');
}

function isFilesystemRootPath(inputPath) {
  const raw = String(inputPath || '').trim();
  if (!raw) {
    return false;
  }
  const resolved = normalizeComparablePath(raw);
  const parsedRoot = path.parse(resolved).root;
  return Boolean(parsedRoot && resolved === normalizeComparablePath(parsedRoot));
}

class HistoryService {
  async createJob({
    discDevice = null,
    status = 'ANALYZING',
    detectedTitle = null,
    mediaType = null,
    jobKind = null
  }) {
    const db = await getDb();
    const startTime = new Date().toISOString();
    const normalizedMediaType = (() => {
      const normalized = normalizeMediaTypeValue(mediaType);
      if (normalized) {
        return normalized;
      }
      const legacy = String(mediaType || '').trim().toLowerCase();
      return legacy === 'converter' ? 'converter' : null;
    })();
    const normalizedJobKind = normalizeJobKindValue(jobKind);

    const result = await db.run(
      `
        INSERT INTO jobs (disc_device, status, start_time, detected_title, last_state, media_type, job_kind, created_at, updated_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
      `,
      [discDevice, status, startTime, detectedTitle, status, normalizedMediaType, normalizedJobKind]
    );
    logger.info('job:created', {
      jobId: result.lastID,
      discDevice,
      status,
      detectedTitle,
      mediaType: normalizedMediaType,
      jobKind: normalizedJobKind
    });

    return this.getJobById(result.lastID);
  }

  async updateJob(jobId, patch) {
    const db = await getDb();
    const fields = [];
    const values = [];

    for (const [key, value] of Object.entries(patch)) {
      fields.push(`${key} = ?`);
      values.push(value);
    }

    fields.push('updated_at = CURRENT_TIMESTAMP');
    values.push(jobId);

    await db.run(`UPDATE jobs SET ${fields.join(', ')} WHERE id = ?`, values);
    logger.debug('job:updated', { jobId, patchKeys: Object.keys(patch) });
    return this.getJobById(jobId, { skipRepair: true });
  }

  async updateJobStatus(jobId, status, extra = {}) {
    return this.updateJob(jobId, {
      status,
      last_state: status,
      ...extra
    });
  }

  async updateRawPathByOldPath(oldRawPath, newRawPath) {
    const db = await getDb();
    const result = await db.run(
      'UPDATE jobs SET raw_path = ?, updated_at = CURRENT_TIMESTAMP WHERE raw_path = ?',
      [newRawPath, oldRawPath]
    );
    logger.info('job:raw-path-bulk-updated', { oldRawPath, newRawPath, changes: result.changes });
    return result.changes;
  }

  async listJobLineageArtifactsByJobIds(jobIds = []) {
    const normalizedIds = Array.isArray(jobIds)
      ? jobIds
        .map((value) => normalizeJobIdValue(value))
        .filter(Boolean)
      : [];
    if (normalizedIds.length === 0) {
      return new Map();
    }

    const db = await getDb();
    const placeholders = normalizedIds.map(() => '?').join(', ');
    const rows = await db.all(
      `
        SELECT id, job_id, source_job_id, media_type, raw_path, output_path, reason, note, created_at
        FROM job_lineage_artifacts
        WHERE job_id IN (${placeholders})
        ORDER BY id ASC
      `,
      normalizedIds
    );

    const byJobId = new Map();
    for (const row of rows) {
      const ownerJobId = normalizeJobIdValue(row?.job_id);
      if (!ownerJobId) {
        continue;
      }
      if (!byJobId.has(ownerJobId)) {
        byJobId.set(ownerJobId, []);
      }
      byJobId.get(ownerJobId).push({
        id: normalizeJobIdValue(row?.id),
        jobId: ownerJobId,
        sourceJobId: normalizeJobIdValue(row?.source_job_id),
        mediaType: normalizeMediaTypeValue(row?.media_type),
        rawPath: String(row?.raw_path || '').trim() || null,
        outputPath: String(row?.output_path || '').trim() || null,
        reason: normalizeLineageReason(row?.reason),
        note: String(row?.note || '').trim() || null,
        createdAt: String(row?.created_at || '').trim() || null
      });
    }

    return byJobId;
  }

  async transferJobLineageArtifacts(sourceJobId, replacementJobId, options = {}) {
    const fromJobId = normalizeJobIdValue(sourceJobId);
    const toJobId = normalizeJobIdValue(replacementJobId);
    if (!fromJobId || !toJobId || fromJobId === toJobId) {
      const error = new Error('Ungültige Job-IDs für Lineage-Transfer.');
      error.statusCode = 400;
      throw error;
    }

    const reason = normalizeLineageReason(options?.reason) || 'job_replaced';
    const note = String(options?.note || '').trim() || null;
    const sourceJob = await this.getJobById(fromJobId);
    if (!sourceJob) {
      const error = new Error(`Quell-Job ${fromJobId} nicht gefunden.`);
      error.statusCode = 404;
      throw error;
    }

    const settings = await settingsService.getSettingsMap();
    const resolvedPaths = resolveEffectiveStoragePathsForJob(settings, sourceJob);
    const rawPath = String(resolvedPaths?.effectiveRawPath || sourceJob?.raw_path || '').trim() || null;
    const outputPath = String(resolvedPaths?.effectiveOutputPath || sourceJob?.output_path || '').trim() || null;
    const mediaType = normalizeMediaTypeValue(resolvedPaths?.mediaType) || 'other';

    const db = await getDb();
    await db.exec('BEGIN');
    try {
      await db.run(
        `
          INSERT INTO job_lineage_artifacts (
            job_id, source_job_id, media_type, raw_path, output_path, reason, note, created_at
          )
          SELECT ?, source_job_id, media_type, raw_path, output_path, reason, note, created_at
          FROM job_lineage_artifacts
          WHERE job_id = ?
        `,
        [toJobId, fromJobId]
      );

      if (rawPath || outputPath) {
        await db.run(
          `
            INSERT INTO job_lineage_artifacts (
              job_id, source_job_id, media_type, raw_path, output_path, reason, note, created_at
            )
            VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
          `,
          [toJobId, fromJobId, mediaType, rawPath, outputPath, reason, note]
        );
      }

      // Preserve known output folders when replacing a job.
      await db.run(
        `
          INSERT OR IGNORE INTO job_output_folders (job_id, output_path, label, created_at)
          SELECT ?, output_path, label, created_at
          FROM job_output_folders
          WHERE job_id = ?
        `,
        [toJobId, fromJobId]
      );
      if (outputPath) {
        await db.run(
          'INSERT OR IGNORE INTO job_output_folders (job_id, output_path, label) VALUES (?, ?, ?)',
          [toJobId, outputPath, 'Lineage-Output']
        );
      }

      await db.exec('COMMIT');
    } catch (error) {
      await db.exec('ROLLBACK');
      throw error;
    }

    logger.info('job:lineage:transferred', {
      sourceJobId: fromJobId,
      replacementJobId: toJobId,
      mediaType,
      reason,
      hasRawPath: Boolean(rawPath),
      hasOutputPath: Boolean(outputPath)
    });
  }

  async retireJobInFavorOf(sourceJobId, replacementJobId, options = {}) {
    const fromJobId = normalizeJobIdValue(sourceJobId);
    const toJobId = normalizeJobIdValue(replacementJobId);
    if (!fromJobId || !toJobId || fromJobId === toJobId) {
      const error = new Error('Ungültige Job-IDs für Job-Ersatz.');
      error.statusCode = 400;
      throw error;
    }

    const reason = normalizeLineageReason(options?.reason) || 'job_replaced';
    const note = String(options?.note || '').trim() || null;

    await this.transferJobLineageArtifacts(fromJobId, toJobId, { reason, note });

    const db = await getDb();
    const pipelineRow = await db.get('SELECT active_job_id FROM pipeline_state WHERE id = 1');
    const activeJobId = normalizeJobIdValue(pipelineRow?.active_job_id);
    const sourceIsActive = activeJobId === fromJobId;

    await db.exec('BEGIN');
    try {
      if (sourceIsActive) {
        await db.run(
          `
            UPDATE pipeline_state
            SET active_job_id = ?, updated_at = CURRENT_TIMESTAMP
            WHERE id = 1
          `,
          [toJobId]
        );
      } else {
        await db.run(
          `
            UPDATE pipeline_state
            SET active_job_id = NULL, updated_at = CURRENT_TIMESTAMP
            WHERE id = 1 AND active_job_id = ?
          `,
          [fromJobId]
        );
      }

      await db.run('DELETE FROM jobs WHERE id = ?', [fromJobId]);
      await db.exec('COMMIT');
    } catch (error) {
      await db.exec('ROLLBACK');
      throw error;
    }

    await this.closeProcessLog(fromJobId);
    this._deleteProcessLogFile(fromJobId);

    logger.warn('job:retired', {
      sourceJobId: fromJobId,
      replacementJobId: toJobId,
      reason,
      sourceWasActive: sourceIsActive
    });

    return {
      retired: true,
      sourceJobId: fromJobId,
      replacementJobId: toJobId,
      reason
    };
  }

  async appendLog(jobId, source, message) {
    this.appendProcessLog(jobId, source, message);
  }

  async cacheAndPromoteExternalPoster(jobId, posterUrl, options = {}) {
    const normalizedJobId = normalizeJobIdValue(jobId);
    const normalizedPosterUrl = String(posterUrl || '').trim();
    const logFailures = options?.logFailures !== false;
    const sourceLabel = String(options?.source || 'Poster').trim() || 'Poster';
    const logPrefix = `${sourceLabel} Link`;

    if (!normalizedJobId) {
      return { ok: false, reason: 'invalid_job_id', localUrl: null };
    }
    if (!normalizedPosterUrl || thumbnailService.isLocalUrl(normalizedPosterUrl)) {
      return { ok: false, reason: 'no_external_url', localUrl: null };
    }

    const cacheResult = await thumbnailService.cacheJobThumbnailDetailed(normalizedJobId, normalizedPosterUrl);
    if (!cacheResult?.ok) {
      if (logFailures) {
        await this.appendLog(
          normalizedJobId,
          'SYSTEM',
          `${logPrefix} konnte nicht heruntergeladen werden: ${normalizedPosterUrl}${cacheResult?.error ? ` (${cacheResult.error})` : ''}`
        );
      }
      return {
        ok: false,
        reason: 'cache_failed',
        localUrl: null,
        error: cacheResult?.error || null
      };
    }

    const promotedUrl = thumbnailService.promoteJobThumbnail(normalizedJobId);
    if (!promotedUrl) {
      if (logFailures) {
        await this.appendLog(
          normalizedJobId,
          'SYSTEM',
          `${logPrefix} konnte nicht finalisiert werden: ${normalizedPosterUrl}`
        );
      }
      return { ok: false, reason: 'promote_failed', localUrl: null };
    }

    try {
      await this.updateJob(normalizedJobId, { poster_url: promotedUrl });
      return { ok: true, reason: 'ok', localUrl: promotedUrl, sourceUrl: normalizedPosterUrl };
    } catch (error) {
      logger.warn('thumbnail:update-job-failed', {
        jobId: normalizedJobId,
        posterUrl: normalizedPosterUrl,
        promotedUrl,
        error: error?.message || String(error)
      });
      if (logFailures) {
        await this.appendLog(
          normalizedJobId,
          'SYSTEM',
          `${logPrefix} wurde heruntergeladen, konnte aber nicht am Job gespeichert werden: ${normalizedPosterUrl}`
        );
      }
      return {
        ok: false,
        reason: 'update_failed',
        localUrl: null,
        error: error?.message || String(error)
      };
    }
  }

  queuePosterCache(jobId, posterUrl, options = {}) {
    const normalizedJobId = normalizeJobIdValue(jobId);
    if (!normalizedJobId) {
      return;
    }
    this.cacheAndPromoteExternalPoster(normalizedJobId, posterUrl, options).catch((error) => {
      logger.warn('thumbnail:queue-cache-failed', {
        jobId: normalizedJobId,
        posterUrl: String(posterUrl || '').trim() || null,
        error: error?.message || String(error)
      });
    });
  }

  appendProcessLog(jobId, source, message) {
    const filePath = toProcessLogPath(jobId);
    const streamKey = toProcessLogStreamKey(jobId);
    if (!filePath || !streamKey) {
      return;
    }
    try {
      fs.mkdirSync(path.dirname(filePath), { recursive: true });
      let stream = processLogStreams.get(streamKey);
      if (!stream) {
        stream = fs.createWriteStream(filePath, {
          flags: 'a',
          encoding: 'utf-8'
        });
        stream.on('error', (error) => {
          logger.warn('job:process-log:stream-error', {
            jobId,
            source,
            error: error?.message || String(error)
          });
        });
        processLogStreams.set(streamKey, stream);
      }
      const line = `[${new Date().toISOString()}] [${source}] ${String(message || '')}\n`;
      stream.write(line);
    } catch (error) {
      logger.warn('job:process-log:append-failed', {
        jobId,
        source,
        error: error?.message || String(error)
      });
    }
  }

  async closeProcessLog(jobId) {
    const streamKey = toProcessLogStreamKey(jobId);
    if (!streamKey) {
      return;
    }
    const stream = processLogStreams.get(streamKey);
    if (!stream) {
      return;
    }
    processLogStreams.delete(streamKey);
    await new Promise((resolve) => {
      stream.end(resolve);
    });
  }

  async resetProcessLog(jobId) {
    await this.closeProcessLog(jobId);
    const filePath = toProcessLogPath(jobId);
    if (!filePath || !fs.existsSync(filePath)) {
      return;
    }
    try {
      fs.unlinkSync(filePath);
    } catch (error) {
      logger.warn('job:process-log:reset-failed', {
        jobId,
        path: filePath,
        error: error?.message || String(error)
      });
    }
  }

  async readProcessLogLines(jobId, options = {}) {
    const includeAll = Boolean(options.includeAll);
    const parsedTail = Number(options.tailLines);
    const tailLines = Number.isFinite(parsedTail) && parsedTail > 0
      ? Math.trunc(parsedTail)
      : 800;
    const filePath = toProcessLogPath(jobId);
    if (!filePath || !fs.existsSync(filePath)) {
      return {
        exists: false,
        lines: [],
        returned: 0,
        total: 0,
        truncated: false
      };
    }

    if (includeAll) {
      const raw = await fs.promises.readFile(filePath, 'utf-8');
      const lines = String(raw || '')
        .split(/\r\n|\n|\r/)
        .filter((line) => line.length > 0);
      return {
        exists: true,
        lines,
        returned: lines.length,
        total: lines.length,
        truncated: false
      };
    }

    const stat = await fs.promises.stat(filePath);
    if (!stat.isFile() || stat.size <= 0) {
      return {
        exists: true,
        lines: [],
        returned: 0,
        total: 0,
        truncated: false
      };
    }

    const readBytes = Math.min(stat.size, PROCESS_LOG_TAIL_MAX_BYTES);
    const start = Math.max(0, stat.size - readBytes);
    const handle = await fs.promises.open(filePath, 'r');
    let buffer = Buffer.alloc(0);
    try {
      buffer = Buffer.alloc(readBytes);
      const { bytesRead } = await handle.read(buffer, 0, readBytes, start);
      buffer = buffer.subarray(0, bytesRead);
    } finally {
      await handle.close();
    }

    let text = buffer.toString('utf-8');
    if (start > 0) {
      const parts = text.split(/\r\n|\n|\r/);
      parts.shift();
      text = parts.join('\n');
    }

    let lines = text.split(/\r\n|\n|\r/).filter((line) => line.length > 0);
    let truncated = start > 0;
    if (lines.length > tailLines) {
      lines = lines.slice(-tailLines);
      truncated = true;
    }

    return {
      exists: true,
      lines,
      returned: lines.length,
      total: lines.length,
      truncated
    };
  }

  buildRecoveredCdEncodePlan(recovery = {}) {
    const tracks = Array.isArray(recovery?.tracks) ? recovery.tracks : [];
    const selectedTracks = Array.isArray(recovery?.selectedTracks) ? recovery.selectedTracks : [];
    const format = normalizeCdFormat(recovery?.format) || null;
    const outputTemplate = String(recovery?.outputTemplate || cdRipService.DEFAULT_CD_OUTPUT_TEMPLATE).trim()
      || cdRipService.DEFAULT_CD_OUTPUT_TEMPLATE;

    if (tracks.length === 0 && selectedTracks.length === 0 && !format) {
      return null;
    }

    return {
      format,
      formatOptions: {},
      selectedTracks: selectedTracks.length > 0
        ? selectedTracks
        : tracks
          .map((track) => Number(track?.position))
          .filter((value) => Number.isFinite(value) && value > 0)
          .map((value) => Math.trunc(value)),
      tracks,
      outputTemplate,
      recoveredFrom: {
        source: 'orphan_raw_import',
        importedLogFiles: Array.isArray(recovery?.importedLogFiles) ? recovery.importedLogFiles : [],
        outputPath: recovery?.outputPath || null,
        outputPathExists: Boolean(recovery?.outputPathExists)
      }
    };
  }

  buildOrphanRawImportMakemkvInfo(options = {}) {
    const mediaProfile = normalizeMediaTypeValue(options.mediaProfile) || 'other';
    const importedAt = String(options.importedAt || new Date().toISOString()).trim() || new Date().toISOString();
    const rawPath = String(options.rawPath || '').trim() || null;
    const existingInfo = options.existingInfo && typeof options.existingInfo === 'object'
      ? options.existingInfo
      : {};
    const recovery = options.recovery && typeof options.recovery === 'object'
      ? options.recovery
      : null;
    const analyzeContextPatch = options.analyzeContextPatch && typeof options.analyzeContextPatch === 'object'
      ? options.analyzeContextPatch
      : {};
    const providedSelectedMetadata = options.selectedMetadata && typeof options.selectedMetadata === 'object'
      ? options.selectedMetadata
      : {};
    const compactMetadataObject = (input) => Object.fromEntries(
      Object.entries(input || {}).filter(([, value]) => {
        if (value === null || value === undefined) {
          return false;
        }
        if (typeof value === 'string') {
          return value.trim().length > 0;
        }
        return true;
      })
    );
    const existingSelectedMetadata = compactMetadataObject(extractSelectedMetadataFromMakemkvInfo(existingInfo));
    const normalizedProvidedSelectedMetadata = compactMetadataObject(providedSelectedMetadata);
    const mergedSelectedMetadata = {
      ...existingSelectedMetadata,
      ...normalizedProvidedSelectedMetadata
    };
    const existingAnalyzeContext = existingInfo.analyzeContext && typeof existingInfo.analyzeContext === 'object'
      ? existingInfo.analyzeContext
      : {};
    const previousImportContext = existingInfo.importContext && typeof existingInfo.importContext === 'object'
      ? existingInfo.importContext
      : {};
    const nextImportContext = {
      ...previousImportContext,
      requestedRawPath: String(options.requestedRawPath || previousImportContext.requestedRawPath || rawPath || '').trim() || null,
      sourceFolderJobId: normalizeJobIdValue(
        options.sourceFolderJobId
        ?? previousImportContext.sourceFolderJobId
        ?? null
      )
    };

    if (recovery?.outputPath) {
      nextImportContext.recoveredOutputPath = recovery.outputPath;
    }
    if (Array.isArray(recovery?.importedLogFiles) && recovery.importedLogFiles.length > 0) {
      nextImportContext.importedLogFiles = recovery.importedLogFiles;
    }

    const nextInfo = {
      ...existingInfo,
      status: 'SUCCESS',
      source: 'orphan_raw_import',
      importedAt,
      rawPath,
      mediaProfile,
      analyzeContext: {
        ...existingAnalyzeContext,
        ...analyzeContextPatch,
        mediaProfile
      },
      importContext: nextImportContext
    };

    if (Object.keys(mergedSelectedMetadata).length > 0) {
      nextInfo.selectedMetadata = mergedSelectedMetadata;
      nextInfo.analyzeContext = {
        ...nextInfo.analyzeContext,
        selectedMetadata: {
          ...(existingAnalyzeContext.selectedMetadata && typeof existingAnalyzeContext.selectedMetadata === 'object'
            ? existingAnalyzeContext.selectedMetadata
            : {}),
          ...mergedSelectedMetadata
        }
      };
    }

    if (mediaProfile === 'cd' && recovery) {
      if (Array.isArray(recovery.tracks) && recovery.tracks.length > 0) {
        nextInfo.tracks = recovery.tracks;
      }
      if (recovery.selectedMetadata && typeof recovery.selectedMetadata === 'object') {
        const recoverySelectedMetadata = compactMetadataObject(recovery.selectedMetadata);
        nextInfo.selectedMetadata = {
          ...(nextInfo.selectedMetadata && typeof nextInfo.selectedMetadata === 'object'
            ? nextInfo.selectedMetadata
            : {}),
          ...recoverySelectedMetadata
        };
        nextInfo.analyzeContext = {
          ...nextInfo.analyzeContext,
          selectedMetadata: {
            ...(nextInfo.analyzeContext?.selectedMetadata && typeof nextInfo.analyzeContext.selectedMetadata === 'object'
              ? nextInfo.analyzeContext.selectedMetadata
              : {}),
            ...nextInfo.selectedMetadata
          }
        };
      }
      nextInfo.cdparanoiaCmd = String(
        recovery.cdparanoiaCmd
        || existingInfo.cdparanoiaCmd
        || 'cdparanoia'
      ).trim() || 'cdparanoia';
      nextInfo.importRecovery = {
        source: 'orphan_raw_import',
        importedAt,
        logFiles: Array.isArray(recovery.importedLogFiles) ? recovery.importedLogFiles : [],
        outputPath: recovery.outputPath || null,
        outputPathExists: Boolean(recovery.outputPathExists),
        recoveredFormat: normalizeCdFormat(recovery.format),
        selectedTracks: Array.isArray(recovery.selectedTracks) ? recovery.selectedTracks : []
      };
    }

    return nextInfo;
  }

  async restoreImportedProcessLog(jobId, logSources = [], options = {}) {
    const normalizedJobId = normalizeJobIdValue(jobId);
    if (!normalizedJobId) {
      return {
        imported: false,
        sourceCount: 0,
        lineCount: 0
      };
    }

    const normalizedSources = (Array.isArray(logSources) ? logSources : [])
      .filter((source) => source && source.filePath && Array.isArray(source.lines) && source.lines.length > 0);
    const importInfo = options.importInfo && typeof options.importInfo === 'object'
      ? options.importInfo
      : null;
    const shouldAppend = Boolean(options.append);
    if (normalizedSources.length === 0 && !importInfo) {
      return {
        imported: false,
        sourceCount: 0,
        lineCount: 0
      };
    }

    const filePath = toProcessLogPath(normalizedJobId);
    if (!filePath) {
      return {
        imported: false,
        sourceCount: 0,
        lineCount: 0
      };
    }

    await this.closeProcessLog(normalizedJobId);
    fs.mkdirSync(path.dirname(filePath), { recursive: true });
    if (!shouldAppend) {
      try {
        fs.unlinkSync(filePath);
      } catch (_error) {
        // ignore missing file on overwrite
      }
    }

    const lines = [];
    if (normalizedSources.length > 0) {
      const fileSummary = normalizedSources
        .map((source) => source.fileName || path.basename(source.filePath))
        .filter(Boolean)
        .join(', ');
      lines.push(
        `[${new Date().toISOString()}] [SYSTEM] Importierte vorhandene Job-Logs: ${fileSummary}`
      );
      for (const source of normalizedSources) {
        lines.push(...source.lines);
      }
    }
    if (importInfo) {
      lines.push(
        `[${new Date().toISOString()}] [SYSTEM] Orphan-RAW-Import Zusatzinfo: ${JSON.stringify(importInfo)}`
      );
    }

    if (lines.length === 0) {
      return {
        imported: false,
        sourceCount: normalizedSources.length,
        lineCount: 0
      };
    }

    try {
      const prefix = shouldAppend && fs.existsSync(filePath) && fs.statSync(filePath).size > 0 ? '\n' : '';
      fs.writeFileSync(filePath, `${prefix}${lines.join('\n')}\n`, {
        encoding: 'utf-8',
        flag: shouldAppend ? 'a' : 'w'
      });
    } catch (error) {
      logger.warn('job:process-log:restore-failed', {
        jobId: normalizedJobId,
        path: filePath,
        error: error?.message || String(error)
      });
      return {
        imported: false,
        sourceCount: normalizedSources.length,
        lineCount: 0
      };
    }

    return {
      imported: normalizedSources.length > 0,
      sourceCount: normalizedSources.length,
      lineCount: lines.length
    };
  }

  async resolveOrphanImportSourceJob(options = {}) {
    const candidateJobIds = Array.isArray(options.candidateJobIds)
      ? options.candidateJobIds
        .map((value) => normalizeJobIdValue(value))
        .filter(Boolean)
      : [];
    const desiredMediaProfile = normalizeMediaTypeValue(options.mediaProfile) || null;
    if (candidateJobIds.length === 0) {
      return null;
    }

    const uniqueCandidateIds = Array.from(new Set(candidateJobIds));
    const db = await getDb();
    const placeholders = uniqueCandidateIds.map(() => '?').join(', ');
    const rows = await db.all(
      `SELECT * FROM jobs WHERE id IN (${placeholders})`,
      uniqueCandidateIds
    );
    if (!Array.isArray(rows) || rows.length === 0) {
      return null;
    }

    const byId = new Map(rows.map((row) => [normalizeJobIdValue(row?.id), row]));
    let best = null;
    let bestScore = -1;

    for (const candidateId of uniqueCandidateIds) {
      const row = byId.get(candidateId);
      if (!row) {
        continue;
      }

      const makemkvInfo = parseJsonSafe(row.makemkv_info_json, {});
      const selectedMetadata = extractSelectedMetadataFromMakemkvInfo(makemkvInfo);
      const analyzeContext = makemkvInfo?.analyzeContext && typeof makemkvInfo.analyzeContext === 'object'
        ? makemkvInfo.analyzeContext
        : {};
      const rowMediaProfile = normalizeMediaTypeValue(
        makemkvInfo?.analyzeContext?.mediaProfile
        || makemkvInfo?.mediaProfile
        || inferMediaTypeFromJobKind(row?.job_kind)
        || row?.media_type
      );
      const posterCandidate = String(
        row?.poster_url
        || selectedMetadata?.coverUrl
        || selectedMetadata?.poster
        || selectedMetadata?.posterUrl
        || ''
      ).trim() || null;
      const hasTitle = Boolean(String(
        selectedMetadata?.title
        || selectedMetadata?.album
        || row?.title
        || row?.detected_title
        || ''
      ).trim());
      const hasArtist = Boolean(String(selectedMetadata?.artist || '').trim());
      const hasPoster = Boolean(posterCandidate);
      const hasExternalMetadata = Boolean(
        row?.omdb_json
        || row?.imdb_id
        || selectedMetadata?.tmdbId
        || analyzeContext?.tmdbId
        || selectedMetadata?.mbId
        || selectedMetadata?.musicBrainzId
        || selectedMetadata?.musicbrainzId
        || selectedMetadata?.musicbrainz_id
        || selectedMetadata?.music_brainz_id
        || selectedMetadata?.musicbrainz
        || selectedMetadata?.mbid
      );
      const isFinished = String(row?.status || row?.last_state || '').trim().toUpperCase() === 'FINISHED';

      let score = 0;
      if (desiredMediaProfile && rowMediaProfile === desiredMediaProfile) {
        score += 10;
      }
      if (hasTitle) {
        score += 6;
      }
      if (desiredMediaProfile === 'cd' && hasArtist) {
        score += 4;
      }
      if (hasPoster) {
        score += 3;
      }
      if (hasExternalMetadata) {
        score += 2;
      }
      if (isFinished) {
        score += 1;
      }

      if (score > bestScore) {
        bestScore = score;
        best = {
          job: row,
          makemkvInfo,
          selectedMetadata,
          analyzeContext,
          mediaProfile: rowMediaProfile,
          posterCandidate
        };
      }
    }

    return best;
  }

  getPreferredPosterCandidateForImport(sourceContext = null, explicitPosterUrl = null) {
    const explicit = String(explicitPosterUrl || '').trim() || null;
    if (explicit) {
      return explicit;
    }
    const sourceJob = sourceContext?.job && typeof sourceContext.job === 'object'
      ? sourceContext.job
      : null;
    const selectedMetadata = sourceContext?.selectedMetadata && typeof sourceContext.selectedMetadata === 'object'
      ? sourceContext.selectedMetadata
      : {};
    return String(
      sourceJob?.poster_url
      || selectedMetadata?.coverUrl
      || selectedMetadata?.poster
      || selectedMetadata?.posterUrl
      || ''
    ).trim() || null;
  }

  async restoreImportedPoster(jobId, sourceContext = null, explicitPosterUrl = null) {
    const normalizedJobId = normalizeJobIdValue(jobId);
    if (!normalizedJobId) {
      return null;
    }

    const sourceJob = sourceContext?.job && typeof sourceContext.job === 'object'
      ? sourceContext.job
      : null;
    const sourceJobId = normalizeJobIdValue(sourceJob?.id);
    const sourcePosterUrl = String(sourceJob?.poster_url || '').trim() || null;
    if (sourceJobId && sourcePosterUrl && thumbnailService.isLocalUrl(sourcePosterUrl)) {
      const copiedUrl = thumbnailService.copyThumbnail(sourceJobId, normalizedJobId);
      if (copiedUrl) {
        await this.updateJob(normalizedJobId, { poster_url: copiedUrl });
        return copiedUrl;
      }
    }

    const preferredPosterUrl = this.getPreferredPosterCandidateForImport(sourceContext, explicitPosterUrl);
    if (!preferredPosterUrl || thumbnailService.isLocalUrl(preferredPosterUrl)) {
      return null;
    }

    const result = await this.cacheAndPromoteExternalPoster(normalizedJobId, preferredPosterUrl, {
      source: 'Coverart',
      logFailures: true
    });
    if (!result?.ok || !result.localUrl) {
      return null;
    }
    return result.localUrl;
  }

  async repairImportedCdJobArtifacts(job, settings = null) {
    if (!job || typeof job !== 'object') {
      return job;
    }

    const makemkvInfo = parseJsonSafe(job.makemkv_info_json, null);
    const mediaProfile = normalizeMediaTypeValue(
      makemkvInfo?.mediaProfile
      || makemkvInfo?.analyzeContext?.mediaProfile
      || inferMediaTypeFromJobKind(job?.job_kind)
      || job?.media_type
    );
    if (String(makemkvInfo?.source || '').trim().toLowerCase() !== 'orphan_raw_import' || mediaProfile !== 'cd') {
      return job;
    }

    const hasTracks = Array.isArray(makemkvInfo?.tracks) && makemkvInfo.tracks.length > 0;
    const hasEncodePlan = Boolean(String(job?.encode_plan_json || '').trim());
    const outputPathRaw = String(job?.output_path || '').trim();
    const hasOutputPath = Boolean(outputPathRaw);
    const outputPathExists = hasOutputPath && (() => {
      try {
        return fs.existsSync(outputPathRaw);
      } catch (_error) {
        return false;
      }
    })();
    const hasExistingLog = hasProcessLogFile(job.id);
    if (hasTracks && hasEncodePlan && hasOutputPath && outputPathExists && hasExistingLog) {
      return job;
    }

    const importContext = makemkvInfo?.importContext && typeof makemkvInfo.importContext === 'object'
      ? makemkvInfo.importContext
      : {};
    const rawPathCandidates = [
      job?.raw_path,
      makemkvInfo?.rawPath,
      importContext?.requestedRawPath,
      importContext?.originalRawPath
    ].filter(Boolean);
    const currentRawPath = String(job?.raw_path || makemkvInfo?.rawPath || '').trim() || null;
    const rawFolderMeta = parseRawFolderMetadata(path.basename(currentRawPath || rawPathCandidates[0] || ''));
    const recovery = recoverCdJobArtifactsForImport({
      currentRawPath,
      rawPathCandidates,
      relatedJobIds: [
        importContext?.sourceFolderJobId,
        rawFolderMeta.folderJobId
      ],
      excludeJobIds: [],
      settings: settings || {},
      baseMetadata: {
        title: job?.title || makemkvInfo?.selectedMetadata?.title || null,
        album: makemkvInfo?.selectedMetadata?.album || job?.title || null,
        artist: makemkvInfo?.selectedMetadata?.artist || null,
        year: makemkvInfo?.selectedMetadata?.year || job?.year || null
      },
      fallbackOutputPath: job?.output_path || null
    });
    const sourceJobContext = await this.resolveOrphanImportSourceJob({
      candidateJobIds: [
        importContext?.sourceFolderJobId,
        rawFolderMeta.folderJobId,
        ...recovery.logSources.map((source) => source?.jobId)
      ],
      mediaProfile: 'cd'
    });
    const sourceJob = sourceJobContext?.job || null;
    const sourceSelectedMetadata = sourceJobContext?.selectedMetadata && typeof sourceJobContext.selectedMetadata === 'object'
      ? sourceJobContext.selectedMetadata
      : {};
    const sourcePosterCandidate = this.getPreferredPosterCandidateForImport(sourceJobContext, null);

    const recoveryHasData = recovery.outputPath || recovery.logSources.length > 0 || recovery.tracks.length > 0;
    if (!recoveryHasData && !sourceJob) {
      return job;
    }

    const importedAt = String(makemkvInfo?.importedAt || job?.end_time || new Date().toISOString()).trim()
      || new Date().toISOString();
    const nextMakemkvInfo = this.buildOrphanRawImportMakemkvInfo({
      importedAt,
      rawPath: currentRawPath,
      requestedRawPath: importContext?.requestedRawPath || currentRawPath,
      sourceFolderJobId: importContext?.sourceFolderJobId || rawFolderMeta.folderJobId || null,
      mediaProfile: 'cd',
      existingInfo: makemkvInfo,
      recovery
    });
    const nextEncodePlan = this.buildRecoveredCdEncodePlan(recovery);
    const patch = {};
    const nextMakemkvInfoJson = JSON.stringify(nextMakemkvInfo);

    if (nextMakemkvInfoJson !== String(job?.makemkv_info_json || '')) {
      patch.makemkv_info_json = nextMakemkvInfoJson;
    }
    if (!hasEncodePlan && nextEncodePlan) {
      patch.encode_plan_json = JSON.stringify(nextEncodePlan);
    }
    const recoveredOutputPath = String(recovery.outputPath || '').trim() || null;
    const normalizedOutputPath = hasOutputPath ? normalizeComparablePath(outputPathRaw) : null;
    const normalizedRecoveredOutputPath = recoveredOutputPath ? normalizeComparablePath(recoveredOutputPath) : null;
    const recoveredOutputPathExists = Boolean(recovery.outputPathExists);
    const shouldPatchOutputPath = Boolean(
      recoveredOutputPath
      && (
        !hasOutputPath
        || (
          !outputPathExists
          && (
            recoveredOutputPathExists
            || recovery.logSources.length > 0
          )
        )
      )
      && normalizedRecoveredOutputPath !== normalizedOutputPath
    );
    if (shouldPatchOutputPath) {
      patch.output_path = recoveredOutputPath;
    }
    if (!job?.title && nextMakemkvInfo?.selectedMetadata?.title) {
      patch.title = nextMakemkvInfo.selectedMetadata.title;
    } else if (!job?.title && (sourceSelectedMetadata?.title || sourceSelectedMetadata?.album || sourceJob?.title)) {
      patch.title = sourceSelectedMetadata.title || sourceSelectedMetadata.album || sourceJob.title;
    }
    if (!job?.year && nextMakemkvInfo?.selectedMetadata?.year) {
      patch.year = nextMakemkvInfo.selectedMetadata.year;
    } else if (!job?.year && (sourceSelectedMetadata?.year || sourceJob?.year)) {
      patch.year = sourceSelectedMetadata.year ?? sourceJob.year ?? null;
    }
    if (!job?.imdb_id) {
      const recoveredMbId = String(
        sourceSelectedMetadata?.mbId
        || sourceSelectedMetadata?.musicBrainzId
        || sourceSelectedMetadata?.musicbrainzId
        || sourceSelectedMetadata?.musicbrainz_id
        || sourceSelectedMetadata?.music_brainz_id
        || sourceSelectedMetadata?.musicbrainz
        || sourceSelectedMetadata?.mbid
        || sourceJob?.imdb_id
        || ''
      ).trim() || null;
      if (recoveredMbId) {
        patch.imdb_id = recoveredMbId;
      }
    }
    if (!job?.omdb_json && sourceJob?.omdb_json) {
      patch.omdb_json = sourceJob.omdb_json;
      patch.selected_from_omdb = Number(sourceJob.selected_from_omdb || 0);
    }
    if (!job?.poster_url && sourcePosterCandidate && !thumbnailService.isLocalUrl(sourcePosterCandidate)) {
      patch.poster_url = sourcePosterCandidate;
    }

    if (!hasExistingLog && recovery.logSources.length > 0) {
      await this.restoreImportedProcessLog(job.id, recovery.logSources, {
        importInfo: nextMakemkvInfo
      });
    }

    let updatedJob = job;
    if (Object.keys(patch).length > 0) {
      updatedJob = await this.updateJob(job.id, patch);
    }

    if (!job?.poster_url && sourcePosterCandidate) {
      await this.restoreImportedPoster(job.id, sourceJobContext, sourcePosterCandidate);
      updatedJob = await this.getJobById(job.id);
    }

    return updatedJob;
  }

  async repairImportedOrphanJobClassification(job, settings = null) {
    if (!job || typeof job !== 'object') {
      return job;
    }

    const makemkvInfo = parseJsonSafe(job.makemkv_info_json, null);
    if (String(makemkvInfo?.source || '').trim().toLowerCase() !== 'orphan_raw_import') {
      return job;
    }

    const settingsMap = settings && typeof settings === 'object'
      ? settings
      : {};
    const resolvedRawPath = String(
      job?.raw_path
      || makemkvInfo?.rawPath
      || ''
    ).trim() || null;
    const folderMeta = parseRawFolderMetadata(path.basename(String(resolvedRawPath || '').trim()));
    const seriesRawPathHint = isLikelySeriesRawPath(resolvedRawPath, settingsMap);
    const detectedMediaType = detectOrphanMediaType(resolvedRawPath, { seriesRawPathHint });
    const mkMediaType = normalizeMediaTypeValue(
      makemkvInfo?.analyzeContext?.mediaProfile
      || makemkvInfo?.mediaProfile
      || inferMediaTypeFromJobKind(job?.job_kind)
      || job?.media_type
    );
    const effectiveMediaType = detectedMediaType === 'other'
      ? (mkMediaType || (seriesRawPathHint ? 'dvd' : 'other'))
      : detectedMediaType;
    const patch = {};

    if (['bluray', 'dvd', 'cd', 'audiobook'].includes(effectiveMediaType)) {
      if (normalizeMediaTypeValue(job?.media_type) !== effectiveMediaType) {
        patch.media_type = effectiveMediaType;
      }
      if (normalizeJobKindValue(job?.job_kind) !== effectiveMediaType) {
        patch.job_kind = effectiveMediaType;
      }
    }

    let nextMakemkvInfo = makemkvInfo && typeof makemkvInfo === 'object'
      ? makemkvInfo
      : {};
    const selectedMetadata = extractSelectedMetadataFromMakemkvInfo(nextMakemkvInfo);
    const analyzeContext = nextMakemkvInfo?.analyzeContext && typeof nextMakemkvInfo.analyzeContext === 'object'
      ? nextMakemkvInfo.analyzeContext
      : {};
    const hasSeriesSignals = hasSeriesMetadataSignals(selectedMetadata, analyzeContext);

    if (effectiveMediaType === 'dvd' && seriesRawPathHint && !hasSeriesSignals) {
      const seriesImportHints = buildSeriesImportHints({
        folderName: path.basename(String(resolvedRawPath || '').trim()),
        rawPath: resolvedRawPath,
        metadata: folderMeta,
        sourceSelectedMetadata: selectedMetadata,
        sourceAnalyzeContext: analyzeContext
      });
      if (seriesImportHints) {
        nextMakemkvInfo = this.buildOrphanRawImportMakemkvInfo({
          importedAt: nextMakemkvInfo?.importedAt || job?.end_time || new Date().toISOString(),
          rawPath: resolvedRawPath,
          requestedRawPath: nextMakemkvInfo?.importContext?.requestedRawPath || resolvedRawPath,
          sourceFolderJobId: nextMakemkvInfo?.importContext?.sourceFolderJobId || folderMeta.folderJobId || null,
          mediaProfile: effectiveMediaType,
          existingInfo: nextMakemkvInfo,
          selectedMetadata: seriesImportHints.selectedMetadata,
          analyzeContextPatch: seriesImportHints.analyzeContextPatch
        });
      }
    } else if (
      effectiveMediaType !== 'other'
      && normalizeMediaTypeValue(nextMakemkvInfo?.analyzeContext?.mediaProfile || nextMakemkvInfo?.mediaProfile) !== effectiveMediaType
    ) {
      nextMakemkvInfo = this.buildOrphanRawImportMakemkvInfo({
        importedAt: nextMakemkvInfo?.importedAt || job?.end_time || new Date().toISOString(),
        rawPath: resolvedRawPath,
        requestedRawPath: nextMakemkvInfo?.importContext?.requestedRawPath || resolvedRawPath,
        sourceFolderJobId: nextMakemkvInfo?.importContext?.sourceFolderJobId || folderMeta.folderJobId || null,
        mediaProfile: effectiveMediaType,
        existingInfo: nextMakemkvInfo
      });
    }

    const nextMakemkvInfoJson = JSON.stringify(nextMakemkvInfo || {});
    if (nextMakemkvInfoJson !== String(job?.makemkv_info_json || '')) {
      patch.makemkv_info_json = nextMakemkvInfoJson;
    }

    if (Object.keys(patch).length === 0) {
      return job;
    }
    return this.updateJob(job.id, patch);
  }

  async getJobById(jobId, options = {}) {
    const db = await getDb();
    const row = await db.get('SELECT * FROM jobs WHERE id = ?', [jobId]);
    if (!row || options?.skipRepair) {
      return row;
    }
    const settings = options?.settings && typeof options.settings === 'object'
      ? options.settings
      : await settingsService.getSettingsMap();
    return this.repairImportedOrphanJobClassification(row, settings);
  }

  async getJobs(filters = {}) {
    const db = await getDb();
    const where = [];
    const values = [];
    const includeFsChecks = filters?.includeFsChecks !== false;
    const includeChildren = filters?.includeChildren === true;
    const rawStatuses = Array.isArray(filters?.statuses)
      ? filters.statuses
      : (typeof filters?.statuses === 'string'
        ? String(filters.statuses).split(',')
        : []);
    const normalizedStatuses = rawStatuses
      .map((value) => String(value || '').trim().toUpperCase())
      .filter(Boolean);
    const limitRaw = Number(filters?.limit);
    const limit = Number.isFinite(limitRaw) && limitRaw > 0
      ? Math.min(Math.trunc(limitRaw), 500)
      : 500;

    if (normalizedStatuses.length > 0) {
      const placeholders = normalizedStatuses.map(() => '?').join(', ');
      where.push(`status IN (${placeholders})`);
      values.push(...normalizedStatuses);
    } else if (filters.status) {
      where.push('status = ?');
      values.push(filters.status);
    }

    if (filters.search) {
      where.push('(title LIKE ? OR imdb_id LIKE ? OR detected_title LIKE ? OR makemkv_info_json LIKE ?)');
      values.push(`%${filters.search}%`, `%${filters.search}%`, `%${filters.search}%`, `%${filters.search}%`);
    }

    if (!includeChildren) {
      where.push(`parent_job_id IS NULL`);
    }

    const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';

    const [jobs, settings] = await Promise.all([
      db.all(
        `
        SELECT j.*
        FROM jobs j
        ${whereClause}
        ORDER BY COALESCE(j.updated_at, j.created_at) DESC, j.id DESC
        LIMIT ${limit}
      `,
        values
      ),
      settingsService.getSettingsMap()
    ]);

    const repairedJobs = await Promise.all(
      jobs.map((job) => this.repairImportedOrphanJobClassification(job, settings))
    );
    const derivedParentIds = new Set(
      repairedJobs
        .map((job) => normalizeJobIdValue(job?.parent_job_id))
        .filter(Boolean)
    );
    const adjustedJobs = repairedJobs.map((job) => {
      const normalizedId = normalizeJobIdValue(job?.id);
      const hasParent = normalizeJobIdValue(job?.parent_job_id) !== null;
      const jobKind = String(job?.job_kind || '').trim().toLowerCase();
      if (hasParent && !jobKind) {
        return { ...job, job_kind: 'dvd_series_child' };
      }
      if (!hasParent && normalizedId && derivedParentIds.has(normalizedId) && !jobKind) {
        return { ...job, job_kind: 'dvd_series_container' };
      }
      return job;
    });

    const containerJobs = adjustedJobs.filter((job) => String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container');
    const containerIds = containerJobs.map((job) => normalizeJobIdValue(job?.id)).filter(Boolean);
    const seriesCandidates = adjustedJobs.filter((job) => {
      const mkInfo = parseJsonSafe(job?.makemkv_info_json, {});
      const analyzeContext = mkInfo?.analyzeContext && typeof mkInfo.analyzeContext === 'object'
        ? mkInfo.analyzeContext
        : {};
      const selected = mkInfo?.analyzeContext?.selectedMetadata || mkInfo?.selectedMetadata || {};
      const tmdbId = Number(selected?.tmdbId || 0) || null;
      const seasonNumber = Number(selected?.seasonNumber || 0) || null;
      return (tmdbId && seasonNumber) || hasSeriesMetadataSignals(selected, analyzeContext);
    });
    const seriesCandidateIds = seriesCandidates.map((job) => normalizeJobIdValue(job?.id)).filter(Boolean);
    let childRows = [];
    let nestedChildRows = [];
    let outputFolders = [];
    if (containerIds.length > 0) {
      const placeholders = containerIds.map(() => '?').join(', ');
      childRows = await db.all(
        `SELECT id, parent_job_id, output_path, raw_path, handbrake_info_json, status, encode_plan_json, makemkv_info_json, rip_successful FROM jobs WHERE parent_job_id IN (${placeholders})`,
        containerIds
      );
      const childIds = childRows.map((row) => normalizeJobIdValue(row?.id)).filter(Boolean);
      if (childIds.length > 0) {
        const childPlaceholders = childIds.map(() => '?').join(', ');
        nestedChildRows = await db.all(
          `SELECT id, parent_job_id, output_path, raw_path, handbrake_info_json, status, encode_plan_json, makemkv_info_json, rip_successful FROM jobs WHERE parent_job_id IN (${childPlaceholders})`,
          childIds
        );
      }
      const summaryJobIds = Array.from(new Set([
        ...childRows.map((row) => normalizeJobIdValue(row?.id)).filter(Boolean),
        ...nestedChildRows.map((row) => normalizeJobIdValue(row?.id)).filter(Boolean)
      ]));
      if (summaryJobIds.length > 0) {
        const childPlaceholders = summaryJobIds.map(() => '?').join(', ');
        outputFolders = await db.all(
          `SELECT job_id, output_path FROM job_output_folders WHERE job_id IN (${childPlaceholders})`,
          summaryJobIds
        );
      }
    }

    const allContainerChildRows = [...childRows, ...nestedChildRows];
    const directDiskRowsByContainerId = new Map();
    const directEpisodeRowsByContainerId = new Map();
    const directChildRowById = new Map();
    for (const row of childRows) {
      const containerId = normalizeJobIdValue(row?.parent_job_id);
      const isEpisodeSubJob = isSeriesBatchEpisodeSubJobRow(row);
      if (containerId) {
        const map = isEpisodeSubJob
          ? directEpisodeRowsByContainerId
          : directDiskRowsByContainerId;
        if (!map.has(containerId)) {
          map.set(containerId, []);
        }
        map.get(containerId).push(row);
      }
      if (isEpisodeSubJob) {
        continue;
      }
      const childId = normalizeJobIdValue(row?.id);
      if (childId) {
        directChildRowById.set(childId, row);
      }
    }
    const nestedRowsByParentId = new Map();
    for (const row of nestedChildRows) {
      const parentId = normalizeJobIdValue(row?.parent_job_id);
      if (!parentId) {
        continue;
      }
      if (!nestedRowsByParentId.has(parentId)) {
        nestedRowsByParentId.set(parentId, []);
      }
      nestedRowsByParentId.get(parentId).push(row);
    }

    const childOutputMap = new Map();
    for (const child of allContainerChildRows) {
      const childId = normalizeJobIdValue(child?.id);
      if (!childId) {
        continue;
      }
      if (!childOutputMap.has(childId)) {
        childOutputMap.set(childId, new Set());
      }
      const childSet = childOutputMap.get(childId);
      const directOutput = String(child?.output_path || '').trim();
      if (directOutput) {
        childSet.add(directOutput);
      }
    }
    for (const folder of outputFolders) {
      const childId = normalizeJobIdValue(folder?.job_id);
      if (!childId) {
        continue;
      }
      if (!childOutputMap.has(childId)) {
        childOutputMap.set(childId, new Set());
      }
      const outputPath = String(folder?.output_path || '').trim();
      if (outputPath) {
        childOutputMap.get(childId).add(outputPath);
      }
    }

    const containerOutputSummary = new Map();
    const containerRawSummary = new Map();
    const containerEncodeSummary = new Map();
    const containerChildSummary = new Map();
    for (const container of containerJobs) {
      const containerId = normalizeJobIdValue(container?.id);
      if (!containerId) {
        continue;
      }
      const mkInfo = parseJsonSafe(container?.makemkv_info_json, {});
      const selectedMetadata = mkInfo?.analyzeContext?.selectedMetadata || mkInfo?.selectedMetadata || {};
      const episodeCount = Number(selectedMetadata?.episodeCount || 0);
      const episodesLength = Array.isArray(selectedMetadata?.episodes) ? selectedMetadata.episodes.length : 0;
      const expectedTotal = episodeCount > 0 ? episodeCount : (episodesLength > 0 ? episodesLength : 0);
      const containerChildRows = directDiskRowsByContainerId.get(containerId) || [];
      const containerEpisodeRows = directEpisodeRowsByContainerId.get(containerId) || [];
      const childIds = containerChildRows
        .map((row) => normalizeJobIdValue(row?.id))
        .filter(Boolean);
      let rawExistsAny = false;
      let encodeSuccessAny = false;
      let rawExistsCount = 0;
      let backupSuccessCount = 0;
      let encodeSuccessCount = 0;
      let expectedFromPlans = 0;
      const seenOutputs = new Set();
      let existingCount = 0;
      let existingCountFromSeriesBatch = 0;
      const totalChildren = childIds.length;
      for (const childId of childIds) {
        const childRow = directChildRowById.get(childId) || null;
        const descendantRows = nestedRowsByParentId.get(childId) || [];
        const relatedRows = [childRow, ...descendantRows].filter(Boolean);
        if (childRow) {
          const rawPath = String(childRow?.raw_path || '').trim();
          if (rawPath) {
            const resolvedChildPaths = resolveEffectiveStoragePathsForJob(settings, childRow);
            const rawStatus = includeFsChecks
              ? inspectDirectory(resolvedChildPaths.effectiveRawPath)
              : buildUnknownDirectoryStatus(resolvedChildPaths.effectiveRawPath);
            if (rawStatus?.exists) {
              rawExistsAny = true;
              rawExistsCount += 1;
            }
          }
          const mkInfo = parseJsonSafe(childRow?.makemkv_info_json, null);
          const ripSuccessful = Number(childRow?.rip_successful || 0) === 1
            || String(mkInfo?.status || '').trim().toUpperCase() === 'SUCCESS';
          if (ripSuccessful) {
            backupSuccessCount += 1;
          }
          let selectedCount = 0;
          const childPlan = parseJsonSafe(childRow?.encode_plan_json, null);
          if (childPlan && typeof childPlan === 'object') {
            const titles = Array.isArray(childPlan?.titles) ? childPlan.titles : [];
            selectedCount = titles.filter((title) => title?.selectedForEncode || title?.encodeInput).length;
          }
          const handbrakeInfo = parseJsonSafe(childRow?.handbrake_info_json, null);
          const hbStatus = String(handbrakeInfo?.status || '').trim().toUpperCase();
          const seriesBatchSummary = handbrakeInfo?.seriesBatch && typeof handbrakeInfo.seriesBatch === 'object'
            ? handbrakeInfo.seriesBatch
            : null;
          const seriesBatchTotal = Number(seriesBatchSummary?.totalCount || 0);
          const seriesBatchFinished = Number(seriesBatchSummary?.finishedCount || 0);
          const seriesBatchErrors = Number(seriesBatchSummary?.errorCount || 0);
          const seriesBatchCancelled = Number(seriesBatchSummary?.cancelledCount || 0);
          const expectedFromNested = descendantRows.length > 0 ? descendantRows.length : 0;
          const expectedFromChild = seriesBatchTotal > 0
            ? seriesBatchTotal
            : (selectedCount > 0 ? selectedCount : expectedFromNested);
          if (expectedFromChild > 0) {
            expectedFromPlans += expectedFromChild;
          }
          if (seriesBatchFinished > 0) {
            existingCountFromSeriesBatch += seriesBatchFinished;
          }

          let childEncodeSuccess = hbStatus === 'SUCCESS';
          if (
            !childEncodeSuccess
            && seriesBatchTotal > 0
            && seriesBatchFinished >= seriesBatchTotal
            && seriesBatchErrors <= 0
            && seriesBatchCancelled <= 0
          ) {
            childEncodeSuccess = true;
          }
          if (!childEncodeSuccess && descendantRows.length > 0) {
            let finishedDescendants = 0;
            let errorDescendants = 0;
            let cancelledDescendants = 0;
            for (const row of descendantRows) {
              const rowState = String(row?.status || row?.last_state || '').trim().toUpperCase();
              if (rowState === 'FINISHED') {
                finishedDescendants += 1;
              } else if (rowState === 'ERROR') {
                errorDescendants += 1;
              } else if (rowState === 'CANCELLED') {
                cancelledDescendants += 1;
              }
            }
            childEncodeSuccess = finishedDescendants >= descendantRows.length
              && errorDescendants === 0
              && cancelledDescendants === 0;
          }
          if (childEncodeSuccess) {
            encodeSuccessAny = true;
            encodeSuccessCount += 1;
          }
        } else if (descendantRows.length > 0) {
          expectedFromPlans += descendantRows.length;
        }
        for (const relatedRow of relatedRows) {
          const relatedRowId = normalizeJobIdValue(relatedRow?.id);
          if (!relatedRowId) {
            continue;
          }
          const outputs = Array.from(childOutputMap.get(relatedRowId) || []);
          for (const outputPath of outputs) {
            if (!outputPath || seenOutputs.has(outputPath)) {
              continue;
            }
            seenOutputs.add(outputPath);
            if (!includeFsChecks || fs.existsSync(outputPath)) {
              existingCount += 1;
            }
          }
        }
      }
      for (const episodeRow of containerEpisodeRows) {
        const episodeRowId = normalizeJobIdValue(episodeRow?.id);
        if (!episodeRowId) {
          continue;
        }
        const outputs = Array.from(childOutputMap.get(episodeRowId) || []);
        for (const outputPath of outputs) {
          if (!outputPath || seenOutputs.has(outputPath)) {
            continue;
          }
          seenOutputs.add(outputPath);
          if (!includeFsChecks || fs.existsSync(outputPath)) {
            existingCount += 1;
          }
        }
      }
      if (existingCountFromSeriesBatch > existingCount) {
        existingCount = existingCountFromSeriesBatch;
      }
      if (!encodeSuccessAny && existingCount > 0) {
        encodeSuccessAny = true;
        if (encodeSuccessCount === 0 && totalChildren > 0) {
          encodeSuccessCount = Math.min(1, totalChildren);
        }
      }
      const expectedFinal = expectedFromPlans > 0
        ? expectedFromPlans
        : (expectedTotal > 0 ? expectedTotal : existingCount);
      containerOutputSummary.set(containerId, {
        existing: existingCount,
        expected: expectedFinal
      });
      containerRawSummary.set(containerId, { exists: rawExistsAny });
      containerEncodeSummary.set(containerId, { success: encodeSuccessAny });
      containerChildSummary.set(containerId, {
        raw: { existing: rawExistsCount, expected: totalChildren },
        backup: { existing: backupSuccessCount, expected: totalChildren },
        encode: { existing: encodeSuccessCount, expected: totalChildren }
      });
    }

    const standaloneSeriesOutputSummary = new Map();
    if (seriesCandidateIds.length > 0) {
      const placeholders = seriesCandidateIds.map(() => '?').join(', ');
      const seriesOutputRows = await db.all(
        `SELECT job_id, output_path FROM job_output_folders WHERE job_id IN (${placeholders})`,
        seriesCandidateIds
      );
      const outputsByJobId = new Map();
      for (const row of seriesOutputRows) {
        const jobId = normalizeJobIdValue(row?.job_id);
        if (!jobId) continue;
        if (!outputsByJobId.has(jobId)) outputsByJobId.set(jobId, new Set());
        const pathValue = String(row?.output_path || '').trim();
        if (pathValue) outputsByJobId.get(jobId).add(pathValue);
      }
      for (const job of seriesCandidates) {
        const jobId = normalizeJobIdValue(job?.id);
        if (!jobId || containerIds.includes(jobId)) {
          continue;
        }
        const mkInfo = parseJsonSafe(job?.makemkv_info_json, {});
        const selected = mkInfo?.analyzeContext?.selectedMetadata || mkInfo?.selectedMetadata || {};
        const episodeCount = Number(selected?.episodeCount || 0);
        const episodesLength = Array.isArray(selected?.episodes) ? selected.episodes.length : 0;
        const expectedFromMetadata = episodeCount > 0 ? episodeCount : (episodesLength > 0 ? episodesLength : 0);
        const plan = parseJsonSafe(job?.encode_plan_json, null);
        const titles = Array.isArray(plan?.titles) ? plan.titles : [];
        const expectedFromPlan = titles.filter((title) => title?.selectedForEncode || title?.encodeInput).length;
        const expectedFinal = expectedFromPlan > 0 ? expectedFromPlan : expectedFromMetadata;
        const outputSet = outputsByJobId.get(jobId) || new Set();
        const directOutput = String(job?.output_path || '').trim();
        if (directOutput) outputSet.add(directOutput);
        let existingCount = 0;
        for (const outputPath of outputSet) {
          if (!outputPath) continue;
          if (!includeFsChecks || fs.existsSync(outputPath)) {
            existingCount += 1;
          }
        }
        standaloneSeriesOutputSummary.set(jobId, {
          existing: existingCount,
          expected: expectedFinal > 0 ? expectedFinal : existingCount
        });
      }
    }

    return adjustedJobs.map((job) => {
      const enriched = enrichJobRow(job, settings, { includeFsChecks });
      const containerId = normalizeJobIdValue(job?.id);
      const summary = containerId
        ? (containerOutputSummary.get(containerId) || standaloneSeriesOutputSummary.get(containerId) || null)
        : (standaloneSeriesOutputSummary.get(containerId) || null);
      const rawSummary = containerId ? containerRawSummary.get(containerId) : null;
      const encodeSummary = containerId ? containerEncodeSummary.get(containerId) : null;
      const childSummary = containerId ? containerChildSummary.get(containerId) : null;
      const rawStatus = rawSummary
        ? {
          ...enriched.rawStatus,
          exists: rawSummary.exists,
          isDirectory: rawSummary.exists
        }
        : enriched.rawStatus;
      const outputStatus = summary
        ? {
          ...enriched.outputStatus,
          exists: summary.existing > 0,
          isFile: summary.existing > 0
        }
        : enriched.outputStatus;
      const encodeSuccess = encodeSummary ? Boolean(encodeSummary.success) : enriched.encodeSuccess;
      return {
        ...enriched,
        rawStatus,
        outputStatus,
        encodeSuccess,
        ...(childSummary ? { seriesChildSummary: childSummary } : {}),
        ...(summary ? { seriesOutputSummary: summary } : {}),
        log_count: includeFsChecks ? (hasProcessLogFile(job.id) ? 1 : 0) : 0
      };
    });
  }

  async getJobsByIds(jobIds = []) {
    const ids = Array.isArray(jobIds)
      ? jobIds
        .map((value) => Number(value))
        .filter((value) => Number.isFinite(value) && value > 0)
        .map((value) => Math.trunc(value))
      : [];
    if (ids.length === 0) {
      return [];
    }

    const [rows, settings] = await Promise.all([
      (async () => {
        const db = await getDb();
        const placeholders = ids.map(() => '?').join(', ');
        return db.all(`SELECT * FROM jobs WHERE id IN (${placeholders})`, ids);
      })(),
      settingsService.getSettingsMap()
    ]);
    const byId = new Map(rows.map((row) => [Number(row.id), row]));
    const repairedRows = await Promise.all(
      ids
        .map((id) => byId.get(id))
        .filter(Boolean)
        .map((job) => this.repairImportedOrphanJobClassification(job, settings))
    );

    const repairedById = new Map(
      repairedRows
        .filter(Boolean)
        .map((row) => [Number(row.id), row])
    );

    return ids
      .map((id) => repairedById.get(id))
      .filter(Boolean)
      .map((job) => ({
        ...enrichJobRow(job, settings),
        log_count: hasProcessLogFile(job.id) ? 1 : 0
      }));
  }

  async findSeriesContainerJob(tmdbId, seasonNumber) {
    const normalizedTmdbId = Number(tmdbId || 0) || null;
    const normalizedSeason = Number(seasonNumber || 0) || null;
    if (!normalizedTmdbId || !normalizedSeason) {
      return null;
    }
    const db = await getDb();
    const row = await db.get(
      `
        SELECT *
        FROM jobs
        WHERE job_kind = 'dvd_series_container'
          AND (
            json_extract(makemkv_info_json, '$.analyzeContext.selectedMetadata.tmdbId') = ?
            OR json_extract(makemkv_info_json, '$.selectedMetadata.tmdbId') = ?
          )
          AND (
            json_extract(makemkv_info_json, '$.analyzeContext.selectedMetadata.seasonNumber') = ?
            OR json_extract(makemkv_info_json, '$.selectedMetadata.seasonNumber') = ?
          )
        ORDER BY id DESC
        LIMIT 1
      `,
      [normalizedTmdbId, normalizedTmdbId, normalizedSeason, normalizedSeason]
    );
    return row || null;
  }

  async findLikelySeriesContainerJob({ title = null, year = null } = {}) {
    const normalizedTitle = normalizeComparableLabel(title || '');
    const normalizedYear = Number.isFinite(Number(year)) && Number(year) > 0
      ? Math.trunc(Number(year))
      : null;
    if (!normalizedTitle) {
      return null;
    }

    const db = await getDb();
    const rows = await db.all(
      `
        SELECT *
        FROM jobs
        WHERE job_kind = 'dvd_series_container'
        ORDER BY id DESC
      `
    );
    const candidates = [];
    for (const row of (Array.isArray(rows) ? rows : [])) {
      const mkInfo = parseJsonSafe(row?.makemkv_info_json, {}) || {};
      const selected = extractSelectedMetadataFromMakemkvInfo(mkInfo);
      const containerTitle = String(
        selected?.title
        || selected?.seriesTitle
        || row?.title
        || row?.detected_title
        || ''
      ).trim();
      const containerNormalizedTitle = normalizeComparableLabel(containerTitle);
      if (!containerNormalizedTitle) {
        continue;
      }

      let score = 0;
      if (containerNormalizedTitle === normalizedTitle) {
        score += 10;
      } else if (containerNormalizedTitle.includes(normalizedTitle) || normalizedTitle.includes(containerNormalizedTitle)) {
        score += 6;
      } else {
        continue;
      }

      const containerYearRaw = Number(selected?.year || row?.year || 0);
      const containerYear = Number.isFinite(containerYearRaw) && containerYearRaw > 0
        ? Math.trunc(containerYearRaw)
        : null;
      if (normalizedYear && containerYear === normalizedYear) {
        score += 4;
      } else if (normalizedYear && containerYear && Math.abs(containerYear - normalizedYear) <= 1) {
        score += 2;
      }

      const tmdbId = normalizePositiveIntegerOrNull(selected?.tmdbId || selected?.providerId || null);
      const seasonNumber = normalizePositiveIntegerOrNull(selected?.seasonNumber || null);
      if (tmdbId && seasonNumber) {
        score += 3;
      }

      candidates.push({
        row,
        score
      });
    }

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

    candidates.sort((left, right) => {
      if (right.score !== left.score) {
        return right.score - left.score;
      }
      return Number(right.row?.id || 0) - Number(left.row?.id || 0);
    });

    const best = candidates[0];
    const second = candidates[1] || null;
    if (second && second.score === best.score) {
      return null;
    }
    return best.row || null;
  }

  async listSeriesSiblingJobs(tmdbId, seasonNumber) {
    const normalizedTmdbId = Number(tmdbId || 0) || null;
    const normalizedSeason = Number(seasonNumber || 0) || null;
    if (!normalizedTmdbId || !normalizedSeason) {
      return [];
    }
    const db = await getDb();
    const rows = await db.all(
      `
        SELECT *
        FROM jobs
        WHERE job_kind != 'dvd_series_container'
          AND (
            json_extract(makemkv_info_json, '$.analyzeContext.selectedMetadata.tmdbId') = ?
            OR json_extract(makemkv_info_json, '$.selectedMetadata.tmdbId') = ?
          )
          AND (
            json_extract(makemkv_info_json, '$.analyzeContext.selectedMetadata.seasonNumber') = ?
            OR json_extract(makemkv_info_json, '$.selectedMetadata.seasonNumber') = ?
          )
        ORDER BY id ASC
      `,
      [normalizedTmdbId, normalizedTmdbId, normalizedSeason, normalizedSeason]
    );
    return Array.isArray(rows) ? rows : [];
  }

  async listChildJobs(parentJobId) {
    const normalizedParentJobId = normalizeJobIdValue(parentJobId);
    if (!normalizedParentJobId) {
      return [];
    }
    const db = await getDb();
    const rows = await db.all(
      `SELECT * FROM jobs WHERE parent_job_id = ? ORDER BY id ASC`,
      [normalizedParentJobId]
    );
    return Array.isArray(rows) ? rows : [];
  }

  async findLatestReplacementJobId(sourceJobId) {
    const normalizedSourceJobId = normalizeJobIdValue(sourceJobId);
    if (!normalizedSourceJobId) {
      return null;
    }

    const db = await getDb();
    const rows = await db.all(
      `
        SELECT a.job_id
        FROM job_lineage_artifacts a
        JOIN jobs j ON j.id = a.job_id
        WHERE a.source_job_id = ?
        ORDER BY a.id DESC
      `,
      [normalizedSourceJobId]
    );

    for (const row of (Array.isArray(rows) ? rows : [])) {
      const candidateJobId = normalizeJobIdValue(row?.job_id);
      if (!candidateJobId || candidateJobId === normalizedSourceJobId) {
        continue;
      }
      return candidateJobId;
    }

    return null;
  }

  async getJobByIdOrReplacement(jobId) {
    const normalizedJobId = normalizeJobIdValue(jobId);
    if (!normalizedJobId) {
      return {
        requestedJobId: null,
        resolvedJobId: null,
        replaced: false,
        job: null
      };
    }

    const directJob = await this.getJobById(normalizedJobId);
    if (directJob) {
      return {
        requestedJobId: normalizedJobId,
        resolvedJobId: normalizedJobId,
        replaced: false,
        job: directJob
      };
    }

    const replacementJobId = await this.findLatestReplacementJobId(normalizedJobId);
    if (!replacementJobId) {
      return {
        requestedJobId: normalizedJobId,
        resolvedJobId: normalizedJobId,
        replaced: false,
        job: null
      };
    }

    const replacementJob = await this.getJobById(replacementJobId);
    return {
      requestedJobId: normalizedJobId,
      resolvedJobId: replacementJobId,
      replaced: Boolean(replacementJob),
      job: replacementJob || null
    };
  }

  async getRunningJobs() {
    const db = await getDb();
    const [rows, settings] = await Promise.all([
      db.all(
        `
        SELECT *
        FROM jobs
        WHERE status IN ('ANALYZING', 'RIPPING', 'ENCODING', 'MEDIAINFO_CHECK', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING')
          AND COALESCE(job_kind, '') != 'dvd_series_container'
        ORDER BY updated_at ASC, id ASC
      `
      ),
      settingsService.getSettingsMap()
    ]);
    return rows.map((job) => ({
      ...enrichJobRow(job, settings),
      log_count: hasProcessLogFile(job.id) ? 1 : 0
    }));
  }

  async getQueueIdleJobs() {
    const db = await getDb();
    const [rows, settings] = await Promise.all([
      db.all(
        `
        SELECT *
        FROM jobs
        WHERE status IN (
          'METADATA_SELECTION',
          'WAITING_FOR_USER_DECISION',
          'READY_TO_START',
          'READY_TO_ENCODE',
          'CD_METADATA_SELECTION',
          'CD_READY_TO_RIP'
        )
          AND COALESCE(job_kind, '') != 'dvd_series_container'
        ORDER BY updated_at ASC, id ASC
      `
      ),
      settingsService.getSettingsMap()
    ]);
    return rows.map((job) => ({
      ...enrichJobRow(job, settings),
      log_count: hasProcessLogFile(job.id) ? 1 : 0
    }));
  }

  async getRunningEncodeJobs() {
    const db = await getDb();
    const [rows, settings] = await Promise.all([
      db.all(
        `
        SELECT *
        FROM jobs
        WHERE status IN ('ENCODING', 'CD_ENCODING')
        ORDER BY updated_at ASC, id ASC
      `
      ),
      settingsService.getSettingsMap()
    ]);
    return rows.map((job) => ({
      ...enrichJobRow(job, settings),
      log_count: hasProcessLogFile(job.id) ? 1 : 0
    }));
  }

  async getRunningFilmEncodeJobs() {
    const db = await getDb();
    const rows = await db.all(
      `SELECT id, status FROM jobs WHERE status = 'ENCODING' ORDER BY updated_at ASC, id ASC`
    );
    return rows;
  }

  async getRunningCdEncodeJobs() {
    const db = await getDb();
    const rows = await db.all(
      `SELECT id, status FROM jobs WHERE status IN ('CD_RIPPING', 'CD_ENCODING') ORDER BY updated_at ASC, id ASC`
    );
    return rows;
  }

  async getJobWithLogs(jobId, options = {}) {
    const db = await getDb();
    const includeFsChecks = options?.includeFsChecks !== false;
    const [loadedJob, settings] = await Promise.all([
      db.get('SELECT * FROM jobs WHERE id = ?', [jobId]),
      settingsService.getSettingsMap()
    ]);
    if (!loadedJob) {
      return null;
    }
    const classifiedJob = await this.repairImportedOrphanJobClassification(loadedJob, settings);
    const job = await this.repairImportedCdJobArtifacts(classifiedJob, settings);
    const childRows = await this.listChildJobs(jobId);
    const childJobs = await Promise.all(
      childRows.map((row) => this.repairImportedOrphanJobClassification(row, settings))
    );
    const isSeriesContainer = String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container';
    const detailChildJobs = isSeriesContainer
      ? childJobs.filter((child) => !isSeriesBatchEpisodeSubJobRow(child))
      : childJobs;
    const parsedTail = Number(options.logTailLines);
    const logTailLines = Number.isFinite(parsedTail) && parsedTail > 0
      ? Math.trunc(parsedTail)
      : 800;
    const includeLiveLog = Boolean(options.includeLiveLog);
    const includeLogs = Boolean(options.includeLogs);
    const includeAllLogs = Boolean(options.includeAllLogs);
    const shouldLoadLogs = includeLiveLog || includeLogs;
    const hasProcessLog = (!shouldLoadLogs && includeFsChecks) ? hasProcessLogFile(jobId) : false;
    const baseLogCount = hasProcessLog ? 1 : 0;
    const enrichedChildren = await Promise.all(
      detailChildJobs.map(async (child) => {
        const base = {
          ...enrichJobRow(child, settings, { includeFsChecks }),
          log_count: includeFsChecks ? (hasProcessLogFile(child.id) ? 1 : 0) : 0
        };
        if (!shouldLoadLogs) {
          return {
            ...base,
            logs: [],
            log: '',
            logMeta: {
              loaded: false,
              total: base.log_count,
              returned: 0,
              truncated: false
            }
          };
        }
        const childLog = await this.readProcessLogLines(child.id, {
          includeAll: includeAllLogs,
          tailLines: logTailLines
        });
        return {
          ...base,
          log: childLog.lines.join('\n'),
          logMeta: {
            loaded: true,
            total: includeAllLogs ? childLog.total : childLog.returned,
            returned: childLog.returned,
            truncated: childLog.truncated
          }
        };
      })
    );

    let seriesChildSummary = null;
    if (isSeriesContainer && enrichedChildren.length > 0) {
      const total = enrichedChildren.length;
      let rawCount = 0;
      let backupCount = 0;
      let encodeCount = 0;
      for (const child of enrichedChildren) {
        if (child?.rawStatus?.exists) {
          rawCount += 1;
        }
        if (child?.backupSuccess) {
          backupCount += 1;
        }
        if (child?.encodeSuccess) {
          encodeCount += 1;
        }
      }
      seriesChildSummary = {
        raw: { existing: rawCount, expected: total },
        backup: { existing: backupCount, expected: total },
        encode: { existing: encodeCount, expected: total }
      };
    }

    const outputFolders = await this.getJobOutputFoldersForLineage(jobId);

    if (!shouldLoadLogs) {
      return {
        ...enrichJobRow(job, settings, { includeFsChecks }),
        outputFolders,
        log_count: baseLogCount,
        children: enrichedChildren,
        ...(seriesChildSummary ? { seriesChildSummary } : {}),
        logs: [],
        log: '',
        logMeta: {
          loaded: false,
          total: baseLogCount,
          returned: 0,
          truncated: false
        }
      };
    }

    const processLog = await this.readProcessLogLines(jobId, {
      includeAll: includeAllLogs,
      tailLines: logTailLines
    });

    return {
      ...enrichJobRow(job, settings, { includeFsChecks }),
      outputFolders,
      log_count: processLog.exists ? processLog.total : 0,
      children: enrichedChildren,
      ...(seriesChildSummary ? { seriesChildSummary } : {}),
      logs: [],
      log: processLog.lines.join('\n'),
      logMeta: {
        loaded: true,
        total: includeAllLogs ? processLog.total : processLog.returned,
        returned: processLog.returned,
        truncated: processLog.truncated
      }
    };
  }

  async getJobArchiveDescriptor(jobId, target, options = {}) {
    const normalizedJobId = normalizeJobIdValue(jobId);
    if (!normalizedJobId) {
      const error = new Error('Ungültige Job-ID.');
      error.statusCode = 400;
      throw error;
    }

    const normalizedTarget = normalizeArchiveTarget(target);
    if (!normalizedTarget) {
      const error = new Error('Ungültiges Download-Ziel. Erlaubt sind raw und output.');
      error.statusCode = 400;
      throw error;
    }

    const [job, settings] = await Promise.all([
      this.getJobById(normalizedJobId),
      settingsService.getSettingsMap()
    ]);

    if (!job) {
      const error = new Error('Job nicht gefunden.');
      error.statusCode = 404;
      throw error;
    }

    const resolvedPaths = resolveEffectiveStoragePathsForJob(settings, job);
    const requestedOutputPath = normalizedTarget === 'output'
      ? (String(options?.outputPath || '').trim() || null)
      : null;
    let sourcePath = normalizedTarget === 'raw'
      ? resolvedPaths.effectiveRawPath
      : resolvedPaths.effectiveOutputPath;

    if (normalizedTarget === 'output' && requestedOutputPath) {
      const trackedFolders = await this.getJobOutputFoldersForLineage(normalizedJobId);
      const allowedOutputPathMap = new Map();
      const registerAllowedOutputPath = (candidatePath) => {
        const raw = String(candidatePath || '').trim();
        if (!raw) {
          return;
        }
        const normalized = normalizeComparablePath(raw);
        if (!normalized || allowedOutputPathMap.has(normalized)) {
          return;
        }
        allowedOutputPathMap.set(normalized, raw);
      };

      registerAllowedOutputPath(resolvedPaths.effectiveOutputPath);
      registerAllowedOutputPath(job?.output_path);
      for (const folder of trackedFolders) {
        registerAllowedOutputPath(folder?.output_path);
      }

      const normalizedRequestedOutputPath = normalizeComparablePath(requestedOutputPath);
      if (!normalizedRequestedOutputPath || !allowedOutputPathMap.has(normalizedRequestedOutputPath)) {
        const error = new Error('Der angeforderte Output-Ordner gehört nicht zu diesem Job.');
        error.statusCode = 404;
        throw error;
      }

      sourcePath = allowedOutputPathMap.get(normalizedRequestedOutputPath);
    }

    if (!sourcePath) {
      const error = new Error(
        normalizedTarget === 'raw'
          ? 'Kein RAW-Pfad für diesen Job vorhanden.'
          : 'Kein Output-Pfad für diesen Job vorhanden.'
      );
      error.statusCode = 404;
      throw error;
    }

    let sourceStat;
    try {
      sourceStat = await fs.promises.stat(sourcePath);
    } catch (_error) {
      const error = new Error(
        normalizedTarget === 'raw'
          ? 'RAW-Pfad wurde nicht gefunden.'
          : 'Output-Pfad wurde nicht gefunden.'
      );
      error.statusCode = 404;
      throw error;
    }

    if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
      const error = new Error('Nur Dateien oder Verzeichnisse können als ZIP heruntergeladen werden.');
      error.statusCode = 400;
      throw error;
    }

    return {
      jobId: normalizedJobId,
      displayTitle: buildJobDisplayTitle(job),
      target: normalizedTarget,
      sourcePath,
      sourceType: sourceStat.isDirectory() ? 'directory' : 'file',
      sourceMtimeMs: Number(sourceStat.mtimeMs || 0),
      sourceModifiedAt: sourceStat.mtime ? sourceStat.mtime.toISOString() : null,
      entryName: path.basename(sourcePath) || (normalizedTarget === 'raw' ? 'raw' : 'output'),
      archiveName: buildJobArchiveName(job, normalizedTarget)
    };
  }

  async getOrphanRawFolders() {
    const settings = await settingsService.getSettingsMap();
    const rawDirs = getOrphanRawScanPathList(settings);
    if (rawDirs.length === 0) {
      const error = new Error('Kein RAW-Pfad verfügbar (weder Settings noch Default-Pfade).');
      error.statusCode = 400;
      throw error;
    }

    const db = await getDb();
    const [linkedRows, existingJobIdRows] = await Promise.all([
      db.all(
        `
          SELECT id, raw_path, status, makemkv_info_json, mediainfo_info_json, encode_plan_json, encode_input_path
          FROM jobs
          WHERE raw_path IS NOT NULL AND TRIM(raw_path) <> ''
        `
      ),
      db.all(`SELECT id FROM jobs`)
    ]);
    const existingJobIdSet = new Set(
      (Array.isArray(existingJobIdRows) ? existingJobIdRows : [])
        .map((row) => normalizeJobIdValue(row?.id))
        .filter(Boolean)
    );

    const linkedPathMap = new Map();
    const addLinkedPath = (candidatePath, rowRef) => {
      const normalizedPath = normalizeComparablePath(candidatePath);
      if (!normalizedPath) {
        return;
      }
      if (!linkedPathMap.has(normalizedPath)) {
        linkedPathMap.set(normalizedPath, []);
      }
      linkedPathMap.get(normalizedPath).push(rowRef);
    };
    for (const row of linkedRows) {
      const resolvedPaths = resolveEffectiveStoragePathsForJob(settings, row);
      const linkedCandidates = [
        normalizeComparablePath(row.raw_path),
        normalizeComparablePath(resolvedPaths.effectiveRawPath)
      ].filter(Boolean);

      for (const linkedPath of linkedCandidates) {
        const rowRef = {
          id: row.id,
          status: row.status
        };
        addLinkedPath(linkedPath, rowRef);

        // /database scannt nur Top-Level-Ordner je RAW-Root.
        // Viele Jobs (insb. Disc-Backups) speichern raw_path jedoch auf einer tieferen Ebene.
        // Deshalb markieren wir zusätzlich den zugehörigen Top-Level-Ordner als "linked".
        for (const rawRoot of rawDirs) {
          const normalizedRawRoot = normalizeComparablePath(rawRoot);
          if (!normalizedRawRoot || !isPathInside(normalizedRawRoot, linkedPath)) {
            continue;
          }
          const relative = String(path.relative(normalizedRawRoot, linkedPath) || '').trim();
          if (!relative || relative === '.' || relative.startsWith('..')) {
            continue;
          }
          const topSegment = relative.split(path.sep).find((segment) => String(segment || '').trim() && segment !== '.');
          if (!topSegment) {
            continue;
          }
          addLinkedPath(path.join(normalizedRawRoot, topSegment), rowRef);
        }
      }
    }

    const orphanRows = [];
    const seenOrphanPaths = new Set();

    for (const rawDir of rawDirs) {
      const rawDirInfo = inspectDirectory(rawDir);
      if (!rawDirInfo.exists || !rawDirInfo.isDirectory) {
        continue;
      }
      const dirEntries = fs.readdirSync(rawDir, { withFileTypes: true });

      for (const entry of dirEntries) {
        if (!entry.isDirectory() || isHiddenDirectoryName(entry.name)) {
          continue;
        }

        const rawPath = path.join(rawDir, entry.name);
        const normalizedPath = normalizeComparablePath(rawPath);
        if (!normalizedPath || linkedPathMap.has(normalizedPath) || seenOrphanPaths.has(normalizedPath)) {
          continue;
        }

        const dirInfo = inspectDirectory(rawPath);
        if (!dirInfo.exists || !dirInfo.isDirectory || dirInfo.isEmpty) {
          continue;
        }

        const stat = fs.statSync(rawPath);
        const metadata = parseRawFolderMetadata(entry.name);
        if (metadata.folderJobId && existingJobIdSet.has(Number(metadata.folderJobId))) {
          continue;
        }
        const detectedMediaType = detectOrphanMediaType(rawPath, {
          seriesRawPathHint: isLikelySeriesRawPath(rawPath, settings)
        });
        orphanRows.push({
          rawPath,
          folderName: entry.name,
          title: metadata.title,
          year: metadata.year,
          imdbId: metadata.imdbId,
          folderJobId: metadata.folderJobId,
          entryCount: Number(dirInfo.entryCount || 0),
          detectedMediaType,
          hasBlurayStructure: detectedMediaType === 'bluray',
          hasDvdStructure: detectedMediaType === 'dvd',
          hasCdStructure: detectedMediaType === 'cd',
          hasAudiobookStructure: detectedMediaType === 'audiobook',
          lastModifiedAt: stat.mtime.toISOString()
        });
        seenOrphanPaths.add(normalizedPath);
      }
    }

    orphanRows.sort((a, b) => String(b.lastModifiedAt).localeCompare(String(a.lastModifiedAt)));
    return {
      rawDir: rawDirs[0] || null,
      rawDirs,
      rows: orphanRows
    };
  }

  async importOrphanRawFolder(rawPath) {
    const settings = await settingsService.getSettingsMap();
    const rawDirs = getOrphanRawScanPathList(settings);
    const requestedRawPath = String(rawPath || '').trim();

    if (!requestedRawPath) {
      const error = new Error('rawPath fehlt.');
      error.statusCode = 400;
      throw error;
    }

    if (rawDirs.length === 0) {
      const error = new Error('Kein RAW-Pfad verfügbar (weder Settings noch Default-Pfade).');
      error.statusCode = 400;
      throw error;
    }

    const insideConfiguredRawDir = rawDirs.some((candidate) => isPathInside(candidate, requestedRawPath));
    if (!insideConfiguredRawDir) {
      const error = new Error(`RAW-Pfad liegt außerhalb der konfigurierten RAW-Verzeichnisse: ${requestedRawPath}`);
      error.statusCode = 400;
      throw error;
    }

    const absRawPath = normalizeComparablePath(requestedRawPath);
    const dirInfo = inspectDirectory(absRawPath);
    if (!dirInfo.exists || !dirInfo.isDirectory) {
      const error = new Error(`RAW-Pfad existiert nicht als Verzeichnis: ${absRawPath}`);
      error.statusCode = 400;
      throw error;
    }
    if (dirInfo.isEmpty) {
      const error = new Error(`RAW-Pfad ist leer: ${absRawPath}`);
      error.statusCode = 400;
      throw error;
    }

    const db = await getDb();
    const linkedRows = await db.all(
      `
        SELECT id, raw_path
        FROM jobs
        WHERE raw_path IS NOT NULL AND TRIM(raw_path) <> ''
      `
    );
    const existing = linkedRows.find((row) => normalizeComparablePath(row.raw_path) === absRawPath);
    if (existing) {
      const error = new Error(`Für RAW-Pfad existiert bereits Job #${existing.id}.`);
      error.statusCode = 409;
      throw error;
    }

    const folderName = path.basename(absRawPath);
    const metadata = parseRawFolderMetadata(folderName);
    let omdbById = null;
    if (metadata.imdbId) {
      try {
        omdbById = await omdbService.fetchByImdbId(metadata.imdbId);
      } catch (error) {
        logger.warn('job:import-orphan-raw:omdb-fetch-failed', {
          rawPath: absRawPath,
          imdbId: metadata.imdbId,
          message: error.message
        });
      }
    }
    const effectiveTitle = omdbById?.title || metadata.title || folderName;
    const importedAt = new Date().toISOString();
    const created = await this.createJob({
      discDevice: null,
      status: 'FINISHED',
      detectedTitle: effectiveTitle
    });

    const renameSteps = [];
    let finalRawPath = absRawPath;
    const renamedRawPath = buildRawPathForJobId(absRawPath, created.id);
    const shouldRenameRawFolder = normalizeComparablePath(renamedRawPath) !== absRawPath;
    if (shouldRenameRawFolder) {
      if (fs.existsSync(renamedRawPath)) {
        await db.run('DELETE FROM jobs WHERE id = ?', [created.id]);
        const error = new Error(`RAW-Ordner für neue Job-ID existiert bereits: ${renamedRawPath}`);
        error.statusCode = 409;
        throw error;
      }

      try {
        fs.renameSync(absRawPath, renamedRawPath);
        finalRawPath = normalizeComparablePath(renamedRawPath);
        renameSteps.push({ from: absRawPath, to: finalRawPath });
      } catch (error) {
        await db.run('DELETE FROM jobs WHERE id = ?', [created.id]);
        const wrapped = new Error(`RAW-Ordner konnte nicht auf neue Job-ID umbenannt werden: ${error.message}`);
        wrapped.statusCode = 500;
        throw wrapped;
      }
    }

    const ripCompleteFolderName = applyRawFolderPrefix(path.basename(finalRawPath), RAW_RIP_COMPLETE_PREFIX);
    const ripCompleteRawPath = path.join(path.dirname(finalRawPath), ripCompleteFolderName);
    const shouldMarkRipComplete = normalizeComparablePath(ripCompleteRawPath) !== normalizeComparablePath(finalRawPath);
    if (shouldMarkRipComplete) {
      if (fs.existsSync(ripCompleteRawPath)) {
        await db.run('DELETE FROM jobs WHERE id = ?', [created.id]);
        const error = new Error(`RAW-Ordner für Rip_Complete-Zustand existiert bereits: ${ripCompleteRawPath}`);
        error.statusCode = 409;
        throw error;
      }

      try {
        const previousRawPath = finalRawPath;
        fs.renameSync(previousRawPath, ripCompleteRawPath);
        finalRawPath = normalizeComparablePath(ripCompleteRawPath);
        renameSteps.push({ from: previousRawPath, to: finalRawPath });
      } catch (error) {
        await db.run('DELETE FROM jobs WHERE id = ?', [created.id]);
        const wrapped = new Error(`RAW-Ordner konnte nicht als Rip_Complete markiert werden: ${error.message}`);
        wrapped.statusCode = 500;
        throw wrapped;
      }
    }

    const seriesRawPathHint = isLikelySeriesRawPath(finalRawPath, settings);
    const detectedMediaType = detectOrphanMediaType(finalRawPath, {
      seriesRawPathHint
    });
    const initialSourceJobContext = await this.resolveOrphanImportSourceJob({
      candidateJobIds: [metadata.folderJobId],
      mediaProfile: detectedMediaType
    });
    const fallbackMediaTypeFromSource = normalizeMediaTypeValue(
      initialSourceJobContext?.mediaProfile
      || initialSourceJobContext?.makemkvInfo?.analyzeContext?.mediaProfile
      || initialSourceJobContext?.makemkvInfo?.mediaProfile
      || null
    );
    const effectiveDetectedMediaType = detectedMediaType === 'other'
      ? (fallbackMediaTypeFromSource || (seriesRawPathHint ? 'dvd' : 'other'))
      : detectedMediaType;
    const initialSourceJob = initialSourceJobContext?.job || null;
    const initialSourceSelectedMetadata = initialSourceJobContext?.selectedMetadata && typeof initialSourceJobContext.selectedMetadata === 'object'
      ? initialSourceJobContext.selectedMetadata
      : {};
    const orphanPosterUrl = omdbById?.poster || null;
    const cdRecovery = effectiveDetectedMediaType === 'cd'
      ? recoverCdJobArtifactsForImport({
        currentRawPath: finalRawPath,
        rawPathCandidates: [
          absRawPath,
          renamedRawPath,
          finalRawPath
        ],
        relatedJobIds: [metadata.folderJobId],
        excludeJobIds: [created.id],
        settings,
        baseMetadata: {
          title: initialSourceSelectedMetadata?.title || initialSourceSelectedMetadata?.album || initialSourceJob?.title || metadata.title || null,
          album: initialSourceSelectedMetadata?.album || initialSourceSelectedMetadata?.title || initialSourceJob?.title || metadata.title || null,
          artist: initialSourceSelectedMetadata?.artist || null,
          year: initialSourceSelectedMetadata?.year || initialSourceJob?.year || metadata.year || null
        }
      })
      : null;
    const sourceJobContext = await this.resolveOrphanImportSourceJob({
      candidateJobIds: [
        metadata.folderJobId,
        ...(cdRecovery?.logSources || []).map((source) => source?.jobId)
      ],
      mediaProfile: effectiveDetectedMediaType
    }) || initialSourceJobContext;
    const sourceJob = sourceJobContext?.job || null;
    const sourceSelectedMetadata = sourceJobContext?.selectedMetadata && typeof sourceJobContext.selectedMetadata === 'object'
      ? sourceJobContext.selectedMetadata
      : {};
    const sourceAnalyzeContext = sourceJobContext?.analyzeContext && typeof sourceJobContext.analyzeContext === 'object'
      ? sourceJobContext.analyzeContext
      : {};
    const sourceHasSeriesSignals = hasSeriesMetadataSignals(sourceSelectedMetadata, sourceAnalyzeContext);
    const seriesImportHints = effectiveDetectedMediaType === 'dvd' && (seriesRawPathHint || sourceHasSeriesSignals)
      ? buildSeriesImportHints({
        folderName: path.basename(finalRawPath),
        rawPath: finalRawPath,
        metadata,
        sourceSelectedMetadata,
        sourceAnalyzeContext
      })
      : null;
    let effectiveSeriesImportHints = seriesImportHints;
    let inferredSeriesContainer = null;
    if (effectiveDetectedMediaType === 'dvd') {
      const hintedTmdbId = normalizePositiveIntegerOrNull(
        effectiveSeriesImportHints?.selectedMetadata?.tmdbId
        ?? effectiveSeriesImportHints?.selectedMetadata?.providerId
        ?? null
      );
      const hintedSeasonNumber = normalizePositiveIntegerOrNull(
        effectiveSeriesImportHints?.selectedMetadata?.seasonNumber
        ?? effectiveSeriesImportHints?.analyzeContextPatch?.seriesLookupHint?.seasonNumber
        ?? null
      );

      if (hintedTmdbId && hintedSeasonNumber) {
        inferredSeriesContainer = await this.findSeriesContainerJob(hintedTmdbId, hintedSeasonNumber);
      }

      if (!inferredSeriesContainer && seriesRawPathHint) {
        inferredSeriesContainer = await this.findLikelySeriesContainerJob({
          title: sourceSelectedMetadata?.title || sourceSelectedMetadata?.seriesTitle || effectiveTitle || metadata.title || null,
          year: sourceSelectedMetadata?.year || sourceJob?.year || metadata.year || null
        });
      }

      if (inferredSeriesContainer) {
        const containerInfo = parseJsonSafe(inferredSeriesContainer?.makemkv_info_json, {}) || {};
        const containerSelectedMetadata = extractSelectedMetadataFromMakemkvInfo(containerInfo);
        const containerAnalyzeContext = containerInfo?.analyzeContext && typeof containerInfo.analyzeContext === 'object'
          ? containerInfo.analyzeContext
          : {};
        const containerTmdbId = normalizePositiveIntegerOrNull(
          containerSelectedMetadata?.tmdbId
          || containerSelectedMetadata?.providerId
          || null
        );
        const containerSeasonNumber = normalizePositiveIntegerOrNull(containerSelectedMetadata?.seasonNumber || null);
        const discNumberHint = normalizePositiveIntegerOrNull(
          effectiveSeriesImportHints?.selectedMetadata?.discNumber
          ?? extractDiscNumberFromText(`${path.basename(finalRawPath)} ${path.basename(absRawPath)}`)
          ?? null
        );

        if (containerTmdbId && containerSeasonNumber) {
          const mergedTitle = String(
            effectiveSeriesImportHints?.selectedMetadata?.title
            || containerSelectedMetadata?.title
            || effectiveTitle
            || inferredSeriesContainer?.title
            || ''
          ).trim() || null;
          effectiveSeriesImportHints = {
            selectedMetadata: {
              ...containerSelectedMetadata,
              ...(effectiveSeriesImportHints?.selectedMetadata && typeof effectiveSeriesImportHints.selectedMetadata === 'object'
                ? effectiveSeriesImportHints.selectedMetadata
                : {}),
              metadataProvider: 'tmdb',
              metadataKind: String(containerSelectedMetadata?.metadataKind || 'series').trim().toLowerCase() || 'series',
              tmdbId: containerTmdbId,
              seasonNumber: containerSeasonNumber,
              title: mergedTitle,
              ...(discNumberHint ? { discNumber: discNumberHint } : {})
            },
            analyzeContextPatch: {
              ...(effectiveSeriesImportHints?.analyzeContextPatch && typeof effectiveSeriesImportHints.analyzeContextPatch === 'object'
                ? effectiveSeriesImportHints.analyzeContextPatch
                : {}),
              metadataProvider: 'tmdb',
              metadataKind: String(containerAnalyzeContext?.metadataKind || containerSelectedMetadata?.metadataKind || 'series').trim().toLowerCase() || 'series',
              seriesLookupHint: {
                ...(containerAnalyzeContext?.seriesLookupHint && typeof containerAnalyzeContext.seriesLookupHint === 'object'
                  ? containerAnalyzeContext.seriesLookupHint
                  : {}),
                query: mergedTitle,
                seasonNumber: containerSeasonNumber,
                ...(discNumberHint ? { discNumber: discNumberHint } : {})
              },
              seriesAnalysis: {
                ...(containerAnalyzeContext?.seriesAnalysis && typeof containerAnalyzeContext.seriesAnalysis === 'object'
                  ? containerAnalyzeContext.seriesAnalysis
                  : {}),
                summary: {
                  ...(containerAnalyzeContext?.seriesAnalysis?.summary && typeof containerAnalyzeContext.seriesAnalysis.summary === 'object'
                    ? containerAnalyzeContext.seriesAnalysis.summary
                    : {}),
                  seriesLike: true,
                  confidence: containerAnalyzeContext?.seriesAnalysis?.summary?.confidence || 'high'
                }
              }
            }
          };
        }
      }
    }
    const sourcePosterCandidate = this.getPreferredPosterCandidateForImport(sourceJobContext, null);
    const recoveredCdEncodePlan = cdRecovery ? this.buildRecoveredCdEncodePlan(cdRecovery) : null;
    const orphanImportInfo = this.buildOrphanRawImportMakemkvInfo({
      importedAt,
      rawPath: finalRawPath,
      requestedRawPath: absRawPath,
      sourceFolderJobId: metadata.folderJobId || null,
      mediaProfile: effectiveDetectedMediaType,
      existingInfo: sourceJobContext?.makemkvInfo || null,
      recovery: cdRecovery,
      selectedMetadata: effectiveSeriesImportHints?.selectedMetadata || null,
      analyzeContextPatch: effectiveSeriesImportHints?.analyzeContextPatch || null
    });
    const inferredSeriesContainerId = normalizeJobIdValue(inferredSeriesContainer?.id);
    const recoveredPosterUrl = orphanPosterUrl
      || (thumbnailService.isLocalUrl(sourcePosterCandidate) ? null : sourcePosterCandidate)
      || null;
    const recoveredExternalId = String(
      omdbById?.imdbId
      || metadata.imdbId
      || sourceSelectedMetadata?.mbId
      || sourceSelectedMetadata?.musicBrainzId
      || sourceSelectedMetadata?.musicbrainzId
      || sourceSelectedMetadata?.musicbrainz_id
      || sourceSelectedMetadata?.music_brainz_id
      || sourceSelectedMetadata?.musicbrainz
      || sourceSelectedMetadata?.mbid
      || sourceJob?.imdb_id
      || ''
    ).trim() || null;
    await this.updateJob(created.id, {
      ...(effectiveDetectedMediaType === 'audiobook' ? { job_kind: 'audiobook', media_type: 'audiobook' } : {}),
      ...(effectiveDetectedMediaType === 'cd' ? { job_kind: 'cd', media_type: 'cd' } : {}),
      ...(effectiveDetectedMediaType === 'dvd'
        ? (inferredSeriesContainerId
            ? { job_kind: 'dvd_series_child', media_type: 'dvd', parent_job_id: inferredSeriesContainerId }
            : { job_kind: 'dvd', media_type: 'dvd' })
        : {}),
      ...(effectiveDetectedMediaType === 'bluray' ? { job_kind: 'bluray', media_type: 'bluray' } : {}),
      status: 'FINISHED',
      last_state: 'FINISHED',
      title: omdbById?.title
        || sourceSelectedMetadata?.title
        || sourceSelectedMetadata?.album
        || sourceJob?.title
        || metadata.title
        || cdRecovery?.selectedMetadata?.title
        || null,
      year: Number.isFinite(Number(omdbById?.year))
        ? Number(omdbById.year)
        : (
          sourceSelectedMetadata?.year
          || sourceJob?.year
          || metadata.year
          || cdRecovery?.selectedMetadata?.year
          || null
        ),
      imdb_id: recoveredExternalId,
      poster_url: recoveredPosterUrl,
      omdb_json: omdbById?.raw ? JSON.stringify(omdbById.raw) : (sourceJob?.omdb_json || null),
      selected_from_omdb: omdbById ? 1 : Number(sourceJob?.selected_from_omdb || 0),
      rip_successful: 1,
      raw_path: finalRawPath,
      output_path: cdRecovery?.outputPath || null,
      handbrake_info_json: null,
      mediainfo_info_json: null,
      encode_plan_json: recoveredCdEncodePlan ? JSON.stringify(recoveredCdEncodePlan) : null,
      encode_input_path: null,
      encode_review_confirmed: 0,
      error_message: null,
      end_time: importedAt,
      makemkv_info_json: JSON.stringify(orphanImportInfo)
    });

    if (cdRecovery?.logSources?.length > 0) {
      await this.restoreImportedProcessLog(created.id, cdRecovery.logSources, {
        importInfo: orphanImportInfo
      });
    }

    // Bild direkt persistieren (kein Rip-Prozess, daher kein Cache-Zwischenschritt)
    if (orphanPosterUrl || sourcePosterCandidate) {
      this.restoreImportedPoster(created.id, sourceJobContext, orphanPosterUrl || sourcePosterCandidate).catch(() => {});
    }

    if (!cdRecovery?.logSources?.length) {
      await this.appendLog(
        created.id,
        'SYSTEM',
        `Orphan-RAW-Import Zusatzinfo: ${JSON.stringify(orphanImportInfo)}`
      );
    }

    await this.appendLog(
      created.id,
      'SYSTEM',
      renameSteps.length > 0
        ? `Historieneintrag aus RAW erstellt (Medientyp: ${effectiveDetectedMediaType}). Ordner umbenannt: ${renameSteps.map((step) => `${step.from} -> ${step.to}`).join(' | ')}`
        : `Historieneintrag aus bestehendem RAW-Ordner erstellt: ${finalRawPath} (Medientyp: ${effectiveDetectedMediaType})`
    );
    if (metadata.imdbId) {
      await this.appendLog(
        created.id,
        'SYSTEM',
        omdbById
          ? `OMDb-Zuordnung via IMDb-ID übernommen: ${omdbById.imdbId} (${omdbById.title || '-'})`
          : `OMDb-Zuordnung via IMDb-ID fehlgeschlagen: ${metadata.imdbId}`
      );
    }
    if (sourceJob) {
      await this.appendLog(
        created.id,
        'SYSTEM',
        `Metadaten aus vorhandenem Job #${sourceJob.id} wiederhergestellt.`
      );
    }
    if (inferredSeriesContainerId) {
      await this.appendLog(
        created.id,
        'SYSTEM',
        `Automatisch dem Serien-Container #${inferredSeriesContainerId} zugeordnet.`
      );
    }

    logger.info('job:import-orphan-raw', {
      jobId: created.id,
      rawPath: absRawPath,
      detectedMediaType: effectiveDetectedMediaType,
      detectedMediaTypeRaw: detectedMediaType,
      seriesRawPathHint
    });

    const imported = await this.getJobById(created.id);
    return enrichJobRow(imported, settings);
  }

  async assignOmdbMetadata(jobId, payload = {}) {
    const job = await this.getJobById(jobId);
    if (!job) {
      const error = new Error('Job nicht gefunden.');
      error.statusCode = 404;
      throw error;
    }

    const parseTmdbId = (value) => {
      const direct = normalizePositiveIntegerOrNull(value);
      if (direct !== null) {
        return direct;
      }
      const text = String(value || '').trim();
      if (!text) {
        return null;
      }
      const providerMatch = text.match(/tmdb:(\d+)/i);
      if (providerMatch?.[1]) {
        return normalizePositiveIntegerOrNull(providerMatch[1]);
      }
      return null;
    };
    const requestedProviderRaw = String(payload.metadataProvider || '').trim().toLowerCase();
    const requestedTmdbId = parseTmdbId(payload?.tmdbId ?? payload?.providerId ?? null);
    const metadataProvider = requestedProviderRaw === 'themoviedb'
      ? 'tmdb'
      : (requestedProviderRaw || (requestedTmdbId !== null ? 'tmdb' : 'omdb'));
    const makemkvInfo = parseJsonSafe(job.makemkv_info_json, {});
    const analyzeContext = makemkvInfo?.analyzeContext && typeof makemkvInfo.analyzeContext === 'object'
      ? makemkvInfo.analyzeContext
      : {};
    const existingSelectedMetadata = extractSelectedMetadataFromMakemkvInfo(makemkvInfo);

    if (metadataProvider === 'tmdb') {
      const manualTitle = String(payload.title || '').trim();
      const manualYearRaw = Number(payload.year);
      const manualYear = Number.isFinite(manualYearRaw) ? Math.trunc(manualYearRaw) : null;
      const manualImdbId = String(payload.imdbId || '').trim().toLowerCase() || null;
      const manualPoster = String(payload.poster || '').trim() || null;
      const manualSeasonName = String(payload.seasonName || '').trim() || null;
      const manualSeasonNumber = normalizePositiveNumberOrNull(payload.seasonNumber);
      const manualDiscNumber = normalizePositiveIntegerOrNull(payload.discNumber);
      const manualEpisodeCountRaw = Number(payload.episodeCount);
      const manualEpisodeCount = Number.isFinite(manualEpisodeCountRaw) && manualEpisodeCountRaw > 0
        ? Math.trunc(manualEpisodeCountRaw)
        : null;
      const manualEpisodes = Array.isArray(payload.episodes) ? payload.episodes : null;
      const manualMetadataKind = String(payload.metadataKind || '').trim().toLowerCase() || null;
      const manualProviderId = String(payload.providerId || '').trim() || null;

      let effectiveTmdbId = requestedTmdbId;
      if (effectiveTmdbId === null) {
        effectiveTmdbId = parseTmdbId(
          existingSelectedMetadata?.tmdbId
          ?? existingSelectedMetadata?.providerId
          ?? analyzeContext?.tmdbId
          ?? analyzeContext?.providerId
          ?? null
        );
      }

      const hasTmdbManualData = Boolean(
        manualTitle
        || manualYear !== null
        || manualImdbId
        || manualPoster
        || manualSeasonName
        || manualSeasonNumber !== null
        || manualDiscNumber !== null
        || manualEpisodeCount !== null
        || (manualEpisodes && manualEpisodes.length > 0)
      );
      if (effectiveTmdbId === null && !hasTmdbManualData) {
        const error = new Error('Keine TMDb-/Metadaten zum Aktualisieren angegeben.');
        error.statusCode = 400;
        throw error;
      }

      const existingYearRaw = Number(existingSelectedMetadata?.year);
      const existingYear = Number.isFinite(existingYearRaw) ? Math.trunc(existingYearRaw) : null;
      let title = manualTitle
        || String(existingSelectedMetadata?.title || '').trim()
        || job.title
        || job.detected_title
        || null;
      let year = manualYear !== null
        ? manualYear
        : (existingYear !== null ? existingYear : (job.year ?? null));
      let imdbId = manualImdbId
        || String(existingSelectedMetadata?.imdbId || job.imdb_id || '').trim().toLowerCase()
        || null;
      let posterUrl = manualPoster
        || String(existingSelectedMetadata?.poster || job.poster_url || '').trim()
        || null;
      let seasonNumber = manualSeasonNumber !== null
        ? manualSeasonNumber
        : normalizePositiveNumberOrNull(
          existingSelectedMetadata?.seasonNumber
          ?? analyzeContext?.seriesLookupHint?.seasonNumber
          ?? null
        );
      let seasonName = manualSeasonName
        || String(existingSelectedMetadata?.seasonName || '').trim()
        || null;
      let episodeCount = manualEpisodeCount !== null
        ? manualEpisodeCount
        : (Number(existingSelectedMetadata?.episodeCount || 0) || 0);
      let episodes = manualEpisodes && manualEpisodes.length > 0
        ? manualEpisodes
        : (Array.isArray(existingSelectedMetadata?.episodes) ? existingSelectedMetadata.episodes : []);
      const discNumber = manualDiscNumber !== null
        ? manualDiscNumber
        : normalizePositiveIntegerOrNull(
          existingSelectedMetadata?.discNumber
          ?? analyzeContext?.seriesLookupHint?.discNumber
          ?? null
        );
      const resolvedJobMediaType = inferMediaType(
        job,
        makemkvInfo,
        job?.mediainfo_info_json,
        job?.encode_plan_json,
        job?.handbrake_info_json
      );
      if (resolvedJobMediaType === 'dvd' && discNumber === null) {
        const error = new Error('Serien-DVD erkannt: Disk-Nummer ist Pflicht (Start bei 1).');
        error.statusCode = 400;
        throw error;
      }
      let metadataKind = manualMetadataKind
        || String(
          existingSelectedMetadata?.metadataKind
          || analyzeContext?.metadataKind
          || ''
        ).trim().toLowerCase()
        || null;
      if (!metadataKind) {
        metadataKind = seasonNumber !== null ? 'season' : 'series';
      }
      let providerId = manualProviderId
        || String(existingSelectedMetadata?.providerId || '').trim()
        || null;
      if (!providerId && effectiveTmdbId !== null) {
        providerId = seasonNumber !== null
          ? `tmdb:${effectiveTmdbId}:season:${seasonNumber}`
          : `tmdb:${effectiveTmdbId}`;
      }

      let tmdbDetails = existingSelectedMetadata?.tmdbDetails && typeof existingSelectedMetadata.tmdbDetails === 'object'
        ? { ...existingSelectedMetadata.tmdbDetails }
        : null;
      if (effectiveTmdbId !== null) {
        try {
          const seriesDetails = await tmdbService.getSeriesDetails(effectiveTmdbId, {
            appendToResponse: ['credits', 'external_ids']
          });
          if (seriesDetails) {
            const detailsSummary = tmdbService.buildSeriesDetailsSummary(seriesDetails);
            const createdBy = tmdbService.normalizeNameList(seriesDetails?.created_by, { maxItems: 3 });
            const genres = tmdbService.normalizeNameList(seriesDetails?.genres, { maxItems: 3 });
            tmdbDetails = {
              ...(detailsSummary && typeof detailsSummary === 'object' ? detailsSummary : {}),
              createdBy,
              genres
            };
            if (!title) {
              title = String(seriesDetails?.name || seriesDetails?.original_name || '').trim() || null;
            }
            if ((!year || Number(year) <= 0) && tmdbDetails?.firstAirDate) {
              const tmdbYear = Number(String(tmdbDetails.firstAirDate).slice(0, 4));
              if (Number.isFinite(tmdbYear) && tmdbYear > 0) {
                year = Math.trunc(tmdbYear);
              }
            }
            if (!imdbId && tmdbDetails?.imdbId) {
              imdbId = String(tmdbDetails.imdbId).trim().toLowerCase() || null;
            }
            if (!posterUrl) {
              posterUrl = tmdbService.buildImageUrl(seriesDetails?.poster_path, 'w342') || null;
            }
          }
        } catch (tmdbDetailsErr) {
          logger.warn('assignOmdbMetadata:tmdb-details-fetch-failed', {
            jobId,
            tmdbId: effectiveTmdbId,
            message: tmdbDetailsErr?.message || String(tmdbDetailsErr)
          });
        }
      }

      if (effectiveTmdbId !== null && seasonNumber !== null) {
        try {
          const seasonDetails = await tmdbService.getSeasonDetails(effectiveTmdbId, seasonNumber);
          const seasonCredits = await tmdbService.getSeasonCredits(effectiveTmdbId, seasonNumber);
          const seasonSummary = tmdbService.buildSeasonSummary(seasonDetails);
          if (seasonSummary) {
            const fetchedEpisodes = Array.isArray(seasonSummary.episodes) ? seasonSummary.episodes : [];
            if (fetchedEpisodes.length > 0 && (!Array.isArray(episodes) || episodes.length === 0)) {
              episodes = fetchedEpisodes;
            }
            const fetchedEpisodeCount = Number(seasonSummary.episodeCount || 0) || 0;
            if (fetchedEpisodeCount > 0) {
              episodeCount = fetchedEpisodeCount;
            }
            if (!seasonName && seasonSummary.name) {
              seasonName = String(seasonSummary.name).trim() || null;
            }
          }
          const seasonVoteAverageRaw = Number(seasonDetails?.vote_average || 0);
          const seasonVoteAverage = Number.isFinite(seasonVoteAverageRaw) && seasonVoteAverageRaw > 0
            ? Number(seasonVoteAverageRaw.toFixed(1))
            : null;
          const seasonRuntimeValues = Array.isArray(seasonDetails?.episodes)
            ? seasonDetails.episodes
              .map((episode) => Number(episode?.runtime || 0))
              .filter((value) => Number.isFinite(value) && value > 0)
            : [];
          const seasonRuntime = tmdbService.formatRuntimeLabel(seasonRuntimeValues);
          const seasonCast = tmdbService.normalizeNameList(seasonCredits?.cast, { maxItems: 6 });
          tmdbDetails = {
            ...(tmdbDetails && typeof tmdbDetails === 'object' ? tmdbDetails : {}),
            seasonNumber,
            seasonVoteAverage,
            seasonRuntime,
            runtime: seasonRuntime || (tmdbDetails?.runtime || null),
            seasonCast
          };
        } catch (tmdbSeasonErr) {
          logger.warn('assignOmdbMetadata:tmdb-season-fetch-failed', {
            jobId,
            tmdbId: effectiveTmdbId,
            seasonNumber,
            message: tmdbSeasonErr?.message || String(tmdbSeasonErr)
          });
        }
      }

      const nextSelectedMetadata = {
        ...(existingSelectedMetadata && typeof existingSelectedMetadata === 'object' ? existingSelectedMetadata : {}),
        title,
        year,
        imdbId,
        poster: posterUrl,
        metadataProvider: 'tmdb',
        providerId,
        tmdbId: effectiveTmdbId,
        metadataKind,
        seasonNumber,
        seasonName,
        episodeCount,
        episodes: Array.isArray(episodes) ? episodes : [],
        ...(discNumber !== null ? { discNumber } : {}),
        ...(tmdbDetails && typeof tmdbDetails === 'object' ? { tmdbDetails } : {})
      };
      const existingAnalyzeSelected = analyzeContext?.selectedMetadata && typeof analyzeContext.selectedMetadata === 'object'
        ? analyzeContext.selectedMetadata
        : {};
      const existingSeriesLookupHint = analyzeContext?.seriesLookupHint && typeof analyzeContext.seriesLookupHint === 'object'
        ? analyzeContext.seriesLookupHint
        : {};
      const nextAnalyzeContext = {
        ...analyzeContext,
        metadataProvider: 'tmdb',
        metadataKind,
        selectedMetadata: {
          ...existingAnalyzeSelected,
          ...nextSelectedMetadata
        },
        seriesLookupHint: {
          ...existingSeriesLookupHint,
          query: String(existingSeriesLookupHint.query || title || '').trim() || null,
          seasonNumber: seasonNumber !== null
            ? seasonNumber
            : normalizePositiveNumberOrNull(existingSeriesLookupHint.seasonNumber ?? null),
          discNumber: discNumber !== null
            ? discNumber
            : normalizePositiveIntegerOrNull(existingSeriesLookupHint.discNumber ?? null)
        }
      };
      const topLevelSelected = makemkvInfo?.selectedMetadata && typeof makemkvInfo.selectedMetadata === 'object'
        ? makemkvInfo.selectedMetadata
        : {};
      const nextMakemkvInfo = {
        ...(makemkvInfo && typeof makemkvInfo === 'object' ? makemkvInfo : {}),
        selectedMetadata: {
          ...topLevelSelected,
          ...nextSelectedMetadata
        },
        analyzeContext: nextAnalyzeContext
      };

      await this.updateJob(jobId, {
        title,
        year,
        imdb_id: imdbId,
        poster_url: posterUrl,
        omdb_json: null,
        selected_from_omdb: 0,
        makemkv_info_json: JSON.stringify(nextMakemkvInfo)
      });

      if (posterUrl && !thumbnailService.isLocalUrl(posterUrl)) {
        this.queuePosterCache(jobId, posterUrl, {
          source: 'TMDb Poster',
          logFailures: true
        });
      }

      await this.appendLog(
        jobId,
        'USER_ACTION',
        effectiveTmdbId !== null
          ? `TMDb-Zuordnung aktualisiert: ${effectiveTmdbId}${seasonNumber !== null ? ` (Staffel ${seasonNumber})` : ''} (${title || '-'})`
          : `TMDb-Metadaten manuell aktualisiert: title="${title || '-'}", year="${year || '-'}", imdb="${imdbId || '-'}"`
      );

      const [updated, settings] = await Promise.all([
        this.getJobById(jobId),
        settingsService.getSettingsMap()
      ]);
      return enrichJobRow(updated, settings);
    }

    const imdbIdInput = String(payload.imdbId || '').trim().toLowerCase();
    let omdb = null;
    if (imdbIdInput) {
      try {
        omdb = await omdbService.fetchByImdbId(imdbIdInput);
      } catch (omdbErr) {
        logger.warn('assignOmdbMetadata:fetch-failed', { jobId, imdbId: imdbIdInput, message: omdbErr.message });
      }
    }

    const manualTitle = String(payload.title || '').trim();
    const manualYearRaw = Number(payload.year);
    const manualYear = Number.isFinite(manualYearRaw) ? Math.trunc(manualYearRaw) : null;
    const manualPoster = String(payload.poster || '').trim() || null;
    const hasManual = manualTitle.length > 0 || manualYear !== null || imdbIdInput.length > 0;
    if (!omdb && !hasManual) {
      const error = new Error('Keine OMDb-/Metadaten zum Aktualisieren angegeben.');
      error.statusCode = 400;
      throw error;
    }

    const title = omdb?.title || manualTitle || job.title || job.detected_title || null;
    const year = Number.isFinite(Number(omdb?.year))
      ? Number(omdb.year)
      : (manualYear !== null ? manualYear : (job.year ?? null));
    const imdbId = omdb?.imdbId || imdbIdInput || job.imdb_id || null;
    const posterUrl = omdb?.poster || manualPoster || job.poster_url || null;
    const selectedFromOmdb = omdb ? 1 : Number(payload.fromOmdb ? 1 : 0);
    const nextSelectedMetadata = {
      ...(existingSelectedMetadata && typeof existingSelectedMetadata === 'object' ? existingSelectedMetadata : {}),
      title,
      year,
      imdbId,
      poster: posterUrl,
      metadataProvider: 'omdb'
    };
    const existingAnalyzeSelected = analyzeContext?.selectedMetadata && typeof analyzeContext.selectedMetadata === 'object'
      ? analyzeContext.selectedMetadata
      : {};
    const nextAnalyzeContext = {
      ...analyzeContext,
      metadataProvider: 'omdb',
      selectedMetadata: {
        ...existingAnalyzeSelected,
        ...nextSelectedMetadata
      }
    };
    const topLevelSelected = makemkvInfo?.selectedMetadata && typeof makemkvInfo.selectedMetadata === 'object'
      ? makemkvInfo.selectedMetadata
      : {};
    const nextMakemkvInfo = {
      ...(makemkvInfo && typeof makemkvInfo === 'object' ? makemkvInfo : {}),
      selectedMetadata: {
        ...topLevelSelected,
        ...nextSelectedMetadata
      },
      analyzeContext: nextAnalyzeContext
    };

    await this.updateJob(jobId, {
      title,
      year,
      imdb_id: imdbId,
      poster_url: posterUrl,
      omdb_json: omdb?.raw ? JSON.stringify(omdb.raw) : (job.omdb_json || null),
      selected_from_omdb: selectedFromOmdb,
      makemkv_info_json: JSON.stringify(nextMakemkvInfo)
    });

    // Bild herunterladen, in persistenten Ordner verschieben und poster_url aktualisieren
    if (posterUrl && !thumbnailService.isLocalUrl(posterUrl)) {
      this.queuePosterCache(jobId, posterUrl, {
        source: 'OMDb Poster',
        logFailures: true
      });
    }

    await this.appendLog(
      jobId,
      'USER_ACTION',
      omdb
        ? `OMDb-Zuordnung aktualisiert: ${omdb.imdbId} (${omdb.title || '-'})`
        : `Metadaten manuell aktualisiert: title="${title || '-'}", year="${year || '-'}", imdb="${imdbId || '-'}"`
    );

    const [updated, settings] = await Promise.all([
      this.getJobById(jobId),
      settingsService.getSettingsMap()
    ]);
    return enrichJobRow(updated, settings);
  }

  async assignCdMetadata(jobId, payload = {}) {
    const job = await this.getJobById(jobId);
    if (!job) {
      const error = new Error('Job nicht gefunden.');
      error.statusCode = 404;
      throw error;
    }

    const title = String(payload.title || '').trim() || null;
    const artist = String(payload.artist || '').trim() || null;
    const yearRaw = Number(payload.year);
    const year = Number.isFinite(yearRaw) && yearRaw > 0 ? Math.trunc(yearRaw) : null;
    const mbId = String(payload.mbId || '').trim() || null;
    const coverUrl = String(payload.coverUrl || '').trim() || null;
    const selectedTracks = Array.isArray(payload.tracks) ? payload.tracks : null;

    if (!title && !artist && !mbId) {
      const error = new Error('Keine CD-Metadaten zum Aktualisieren angegeben.');
      error.statusCode = 400;
      throw error;
    }

    const cdInfo = parseJsonSafe(job.makemkv_info_json, {});
    const tocTracks = Array.isArray(cdInfo.tracks) ? cdInfo.tracks : [];

    let mergedTracks = tocTracks;
    if (selectedTracks && tocTracks.length > 0) {
      mergedTracks = tocTracks.map((t) => {
        const selected = selectedTracks.find((st) => Number(st.position) === Number(t.position));
        const resolvedTitle = String(selected?.title || t.title || `Track ${t.position}`).replace(/\s+/g, ' ').trim();
        const resolvedArtist = String(selected?.artist || t.artist || artist || '').replace(/\s+/g, ' ').trim() || null;
        return {
          ...t,
          title: resolvedTitle,
          artist: resolvedArtist,
          selected: selected ? Boolean(selected.selected) : true
        };
      });
    }

    const prevSelected = cdInfo.selectedMetadata && typeof cdInfo.selectedMetadata === 'object' ? cdInfo.selectedMetadata : {};
    const updatedCdInfo = {
      ...cdInfo,
      tracks: mergedTracks,
      selectedMetadata: {
        ...prevSelected,
        title: title || prevSelected.title || null,
        artist: artist || prevSelected.artist || null,
        year: year !== null ? year : (prevSelected.year || null),
        mbId: mbId || prevSelected.mbId || null,
        coverUrl: coverUrl || prevSelected.coverUrl || null
      }
    };

    await this.updateJob(jobId, {
      title: title || null,
      year: year || null,
      imdb_id: mbId || null,
      poster_url: coverUrl || null,
      makemkv_info_json: JSON.stringify(updatedCdInfo)
    });

    if (coverUrl && !thumbnailService.isLocalUrl(coverUrl)) {
      this.queuePosterCache(jobId, coverUrl, {
        source: 'Coverart',
        logFailures: true
      });
    }

    await this.appendLog(
      jobId,
      'USER_ACTION',
      `CD-Metadaten aktualisiert: album="${title || '-'}", artist="${artist || '-'}", year="${year || '-'}", mbId="${mbId || '-'}"`
    );

    const [updated, settings] = await Promise.all([
      this.getJobById(jobId),
      settingsService.getSettingsMap()
    ]);
    return enrichJobRow(updated, settings);
  }

  async acknowledgeJobError(jobId) {
    const normalizedJobId = normalizeJobIdValue(jobId);
    if (!normalizedJobId) {
      const error = new Error('Ungültige Job-ID.');
      error.statusCode = 400;
      throw error;
    }

    const job = await this.getJobById(normalizedJobId);
    if (!job) {
      const error = new Error('Job nicht gefunden.');
      error.statusCode = 404;
      throw error;
    }

    const hasErrorMessage = Boolean(String(job?.error_message || '').trim());
    const statusUpper = String(job?.status || '').trim().toUpperCase();
    const setCancelled = statusUpper === 'ERROR';

    if (!hasErrorMessage && !setCancelled) {
      const [updatedUnchanged, unchangedSettings] = await Promise.all([
        this.getJobById(normalizedJobId),
        settingsService.getSettingsMap()
      ]);
      return enrichJobRow(updatedUnchanged, unchangedSettings);
    }

    await this.updateJob(normalizedJobId, {
      error_message: null,
      ...(setCancelled ? { status: 'CANCELLED' } : {}),
      ...(!job?.end_time ? { end_time: new Date().toISOString() } : {})
    });

    await this.appendLog(
      normalizedJobId,
      'USER_ACTION',
      'Fehlermeldung quittiert.'
    );

    const [updated, settings] = await Promise.all([
      this.getJobById(normalizedJobId),
      settingsService.getSettingsMap()
    ]);
    return enrichJobRow(updated, settings);
  }

  async _resolveRelatedJobsForDeletion(jobId, options = {}) {
    const includeRelated = options?.includeRelated !== false;
    const includeLogLinks = options?.includeLogLinks !== false;
    const normalizedJobId = normalizeJobIdValue(jobId);
    if (!normalizedJobId) {
      const error = new Error('Ungültige Job-ID.');
      error.statusCode = 400;
      throw error;
    }

    const db = await getDb();
    const rows = await db.all('SELECT * FROM jobs ORDER BY id ASC');
    const byId = new Map(rows.map((row) => [Number(row.id), row]));
    const primary = byId.get(normalizedJobId);
    if (!primary) {
      const error = new Error('Job nicht gefunden.');
      error.statusCode = 404;
      throw error;
    }

    if (!includeRelated) {
      return [primary];
    }

    const childrenByParent = new Map();
    const childrenBySource = new Map();
    for (const row of rows) {
      const rowId = normalizeJobIdValue(row?.id);
      if (!rowId) {
        continue;
      }
      const parentJobId = normalizeJobIdValue(row?.parent_job_id);
      if (parentJobId) {
        if (!childrenByParent.has(parentJobId)) {
          childrenByParent.set(parentJobId, new Set());
        }
        childrenByParent.get(parentJobId).add(rowId);
      }
      const sourceJobId = parseSourceJobIdFromPlan(row?.encode_plan_json);
      if (sourceJobId) {
        if (!childrenBySource.has(sourceJobId)) {
          childrenBySource.set(sourceJobId, new Set());
        }
        childrenBySource.get(sourceJobId).add(rowId);
      }
    }

    const pending = [normalizedJobId];
    const visited = new Set();
    const enqueue = (value) => {
      const id = normalizeJobIdValue(value);
      if (!id || visited.has(id)) {
        return;
      }
      pending.push(id);
    };

    while (pending.length > 0) {
      const currentId = normalizeJobIdValue(pending.shift());
      if (!currentId || visited.has(currentId)) {
        continue;
      }
      visited.add(currentId);

      const row = byId.get(currentId);
      if (!row) {
        continue;
      }

      enqueue(row.parent_job_id);
      enqueue(parseSourceJobIdFromPlan(row.encode_plan_json));

      for (const childId of (childrenByParent.get(currentId) || [])) {
        enqueue(childId);
      }
      for (const childId of (childrenBySource.get(currentId) || [])) {
        enqueue(childId);
      }

      if (includeLogLinks) {
        try {
          const processLog = await this.readProcessLogLines(currentId, { includeAll: true });
          const linkedJobIds = parseRetryLinkedJobIdsFromLogLines(processLog.lines);
          for (const linkedId of linkedJobIds) {
            enqueue(linkedId);
          }
        } catch (_error) {
          // optional fallback links from process logs; ignore read errors
        }
      }
    }

    return Array.from(visited)
      .map((id) => byId.get(id))
      .filter(Boolean)
      .sort((left, right) => Number(left.id || 0) - Number(right.id || 0));
  }

  _collectDeleteCandidatesForJob(job, settings = null, options = {}) {
    const normalizedJobId = normalizeJobIdValue(job?.id);
    const resolvedPaths = resolveEffectiveStoragePathsForJob(settings, job);
    const lineageArtifacts = Array.isArray(options?.lineageArtifacts) ? options.lineageArtifacts : [];
    const trackedOutputPaths = Array.isArray(options?.trackedOutputPaths)
      ? options.trackedOutputPaths
      : [];
    const toNormalizedPath = (value) => {
      const raw = String(value || '').trim();
      if (!raw) {
        return null;
      }
      return normalizeComparablePath(raw);
    };
    const unique = (values = []) => Array.from(new Set((Array.isArray(values) ? values : []).filter(Boolean)));
    const sanitizeRoots = (values = []) => unique(values).filter((root) => !isFilesystemRootPath(root));

    const artifactRawPaths = lineageArtifacts
      .map((artifact) => toNormalizedPath(artifact?.rawPath))
      .filter(Boolean);
    const artifactMoviePaths = lineageArtifacts
      .map((artifact) => toNormalizedPath(artifact?.outputPath))
      .filter(Boolean);

    const explicitRawPaths = unique([
      toNormalizedPath(job?.raw_path),
      toNormalizedPath(resolvedPaths?.effectiveRawPath),
      ...artifactRawPaths
    ]);
    const explicitMovieSeedPaths = unique([
      toNormalizedPath(job?.output_path),
      toNormalizedPath(resolvedPaths?.effectiveOutputPath),
      ...trackedOutputPaths.map((candidatePath) => toNormalizedPath(candidatePath)),
      ...artifactMoviePaths
    ]);
    const inferredSiblingMoviePaths = explicitMovieSeedPaths.flatMap((candidatePath) => inferSiblingOutputFolders(candidatePath));
    const explicitMoviePaths = unique([
      ...explicitMovieSeedPaths,
      ...inferredSiblingMoviePaths.map((candidatePath) => toNormalizedPath(candidatePath))
    ]);

    const rawRoots = sanitizeRoots([
      ...getConfiguredMediaPathList(settings || {}, 'raw_dir'),
      toNormalizedPath(resolvedPaths?.rawDir),
      ...explicitRawPaths.map((candidatePath) => toNormalizedPath(path.dirname(candidatePath)))
    ]);
    // Only effective base dir for this job becomes protectedRoot (must never be deleted itself)
    const movieRoots = sanitizeRoots([
      toNormalizedPath(resolvedPaths?.movieDir)
    ]);
    // Includes parent dirs of output files for addCandidate safety checks
    const movieAllowedPaths = sanitizeRoots([
      ...movieRoots,
      ...explicitMoviePaths,
      ...explicitMoviePaths.map((candidatePath) => toNormalizedPath(path.dirname(candidatePath)))
    ]);

    const rawCandidates = [];
    const movieCandidates = [];
    const addCandidate = (bucket, target, candidatePath, source, allowedRoots = []) => {
      const normalizedPath = toNormalizedPath(candidatePath);
      if (!normalizedPath) {
        return;
      }
      if (isFilesystemRootPath(normalizedPath)) {
        return;
      }
      const roots = Array.isArray(allowedRoots) ? allowedRoots.filter(Boolean) : [];
      if (roots.length > 0 && !roots.some((root) => isPathInside(root, normalizedPath))) {
        return;
      }
      bucket.push({
        target,
        path: normalizedPath,
        source,
        jobId: normalizedJobId
      });
    };

    const artifactRawPathSet = new Set(artifactRawPaths);
    for (const rawPath of explicitRawPaths) {
      addCandidate(
        rawCandidates,
        'raw',
        rawPath,
        artifactRawPathSet.has(rawPath) ? 'lineage_raw_path' : 'raw_path',
        rawRoots
      );
    }

    const rawFolderNames = new Set();
    for (const rawPath of explicitRawPaths) {
      const folderName = String(path.basename(rawPath || '') || '').trim();
      if (!folderName || folderName === '.' || folderName === path.sep) {
        continue;
      }
      rawFolderNames.add(folderName);
      const stripped = stripRawFolderStatePrefix(folderName);
      if (stripped) {
        rawFolderNames.add(stripped);
        rawFolderNames.add(applyRawFolderPrefix(stripped, RAW_INCOMPLETE_PREFIX));
        rawFolderNames.add(applyRawFolderPrefix(stripped, RAW_RIP_COMPLETE_PREFIX));
      }
    }
    for (const rootPath of rawRoots) {
      for (const folderName of rawFolderNames) {
        addCandidate(rawCandidates, 'raw', path.join(rootPath, folderName), 'raw_variant', rawRoots);
      }
    }

    if (normalizedJobId) {
      for (const rootPath of rawRoots) {
        try {
          if (!fs.existsSync(rootPath) || !fs.lstatSync(rootPath).isDirectory()) {
            continue;
          }
          const entries = fs.readdirSync(rootPath, { withFileTypes: true });
          for (const entry of entries) {
            if (!entry?.isDirectory?.()) {
              continue;
            }
            const metadata = parseRawFolderMetadata(entry.name);
            if (normalizeJobIdValue(metadata?.folderJobId) === normalizedJobId) {
              addCandidate(
                rawCandidates,
                'raw',
                path.join(rootPath, entry.name),
                'raw_jobid_scan',
                rawRoots
              );
            }
          }
        } catch (_error) {
          // ignore fs errors while collecting optional candidates
        }
      }
    }

    const artifactMoviePathSet = new Set(artifactMoviePaths);
    const includeMovieParentCandidate = resolvedPaths?.mediaType !== 'converter';
    for (const outputPath of explicitMoviePaths) {
      addCandidate(
        movieCandidates,
        'movie',
        outputPath,
        artifactMoviePathSet.has(outputPath) ? 'lineage_output_path' : 'output_path',
        movieAllowedPaths
      );
      const parentDir = toNormalizedPath(path.dirname(outputPath));
      if (includeMovieParentCandidate && parentDir && !movieRoots.includes(parentDir)) {
        addCandidate(
          movieCandidates,
          'movie',
          parentDir,
          artifactMoviePathSet.has(outputPath) ? 'lineage_output_parent' : 'output_parent',
          movieAllowedPaths
        );
      }
    }

    if (normalizedJobId && resolvedPaths?.mediaType !== 'cd') {
      const incompleteName = `Incomplete_job-${normalizedJobId}`;
      for (const rootPath of movieRoots) {
        addCandidate(movieCandidates, 'movie', path.join(rootPath, incompleteName), 'movie_incomplete_folder', movieRoots);
        try {
          if (!fs.existsSync(rootPath) || !fs.lstatSync(rootPath).isDirectory()) {
            continue;
          }
          const entries = fs.readdirSync(rootPath, { withFileTypes: true });
          for (const entry of entries) {
            if (!entry?.isDirectory?.()) {
              continue;
            }
            const match = String(entry.name || '').match(/^incomplete_job-(\d+)\s*$/i);
            if (normalizeJobIdValue(match?.[1]) !== normalizedJobId) {
              continue;
            }
            addCandidate(
              movieCandidates,
              'movie',
              path.join(rootPath, entry.name),
              'movie_incomplete_scan',
              movieRoots
            );
          }
        } catch (_error) {
          // ignore fs errors while collecting optional candidates
        }
      }
    }

    return {
      rawCandidates,
      movieCandidates,
      rawRoots,
      movieRoots
    };
  }

  _buildDeletePreviewFromJobs(jobs = [], settings = null, lineageArtifactsByJobId = null, outputFoldersByJobId = null) {
    const rows = Array.isArray(jobs) ? jobs : [];
    const artifactsMap = lineageArtifactsByJobId instanceof Map ? lineageArtifactsByJobId : new Map();
    const trackedFoldersMap = outputFoldersByJobId instanceof Map ? outputFoldersByJobId : new Map();
    const candidateMap = new Map();
    const protectedRoots = {
      raw: new Set(),
      movie: new Set()
    };
    const upsertCandidate = (candidate) => {
      const target = String(candidate?.target || '').trim().toLowerCase();
      const candidatePath = String(candidate?.path || '').trim();
      if (!target || !candidatePath) {
        return;
      }
      const key = `${target}:${candidatePath}`;
      if (!candidateMap.has(key)) {
        candidateMap.set(key, {
          target,
          path: candidatePath,
          jobIds: new Set(),
          sources: new Set()
        });
      }
      const row = candidateMap.get(key);
      const candidateJobId = normalizeJobIdValue(candidate?.jobId);
      if (candidateJobId) {
        row.jobIds.add(candidateJobId);
      }
      const source = String(candidate?.source || '').trim();
      if (source) {
        row.sources.add(source);
      }
    };

    for (const job of rows) {
      const lineageArtifacts = artifactsMap.get(normalizeJobIdValue(job?.id)) || [];
      const trackedFolders = trackedFoldersMap.get(normalizeJobIdValue(job?.id)) || [];
      const collected = this._collectDeleteCandidatesForJob(job, settings, {
        lineageArtifacts,
        trackedOutputPaths: trackedFolders
          .map((folder) => String(folder?.output_path || '').trim())
          .filter(Boolean)
      });
      for (const rootPath of collected.rawRoots || []) {
        protectedRoots.raw.add(rootPath);
      }
      for (const rootPath of collected.movieRoots || []) {
        protectedRoots.movie.add(rootPath);
      }
      for (const candidate of collected.rawCandidates || []) {
        upsertCandidate(candidate);
      }
      for (const candidate of collected.movieCandidates || []) {
        upsertCandidate(candidate);
      }
    }

    const hasTrackedOutputSource = (sources = []) => {
      const normalizedSources = Array.isArray(sources)
        ? sources
          .map((source) => String(source || '').trim().toLowerCase())
          .filter(Boolean)
        : [];
      return normalizedSources.includes('output_path') || normalizedSources.includes('lineage_output_path');
    };

    const buildList = (target) => Array.from(candidateMap.values())
      .filter((row) => row.target === target)
      .map((row) => {
        const inspection = inspectDeletionPath(row.path);
        const sources = Array.from(row.sources).sort((left, right) => left.localeCompare(right));
        // Do not expose stale output file paths from DB/lineage in delete preview.
        // They cannot be deleted anyway and show up as "ghost paths" in UI/QA checks.
        if (target === 'movie' && !inspection.exists && hasTrackedOutputSource(sources)) {
          return null;
        }
        return {
          target,
          path: row.path,
          exists: Boolean(inspection.exists),
          isDirectory: Boolean(inspection.isDirectory),
          isFile: Boolean(inspection.isFile),
          jobIds: Array.from(row.jobIds).sort((left, right) => left - right),
          sources
        };
      })
      .filter(Boolean)
      .sort((left, right) => String(left.path || '').localeCompare(String(right.path || ''), 'de'));

    return {
      pathCandidates: {
        raw: buildList('raw'),
        movie: buildList('movie')
      },
      protectedRoots: {
        raw: Array.from(protectedRoots.raw).sort((left, right) => left.localeCompare(right)),
        movie: Array.from(protectedRoots.movie).sort((left, right) => left.localeCompare(right))
      }
    };
  }

  async getJobDeletePreview(jobId, options = {}) {
    const includeRelated = options?.includeRelated !== false;
    const normalizedJobId = normalizeJobIdValue(jobId);
    if (!normalizedJobId) {
      const error = new Error('Ungültige Job-ID.');
      error.statusCode = 400;
      throw error;
    }

    const jobs = await this._resolveRelatedJobsForDeletion(normalizedJobId, { includeRelated });
    const settings = await settingsService.getSettingsMap();
    const relatedJobIds = jobs.map((job) => normalizeJobIdValue(job?.id)).filter(Boolean);
    const [lineageArtifactsByJobId, outputFoldersByJobId] = await Promise.all([
      this.listJobLineageArtifactsByJobIds(relatedJobIds),
      this.listJobOutputFoldersByJobIds(relatedJobIds)
    ]);
    const preview = this._buildDeletePreviewFromJobs(
      jobs,
      settings,
      lineageArtifactsByJobId,
      outputFoldersByJobId
    );
    const relatedJobs = jobs.map((job) => ({
      id: Number(job.id),
      parentJobId: normalizeJobIdValue(job.parent_job_id),
      title: buildJobDisplayTitle(job),
      status: String(job.status || '').trim() || null,
      discDevice: String(job.disc_device || '').trim() || null,
      isPrimary: Number(job.id) === normalizedJobId,
      createdAt: String(job.created_at || '').trim() || null
    }));
    const existingRawCandidates = preview.pathCandidates.raw.filter((row) => row.exists).length;
    const existingMovieCandidates = preview.pathCandidates.movie.filter((row) => row.exists).length;

    return {
      jobId: normalizedJobId,
      includeRelated,
      relatedJobs,
      pathCandidates: preview.pathCandidates,
      protectedRoots: preview.protectedRoots,
      counts: {
        relatedJobs: relatedJobs.length,
        rawCandidates: preview.pathCandidates.raw.length,
        movieCandidates: preview.pathCandidates.movie.length,
        existingRawCandidates,
        existingMovieCandidates
      }
    };
  }

  _deletePathsFromPreview(preview, target = 'both', options = {}) {
    const normalizedTarget = String(target || 'both').trim().toLowerCase();
    const includesRaw = normalizedTarget === 'raw' || normalizedTarget === 'both';
    const includesMovie = normalizedTarget === 'movie' || normalizedTarget === 'both';
    const selectedMoviePathFilter = new Set(
      (Array.isArray(options?.selectedMoviePaths) ? options.selectedMoviePaths : [])
        .map((moviePath) => String(moviePath || '').trim())
        .filter(Boolean)
        .map((moviePath) => normalizeComparablePath(moviePath))
    );

    const summary = {
      target: normalizedTarget,
      raw: { attempted: includesRaw, deleted: false, filesDeleted: 0, dirsRemoved: 0, pathsDeleted: 0, reason: null },
      movie: { attempted: includesMovie, deleted: false, filesDeleted: 0, dirsRemoved: 0, pathsDeleted: 0, reason: null },
      selectedMoviePaths: Array.from(selectedMoviePathFilter),
      deletedPaths: []
    };

    const applyTarget = (targetKey) => {
      let candidates = (Array.isArray(preview?.pathCandidates?.[targetKey]) ? preview.pathCandidates[targetKey] : [])
        .filter((item) => Boolean(item?.exists) && (Boolean(item?.isDirectory) || Boolean(item?.isFile)));
      if (targetKey === 'movie' && selectedMoviePathFilter.size > 0) {
        candidates = candidates.filter((item) => {
          const candidatePath = String(item?.path || '').trim();
          if (!candidatePath) {
            return false;
          }
          return selectedMoviePathFilter.has(normalizeComparablePath(candidatePath));
        });
      }
      if (candidates.length === 0) {
        summary[targetKey].reason = targetKey === 'movie' && selectedMoviePathFilter.size > 0
          ? 'Keine ausgewählten Audio/Video-Ordner gefunden.'
          : 'Keine passenden Dateien/Ordner gefunden.';
        return;
      }

      const protectedRoots = new Set(
        (Array.isArray(preview?.protectedRoots?.[targetKey]) ? preview.protectedRoots[targetKey] : [])
          .map((rootPath) => String(rootPath || '').trim())
          .filter(Boolean)
          .map((rootPath) => normalizeComparablePath(rootPath))
      );

      const orderedCandidates = [...candidates].sort(
        (left, right) => String(right?.path || '').length - String(left?.path || '').length
      );
      for (const candidate of orderedCandidates) {
        const candidatePath = String(candidate?.path || '').trim();
        if (!candidatePath) {
          continue;
        }
        const inspection = inspectDeletionPath(candidatePath);
        if (!inspection.exists) {
          continue;
        }

        if (inspection.isDirectory) {
          const keepRoot = protectedRoots.has(inspection.path);
          const result = deleteFilesRecursively(inspection.path, keepRoot);
          const filesDeleted = Number(result?.filesDeleted || 0);
          const dirsRemoved = Number(result?.dirsRemoved || 0);
          const directoryRemoved = !keepRoot && !fs.existsSync(inspection.path);
          const changed = filesDeleted > 0 || dirsRemoved > 0 || directoryRemoved;
          summary[targetKey].filesDeleted += filesDeleted;
          summary[targetKey].dirsRemoved += dirsRemoved;
          if (changed) {
            summary[targetKey].pathsDeleted += 1;
            summary.deletedPaths.push({
              target: targetKey,
              path: inspection.path,
              type: 'directory',
              keepRoot,
              jobIds: Array.isArray(candidate?.jobIds) ? candidate.jobIds : []
            });
          }
          continue;
        }

        fs.unlinkSync(inspection.path);
        summary[targetKey].filesDeleted += 1;
        summary[targetKey].pathsDeleted += 1;
        summary.deletedPaths.push({
          target: targetKey,
          path: inspection.path,
          type: 'file',
          keepRoot: false,
          jobIds: Array.isArray(candidate?.jobIds) ? candidate.jobIds : []
        });
      }

      summary[targetKey].deleted = summary[targetKey].pathsDeleted > 0
        || summary[targetKey].filesDeleted > 0
        || summary[targetKey].dirsRemoved > 0;
      if (!summary[targetKey].deleted) {
        summary[targetKey].reason = 'Keine vorhandenen Dateien/Ordner gelöscht.';
      }
    };

    if (includesRaw) {
      applyTarget('raw');
    }
    if (includesMovie) {
      applyTarget('movie');
    }

    return summary;
  }

  _deleteProcessLogFile(jobId) {
    const processLogPath = toProcessLogPath(jobId);
    if (!processLogPath || !fs.existsSync(processLogPath)) {
      return;
    }
    try {
      fs.unlinkSync(processLogPath);
    } catch (error) {
      logger.warn('job:process-log:delete-failed', {
        jobId,
        path: processLogPath,
        error: error?.message || String(error)
      });
    }
  }

  async deleteJobFiles(jobId, target = 'both') {
    const allowedTargets = new Set(['raw', 'movie', 'both']);
    if (!allowedTargets.has(target)) {
      const error = new Error(`Ungültiges target '${target}'. Erlaubt: raw, movie, both.`);
      error.statusCode = 400;
      throw error;
    }

    const job = await this.getJobById(jobId);
    if (!job) {
      const error = new Error('Job nicht gefunden.');
      error.statusCode = 404;
      throw error;
    }

    const settings = await settingsService.getSettingsMap();
    const resolvedPaths = resolveEffectiveStoragePathsForJob(settings, job);
    const effectiveRawPath = resolvedPaths.effectiveRawPath;
    const effectiveOutputPath = resolvedPaths.effectiveOutputPath;
    const effectiveRawDir = resolvedPaths.rawDir;
    const effectiveMovieDir = resolvedPaths.movieDir;
    const summary = {
      target,
      raw: { attempted: false, deleted: false, filesDeleted: 0, dirsRemoved: 0, reason: null },
      movie: { attempted: false, deleted: false, filesDeleted: 0, dirsRemoved: 0, reason: null }
    };
    let movieDeletedPathForTracking = null;
    let movieCandidatePathForTracking = null;

    if (target === 'raw' || target === 'both') {
      summary.raw.attempted = true;
      if (!effectiveRawPath) {
        summary.raw.reason = 'Kein raw_path im Job gesetzt.';
      } else if (!effectiveRawDir) {
        const error = new Error(`Kein gültiger RAW-Basispfad für Job ${jobId} (${resolvedPaths.mediaType || 'unknown'}).`);
        error.statusCode = 400;
        throw error;
      } else if (!isPathInside(effectiveRawDir, effectiveRawPath)) {
        const error = new Error(`RAW-Pfad liegt außerhalb des effektiven RAW-Basispfads: ${effectiveRawPath}`);
        error.statusCode = 400;
        throw error;
      } else if (!fs.existsSync(effectiveRawPath)) {
        summary.raw.reason = 'RAW-Pfad existiert nicht.';
      } else {
        const rawPath = normalizeComparablePath(effectiveRawPath);
        const rawRoot = normalizeComparablePath(effectiveRawDir);
        const stat = fs.lstatSync(rawPath);
        const isFile = stat.isFile();
        const plan = resolvedPaths.encodePlan || {};

        // Für Converter-Jobs: nur die zum Job gehörenden Dateien löschen.
        // Der übergeordnete Ordner wird nur entfernt, wenn er danach leer ist.
        // Ausnahme: Ordner-Jobs (isFolder=true) haben einen dedizierten Ordner → komplett löschen.
        const isConverterFolder = resolvedPaths.mediaType === 'converter' && Boolean(plan.isFolder);
        const isSharedAudio = resolvedPaths.mediaType === 'converter' && Boolean(plan.isSharedAudio);
        const isConverterFileJob = resolvedPaths.mediaType === 'converter' && !isConverterFolder;

        if (isConverterFileJob) {
          // Bestimme die zu löschenden Dateien dieses Jobs
          let filesToDelete = null;
          if (isSharedAudio && Array.isArray(plan.inputPaths) && plan.inputPaths.length > 0) {
            // Shared-Audio-Job: nur die explizit gelisteten Einzeldateien entfernen
            filesToDelete = plan.inputPaths.map(normalizeComparablePath).filter(Boolean);
          } else if (isFile) {
            // Einzeldatei-Job: nur diese eine Datei entfernen
            filesToDelete = [rawPath];
          }

          if (filesToDelete) {
            let filesDeleted = 0;
            const parentDirs = new Set();
            for (const filePath of filesToDelete) {
              if (!isPathInside(rawRoot, filePath)) continue;
              if (!fs.existsSync(filePath)) continue;
              const fStat = fs.lstatSync(filePath);
              if (!fStat.isFile()) continue;
              fs.unlinkSync(filePath);
              filesDeleted++;
              const parentDir = normalizeComparablePath(path.dirname(filePath));
              if (parentDir && parentDir !== rawRoot && isPathInside(rawRoot, parentDir)) {
                parentDirs.add(parentDir);
              }
            }
            // Übergeordneten Ordner löschen, wenn er nach dem Löschen leer ist
            let dirsRemoved = 0;
            for (const parentDir of parentDirs) {
              try {
                if (!fs.existsSync(parentDir)) continue;
                const remaining = fs.readdirSync(parentDir);
                if (remaining.length === 0) {
                  fs.rmdirSync(parentDir);
                  dirsRemoved++;
                }
              } catch (_err) { /* ignore */ }
            }
            summary.raw.deleted = true;
            summary.raw.filesDeleted = filesDeleted;
            summary.raw.dirsRemoved = dirsRemoved;
          } else {
            // Fallback: rawPath ist ein Verzeichnis ohne explizite Dateiliste
            const keepRoot = rawPath === rawRoot;
            const result = deleteFilesRecursively(rawPath, keepRoot);
            summary.raw.deleted = true;
            summary.raw.filesDeleted = result.filesDeleted;
            summary.raw.dirsRemoved = result.dirsRemoved;
          }
        } else {
          // Regulärer Job oder dedizierter Converter-Ordner-Job (isFolder=true): gesamten Pfad löschen
          const keepRoot = rawPath === rawRoot;
          const result = deleteFilesRecursively(rawPath, keepRoot);
          summary.raw.deleted = true;
          summary.raw.filesDeleted = result.filesDeleted;
          summary.raw.dirsRemoved = result.dirsRemoved;
        }
      }
    }

    if (target === 'movie' || target === 'both') {
      summary.movie.attempted = true;
      if (!effectiveOutputPath) {
        summary.movie.reason = 'Kein output_path im Job gesetzt.';
      } else if (!effectiveMovieDir) {
        const error = new Error(`Kein gültiger Movie-Basispfad für Job ${jobId} (${resolvedPaths.mediaType || 'unknown'}).`);
        error.statusCode = 400;
        throw error;
      } else if (!isPathInside(effectiveMovieDir, effectiveOutputPath)) {
        const error = new Error(`Movie-Pfad liegt außerhalb des effektiven Movie-Basispfads: ${effectiveOutputPath}`);
        error.statusCode = 400;
        throw error;
      } else if (!fs.existsSync(effectiveOutputPath)) {
        const movieRoot = normalizeComparablePath(effectiveMovieDir);
        const trackedFolders = await this.getJobOutputFolders(jobId);
        const trackedExistingPaths = Array.from(new Set(
          trackedFolders
            .map((row) => normalizeComparablePath(row?.output_path))
            .filter(Boolean)
            .filter((candidatePath) => isPathInside(effectiveMovieDir, candidatePath))
            .filter((candidatePath) => candidatePath !== movieRoot)
            .filter((candidatePath) => fs.existsSync(candidatePath))
        ));

        if (trackedExistingPaths.length === 1) {
          const trackedPath = trackedExistingPaths[0];
          const stat = fs.lstatSync(trackedPath);
          if (stat.isDirectory()) {
            const keepRoot = trackedPath === movieRoot;
            const result = deleteFilesRecursively(trackedPath, keepRoot);
            summary.movie.deleted = true;
            summary.movie.filesDeleted = result.filesDeleted;
            summary.movie.dirsRemoved = result.dirsRemoved;
            movieDeletedPathForTracking = trackedPath;
            movieCandidatePathForTracking = trackedPath;
            summary.movie.reason = null;
          } else {
            const parentDir = normalizeComparablePath(path.dirname(trackedPath));
            const isConverterJob = resolvedPaths.mediaType === 'converter';
            const canDeleteParentDir = !isConverterJob
              && parentDir
              && parentDir !== movieRoot
              && isPathInside(movieRoot, parentDir)
              && fs.existsSync(parentDir)
              && fs.lstatSync(parentDir).isDirectory();

            if (canDeleteParentDir) {
              const result = deleteFilesRecursively(parentDir, false);
              summary.movie.deleted = true;
              summary.movie.filesDeleted = result.filesDeleted;
              summary.movie.dirsRemoved = result.dirsRemoved;
              movieDeletedPathForTracking = parentDir;
              movieCandidatePathForTracking = trackedPath;
            } else {
              fs.unlinkSync(trackedPath);
              summary.movie.deleted = true;
              summary.movie.filesDeleted = 1;
              summary.movie.dirsRemoved = 0;
              movieDeletedPathForTracking = trackedPath;
              movieCandidatePathForTracking = trackedPath;
            }
            summary.movie.reason = null;
          }
        } else if (trackedExistingPaths.length > 1) {
          summary.movie.reason = 'Mehrere bekannte Output-Ordner gefunden. Bitte gezielt über die Ordnerauswahl löschen.';
        } else {
          summary.movie.reason = 'Movie-Datei/Pfad existiert nicht.';
        }
      } else {
        const outputPath = normalizeComparablePath(effectiveOutputPath);
        const movieRoot = normalizeComparablePath(effectiveMovieDir);
        const stat = fs.lstatSync(outputPath);
        if (stat.isDirectory()) {
          const keepRoot = outputPath === movieRoot;
          const result = deleteFilesRecursively(outputPath, keepRoot ? true : false);
          summary.movie.deleted = true;
          summary.movie.filesDeleted = result.filesDeleted;
          summary.movie.dirsRemoved = result.dirsRemoved;
          movieDeletedPathForTracking = outputPath;
          movieCandidatePathForTracking = outputPath;
        } else {
          const parentDir = normalizeComparablePath(path.dirname(outputPath));
          // Converter jobs output a single file — never delete the parent dir
          const isConverterJob = resolvedPaths.mediaType === 'converter';
          const canDeleteParentDir = !isConverterJob
            && parentDir
            && parentDir !== movieRoot
            && isPathInside(movieRoot, parentDir)
            && fs.existsSync(parentDir)
            && fs.lstatSync(parentDir).isDirectory();

          if (canDeleteParentDir) {
            const result = deleteFilesRecursively(parentDir, false);
            summary.movie.deleted = true;
            summary.movie.filesDeleted = result.filesDeleted;
            summary.movie.dirsRemoved = result.dirsRemoved;
            movieDeletedPathForTracking = parentDir;
            movieCandidatePathForTracking = outputPath;
          } else {
            fs.unlinkSync(outputPath);
            summary.movie.deleted = true;
            summary.movie.filesDeleted = 1;
            summary.movie.dirsRemoved = 0;
            movieDeletedPathForTracking = outputPath;
            movieCandidatePathForTracking = outputPath;
          }
        }
      }
    }

    // Remove deleted movie paths from output folders tracking
    if (summary.movie?.deleted) {
      const trackingCleanupPaths = Array.from(new Set([
        movieCandidatePathForTracking,
        movieDeletedPathForTracking
      ].filter(Boolean)));
      for (const candidatePath of trackingCleanupPaths) {
        await this.removeJobOutputFolder(jobId, candidatePath);
      }
    }

    await this.appendLog(
      jobId,
      'USER_ACTION',
      `Dateien gelöscht (${target}) - raw=${JSON.stringify(summary.raw)} movie=${JSON.stringify(summary.movie)}`
    );
    logger.info('job:delete-files', { jobId, summary });

    const [updated, enrichSettings] = await Promise.all([
      this.getJobById(jobId),
      settingsService.getSettingsMap()
    ]);
    return {
      summary,
      job: enrichJobRow(updated, enrichSettings)
    };
  }

  async addJobOutputFolder(jobId, outputPath, label = null) {
    const normalizedJobId = Number(jobId);
    const normalizedPath = String(outputPath || '').trim();
    if (!normalizedJobId || !normalizedPath) return null;
    const db = await getDb();
    await db.run(
      'INSERT OR IGNORE INTO job_output_folders (job_id, output_path, label) VALUES (?, ?, ?)',
      [normalizedJobId, normalizedPath, label || null]
    );
  }

  async getJobOutputFolders(jobId) {
    const normalizedJobId = Number(jobId);
    if (!normalizedJobId) return [];
    const db = await getDb();
    const rows = await db.all(
      'SELECT id, job_id, output_path, label, created_at FROM job_output_folders WHERE job_id = ? ORDER BY id ASC',
      [normalizedJobId]
    );
    return rows || [];
  }

  async listJobOutputFoldersByJobIds(jobIds = []) {
    const normalizedJobIds = Array.isArray(jobIds)
      ? jobIds
        .map((value) => normalizeJobIdValue(value))
        .filter(Boolean)
      : [];
    if (normalizedJobIds.length === 0) {
      return new Map();
    }

    const db = await getDb();
    const placeholders = normalizedJobIds.map(() => '?').join(', ');
    const rows = await db.all(
      `
        SELECT id, job_id, output_path, label, created_at
        FROM job_output_folders
        WHERE job_id IN (${placeholders})
        ORDER BY id ASC
      `,
      normalizedJobIds
    );

    const byJobId = new Map();
    for (const row of (Array.isArray(rows) ? rows : [])) {
      const ownerJobId = normalizeJobIdValue(row?.job_id);
      if (!ownerJobId) {
        continue;
      }
      if (!byJobId.has(ownerJobId)) {
        byJobId.set(ownerJobId, []);
      }
      byJobId.get(ownerJobId).push({
        id: normalizeJobIdValue(row?.id),
        job_id: ownerJobId,
        output_path: String(row?.output_path || '').trim() || null,
        label: String(row?.label || '').trim() || null,
        created_at: String(row?.created_at || '').trim() || null
      });
    }

    return byJobId;
  }

  async getJobOutputFoldersForLineage(jobId, options = {}) {
    const normalizedJobId = normalizeJobIdValue(jobId);
    if (!normalizedJobId) return [];
    const includeMissing = Boolean(options?.includeMissing);

    const relatedJobs = await this._resolveRelatedJobsForDeletion(normalizedJobId, {
      includeRelated: true,
      includeLogLinks: false
    });
    const relatedJobIds = Array.from(new Set(
      relatedJobs
        .map((row) => normalizeJobIdValue(row?.id))
        .filter(Boolean)
    ));
    if (relatedJobIds.length === 0) {
      return [];
    }

    const db = await getDb();
    const placeholders = relatedJobIds.map(() => '?').join(', ');
    const trackedRows = await db.all(
      `
        SELECT id, job_id, output_path, label, created_at
        FROM job_output_folders
        WHERE job_id IN (${placeholders})
        ORDER BY id ASC
      `,
      relatedJobIds
    );
    const artifactsByJobId = await this.listJobLineageArtifactsByJobIds(relatedJobIds);

    const merged = [];
    const seen = new Set();
    const addFolder = (rawFolder, defaults = {}) => {
      const outputPath = String(rawFolder?.output_path || rawFolder?.outputPath || '').trim();
      if (!outputPath) {
        return;
      }
      const normalized = normalizeComparablePath(outputPath);
      if (!normalized || seen.has(normalized)) {
        return;
      }
      let exists = false;
      try {
        exists = fs.existsSync(normalized);
      } catch (_error) {
        exists = false;
      }
      if (!includeMissing && !exists) {
        return;
      }
      seen.add(normalized);
      merged.push({
        id: normalizeJobIdValue(rawFolder?.id),
        job_id: normalizeJobIdValue(rawFolder?.job_id ?? rawFolder?.jobId ?? defaults.job_id ?? normalizedJobId),
        output_path: outputPath,
        label: String(rawFolder?.label || defaults.label || '').trim() || null,
        created_at: String(rawFolder?.created_at || rawFolder?.createdAt || '').trim() || null,
        exists
      });
    };

    for (const row of (Array.isArray(trackedRows) ? trackedRows : [])) {
      addFolder(row);
    }
    for (const relatedJob of relatedJobs) {
      addFolder({
        output_path: relatedJob?.output_path || null,
        job_id: relatedJob?.id || null
      });
      const artifacts = artifactsByJobId.get(normalizeJobIdValue(relatedJob?.id)) || [];
      for (const artifact of artifacts) {
        addFolder({
          output_path: artifact?.outputPath || null,
          job_id: relatedJob?.id || null,
          label: 'Lineage-Output',
          created_at: artifact?.createdAt || null
        });
      }
    }

    // Fallback for legacy runs before output-folder tracking:
    // detect numbered sibling directories on filesystem
    //   /path/Album (1995), /path/Album (1995)_2, /path/Album (1995)_3, ...
    const seedPaths = merged
      .filter((row) => Boolean(row?.exists))
      .map((row) => String(row?.output_path || '').trim())
      .filter(Boolean);
    for (const seedPath of seedPaths) {
      const siblings = inferSiblingOutputFolders(seedPath);
      for (const siblingPath of siblings) {
        addFolder({
          output_path: siblingPath,
          job_id: normalizedJobId,
          label: 'Auto-erkannt'
        });
      }
    }

    merged.sort((left, right) => compareOutputFolderPaths(left?.output_path, right?.output_path));
    return merged;
  }

  async removeJobOutputFolder(jobId, outputPath) {
    const normalizedJobId = Number(jobId);
    const normalizedPath = normalizeComparablePath(outputPath);
    if (!normalizedJobId || !normalizedPath) return;
    const db = await getDb();
    const rows = await db.all(
      'SELECT id, output_path FROM job_output_folders WHERE job_id = ?',
      [normalizedJobId]
    );
    for (const row of (Array.isArray(rows) ? rows : [])) {
      const candidatePath = normalizeComparablePath(row?.output_path);
      if (!candidatePath || candidatePath !== normalizedPath) {
        continue;
      }
      await db.run('DELETE FROM job_output_folders WHERE id = ?', [Number(row.id)]);
    }
  }

  async removeJobOutputFolderFromJobs(jobIds = [], outputPath) {
    const normalizedPath = normalizeComparablePath(outputPath);
    const normalizedJobIds = Array.isArray(jobIds)
      ? jobIds
        .map((value) => normalizeJobIdValue(value))
        .filter(Boolean)
      : [];
    if (!normalizedPath || normalizedJobIds.length === 0) return;
    const db = await getDb();
    const placeholders = normalizedJobIds.map(() => '?').join(', ');
    const rows = await db.all(
      `SELECT id, output_path FROM job_output_folders WHERE job_id IN (${placeholders})`,
      normalizedJobIds
    );
    for (const row of (Array.isArray(rows) ? rows : [])) {
      const candidatePath = normalizeComparablePath(row?.output_path);
      if (!candidatePath || candidatePath !== normalizedPath) {
        continue;
      }
      await db.run('DELETE FROM job_output_folders WHERE id = ?', [Number(row.id)]);
    }
  }

  async deleteSpecificOutputFolders(jobId, folderPaths = []) {
    const paths = Array.isArray(folderPaths) ? folderPaths.filter((p) => typeof p === 'string' && p.trim()) : [];
    if (paths.length === 0) return { deleted: [], failed: [] };

    const job = await this.getJobById(jobId);
    if (!job) {
      const error = new Error('Job nicht gefunden.');
      error.statusCode = 404;
      throw error;
    }

    const settings = await settingsService.getSettingsMap();
    const resolvedPaths = resolveEffectiveStoragePathsForJob(settings, job);
    const effectiveMovieDir = resolvedPaths.movieDir;
    if (!effectiveMovieDir) {
      const error = new Error('Kein gültiger Movie-Basispfad konfiguriert.');
      error.statusCode = 400;
      throw error;
    }

    const deleted = [];
    const failed = [];
    const relatedJobs = await this._resolveRelatedJobsForDeletion(jobId, {
      includeRelated: true,
      includeLogLinks: false
    });
    const relatedJobIds = Array.from(new Set(
      relatedJobs
        .map((row) => normalizeJobIdValue(row?.id))
        .filter(Boolean)
    ));
    const trackedOutputFolders = await this.getJobOutputFoldersForLineage(jobId, { includeMissing: true });
    const trackedOutputPathSet = new Set(
      trackedOutputFolders
        .map((folder) => normalizeComparablePath(folder?.output_path))
        .filter(Boolean)
    );

    for (const folderPath of paths) {
      const normalizedFolderPath = normalizeComparablePath(folderPath);
      const isInsideEffectiveRoot = isPathInside(effectiveMovieDir, normalizedFolderPath);
      const isTrackedPath = trackedOutputPathSet.has(normalizedFolderPath);
      if (!isInsideEffectiveRoot && !isTrackedPath) {
        failed.push({ path: folderPath, reason: 'Pfad liegt außerhalb des Output-Verzeichnisses.' });
        continue;
      }
      if (!fs.existsSync(normalizedFolderPath)) {
        await this.removeJobOutputFolderFromJobs(relatedJobIds, normalizedFolderPath);
        deleted.push(normalizedFolderPath);
        continue;
      }
      try {
        const stat = fs.lstatSync(normalizedFolderPath);
        const movieRoot = normalizeComparablePath(effectiveMovieDir);
        if (stat.isDirectory()) {
          const keepRoot = normalizedFolderPath === movieRoot;
          deleteFilesRecursively(normalizedFolderPath, keepRoot);
        } else {
          const parentDir = normalizeComparablePath(path.dirname(normalizedFolderPath));
          const canDeleteParent = parentDir && parentDir !== movieRoot
            && isPathInside(movieRoot, parentDir)
            && fs.existsSync(parentDir)
            && fs.lstatSync(parentDir).isDirectory();
          if (canDeleteParent) {
            deleteFilesRecursively(parentDir, false);
          } else {
            fs.unlinkSync(normalizedFolderPath);
          }
        }
        await this.removeJobOutputFolderFromJobs(relatedJobIds, normalizedFolderPath);
        deleted.push(normalizedFolderPath);
      } catch (error) {
        failed.push({ path: folderPath, reason: error.message });
      }
    }

    await this.appendLog(
      jobId,
      'USER_ACTION',
      `Output-Ordner gelöscht: ${deleted.length > 0 ? deleted.join(', ') : 'keine'}${failed.length > 0 ? ` | Fehler: ${failed.map((f) => f.path).join(', ')}` : ''}`
    );
    return { deleted, failed };
  }

  async deleteJob(jobId, fileTarget = 'none', options = {}) {
    const allowedTargets = new Set(['none', 'raw', 'movie', 'both']);
    if (!allowedTargets.has(fileTarget)) {
      const error = new Error(`Ungültiges target '${fileTarget}'. Erlaubt: none, raw, movie, both.`);
      error.statusCode = 400;
      throw error;
    }

    const includeRelated = Boolean(options?.includeRelated);
    if (!includeRelated) {
      const existing = await this.getJobById(jobId);
      if (!existing) {
        const error = new Error('Job nicht gefunden.');
        error.statusCode = 404;
        throw error;
      }

      let fileSummary = null;
      if (fileTarget !== 'none') {
        const preview = await this.getJobDeletePreview(jobId, { includeRelated: false });
        fileSummary = this._deletePathsFromPreview(preview, fileTarget, {
          selectedMoviePaths: options?.selectedMoviePaths
        });
      }

      const db = await getDb();
      const pipelineRow = await db.get(
        'SELECT state, active_job_id FROM pipeline_state WHERE id = 1'
      );

      const isActivePipelineJob = Number(pipelineRow?.active_job_id || 0) === Number(jobId);
      const runningStates = new Set(['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING']);

      if (isActivePipelineJob && runningStates.has(String(pipelineRow?.state || ''))) {
        const error = new Error('Aktiver Pipeline-Job kann nicht gelöscht werden. Bitte zuerst abbrechen.');
        error.statusCode = 409;
        throw error;
      }

      await db.exec('BEGIN');
      try {
        if (isActivePipelineJob) {
          await db.run(
            `
              UPDATE pipeline_state
              SET
                state = 'IDLE',
                active_job_id = NULL,
                progress = 0,
                eta = NULL,
                status_text = 'Bereit',
                context_json = '{}',
                updated_at = CURRENT_TIMESTAMP
              WHERE id = 1
            `
          );
        } else {
          await db.run(
            `
              UPDATE pipeline_state
              SET
                active_job_id = NULL,
                updated_at = CURRENT_TIMESTAMP
              WHERE id = 1 AND active_job_id = ?
            `,
            [jobId]
          );
        }

        await db.run('DELETE FROM jobs WHERE id = ?', [jobId]);
        await db.exec('COMMIT');
      } catch (error) {
        await db.exec('ROLLBACK');
        throw error;
      }

      await this.closeProcessLog(jobId);
      this._deleteProcessLogFile(jobId);
      thumbnailService.deleteThumbnail(jobId);

      logger.warn('job:deleted', {
        jobId,
        fileTarget,
        includeRelated: false,
        pipelineStateReset: isActivePipelineJob,
        filesDeleted: fileSummary
          ? {
            raw: fileSummary.raw?.filesDeleted ?? 0,
            movie: fileSummary.movie?.filesDeleted ?? 0
          }
          : { raw: 0, movie: 0 }
      });

      return {
        deleted: true,
        jobId,
        fileTarget,
        includeRelated: false,
        deletedJobIds: [Number(jobId)],
        fileSummary
      };
    }

    const normalizedJobId = normalizeJobIdValue(jobId);
    const preview = await this.getJobDeletePreview(normalizedJobId, { includeRelated: true });
    const deleteJobIds = Array.isArray(preview?.relatedJobs)
      ? preview.relatedJobs
        .map((row) => normalizeJobIdValue(row?.id))
        .filter(Boolean)
      : [];
    if (deleteJobIds.length === 0) {
      const error = new Error('Keine löschbaren Historien-Einträge gefunden.');
      error.statusCode = 404;
      throw error;
    }

    const db = await getDb();
    const pipelineRow = await db.get('SELECT state, active_job_id FROM pipeline_state WHERE id = 1');
    const activePipelineJobId = normalizeJobIdValue(pipelineRow?.active_job_id);
    const activeJobIncluded = Boolean(activePipelineJobId && deleteJobIds.includes(activePipelineJobId));
    const runningStates = new Set(['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING']);
    if (activeJobIncluded && runningStates.has(String(pipelineRow?.state || ''))) {
      const error = new Error('Aktiver Pipeline-Job kann nicht gelöscht werden. Bitte zuerst abbrechen.');
      error.statusCode = 409;
      throw error;
    }

    let fileSummary = null;
    if (fileTarget !== 'none') {
      fileSummary = this._deletePathsFromPreview(preview, fileTarget, {
        selectedMoviePaths: options?.selectedMoviePaths
      });
    }

    await db.exec('BEGIN');
    try {
      if (activeJobIncluded) {
        await db.run(
          `
            UPDATE pipeline_state
            SET
              state = 'IDLE',
              active_job_id = NULL,
              progress = 0,
              eta = NULL,
              status_text = 'Bereit',
              context_json = '{}',
              updated_at = CURRENT_TIMESTAMP
            WHERE id = 1
          `
        );
      } else {
        const placeholders = deleteJobIds.map(() => '?').join(', ');
        await db.run(
          `
            UPDATE pipeline_state
            SET
              active_job_id = NULL,
              updated_at = CURRENT_TIMESTAMP
            WHERE id = 1 AND active_job_id IN (${placeholders})
          `,
          deleteJobIds
        );
      }

      const deletePlaceholders = deleteJobIds.map(() => '?').join(', ');
      await db.run(`DELETE FROM jobs WHERE id IN (${deletePlaceholders})`, deleteJobIds);
      await db.exec('COMMIT');
    } catch (error) {
      await db.exec('ROLLBACK');
      throw error;
    }

    for (const deletedJobId of deleteJobIds) {
      await this.closeProcessLog(deletedJobId);
      this._deleteProcessLogFile(deletedJobId);
      thumbnailService.deleteThumbnail(deletedJobId);
    }

    logger.warn('job:deleted', {
      jobId: normalizedJobId,
      fileTarget,
      includeRelated: true,
      deletedJobIds: deleteJobIds,
      deletedJobCount: deleteJobIds.length,
      pipelineStateReset: activeJobIncluded,
      filesDeleted: fileSummary
        ? {
          raw: fileSummary.raw?.filesDeleted ?? 0,
          movie: fileSummary.movie?.filesDeleted ?? 0
        }
        : { raw: 0, movie: 0 }
    });

    return {
      deleted: true,
      jobId: normalizedJobId,
      fileTarget,
      includeRelated: true,
      deletedJobIds: deleteJobIds,
      deletedJobs: preview.relatedJobs,
      fileSummary
    };
  }
}

module.exports = new HistoryService();
