const API_BASE = import.meta.env.VITE_API_BASE || '/api';
const GET_RESPONSE_CACHE = new Map();

function invalidateCachedGet(prefixes = []) {
  const list = Array.isArray(prefixes) ? prefixes.filter(Boolean) : [];
  if (list.length === 0) {
    GET_RESPONSE_CACHE.clear();
    return;
  }
  for (const key of GET_RESPONSE_CACHE.keys()) {
    if (list.some((prefix) => key.startsWith(prefix))) {
      GET_RESPONSE_CACHE.delete(key);
    }
  }
}

function refreshCachedGet(path, ttlMs) {
  const cacheKey = String(path || '');
  const nextEntry = GET_RESPONSE_CACHE.get(cacheKey) || {
    value: undefined,
    expiresAt: 0,
    promise: null
  };
  const nextPromise = request(path)
    .then((payload) => {
      GET_RESPONSE_CACHE.set(cacheKey, {
        value: payload,
        expiresAt: Date.now() + Math.max(1000, Number(ttlMs || 0)),
        promise: null
      });
      return payload;
    })
    .catch((error) => {
      const current = GET_RESPONSE_CACHE.get(cacheKey);
      if (current && current.promise === nextPromise) {
        GET_RESPONSE_CACHE.set(cacheKey, {
          value: current.value,
          expiresAt: current.expiresAt || 0,
          promise: null
        });
      }
      throw error;
    });
  GET_RESPONSE_CACHE.set(cacheKey, {
    value: nextEntry.value,
    expiresAt: nextEntry.expiresAt || 0,
    promise: nextPromise
  });
  return nextPromise;
}

async function requestCachedGet(path, options = {}) {
  const ttlMs = Math.max(1000, Number(options?.ttlMs || 0));
  const forceRefresh = Boolean(options?.forceRefresh);
  const cacheKey = String(path || '');
  const current = GET_RESPONSE_CACHE.get(cacheKey);
  const now = Date.now();

  if (!forceRefresh && current && current.value !== undefined) {
    if (current.expiresAt > now) {
      return Promise.resolve(current.value);
    }
    if (!current.promise) {
      void refreshCachedGet(path, ttlMs);
    }
    return Promise.resolve(current.value);
  }

  if (!forceRefresh && current?.promise) {
    return current.promise;
  }

  return refreshCachedGet(path, ttlMs);
}

function afterMutationInvalidate(prefixes = []) {
  invalidateCachedGet(prefixes);
}

function buildHandBrakePresetList(payload) {
  const existing = Array.isArray(payload?.presets) ? payload.presets : [];
  if (existing.length > 0) {
    return existing;
  }
  const options = Array.isArray(payload?.options) ? payload.options : [];
  const list = [];
  for (const option of options) {
    if (!option || option.disabled) {
      continue;
    }
    const rawValue = option.value ?? option.name ?? option.label ?? '';
    const name = String(rawValue || '').trim();
    if (!name || name.startsWith('__group__')) {
      continue;
    }
    const category = option.category ? String(option.category).trim() : '';
    list.push({
      name,
      category: category || null
    });
  }
  return list;
}

function normalizeHandBrakePresetPayload(payload) {
  if (!payload || typeof payload !== 'object') {
    return payload;
  }
  const existing = Array.isArray(payload.presets) ? payload.presets : [];
  if (existing.length > 0) {
    return payload;
  }
  return {
    ...payload,
    presets: buildHandBrakePresetList(payload)
  };
}

async function request(path, options = {}) {
  const isFormDataBody = typeof FormData !== 'undefined' && options?.body instanceof FormData;
  const mergedHeaders = {
    ...(isFormDataBody ? {} : { 'Content-Type': 'application/json' }),
    ...(options.headers || {})
  };
  const response = await fetch(`${API_BASE}${path}`, {
    headers: mergedHeaders,
    ...options
  });

  if (!response.ok) {
    let errorPayload = null;
    let message = `HTTP ${response.status}`;
    try {
      errorPayload = await response.json();
      message = errorPayload?.error?.message || message;
    } catch (_error) {
      // ignore parse errors
    }
    const error = new Error(message);
    error.status = response.status;
    error.details = errorPayload?.error?.details || null;
    throw error;
  }

  const contentType = response.headers.get('content-type') || '';
  if (contentType.includes('application/json')) {
    return response.json();
  }

  return response.text();
}

function resolveFilenameFromDisposition(contentDisposition, fallback = 'download.zip') {
  const raw = String(contentDisposition || '').trim();
  if (!raw) {
    return fallback;
  }

  const encodedMatch = raw.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
  if (encodedMatch?.[1]) {
    try {
      return decodeURIComponent(encodedMatch[1]);
    } catch (_error) {
      // ignore malformed content-disposition values
    }
  }

  const plainMatch = raw.match(/filename\s*=\s*"([^"]+)"/i) || raw.match(/filename\s*=\s*([^;]+)/i);
  if (plainMatch?.[1]) {
    return String(plainMatch[1]).trim();
  }

  return fallback;
}

async function download(path, options = {}) {
  const response = await fetch(`${API_BASE}${path}`, {
    headers: options?.headers || {},
    method: options?.method || 'GET'
  });

  if (!response.ok) {
    let errorPayload = null;
    let message = `HTTP ${response.status}`;
    try {
      errorPayload = await response.json();
      message = errorPayload?.error?.message || message;
    } catch (_error) {
      // ignore parse errors
    }
    const error = new Error(message);
    error.status = response.status;
    error.details = errorPayload?.error?.details || null;
    throw error;
  }

  const blob = await response.blob();
  const objectUrl = URL.createObjectURL(blob);
  const link = document.createElement('a');
  const fallbackFilename = String(options?.filename || 'download.zip').trim() || 'download.zip';
  const filename = resolveFilenameFromDisposition(response.headers.get('content-disposition'), fallbackFilename);

  link.href = objectUrl;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  link.remove();
  window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);

  return {
    filename,
    sizeBytes: blob.size
  };
}

async function requestWithXhr(path, options = {}) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    const method = String(options?.method || 'GET').trim().toUpperCase() || 'GET';
    const url = `${API_BASE}${path}`;
    const headers = options?.headers && typeof options.headers === 'object' ? options.headers : {};
    const signal = options?.signal;
    const onUploadProgress = typeof options?.onUploadProgress === 'function'
      ? options.onUploadProgress
      : null;

    let finished = false;
    let abortListener = null;

    const cleanup = () => {
      if (signal && abortListener) {
        signal.removeEventListener('abort', abortListener);
      }
    };

    const settle = (callback) => {
      if (finished) {
        return;
      }
      finished = true;
      cleanup();
      callback();
    };

    xhr.open(method, url, true);
    xhr.responseType = 'text';

    Object.entries(headers).forEach(([key, value]) => {
      if (value == null) {
        return;
      }
      xhr.setRequestHeader(key, String(value));
    });

    if (onUploadProgress && xhr.upload) {
      xhr.upload.onprogress = (event) => {
        const loaded = Number(event?.loaded || 0);
        const total = Number(event?.total || 0);
        const hasKnownTotal = Boolean(event?.lengthComputable && total > 0);
        onUploadProgress({
          loaded,
          total: hasKnownTotal ? total : null,
          percent: hasKnownTotal ? (loaded / total) * 100 : null
        });
      };
    }

    xhr.onerror = () => {
      settle(() => {
        reject(new Error('Netzwerkfehler'));
      });
    };

    xhr.onabort = () => {
      settle(() => {
        const error = new Error('Request abgebrochen.');
        error.name = 'AbortError';
        reject(error);
      });
    };

    xhr.onload = () => {
      settle(() => {
        const contentType = xhr.getResponseHeader('content-type') || '';
        const rawText = xhr.responseText || '';

        if (xhr.status < 200 || xhr.status >= 300) {
          let errorPayload = null;
          let message = `HTTP ${xhr.status}`;
          try {
            errorPayload = rawText ? JSON.parse(rawText) : null;
            message = errorPayload?.error?.message || message;
          } catch (_error) {
            // ignore parse errors
          }
          const error = new Error(message);
          error.status = xhr.status;
          error.details = errorPayload?.error?.details || null;
          reject(error);
          return;
        }

        if (contentType.includes('application/json')) {
          try {
            resolve(rawText ? JSON.parse(rawText) : {});
          } catch (_error) {
            reject(new Error('Ungültige JSON-Antwort vom Server.'));
          }
          return;
        }

        resolve(rawText);
      });
    };

    if (signal) {
      if (signal.aborted) {
        xhr.abort();
        return;
      }
      abortListener = () => {
        if (!finished) {
          xhr.abort();
        }
      };
      signal.addEventListener('abort', abortListener, { once: true });
    }

    xhr.send(options?.body ?? null);
  });
}

export const api = {
  getSettings(options = {}) {
    return requestCachedGet('/settings', {
      ttlMs: 5 * 60 * 1000,
      forceRefresh: options.forceRefresh
    });
  },
  getEffectivePaths(options = {}) {
    return requestCachedGet('/settings/effective-paths', {
      ttlMs: 30 * 1000,
      forceRefresh: options.forceRefresh
    });
  },
  getActivationBytes(options = {}) {
    return requestCachedGet('/settings/activation-bytes', {
      ttlMs: 0,
      forceRefresh: options.forceRefresh ?? true
    });
  },
  async saveActivationBytes(checksum, activationBytes) {
    const result = await request('/settings/activation-bytes', {
      method: 'POST',
      body: JSON.stringify({ checksum, activationBytes })
    });
    afterMutationInvalidate(['/settings/activation-bytes']);
    return result;
  },
  getPendingActivation() {
    return request('/pipeline/audiobook/pending-activation');
  },
  getDetectedDrives() {
    return request('/settings/drives');
  },
  forceUnlockDrive(payload = {}) {
    return request('/settings/drives/force-unlock', {
      method: 'POST',
      body: JSON.stringify(payload || {})
    });
  },
  async getHandBrakePresets(options = {}) {
    const result = await requestCachedGet('/settings/handbrake-presets', {
      ttlMs: 10 * 60 * 1000,
      forceRefresh: options.forceRefresh
    });
    return normalizeHandBrakePresetPayload(result);
  },
  getScripts(options = {}) {
    return requestCachedGet('/settings/scripts', {
      ttlMs: 2 * 60 * 1000,
      forceRefresh: options.forceRefresh
    });
  },
  async createScript(payload = {}) {
    const result = await request('/settings/scripts', {
      method: 'POST',
      body: JSON.stringify(payload || {})
    });
    afterMutationInvalidate(['/settings/scripts']);
    return result;
  },
  async reorderScripts(orderedScriptIds = []) {
    const result = await request('/settings/scripts/reorder', {
      method: 'POST',
      body: JSON.stringify({
        orderedScriptIds: Array.isArray(orderedScriptIds) ? orderedScriptIds : []
      })
    });
    afterMutationInvalidate(['/settings/scripts']);
    return result;
  },
  async updateScript(scriptId, payload = {}) {
    const result = await request(`/settings/scripts/${encodeURIComponent(scriptId)}`, {
      method: 'PUT',
      body: JSON.stringify(payload || {})
    });
    afterMutationInvalidate(['/settings/scripts']);
    return result;
  },
  async deleteScript(scriptId) {
    const result = await request(`/settings/scripts/${encodeURIComponent(scriptId)}`, {
      method: 'DELETE'
    });
    afterMutationInvalidate(['/settings/scripts']);
    return result;
  },
  testScript(scriptId) {
    return request(`/settings/scripts/${encodeURIComponent(scriptId)}/test`, {
      method: 'POST'
    });
  },
  getScriptChains(options = {}) {
    return requestCachedGet('/settings/script-chains', {
      ttlMs: 2 * 60 * 1000,
      forceRefresh: options.forceRefresh
    });
  },
  async createScriptChain(payload = {}) {
    const result = await request('/settings/script-chains', {
      method: 'POST',
      body: JSON.stringify(payload)
    });
    afterMutationInvalidate(['/settings/script-chains']);
    return result;
  },
  async reorderScriptChains(orderedChainIds = []) {
    const result = await request('/settings/script-chains/reorder', {
      method: 'POST',
      body: JSON.stringify({
        orderedChainIds: Array.isArray(orderedChainIds) ? orderedChainIds : []
      })
    });
    afterMutationInvalidate(['/settings/script-chains']);
    return result;
  },
  async updateScriptChain(chainId, payload = {}) {
    const result = await request(`/settings/script-chains/${encodeURIComponent(chainId)}`, {
      method: 'PUT',
      body: JSON.stringify(payload)
    });
    afterMutationInvalidate(['/settings/script-chains']);
    return result;
  },
  async deleteScriptChain(chainId) {
    const result = await request(`/settings/script-chains/${encodeURIComponent(chainId)}`, {
      method: 'DELETE'
    });
    afterMutationInvalidate(['/settings/script-chains']);
    return result;
  },
  testScriptChain(chainId) {
    return request(`/settings/script-chains/${encodeURIComponent(chainId)}/test`, {
      method: 'POST'
    });
  },
  async getPref(key) {
    return request(`/settings/prefs/${encodeURIComponent(key)}`);
  },
  async setPref(key, value) {
    return request(`/settings/prefs/${encodeURIComponent(key)}`, {
      method: 'PUT',
      body: JSON.stringify({ value })
    });
  },
  async updateSetting(key, value) {
    const result = await request(`/settings/${encodeURIComponent(key)}`, {
      method: 'PUT',
      body: JSON.stringify({ value })
    });
    afterMutationInvalidate(['/settings', '/settings/handbrake-presets']);
    return result;
  },
  async updateSettingsBulk(settings) {
    const result = await request('/settings', {
      method: 'PUT',
      body: JSON.stringify({ settings })
    });
    afterMutationInvalidate(['/settings', '/settings/handbrake-presets']);
    return result;
  },
  testPushover(payload = {}) {
    return request('/settings/pushover/test', {
      method: 'POST',
      body: JSON.stringify(payload)
    });
  },
  async recoverMissingCoverArt(payload = {}) {
    const result = await request('/settings/coverart/recover', {
      method: 'POST',
      body: JSON.stringify(payload || {})
    });
    afterMutationInvalidate(['/history', '/settings']);
    return result;
  },
  getPipelineState() {
    return request('/pipeline/state');
  },
  getRuntimeActivities() {
    return request('/runtime/activities');
  },
  cancelRuntimeActivity(activityId, payload = {}) {
    return request(`/runtime/activities/${encodeURIComponent(activityId)}/cancel`, {
      method: 'POST',
      body: JSON.stringify(payload || {})
    });
  },
  requestRuntimeNextStep(activityId, payload = {}) {
    return request(`/runtime/activities/${encodeURIComponent(activityId)}/next-step`, {
      method: 'POST',
      body: JSON.stringify(payload || {})
    });
  },
  clearRuntimeRecentActivities() {
    return request('/runtime/activities/clear-recent', {
      method: 'POST',
      body: JSON.stringify({})
    });
  },
  async analyzeDisc(devicePath = null) {
    const body = devicePath ? { devicePath } : {};
    const result = await request('/pipeline/analyze', {
      method: 'POST',
      body: JSON.stringify(body)
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async rescanDisc() {
    const result = await request('/pipeline/rescan-disc', {
      method: 'POST'
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async rescanDrive(devicePath) {
    const result = await request('/pipeline/rescan-drive', {
      method: 'POST',
      body: JSON.stringify({ devicePath })
    });
    return result;
  },
  searchOmdb(q) {
    return request(`/pipeline/omdb/search?q=${encodeURIComponent(q)}`);
  },
  searchTmdbSeries(q, seasonNumber = null) {
    const params = new URLSearchParams();
    params.set('q', String(q || ''));
    if (seasonNumber !== undefined && seasonNumber !== null && String(seasonNumber).trim() !== '') {
      params.set('season', String(seasonNumber).trim());
    }
    return request(`/pipeline/tmdb/series/search?${params.toString()}`);
  },
  searchMusicBrainz(q) {
    return request(`/pipeline/cd/musicbrainz/search?q=${encodeURIComponent(q)}`);
  },
  getMusicBrainzRelease(mbId) {
    return request(`/pipeline/cd/musicbrainz/release/${encodeURIComponent(String(mbId || '').trim())}`);
  },
  async selectCdMetadata(payload) {
    const result = await request('/pipeline/cd/select-metadata', {
      method: 'POST',
      body: JSON.stringify(payload)
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async startCdRip(jobId, ripConfig) {
    const result = await request(`/pipeline/cd/start/${jobId}`, {
      method: 'POST',
      body: JSON.stringify(ripConfig || {})
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async uploadAudiobook(file, payload = {}, options = {}) {
    const formData = new FormData();
    if (file) {
      formData.append('file', file);
    }
    if (payload?.format) {
      formData.append('format', String(payload.format));
    }
    if (payload?.startImmediately !== undefined) {
      formData.append('startImmediately', String(payload.startImmediately));
    }
    const result = await requestWithXhr('/pipeline/audiobook/upload', {
      method: 'POST',
      body: formData,
      signal: options?.signal,
      onUploadProgress: options?.onProgress
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async startAudiobook(jobId, payload = {}) {
    const result = await request(`/pipeline/audiobook/start/${jobId}`, {
      method: 'POST',
      body: JSON.stringify(payload || {})
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  getAudiobookJobs() {
    return request('/pipeline/audiobook/jobs');
  },
  getAudiobookOutputTree() {
    return request('/pipeline/audiobook/output-tree');
  },
  async selectMetadata(payload) {
    const result = await request('/pipeline/select-metadata', {
      method: 'POST',
      body: JSON.stringify(payload)
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async startJob(jobId) {
    const result = await request(`/pipeline/start/${jobId}`, {
      method: 'POST'
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async confirmEncodeReview(jobId, payload = {}) {
    const result = await request(`/pipeline/confirm-encode/${jobId}`, {
      method: 'POST',
      body: JSON.stringify(payload || {})
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async cancelPipeline(jobId = null) {
    const result = await request('/pipeline/cancel', {
      method: 'POST',
      body: JSON.stringify({ jobId })
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async retryJob(jobId) {
    const result = await request(`/pipeline/retry/${jobId}`, {
      method: 'POST'
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async resumeReadyJob(jobId) {
    const result = await request(`/pipeline/resume-ready/${jobId}`, {
      method: 'POST'
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async reencodeJob(jobId, options = {}) {
    const body = {};
    if (options.keepBoth) body.keepBoth = true;
    if (Array.isArray(options.deleteFolders) && options.deleteFolders.length > 0) body.deleteFolders = options.deleteFolders;
    const result = await request(`/pipeline/reencode/${jobId}`, {
      method: 'POST',
      body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async restartReviewFromRaw(jobId, options = {}) {
    const body = {};
    if (options.keepBoth) body.keepBoth = true;
    if (Array.isArray(options.deleteFolders) && options.deleteFolders.length > 0) body.deleteFolders = options.deleteFolders;
    if (options.reuseCurrentJob) body.reuseCurrentJob = true;
    const result = await request(`/pipeline/restart-review/${jobId}`, {
      method: 'POST',
      body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async restartEncodeWithLastSettings(jobId, options = {}) {
    const body = {};
    if (options.keepBoth) body.keepBoth = true;
    if (Array.isArray(options.deleteFolders) && options.deleteFolders.length > 0) body.deleteFolders = options.deleteFolders;
    if (options.restartMode) body.restartMode = options.restartMode;
    const result = await request(`/pipeline/restart-encode/${jobId}`, {
      method: 'POST',
      body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  getOutputFolders(jobId) {
    return request(`/pipeline/output-folders/${jobId}`);
  },
  async deleteOutputFolders(jobId, folderPaths = []) {
    const result = await request(`/pipeline/delete-output-folders/${jobId}`, {
      method: 'POST',
      body: JSON.stringify({ folderPaths })
    });
    afterMutationInvalidate(['/history']);
    return result;
  },
  async restartCdReviewFromRaw(jobId, options = {}) {
    const body = {};
    if (options.keepBoth) body.keepBoth = true;
    if (Array.isArray(options.deleteFolders) && options.deleteFolders.length > 0) body.deleteFolders = options.deleteFolders;
    const result = await request(`/pipeline/restart-cd-review/${jobId}`, {
      method: 'POST',
      body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  getPipelineQueue() {
    return request('/pipeline/queue');
  },
  async reorderPipelineQueue(orderedEntryIds = []) {
    const result = await request('/pipeline/queue/reorder', {
      method: 'POST',
      body: JSON.stringify({ orderedEntryIds: Array.isArray(orderedEntryIds) ? orderedEntryIds : [] })
    });
    afterMutationInvalidate(['/pipeline/queue']);
    return result;
  },
  async addQueueEntry(payload = {}) {
    const result = await request('/pipeline/queue/entry', {
      method: 'POST',
      body: JSON.stringify(payload)
    });
    afterMutationInvalidate(['/pipeline/queue']);
    return result;
  },
  async removeQueueEntry(entryId) {
    const result = await request(`/pipeline/queue/entry/${encodeURIComponent(entryId)}`, {
      method: 'DELETE'
    });
    afterMutationInvalidate(['/pipeline/queue']);
    return result;
  },
  getJobs(params = {}) {
    const query = new URLSearchParams();
    if (params.status) query.set('status', params.status);
    if (Array.isArray(params.statuses) && params.statuses.length > 0) {
      query.set('statuses', params.statuses.join(','));
    }
    if (params.search) query.set('search', params.search);
    if (Number.isFinite(Number(params.limit)) && Number(params.limit) > 0) {
      query.set('limit', String(Math.trunc(Number(params.limit))));
    }
    if (params.lite) {
      query.set('lite', '1');
    }
    if (params.includeChildren) {
      query.set('includeChildren', '1');
    }
    const suffix = query.toString() ? `?${query.toString()}` : '';
    return request(`/history${suffix}`);
  },
  getOrphanRawFolders() {
    return request('/history/orphan-raw');
  },
  async importOrphanRawFolder(rawPath) {
    const result = await request('/history/orphan-raw/import', {
      method: 'POST',
      body: JSON.stringify({ rawPath })
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  async assignJobOmdb(jobId, payload = {}) {
    const result = await request(`/history/${jobId}/omdb/assign`, {
      method: 'POST',
      body: JSON.stringify(payload || {})
    });
    afterMutationInvalidate(['/history']);
    return result;
  },
  async assignJobCdMetadata(jobId, payload = {}) {
    const result = await request(`/history/${jobId}/cd/assign`, {
      method: 'POST',
      body: JSON.stringify(payload || {})
    });
    afterMutationInvalidate(['/history']);
    return result;
  },
  async acknowledgeJobError(jobId) {
    const result = await request(`/history/${jobId}/error/ack`, {
      method: 'POST'
    });
    afterMutationInvalidate(['/history']);
    return result;
  },
  async deleteJobFiles(jobId, target = 'both') {
    const result = await request(`/history/${jobId}/delete-files`, {
      method: 'POST',
      body: JSON.stringify({ target })
    });
    afterMutationInvalidate(['/history']);
    return result;
  },
  getJobDeletePreview(jobId, options = {}) {
    const includeRelated = options?.includeRelated !== false;
    const query = new URLSearchParams();
    query.set('includeRelated', includeRelated ? '1' : '0');
    return request(`/history/${jobId}/delete-preview?${query.toString()}`);
  },
  async deleteJobEntry(jobId, target = 'none', options = {}) {
    const includeRelated = Boolean(options?.includeRelated);
    const resetDriveState = Boolean(options?.resetDriveState);
    const selectedMoviePaths = Array.isArray(options?.selectedMoviePaths)
      ? options.selectedMoviePaths
        .map((item) => String(item || '').trim())
        .filter(Boolean)
      : null;
    const hasKeepDetectedDevice = options?.keepDetectedDevice !== undefined;
    const keepDetectedDevice = hasKeepDetectedDevice
      ? Boolean(options.keepDetectedDevice)
      : null;
    const result = await request(`/history/${jobId}/delete`, {
      method: 'POST',
      body: JSON.stringify({
        target,
        includeRelated,
        resetDriveState,
        ...(hasKeepDetectedDevice ? { keepDetectedDevice } : {}),
        ...(selectedMoviePaths ? { selectedMoviePaths } : {})
      })
    });
    afterMutationInvalidate(['/history', '/pipeline/queue']);
    return result;
  },
  requestJobArchive(jobId, target = 'raw', options = {}) {
    const outputPath = String(options?.outputPath || '').trim();
    return request(`/downloads/history/${jobId}`, {
      method: 'POST',
      body: JSON.stringify({
        target,
        ...(outputPath ? { outputPath } : {})
      })
    });
  },
  getDownloads() {
    return request('/downloads');
  },
  getDownloadsSummary() {
    return request('/downloads/summary');
  },
  downloadPreparedArchive(downloadId) {
    const link = document.createElement('a');
    link.href = `${API_BASE}/downloads/${encodeURIComponent(downloadId)}/file`;
    link.download = '';
    document.body.appendChild(link);
    link.click();
    link.remove();
    return Promise.resolve();
  },
  deleteDownload(downloadId) {
    return request(`/downloads/${encodeURIComponent(downloadId)}`, {
      method: 'DELETE'
    });
  },
  getJob(jobId, options = {}) {
    const query = new URLSearchParams();
    const includeLiveLog = Boolean(options.includeLiveLog);
    const includeLogs = Boolean(options.includeLogs);
    const includeAllLogs = Boolean(options.includeAllLogs);
    if (options.includeLiveLog) {
      query.set('includeLiveLog', '1');
    }
    if (options.includeLogs) {
      query.set('includeLogs', '1');
    }
    if (options.includeAllLogs) {
      query.set('includeAllLogs', '1');
    }
    if (Number.isFinite(Number(options.logTailLines)) && Number(options.logTailLines) > 0) {
      query.set('logTailLines', String(Math.trunc(Number(options.logTailLines))));
    }
    if (options.lite) {
      query.set('lite', '1');
    }
    const suffix = query.toString() ? `?${query.toString()}` : '';
    const path = `/history/${jobId}${suffix}`;
    const canUseCache = !includeLiveLog && !includeLogs && !includeAllLogs;
    if (!canUseCache) {
      return request(path);
    }
    return requestCachedGet(path, {
      ttlMs: 8000,
      forceRefresh: options.forceRefresh
    });
  },
  // ── User Presets ───────────────────────────────────────────────────────────
  getUserPresets(mediaType = null, options = {}) {
    const suffix = mediaType ? `?media_type=${encodeURIComponent(mediaType)}` : '';
    return requestCachedGet(`/settings/user-presets${suffix}`, {
      ttlMs: 2 * 60 * 1000,
      forceRefresh: options.forceRefresh
    });
  },
  async createUserPreset(payload = {}) {
    const result = await request('/settings/user-presets', {
      method: 'POST',
      body: JSON.stringify(payload)
    });
    afterMutationInvalidate(['/settings/user-presets']);
    return result;
  },
  async updateUserPreset(id, payload = {}) {
    const result = await request(`/settings/user-presets/${encodeURIComponent(id)}`, {
      method: 'PUT',
      body: JSON.stringify(payload)
    });
    afterMutationInvalidate(['/settings/user-presets']);
    return result;
  },
  async deleteUserPreset(id) {
    const result = await request(`/settings/user-presets/${encodeURIComponent(id)}`, {
      method: 'DELETE'
    });
    afterMutationInvalidate(['/settings/user-presets']);
    return result;
  },

  // ── Cron Jobs ──────────────────────────────────────────────────────────────
  getCronJobs() {
    return request('/crons');
  },
  getCronJob(id) {
    return request(`/crons/${encodeURIComponent(id)}`);
  },
  createCronJob(payload = {}) {
    return request('/crons', {
      method: 'POST',
      body: JSON.stringify(payload)
    });
  },
  updateCronJob(id, payload = {}) {
    return request(`/crons/${encodeURIComponent(id)}`, {
      method: 'PUT',
      body: JSON.stringify(payload)
    });
  },
  deleteCronJob(id) {
    return request(`/crons/${encodeURIComponent(id)}`, {
      method: 'DELETE'
    });
  },
  getCronJobLogs(id, limit = 20) {
    return request(`/crons/${encodeURIComponent(id)}/logs?limit=${limit}`);
  },
  runCronJobNow(id) {
    return request(`/crons/${encodeURIComponent(id)}/run`, {
      method: 'POST'
    });
  },
  validateCronExpression(cronExpression) {
    return request('/crons/validate-expression', {
      method: 'POST',
      body: JSON.stringify({ cronExpression })
    });
  },

  // ── Converter ──────────────────────────────────────────────────────────────
  converterGetTree() {
    return request('/converter/tree');
  },
  converterBrowse(parent = null) {
    const q = parent ? `?parent=${encodeURIComponent(parent)}` : '';
    return request(`/converter/browse${q}`);
  },
  converterScan() {
    return request('/converter/scan', { method: 'POST' });
  },
  converterCreateJobs(entries) {
    return request('/converter/create-jobs', {
      method: 'POST',
      body: JSON.stringify({ entries })
    });
  },
  converterCreateJobsFromSelection(relPaths, audioMode = 'individual') {
    return request('/converter/jobs/from-selection', {
      method: 'POST',
      body: JSON.stringify({ relPaths, audioMode })
    });
  },
  converterAssignFilesToJob(jobId, relPaths = []) {
    afterMutationInvalidate(['/converter/jobs']);
    return request(`/converter/jobs/${encodeURIComponent(jobId)}/assign-files`, {
      method: 'POST',
      body: JSON.stringify({ relPaths })
    });
  },
  converterRemoveFileFromJob(jobId, relPath) {
    afterMutationInvalidate(['/converter/jobs']);
    return request(`/converter/jobs/${encodeURIComponent(jobId)}/remove-file`, {
      method: 'POST',
      body: JSON.stringify({ relPath })
    });
  },
  converterRemoveInputFromJob(jobId, inputPath) {
    afterMutationInvalidate(['/converter/jobs']);
    return request(`/converter/jobs/${encodeURIComponent(jobId)}/remove-input`, {
      method: 'POST',
      body: JSON.stringify({ inputPath })
    });
  },
  converterUpdateJobConfig(jobId, config = {}) {
    return request(`/converter/jobs/${encodeURIComponent(jobId)}/config`, {
      method: 'POST',
      body: JSON.stringify(config)
    });
  },
  converterUploadFiles(formData) {
    return request('/converter/upload', { method: 'POST', body: formData });
  },
  getConverterJobs() {
    return request('/converter/jobs');
  },
  startConverterJob(jobId, config = {}) {
    afterMutationInvalidate(['/converter/jobs']);
    return request(`/converter/jobs/${encodeURIComponent(jobId)}/start`, {
      method: 'POST',
      body: JSON.stringify(config)
    });
  },
  cancelConverterJob(jobId) {
    afterMutationInvalidate(['/converter/jobs']);
    return request(`/converter/jobs/${encodeURIComponent(jobId)}/cancel`, {
      method: 'POST'
    });
  },
  deleteConverterJob(jobId) {
    afterMutationInvalidate(['/converter/jobs']);
    return request(`/converter/jobs/${encodeURIComponent(jobId)}`, {
      method: 'DELETE'
    });
  },

  // ── Converter Datei-Operationen ──────────────────────────────────────────
  converterDeleteFile(relPath) {
    return request('/converter/files', { method: 'DELETE', body: JSON.stringify({ relPath }) });
  },
  converterRenameFile(relPath, newName) {
    return request('/converter/files/rename', { method: 'POST', body: JSON.stringify({ relPath, newName }) });
  },
  converterMoveFile(relPath, targetParentRelPath) {
    return request('/converter/files/move', { method: 'POST', body: JSON.stringify({ relPath, targetParentRelPath }) });
  },
  converterCreateFolder(parentRelPath, name) {
    return request('/converter/files/folder', { method: 'POST', body: JSON.stringify({ parentRelPath, name }) });
  }
};

export { API_BASE };
