const path = require('path');
const { sanitizeFileName } = require('../utils/files');

const SUPPORTED_INPUT_EXTENSIONS = new Set(['.aax']);
const SUPPORTED_OUTPUT_FORMATS = new Set(['m4b', 'mp3', 'flac']);
const DEFAULT_AUDIOBOOK_RAW_TEMPLATE = '{author} - {title} ({year})';
const DEFAULT_AUDIOBOOK_OUTPUT_TEMPLATE = '{author}/{author} - {title} ({year})';
const DEFAULT_AUDIOBOOK_CHAPTER_OUTPUT_TEMPLATE = '{author}/{author} - {title} ({year})/{chapterNr} {chapterTitle}';
const AUDIOBOOK_FORMAT_DEFAULTS = {
  m4b: {},
  flac: {
    flacCompression: 5
  },
  mp3: {
    mp3Mode: 'cbr',
    mp3Bitrate: 192,
    mp3Quality: 4
  }
};

function normalizeText(value) {
  return String(value || '')
    .normalize('NFC')
    .replace(/[♥❤♡❥❣❦❧]/gu, ' ')
    .replace(/\p{C}+/gu, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function parseOptionalYear(value) {
  const text = normalizeText(value);
  if (!text) {
    return null;
  }
  const match = text.match(/\b(19|20)\d{2}\b/);
  if (!match) {
    return null;
  }
  return Number(match[0]);
}

function parseOptionalNumber(value) {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : null;
}

function parseTimebaseToSeconds(value) {
  const raw = String(value || '').trim();
  if (!raw) {
    return null;
  }
  if (/^\d+\/\d+$/u.test(raw)) {
    const [num, den] = raw.split('/').map(Number);
    if (Number.isFinite(num) && Number.isFinite(den) && den !== 0) {
      return num / den;
    }
  }
  const parsed = Number(raw);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}

function secondsToMs(value) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed < 0) {
    return null;
  }
  return Math.max(0, Math.round(parsed * 1000));
}

function ticksToMs(value, timebase) {
  const ticks = Number(value);
  const factor = parseTimebaseToSeconds(timebase);
  if (!Number.isFinite(ticks) || ticks < 0 || !Number.isFinite(factor) || factor <= 0) {
    return null;
  }
  return Math.max(0, Math.round(ticks * factor * 1000));
}

function normalizeOutputFormat(value) {
  const format = String(value || '').trim().toLowerCase();
  return SUPPORTED_OUTPUT_FORMATS.has(format) ? format : 'mp3';
}

function clonePlainObject(value) {
  return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {};
}

function clampInteger(value, min, max, fallback) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) {
    return fallback;
  }
  return Math.max(min, Math.min(max, Math.trunc(parsed)));
}

function getDefaultFormatOptions(format) {
  const normalizedFormat = normalizeOutputFormat(format);
  return clonePlainObject(AUDIOBOOK_FORMAT_DEFAULTS[normalizedFormat]);
}

function normalizeFormatOptions(format, formatOptions = {}) {
  const normalizedFormat = normalizeOutputFormat(format);
  const source = clonePlainObject(formatOptions);
  const defaults = getDefaultFormatOptions(normalizedFormat);

  if (normalizedFormat === 'flac') {
    return {
      flacCompression: clampInteger(source.flacCompression, 0, 8, defaults.flacCompression)
    };
  }

  if (normalizedFormat === 'mp3') {
    const mp3Mode = String(source.mp3Mode || defaults.mp3Mode || 'cbr').trim().toLowerCase() === 'vbr'
      ? 'vbr'
      : 'cbr';
    const allowedBitrates = new Set([128, 160, 192, 256, 320]);
    const normalizedBitrate = clampInteger(source.mp3Bitrate, 96, 320, defaults.mp3Bitrate);
    return {
      mp3Mode,
      mp3Bitrate: allowedBitrates.has(normalizedBitrate) ? normalizedBitrate : defaults.mp3Bitrate,
      mp3Quality: clampInteger(source.mp3Quality, 0, 9, defaults.mp3Quality)
    };
  }

  return {};
}

function normalizeInputExtension(filePath) {
  return path.extname(String(filePath || '')).trim().toLowerCase();
}

function isSupportedInputFile(filePath) {
  return SUPPORTED_INPUT_EXTENSIONS.has(normalizeInputExtension(filePath));
}

function normalizeTagMap(tags = null) {
  const source = tags && typeof tags === 'object' ? tags : {};
  const result = {};
  for (const [key, value] of Object.entries(source)) {
    const normalizedKey = String(key || '').trim().toLowerCase();
    if (!normalizedKey) {
      continue;
    }
    const normalizedValue = normalizeText(value);
    if (!normalizedValue) {
      continue;
    }
    result[normalizedKey] = normalizedValue;
  }
  return result;
}

function pickTag(tags, keys = []) {
  const normalized = normalizeTagMap(tags);
  for (const key of keys) {
    const value = normalized[String(key || '').trim().toLowerCase()];
    if (value) {
      return value;
    }
  }
  return null;
}

function sanitizeTemplateValue(value, fallback = '') {
  const normalized = normalizeText(value);
  if (!normalized) {
    return fallback;
  }
  return sanitizeFileName(normalized);
}

function normalizeChapterTitle(value, index) {
  const normalized = normalizeText(value);
  return normalized || `Kapitel ${index}`;
}

function buildChapterList(probe = null) {
  const chapters = Array.isArray(probe?.chapters) ? probe.chapters : [];
  return chapters.map((chapter, index) => {
    const chapterIndex = index + 1;
    const tags = normalizeTagMap(chapter?.tags);
    const startSeconds = parseOptionalNumber(chapter?.start_time);
    const endSeconds = parseOptionalNumber(chapter?.end_time);
    const startMs = secondsToMs(startSeconds) ?? ticksToMs(chapter?.start, chapter?.time_base) ?? 0;
    const endMs = secondsToMs(endSeconds) ?? ticksToMs(chapter?.end, chapter?.time_base) ?? 0;
    const title = normalizeChapterTitle(tags.title || tags.chapter, chapterIndex);
    return {
      index: chapterIndex,
      title,
      startSeconds: Number((startMs / 1000).toFixed(3)),
      endSeconds: Number((endMs / 1000).toFixed(3)),
      startMs,
      endMs,
      timeBase: String(chapter?.time_base || '').trim() || null
    };
  });
}

function normalizeChapterList(chapters = [], options = {}) {
  const source = Array.isArray(chapters) ? chapters : [];
  const durationMs = Number(options?.durationMs || 0);
  const fallbackTitle = normalizeText(options?.fallbackTitle || '');
  const createFallback = options?.createFallback === true;

  const normalized = source.map((chapter, index) => {
    const chapterIndex = Number(chapter?.index);
    const safeIndex = Number.isFinite(chapterIndex) && chapterIndex > 0
      ? Math.trunc(chapterIndex)
      : index + 1;
    const rawStartMs = parseOptionalNumber(chapter?.startMs)
      ?? secondsToMs(chapter?.startSeconds)
      ?? ticksToMs(chapter?.start, chapter?.timeBase || chapter?.time_base)
      ?? 0;
    const rawEndMs = parseOptionalNumber(chapter?.endMs)
      ?? secondsToMs(chapter?.endSeconds)
      ?? ticksToMs(chapter?.end, chapter?.timeBase || chapter?.time_base)
      ?? 0;
    return {
      index: safeIndex,
      title: normalizeChapterTitle(chapter?.title, safeIndex),
      startMs: Math.max(0, rawStartMs),
      endMs: Math.max(0, rawEndMs)
    };
  });

  const repaired = normalized.map((chapter, index) => {
    const nextStartMs = normalized[index + 1]?.startMs ?? null;
    let endMs = chapter.endMs;
    if (!(endMs > chapter.startMs)) {
      if (Number.isFinite(nextStartMs) && nextStartMs > chapter.startMs) {
        endMs = nextStartMs;
      } else if (durationMs > chapter.startMs) {
        endMs = durationMs;
      } else {
        endMs = chapter.startMs;
      }
    }
    return {
      ...chapter,
      endMs,
      startSeconds: Number((chapter.startMs / 1000).toFixed(3)),
      endSeconds: Number((endMs / 1000).toFixed(3)),
      durationMs: Math.max(0, endMs - chapter.startMs)
    };
  }).filter((chapter) => chapter.endMs > chapter.startMs || normalized.length === 1);

  if (repaired.length > 0) {
    return repaired;
  }

  if (createFallback && durationMs > 0) {
    return [{
      index: 1,
      title: fallbackTitle || 'Kapitel 1',
      startMs: 0,
      endMs: durationMs,
      startSeconds: 0,
      endSeconds: Number((durationMs / 1000).toFixed(3)),
      durationMs
    }];
  }

  return [];
}

function looksLikeDescription(value) {
  const normalized = normalizeText(value);
  if (!normalized) {
    return false;
  }
  return normalized.length >= 120 || /[.!?]\s/u.test(normalized);
}

function detectCoverStream(probe = null) {
  const streams = Array.isArray(probe?.streams) ? probe.streams : [];
  for (const stream of streams) {
    const codecType = String(stream?.codec_type || '').trim().toLowerCase();
    const codecName = String(stream?.codec_name || '').trim().toLowerCase();
    const dispositionAttachedPic = Number(stream?.disposition?.attached_pic || 0) === 1;
    const mimetype = String(stream?.tags?.mimetype || '').trim().toLowerCase();
    const looksLikeImageStream = codecType === 'video'
      && (dispositionAttachedPic || mimetype.startsWith('image/') || ['jpeg', 'jpg', 'png', 'mjpeg'].includes(codecName));

    if (!looksLikeImageStream) {
      continue;
    }

    const streamIndex = Number(stream?.index);
    return {
      streamIndex: Number.isFinite(streamIndex) ? Math.trunc(streamIndex) : 0,
      codecName: codecName || null,
      mimetype: mimetype || null,
      attachedPic: dispositionAttachedPic
    };
  }
  return null;
}

function parseProbeOutput(rawOutput) {
  if (!rawOutput) {
    return null;
  }
  try {
    return JSON.parse(rawOutput);
  } catch (_error) {
    return null;
  }
}

function buildMetadataFromProbe(probe = null, originalName = null) {
  const format = probe?.format && typeof probe.format === 'object' ? probe.format : {};
  const tags = normalizeTagMap(format.tags);
  const originalBaseName = path.basename(String(originalName || ''), path.extname(String(originalName || '')));
  const fallbackTitle = normalizeText(originalBaseName) || 'Audiobook';
  const title = pickTag(tags, ['title', 'album']) || fallbackTitle;
  const author = pickTag(tags, ['author', 'artist', 'writer', 'album_artist', 'composer']) || 'Unknown Author';
  const description = pickTag(tags, [
    'description',
    'synopsis',
    'summary',
    'long_description',
    'longdescription',
    'publisher_summary',
    'publishersummary',
    'comment'
  ]) || null;
  let narrator = pickTag(tags, ['narrator', 'performer', 'album_artist']) || null;
  if (narrator && (narrator === author || narrator === description || looksLikeDescription(narrator))) {
    narrator = null;
  }
  const series = pickTag(tags, ['series', 'grouping', 'series_title', 'show']) || null;
  const part = pickTag(tags, ['part', 'part_number', 'disc', 'discnumber', 'volume']) || null;
  const year = parseOptionalYear(pickTag(tags, ['date', 'year', 'creation_time']));
  const durationSeconds = Number(format.duration || 0);
  const durationMs = Number.isFinite(durationSeconds) && durationSeconds > 0
    ? Math.round(durationSeconds * 1000)
    : 0;
  const chapters = normalizeChapterList(buildChapterList(probe), {
    durationMs,
    fallbackTitle: title,
    createFallback: false
  });
  const cover = detectCoverStream(probe);
  return {
    title,
    author,
    narrator,
    description,
    series,
    part,
    year,
    album: title,
    artist: author,
    durationMs,
    chapters,
    cover,
    hasEmbeddedCover: Boolean(cover),
    tags
  };
}

function normalizeTemplateTokenKey(rawKey) {
  const key = String(rawKey || '').trim().toLowerCase();
  if (!key) {
    return '';
  }
  if (key === 'artist') {
    return 'author';
  }
  if (key === 'chapternr' || key === 'chapternumberpadded' || key === 'chapternopadded') {
    return 'chapterNr';
  }
  if (key === 'chapterno' || key === 'chapternumber' || key === 'chapternum') {
    return 'chapterNo';
  }
  if (key === 'chaptertitle') {
    return 'chapterTitle';
  }
  return key;
}

function cleanupRenderedTemplate(value) {
  return String(value || '')
    .replace(/\(\s*\)/g, '')
    .replace(/\[\s*]/g, '')
    .replace(/\s{2,}/g, ' ')
    .trim();
}

function renderTemplate(template, values) {
  const source = String(template || DEFAULT_AUDIOBOOK_OUTPUT_TEMPLATE).trim()
    || DEFAULT_AUDIOBOOK_OUTPUT_TEMPLATE;
  const rendered = source.replace(/\$\{([^}]+)\}|\{([^{}]+)\}/g, (_, keyA, keyB) => {
    const normalizedKey = normalizeTemplateTokenKey(keyA || keyB);
    const rawValue = values?.[normalizedKey];
    if (rawValue === undefined || rawValue === null || rawValue === '') {
      return '';
    }
    return String(rawValue);
  });
  return cleanupRenderedTemplate(rendered);
}

function buildTemplateValues(metadata = {}, format = null, chapter = null) {
  const chapterIndex = Number(chapter?.index || chapter?.chapterNo || 0);
  const safeChapterIndex = Number.isFinite(chapterIndex) && chapterIndex > 0 ? Math.trunc(chapterIndex) : 1;
  const author = sanitizeTemplateValue(metadata.author || metadata.artist || 'Unknown Author', 'Unknown Author');
  const title = sanitizeTemplateValue(metadata.title || metadata.album || 'Unknown Audiobook', 'Unknown Audiobook');
  const narrator = sanitizeTemplateValue(metadata.narrator || '');
  const series = sanitizeTemplateValue(metadata.series || '');
  const part = sanitizeTemplateValue(metadata.part || '');
  const chapterTitle = sanitizeTemplateValue(chapter?.title || `Kapitel ${safeChapterIndex}`, `Kapitel ${safeChapterIndex}`);
  const year = metadata.year ? String(metadata.year) : '';
  return {
    author,
    title,
    narrator,
    series,
    part,
    year,
    format: format ? String(format).trim().toLowerCase() : '',
    chapterNr: String(safeChapterIndex).padStart(2, '0'),
    chapterNo: String(safeChapterIndex),
    chapterTitle
  };
}

function splitRenderedPath(value) {
  return String(value || '')
    .replace(/\\/g, '/')
    .replace(/\/+/g, '/')
    .replace(/^\/+|\/+$/g, '')
    .split('/')
    .map((segment) => sanitizeFileName(segment))
    .filter(Boolean);
}

function resolveTemplatePathParts(template, values, fallbackBaseName) {
  const rendered = renderTemplate(template, values);
  const parts = splitRenderedPath(rendered);
  if (parts.length === 0) {
    return {
      folderParts: [],
      baseName: sanitizeFileName(fallbackBaseName || 'untitled')
    };
  }
  return {
    folderParts: parts.slice(0, -1),
    baseName: parts[parts.length - 1]
  };
}

function buildRawStoragePaths(metadata, jobId, rawBaseDir, rawTemplate = DEFAULT_AUDIOBOOK_RAW_TEMPLATE, inputFileName = 'input.aax') {
  const ext = normalizeInputExtension(inputFileName) || '.aax';
  const values = buildTemplateValues(metadata);
  const fallbackBaseName = path.basename(String(inputFileName || 'input.aax'), ext);
  const { folderParts, baseName } = resolveTemplatePathParts(rawTemplate, values, fallbackBaseName);
  const rawDirName = `${baseName} - RAW - job-${jobId}`;
  const rawDir = path.join(String(rawBaseDir || ''), ...folderParts, rawDirName);
  const rawFilePath = path.join(rawDir, `${baseName}${ext}`);
  return {
    rawDir,
    rawFilePath,
    rawFileName: `${baseName}${ext}`,
    rawDirName
  };
}

function buildOutputPath(metadata, movieBaseDir, outputTemplate = DEFAULT_AUDIOBOOK_OUTPUT_TEMPLATE, outputFormat = 'mp3') {
  const normalizedFormat = normalizeOutputFormat(outputFormat);
  const values = buildTemplateValues(metadata, normalizedFormat);
  const fallbackBaseName = values.title || 'audiobook';
  const { folderParts, baseName } = resolveTemplatePathParts(outputTemplate, values, fallbackBaseName);
  return path.join(String(movieBaseDir || ''), ...folderParts, `${baseName}.${normalizedFormat}`);
}

function findCommonDirectory(paths = []) {
  const segmentsList = (Array.isArray(paths) ? paths : [])
    .map((entry) => String(entry || '').trim())
    .filter(Boolean)
    .map((entry) => path.resolve(entry).split(path.sep).filter((segment, index, list) => !(index === 0 && list[0] === '')));

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

  const common = [...segmentsList[0]];
  for (let index = 1; index < segmentsList.length; index += 1) {
    const next = segmentsList[index];
    let matchLength = 0;
    while (matchLength < common.length && matchLength < next.length && common[matchLength] === next[matchLength]) {
      matchLength += 1;
    }
    common.length = matchLength;
    if (common.length === 0) {
      break;
    }
  }

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

  return path.join(path.sep, ...common);
}

function buildChapterOutputPlan(
  metadata,
  chapters,
  movieBaseDir,
  chapterTemplate = DEFAULT_AUDIOBOOK_CHAPTER_OUTPUT_TEMPLATE,
  outputFormat = 'mp3'
) {
  const normalizedFormat = normalizeOutputFormat(outputFormat);
  const normalizedChapters = normalizeChapterList(chapters, {
    durationMs: metadata?.durationMs,
    fallbackTitle: metadata?.title || metadata?.album || 'Audiobook',
    createFallback: true
  });
  const outputFiles = normalizedChapters.map((chapter, index) => {
    const values = buildTemplateValues(metadata, normalizedFormat, chapter);
    const fallbackBaseName = `${values.chapterNr} ${values.chapterTitle}`.trim() || `Kapitel ${index + 1}`;
    const { folderParts, baseName } = resolveTemplatePathParts(chapterTemplate, values, fallbackBaseName);
    const outputPath = path.join(String(movieBaseDir || ''), ...folderParts, `${baseName}.${normalizedFormat}`);
    return {
      chapter,
      outputPath
    };
  });
  const outputDir = findCommonDirectory(outputFiles.map((entry) => path.dirname(entry.outputPath)))
    || String(movieBaseDir || '').trim()
    || '.';

  return {
    outputDir,
    outputFiles,
    chapters: normalizedChapters,
    format: normalizedFormat
  };
}

function buildProbeCommand(ffprobeCommand, inputPath) {
  const cmd = String(ffprobeCommand || 'ffprobe').trim() || 'ffprobe';
  return {
    cmd,
    args: [
      '-v', 'quiet',
      '-print_format', 'json',
      '-show_format',
      '-show_streams',
      '-show_chapters',
      inputPath
    ]
  };
}

function pushMetadataArg(args, key, value) {
  const normalizedKey = String(key || '').trim();
  const normalizedValue = normalizeText(value);
  if (!normalizedKey || !normalizedValue) {
    return;
  }
  args.push('-metadata', `${normalizedKey}=${normalizedValue}`);
}

function buildMetadataArgs(metadata = {}, options = {}) {
  const source = metadata && typeof metadata === 'object' ? metadata : {};
  const titleOverride = normalizeText(options?.title || '');
  const albumOverride = normalizeText(options?.album || '');
  const trackNo = Number(options?.trackNo || 0);
  const trackTotal = Number(options?.trackTotal || 0);
  const args = [];
  const bookTitle = normalizeText(source.title || source.album || '');
  const author = normalizeText(source.author || source.artist || '');

  pushMetadataArg(args, 'title', titleOverride || bookTitle);
  pushMetadataArg(args, 'album', albumOverride || bookTitle);
  pushMetadataArg(args, 'artist', author);
  pushMetadataArg(args, 'album_artist', author);
  pushMetadataArg(args, 'author', author);
  pushMetadataArg(args, 'narrator', source.narrator);
  pushMetadataArg(args, 'performer', source.narrator);
  pushMetadataArg(args, 'grouping', source.series);
  pushMetadataArg(args, 'series', source.series);
  pushMetadataArg(args, 'disc', source.part);
  pushMetadataArg(args, 'description', source.description);
  pushMetadataArg(args, 'comment', source.description);
  if (source.year) {
    pushMetadataArg(args, 'date', String(source.year));
    pushMetadataArg(args, 'year', String(source.year));
  }
  if (Number.isFinite(trackNo) && trackNo > 0) {
    const formattedTrack = Number.isFinite(trackTotal) && trackTotal > 0
      ? `${Math.trunc(trackNo)}/${Math.trunc(trackTotal)}`
      : String(Math.trunc(trackNo));
    pushMetadataArg(args, 'track', formattedTrack);
  }
  return args;
}

function buildCodecArgs(format, normalizedOptions) {
  if (format === 'm4b') {
    return ['-c:a', 'copy'];
  }
  if (format === 'flac') {
    return ['-codec:a', 'flac', '-compression_level', String(normalizedOptions.flacCompression)];
  }
  if (normalizedOptions.mp3Mode === 'vbr') {
    return ['-codec:a', 'libmp3lame', '-q:a', String(normalizedOptions.mp3Quality)];
  }
  return ['-codec:a', 'libmp3lame', '-b:a', `${normalizedOptions.mp3Bitrate}k`];
}

function buildEncodeCommand(ffmpegCommand, inputPath, outputPath, outputFormat = 'mp3', formatOptions = {}, options = {}) {
  const cmd = String(ffmpegCommand || 'ffmpeg').trim() || 'ffmpeg';
  const format = normalizeOutputFormat(outputFormat);
  const normalizedOptions = normalizeFormatOptions(format, formatOptions);
  const extra = options && typeof options === 'object' ? options : {};
  const commonArgs = [
    '-y',
    ...(extra.activationBytes ? ['-activation_bytes', extra.activationBytes] : []),
    '-i', inputPath
  ];
  if (extra.chapterMetadataPath) {
    commonArgs.push('-f', 'ffmetadata', '-i', extra.chapterMetadataPath);
  }
  commonArgs.push(
    '-map', '0:a:0?',
    '-map_metadata', '0',
    '-map_chapters', extra.chapterMetadataPath ? '1' : '0',
    '-vn',
    '-sn',
    '-dn'
  );
  const metadataArgs = buildMetadataArgs(extra.metadata, extra.metadataOptions);
  const codecArgs = buildCodecArgs(format, normalizedOptions);
  return {
    cmd,
    args: [...commonArgs, ...codecArgs, ...metadataArgs, outputPath],
    metadataArgs,
    formatOptions: normalizedOptions
  };
}

function formatSecondsArg(value) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed < 0) {
    return '0';
  }
  return parsed.toFixed(3).replace(/\.?0+$/u, '');
}

function buildChapterEncodeCommand(
  ffmpegCommand,
  inputPath,
  outputPath,
  outputFormat = 'mp3',
  formatOptions = {},
  metadata = {},
  chapter = {},
  chapterTotal = 1,
  options = {}
) {
  const cmd = String(ffmpegCommand || 'ffmpeg').trim() || 'ffmpeg';
  const format = normalizeOutputFormat(outputFormat);
  const normalizedOptions = normalizeFormatOptions(format, formatOptions);
  const extra = options && typeof options === 'object' ? options : {};
  const safeChapter = normalizeChapterList([chapter], {
    durationMs: metadata?.durationMs,
    fallbackTitle: metadata?.title || 'Kapitel',
    createFallback: true
  })[0];
  const durationSeconds = Number(((safeChapter?.durationMs || 0) / 1000).toFixed(3));
  const metadataArgs = buildMetadataArgs(metadata, {
    title: safeChapter?.title,
    album: metadata?.title || metadata?.album || null,
    trackNo: safeChapter?.index || 1,
    trackTotal: chapterTotal
  });
  const codecArgs = buildCodecArgs(format, normalizedOptions);
  return {
    cmd,
    args: [
      '-y',
      ...(extra.activationBytes ? ['-activation_bytes', extra.activationBytes] : []),
      '-i', inputPath,
      '-ss', formatSecondsArg(safeChapter?.startSeconds),
      '-t', formatSecondsArg(durationSeconds),
      '-map', '0:a:0?',
      '-map_metadata', '-1',
      '-map_chapters', '-1',
      '-vn',
      '-sn',
      '-dn',
      ...codecArgs,
      ...metadataArgs,
      outputPath
    ],
    metadataArgs,
    formatOptions: normalizedOptions
  };
}

function escapeFfmetadataValue(value) {
  return String(value == null ? '' : value)
    .replace(/\\/g, '\\\\')
    .replace(/=/g, '\\=')
    .replace(/;/g, '\\;')
    .replace(/#/g, '\\#')
    .replace(/\r?\n/g, ' ');
}

function buildChapterMetadataContent(chapters = [], metadata = {}) {
  const normalizedChapters = normalizeChapterList(chapters, {
    durationMs: metadata?.durationMs,
    fallbackTitle: metadata?.title || metadata?.album || 'Audiobook',
    createFallback: true
  });

  const chapterBlocks = normalizedChapters.map((chapter) => {
    const startMs = Math.max(0, Math.round(chapter.startMs || 0));
    const endMs = Math.max(startMs, Math.round(chapter.endMs || startMs));
    return [
      '[CHAPTER]',
      'TIMEBASE=1/1000',
      `START=${startMs}`,
      `END=${endMs}`,
      `title=${escapeFfmetadataValue(chapter.title || `Kapitel ${chapter.index || 1}`)}`
    ].join('\n');
  }).join('\n\n');

  return `;FFMETADATA1\n\n${chapterBlocks}`;
}

function buildCoverExtractionCommand(ffmpegCommand, inputPath, outputPath, cover = null) {
  const cmd = String(ffmpegCommand || 'ffmpeg').trim() || 'ffmpeg';
  const streamIndex = Number(cover?.streamIndex);
  const streamSpecifier = Number.isFinite(streamIndex) && streamIndex >= 0
    ? `0:${Math.trunc(streamIndex)}`
    : '0:v:0';
  return {
    cmd,
    args: [
      '-y',
      '-i', inputPath,
      '-map', streamSpecifier,
      '-an',
      '-sn',
      '-dn',
      '-frames:v', '1',
      '-c:v', 'mjpeg',
      '-q:v', '2',
      outputPath
    ]
  };
}

function parseFfmpegTimestampToMs(rawValue) {
  const value = String(rawValue || '').trim();
  const match = value.match(/^(\d+):(\d{2}):(\d{2})(?:\.(\d+))?$/);
  if (!match) {
    return null;
  }
  const hours = Number(match[1]);
  const minutes = Number(match[2]);
  const seconds = Number(match[3]);
  const fraction = match[4] ? Number(`0.${match[4]}`) : 0;
  if (!Number.isFinite(hours) || !Number.isFinite(minutes) || !Number.isFinite(seconds)) {
    return null;
  }
  return Math.round((((hours * 60) + minutes) * 60 + seconds + fraction) * 1000);
}

function buildProgressParser(totalDurationMs) {
  const durationMs = Number(totalDurationMs || 0);
  if (!Number.isFinite(durationMs) || durationMs <= 0) {
    return null;
  }
  return (line) => {
    const match = String(line || '').match(/time=(\d+:\d{2}:\d{2}(?:\.\d+)?)/i);
    if (!match) {
      return null;
    }
    const currentMs = parseFfmpegTimestampToMs(match[1]);
    if (!Number.isFinite(currentMs)) {
      return null;
    }
    const percent = Math.max(0, Math.min(100, Number(((currentMs / durationMs) * 100).toFixed(2))));
    return {
      percent,
      eta: null
    };
  };
}

module.exports = {
  SUPPORTED_INPUT_EXTENSIONS,
  SUPPORTED_OUTPUT_FORMATS,
  DEFAULT_AUDIOBOOK_RAW_TEMPLATE,
  DEFAULT_AUDIOBOOK_OUTPUT_TEMPLATE,
  DEFAULT_AUDIOBOOK_CHAPTER_OUTPUT_TEMPLATE,
  AUDIOBOOK_FORMAT_DEFAULTS,
  normalizeOutputFormat,
  getDefaultFormatOptions,
  normalizeFormatOptions,
  isSupportedInputFile,
  buildMetadataFromProbe,
  normalizeChapterList,
  buildRawStoragePaths,
  buildOutputPath,
  buildChapterOutputPlan,
  buildProbeCommand,
  parseProbeOutput,
  buildEncodeCommand,
  buildChapterEncodeCommand,
  buildChapterMetadataContent,
  buildCoverExtractionCommand,
  buildProgressParser
};
