const express = require('express');
const fs = require('fs');
const os = require('os');
const path = require('path');
const multer = require('multer');
const asyncHandler = require('../middleware/asyncHandler');
const pipelineService = require('../services/pipelineService');
const historyService = require('../services/historyService');
const diskDetectionService = require('../services/diskDetectionService');
const hardwareMonitorService = require('../services/hardwareMonitorService');
const settingsService = require('../services/settingsService');
const logger = require('../services/logger').child('PIPELINE_ROUTE');
const activationBytesService = require('../services/activationBytesService');
const { defaultAudiobookDir } = require('../config');
const { getDb } = require('../db/database');

const router = express.Router();
const audiobookUpload = multer({
  dest: path.join(os.tmpdir(), 'ripster-audiobook-uploads')
});

const AUDIOBOOK_TREE_MAX_DEPTH = 8;

function buildAudiobookOutputTree(rootDir, relPath = '', depth = 0) {
  if (depth >= AUDIOBOOK_TREE_MAX_DEPTH) {
    return [];
  }
  const absDir = relPath ? path.join(rootDir, relPath) : rootDir;
  let dirents = [];
  try {
    dirents = fs.readdirSync(absDir, { withFileTypes: true });
  } catch (_error) {
    return [];
  }

  dirents.sort((left, right) => {
    const leftOrder = left.isDirectory() ? 0 : 1;
    const rightOrder = right.isDirectory() ? 0 : 1;
    if (leftOrder !== rightOrder) {
      return leftOrder - rightOrder;
    }
    return left.name.localeCompare(right.name, undefined, { sensitivity: 'base' });
  });

  const nodes = [];
  for (const dirent of dirents) {
    if (dirent.name.startsWith('.')) {
      continue;
    }
    const childRelPath = relPath ? `${relPath}/${dirent.name}` : dirent.name;
    const childAbsPath = path.join(rootDir, childRelPath);
    if (dirent.isDirectory()) {
      const children = buildAudiobookOutputTree(rootDir, childRelPath, depth + 1);
      const size = children.reduce((sum, item) => sum + Number(item?.size || 0), 0);
      let mtime = null;
      try {
        mtime = fs.statSync(childAbsPath).mtime.toISOString();
      } catch (_error) {
        mtime = null;
      }
      nodes.push({
        name: dirent.name,
        type: 'folder',
        path: childRelPath,
        size,
        mtime,
        children
      });
      continue;
    }
    if (dirent.isFile()) {
      let size = 0;
      let mtime = null;
      try {
        const stat = fs.statSync(childAbsPath);
        size = Number(stat?.size || 0);
        mtime = stat?.mtime ? stat.mtime.toISOString() : null;
      } catch (_error) {
        size = 0;
        mtime = null;
      }
      nodes.push({
        name: dirent.name,
        type: 'file',
        path: childRelPath,
        size,
        mtime
      });
    }
  }
  return nodes;
}

router.get(
  '/state',
  asyncHandler(async (req, res) => {
    logger.debug('get:state', { reqId: req.reqId });
    res.json({
      pipeline: pipelineService.getSnapshot(),
      hardwareMonitoring: hardwareMonitorService.getSnapshot()
    });
  })
);

router.post(
  '/analyze',
  asyncHandler(async (req, res) => {
    const devicePath = String(req.body?.devicePath || '').trim() || null;
    logger.info('post:analyze', { reqId: req.reqId, devicePath });
    const result = await pipelineService.analyzeDisc(devicePath);
    res.json({ result });
  })
);

router.get(
  '/cd/drives',
  asyncHandler(async (req, res) => {
    logger.debug('get:cd:drives', { reqId: req.reqId });
    const snapshot = pipelineService.getSnapshot();
    res.json({ cdDrives: snapshot.cdDrives || {} });
  })
);

router.post(
  '/rescan-disc',
  asyncHandler(async (req, res) => {
    logger.info('post:rescan-disc', { reqId: req.reqId });
    const result = await diskDetectionService.rescanAndEmit();
    res.json({ result });
  })
);

router.post(
  '/rescan-drive',
  asyncHandler(async (req, res) => {
    const devicePath = String(req.body?.devicePath || '').trim();
    logger.info('post:rescan-drive', { reqId: req.reqId, devicePath });
    if (!devicePath) {
      const err = new Error('devicePath ist erforderlich');
      err.statusCode = 400;
      throw err;
    }
    const result = await diskDetectionService.rescanDriveAndEmit(devicePath);
    res.json({ result });
  })
);

router.get(
  '/omdb/search',
  asyncHandler(async (req, res) => {
    const query = req.query.q || '';
    logger.info('get:omdb:search', { reqId: req.reqId, query });
    const results = await pipelineService.searchOmdb(String(query));
    res.json({ results });
  })
);

router.get(
  '/tmdb/series/search',
  asyncHandler(async (req, res) => {
    const query = req.query.q || '';
    const seasonNumber = req.query.season || null;
    logger.info('get:tmdb:series-search', { reqId: req.reqId, query, seasonNumber });
    const results = await pipelineService.searchTmdbSeries(String(query), seasonNumber);
    res.json({ results });
  })
);

router.get(
  '/cd/musicbrainz/search',
  asyncHandler(async (req, res) => {
    const query = req.query.q || '';
    logger.info('get:cd:musicbrainz:search', { reqId: req.reqId, query });
    const results = await pipelineService.searchMusicBrainz(String(query));
    res.json({ results });
  })
);

router.get(
  '/cd/musicbrainz/release/:mbId',
  asyncHandler(async (req, res) => {
    const mbId = String(req.params.mbId || '').trim();
    if (!mbId) {
      const error = new Error('mbId fehlt.');
      error.statusCode = 400;
      throw error;
    }
    logger.info('get:cd:musicbrainz:release', { reqId: req.reqId, mbId });
    const release = await pipelineService.getMusicBrainzReleaseById(mbId);
    res.json({ release });
  })
);

router.post(
  '/cd/select-metadata',
  asyncHandler(async (req, res) => {
    const { jobId, title, artist, year, mbId, coverUrl, tracks } = req.body;
    if (!jobId) {
      const error = new Error('jobId fehlt.');
      error.statusCode = 400;
      throw error;
    }
    logger.info('post:cd:select-metadata', { reqId: req.reqId, jobId, title, artist, year, mbId });
    const job = await pipelineService.selectCdMetadata({
      jobId: Number(jobId),
      title,
      artist,
      year,
      mbId,
      coverUrl,
      tracks
    });
    res.json({ job });
  })
);

router.post(
  '/cd/start/:jobId',
  asyncHandler(async (req, res) => {
    const jobId = Number(req.params.jobId);
    const ripConfig = req.body || {};
    logger.info('post:cd:start', {
      reqId: req.reqId,
      jobId,
      format: ripConfig.format,
      selectedPreEncodeScriptIdsCount: Array.isArray(ripConfig?.selectedPreEncodeScriptIds)
        ? ripConfig.selectedPreEncodeScriptIds.length
        : 0,
      selectedPostEncodeScriptIdsCount: Array.isArray(ripConfig?.selectedPostEncodeScriptIds)
        ? ripConfig.selectedPostEncodeScriptIds.length
        : 0,
      selectedPreEncodeChainIdsCount: Array.isArray(ripConfig?.selectedPreEncodeChainIds)
        ? ripConfig.selectedPreEncodeChainIds.length
        : 0,
      selectedPostEncodeChainIdsCount: Array.isArray(ripConfig?.selectedPostEncodeChainIds)
        ? ripConfig.selectedPostEncodeChainIds.length
        : 0
    });
    const result = await pipelineService.enqueueOrStartCdAction(
      jobId,
      ripConfig,
      () => pipelineService.startCdRip(jobId, ripConfig)
    );
    res.json({ result });
  })
);

router.post(
  '/audiobook/upload',
  audiobookUpload.single('file'),
  asyncHandler(async (req, res) => {
    if (!req.file) {
      const error = new Error('Upload-Datei fehlt.');
      error.statusCode = 400;
      throw error;
    }
    logger.info('post:audiobook:upload', {
      reqId: req.reqId,
      originalName: req.file.originalname,
      sizeBytes: Number(req.file.size || 0),
      mimeType: String(req.file.mimetype || '').trim() || null,
      tempPath: String(req.file.path || '').trim() || null
    });
    const result = await pipelineService.createFileJob({
      kind: 'audiobook_upload',
      file: req.file,
      options: {
        format: req.body?.format,
        startImmediately: req.body?.startImmediately
      }
    });
    res.json({ result });
  })
);

router.get(
  '/audiobook/pending-activation',
  asyncHandler(async (req, res) => {
    const db = await getDb();
    // Jobs die eine Checksum haben, aber noch keine Activation Bytes im Cache
    const pending = await db.all(`
      SELECT j.id AS jobId, j.aax_checksum AS checksum
      FROM jobs j
      WHERE j.aax_checksum IS NOT NULL
        AND j.status NOT IN ('DONE', 'ERROR', 'CANCELLED')
        AND NOT EXISTS (
          SELECT 1 FROM aax_activation_bytes ab WHERE ab.checksum = j.aax_checksum
        )
      ORDER BY j.created_at DESC
    `);
    res.json({ pending });
  })
);

router.post(
  '/audiobook/start/:jobId',
  asyncHandler(async (req, res) => {
    const jobId = Number(req.params.jobId);
    const config = req.body || {};
    logger.info('post:audiobook:start', {
      reqId: req.reqId,
      jobId,
      format: config?.format,
      formatOptions: config?.formatOptions && typeof config.formatOptions === 'object'
        ? config.formatOptions
        : null
    });
    const result = await pipelineService.startAudiobookWithConfig(jobId, config);
    res.json({ result });
  })
);

router.get(
  '/audiobook/jobs',
  asyncHandler(async (_req, res) => {
    const jobs = await pipelineService.getAudiobookJobs();
    res.json({ jobs });
  })
);

router.get(
  '/audiobook/output-tree',
  asyncHandler(async (_req, res) => {
    const settings = await settingsService.getEffectiveSettingsMap('audiobook');
    const configuredOutputDir = String(settings?.movie_dir || '').trim();
    const rawOutputDir = configuredOutputDir || defaultAudiobookDir || null;
    const outputDir = rawOutputDir
      ? (path.isAbsolute(rawOutputDir) ? rawOutputDir : path.resolve(process.cwd(), rawOutputDir))
      : null;

    if (!outputDir || !fs.existsSync(outputDir)) {
      res.json({ outputDir, tree: null });
      return;
    }

    const children = buildAudiobookOutputTree(outputDir, '', 0);
    const size = children.reduce((sum, item) => sum + Number(item?.size || 0), 0);
    res.json({
      outputDir,
      tree: {
        name: path.basename(outputDir) || 'audiobooks',
        type: 'folder',
        path: '',
        size,
        mtime: null,
        children
      }
    });
  })
);

router.post(
  '/select-metadata',
  asyncHandler(async (req, res) => {
    const {
      jobId,
      title,
      year,
      imdbId,
      poster,
      fromOmdb,
      selectedPlaylist,
      selectedHandBrakeTitleId,
      selectedHandBrakeTitleIds,
      metadataProvider,
      providerId,
      tmdbId,
      metadataKind,
      seasonNumber,
      seasonName,
      episodeCount,
      episodes,
      discNumber
    } = req.body;

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

    logger.info('post:select-metadata', {
      reqId: req.reqId,
      jobId,
      title,
      year,
      imdbId,
      poster,
      fromOmdb,
      selectedPlaylist,
      selectedHandBrakeTitleId,
      selectedHandBrakeTitleIds: Array.isArray(selectedHandBrakeTitleIds) ? selectedHandBrakeTitleIds : null,
      metadataProvider,
      providerId,
      tmdbId,
      seasonNumber,
      discNumber
    });

    const job = await pipelineService.selectMetadata({
      jobId: Number(jobId),
      title,
      year,
      imdbId,
      poster,
      fromOmdb,
      selectedPlaylist,
      selectedHandBrakeTitleId,
      selectedHandBrakeTitleIds,
      metadataProvider,
      providerId,
      tmdbId,
      metadataKind,
      seasonNumber,
      seasonName,
      episodeCount,
      episodes,
      discNumber
    });

    res.json({ job });
  })
);

router.post(
  '/start/:jobId',
  asyncHandler(async (req, res) => {
    const jobId = Number(req.params.jobId);
    logger.info('post:start-job', { reqId: req.reqId, jobId });
    const result = await pipelineService.startPreparedJob(jobId);
    res.json({ result });
  })
);

router.post(
  '/confirm-encode/:jobId',
  asyncHandler(async (req, res) => {
    const jobId = Number(req.params.jobId);
    const selectedEncodeTitleId = req.body?.selectedEncodeTitleId ?? null;
    const selectedEncodeTitleIds = req.body?.selectedEncodeTitleIds ?? null;
    const selectedTrackSelection = req.body?.selectedTrackSelection ?? null;
    const episodeAssignments = req.body?.episodeAssignments ?? null;
    const selectedPostEncodeScriptIds = req.body?.selectedPostEncodeScriptIds;
    const selectedPreEncodeScriptIds = req.body?.selectedPreEncodeScriptIds;
    const selectedPostEncodeChainIds = req.body?.selectedPostEncodeChainIds;
    const selectedPreEncodeChainIds = req.body?.selectedPreEncodeChainIds;
    const skipPipelineStateUpdate = Boolean(req.body?.skipPipelineStateUpdate);
    const selectedUserPresetId = req.body?.selectedUserPresetId ?? null;
    const selectedHandBrakePreset = req.body?.selectedHandBrakePreset ?? null;
    logger.info('post:confirm-encode', {
      reqId: req.reqId,
      jobId,
      selectedEncodeTitleId,
      selectedEncodeTitleIdsCount: Array.isArray(selectedEncodeTitleIds) ? selectedEncodeTitleIds.length : 0,
      selectedTrackSelectionProvided: Boolean(selectedTrackSelection),
      episodeAssignmentsProvided: Boolean(episodeAssignments && typeof episodeAssignments === 'object'),
      skipPipelineStateUpdate,
      selectedUserPresetId,
      selectedHandBrakePreset,
      selectedPostEncodeScriptIdsCount: Array.isArray(selectedPostEncodeScriptIds)
        ? selectedPostEncodeScriptIds.length
        : 0,
      selectedPreEncodeScriptIdsCount: Array.isArray(selectedPreEncodeScriptIds)
        ? selectedPreEncodeScriptIds.length
        : 0,
      selectedPostEncodeChainIdsCount: Array.isArray(selectedPostEncodeChainIds)
        ? selectedPostEncodeChainIds.length
        : 0,
      selectedPreEncodeChainIdsCount: Array.isArray(selectedPreEncodeChainIds)
        ? selectedPreEncodeChainIds.length
        : 0
    });
    const job = await pipelineService.confirmEncodeReview(jobId, {
      selectedEncodeTitleId,
      selectedEncodeTitleIds,
      selectedTrackSelection,
      episodeAssignments,
      selectedPostEncodeScriptIds,
      selectedPreEncodeScriptIds,
      selectedPostEncodeChainIds,
      selectedPreEncodeChainIds,
      skipPipelineStateUpdate,
      selectedUserPresetId,
      selectedHandBrakePreset
    });
    res.json({ job });
  })
);

router.post(
  '/cancel',
  asyncHandler(async (req, res) => {
    const rawJobId = req.body?.jobId;
    const jobId = rawJobId === null || rawJobId === undefined || String(rawJobId).trim() === ''
      ? null
      : Number(rawJobId);
    logger.warn('post:cancel', { reqId: req.reqId, jobId });
    const result = await pipelineService.cancel(jobId);
    res.json({ result });
  })
);

router.post(
  '/retry/:jobId',
  asyncHandler(async (req, res) => {
    const jobId = Number(req.params.jobId);
    logger.info('post:retry', { reqId: req.reqId, jobId });
    const result = await pipelineService.retry(jobId);
    res.json({ result });
  })
);

router.post(
  '/resume-ready/:jobId',
  asyncHandler(async (req, res) => {
    const jobId = Number(req.params.jobId);
    logger.info('post:resume-ready', { reqId: req.reqId, jobId });
    const job = await pipelineService.resumeReadyToEncodeJob(jobId);
    res.json({ job });
  })
);

router.get(
  '/output-folders/:jobId',
  asyncHandler(async (req, res) => {
    const jobId = Number(req.params.jobId);
    const folders = await historyService.getJobOutputFoldersForLineage(jobId);
    res.json({ folders });
  })
);

router.post(
  '/delete-output-folders/:jobId',
  asyncHandler(async (req, res) => {
    const jobId = Number(req.params.jobId);
    const folderPaths = Array.isArray(req.body?.folderPaths) ? req.body.folderPaths : [];
    logger.info('post:delete-output-folders', { reqId: req.reqId, jobId, count: folderPaths.length });
    const result = await historyService.deleteSpecificOutputFolders(jobId, folderPaths);
    res.json({ result });
  })
);

router.post(
  '/reencode/:jobId',
  asyncHandler(async (req, res) => {
    const jobId = Number(req.params.jobId);
    const keepBoth = Boolean(req.body?.keepBoth);
    const deleteFolders = Array.isArray(req.body?.deleteFolders) ? req.body.deleteFolders : [];
    logger.info('post:reencode', { reqId: req.reqId, jobId, keepBoth, deleteFolderCount: deleteFolders.length });
    const result = await pipelineService.reencodeFromRaw(jobId, { keepBoth, deleteFolders });
    res.json({ result });
  })
);

router.post(
  '/restart-review/:jobId',
  asyncHandler(async (req, res) => {
    const jobId = Number(req.params.jobId);
    const keepBoth = Boolean(req.body?.keepBoth);
    const deleteFolders = Array.isArray(req.body?.deleteFolders) ? req.body.deleteFolders : [];
    const reuseCurrentJob = Boolean(req.body?.reuseCurrentJob);
    logger.info('post:restart-review', {
      reqId: req.reqId,
      jobId,
      keepBoth,
      reuseCurrentJob,
      deleteFolderCount: deleteFolders.length
    });
    const result = await pipelineService.restartReviewFromRaw(jobId, { keepBoth, deleteFolders, reuseCurrentJob });
    res.json({ result });
  })
);

router.post(
  '/restart-encode/:jobId',
  asyncHandler(async (req, res) => {
    const jobId = Number(req.params.jobId);
    const keepBoth = Boolean(req.body?.keepBoth);
    const deleteFolders = Array.isArray(req.body?.deleteFolders) ? req.body.deleteFolders : [];
    const restartMode = String(req.body?.restartMode || 'all').trim().toLowerCase() || 'all';
    logger.info('post:restart-encode', {
      reqId: req.reqId,
      jobId,
      keepBoth,
      restartMode,
      deleteFolderCount: deleteFolders.length
    });
    const result = await pipelineService.restartEncodeWithLastSettings(jobId, {
      keepBoth,
      deleteFolders,
      restartMode
    });
    res.json({ result });
  })
);

router.post(
  '/restart-cd-review/:jobId',
  asyncHandler(async (req, res) => {
    const jobId = Number(req.params.jobId);
    const keepBoth = Boolean(req.body?.keepBoth);
    const deleteFolders = Array.isArray(req.body?.deleteFolders) ? req.body.deleteFolders : [];
    logger.info('post:restart-cd-review', { reqId: req.reqId, jobId, keepBoth, deleteFolderCount: deleteFolders.length });
    const result = await pipelineService.restartCdReviewFromRaw(jobId, { keepBoth, deleteFolders });
    res.json({ result });
  })
);

router.get(
  '/queue',
  asyncHandler(async (req, res) => {
    logger.debug('get:queue', { reqId: req.reqId });
    const queue = await pipelineService.getQueueSnapshot();
    res.json({ queue });
  })
);

router.post(
  '/queue/reorder',
  asyncHandler(async (req, res) => {
    // Accept orderedEntryIds (new) or orderedJobIds (legacy fallback for job-only queues).
    const orderedEntryIds = Array.isArray(req.body?.orderedEntryIds)
      ? req.body.orderedEntryIds
      : (Array.isArray(req.body?.orderedJobIds) ? req.body.orderedJobIds : []);
    logger.info('post:queue:reorder', { reqId: req.reqId, orderedEntryIds });
    const queue = await pipelineService.reorderQueue(orderedEntryIds);
    res.json({ queue });
  })
);

router.post(
  '/queue/entry',
  asyncHandler(async (req, res) => {
    const { type, scriptId, chainId, waitSeconds, insertAfterEntryId } = req.body || {};
    logger.info('post:queue:entry', { reqId: req.reqId, type });
    const result = await pipelineService.enqueueNonJobEntry(
      type,
      { scriptId, chainId, waitSeconds },
      insertAfterEntryId ?? null
    );
    const queue = await pipelineService.getQueueSnapshot();
    res.json({ result, queue });
  })
);

router.delete(
  '/queue/entry/:entryId',
  asyncHandler(async (req, res) => {
    const entryId = req.params.entryId;
    logger.info('delete:queue:entry', { reqId: req.reqId, entryId });
    const queue = await pipelineService.removeQueueEntry(entryId);
    res.json({ queue });
  })
);

module.exports = router;
