import * as express from 'express';
import * as path from 'path';
import * as fs from 'fs';
import * as morgan from 'morgan';
import { spawn } from 'child_process';
import { v4 as uuid } from 'uuid';
import signaling from './signaling';
import { log, LogLevel } from './log';
import Options from './class/options';
import { reset as resetHandler } from './class/httphandler';
import { initSwagger } from './swagger';
import { ServerAudioRecorderManager } from './class/serveraudiorecorder';

const cors = require('cors');
const multer = require('multer');

const AVATAR_UPLOAD_LIMIT_BYTES = 2 * 1024 * 1024;
const ALLOWED_AVATAR_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
const ALLOWED_AVATAR_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']);
const DEFAULT_RECORDING_UPLOAD_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
const ALLOWED_RECORDING_MIME_TYPES = new Set(['video/webm', 'video/mp4', 'application/octet-stream']);
const ALLOWED_RECORDING_EXTENSIONS = new Set(['.webm', '.mp4']);

type RecordingMetadata = {
  id?: string;
  meetingId?: string;
  filename?: string;
  originalFilename?: string;
  mimetype?: string;
  size?: number;
  userId?: string;
  host?: RecordingPerson;
  participants?: RecordingPerson[];
  uploadedAt?: string;
  updatedAt?: string;
};

type RecordingPerson = {
  participantId?: string;
  userId?: string;
  id?: string;
  name?: string;
  avatar?: string;
  role?: string;
  status?: string;
  mediaState?: any;
};

function safeAvatarExtension(file: any): string {
  const originalExt = path.extname(file.originalname || '').toLowerCase();
  if (ALLOWED_AVATAR_EXTENSIONS.has(originalExt)) {
    return originalExt;
  }

  switch (file.mimetype) {
    case 'image/jpeg':
      return '.jpg';
    case 'image/png':
      return '.png';
    case 'image/webp':
      return '.webp';
    case 'image/gif':
      return '.gif';
    default:
      return '';
  }
}

function isAllowedAvatar(file: any): boolean {
  return ALLOWED_AVATAR_MIME_TYPES.has(file.mimetype) && safeAvatarExtension(file).length > 0;
}

function getRecordingRoot(): string {
  return path.resolve(process.env.RECORDING_DIR || path.join(process.cwd(), 'recordings'));
}

function getRecordingUploadLimitBytes(): number {
  const value = Number(process.env.RECORDING_MAX_BYTES);
  return Number.isFinite(value) && value > 0 ? value : DEFAULT_RECORDING_UPLOAD_LIMIT_BYTES;
}

function sanitizePathSegment(value: string | undefined, fallback: string): string {
  const sanitized = (value || fallback).replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120);
  return sanitized || fallback;
}

function isPathInside(parent: string, child: string): boolean {
  const relative = path.relative(parent, child);
  return relative.length === 0 || (!relative.startsWith('..') && !path.isAbsolute(relative));
}

function safeRecordingExtension(file: any): string {
  const originalExt = path.extname(file.originalname || '').toLowerCase();
  if (ALLOWED_RECORDING_EXTENSIONS.has(originalExt)) {
    return originalExt;
  }

  switch (normalizeMimeType(file.mimetype)) {
    case 'video/mp4':
      return '.mp4';
    case 'video/webm':
      return '.webm';
    default:
      return '';
  }
}

function normalizeMimeType(mimetype: string | undefined): string {
  return (mimetype || '').split(';')[0].trim().toLowerCase();
}

function isAllowedRecording(file: any): boolean {
  const mimetype = normalizeMimeType(file.mimetype);
  const ext = safeRecordingExtension(file);
  const isCompatibleMime = ALLOWED_RECORDING_MIME_TYPES.has(mimetype) || mimetype.startsWith('video/') || mimetype === 'text/plain' || mimetype === '';
  return ext.length > 0 && isCompatibleMime;
}

function getRecordingMimeTypeFromExtension(ext: string): string {
  return ext.toLowerCase() === '.mp4' ? 'video/mp4' : 'video/webm';
}

function parseJsonField(value: any): any {
  if (value === undefined || value === null || value === '') {
    return undefined;
  }

  if (typeof value !== 'string') {
    return value;
  }

  try {
    return JSON.parse(value);
  } catch (_error) {
    return undefined;
  }
}

function sanitizeMetadataString(value: any, maxLength = 200): string {
  if (value === undefined || value === null) {
    return '';
  }

  return String(value)
    .split('')
    .filter((char) => {
      const code = char.charCodeAt(0);
      return code > 31 && code !== 127;
    })
    .join('')
    .trim()
    .slice(0, maxLength);
}

function sanitizeRecordingPerson(value: any, fallbackRole: string): RecordingPerson | undefined {
  const parsed = parseJsonField(value);
  if (!parsed || typeof parsed !== 'object') {
    return undefined;
  }

  const person: RecordingPerson = {
    participantId: sanitizeMetadataString(parsed.participantId || parsed.connectionId, 120),
    userId: sanitizeMetadataString(parsed.userId || parsed.id, 120),
    id: sanitizeMetadataString(parsed.id || parsed.userId, 120),
    name: sanitizeMetadataString(parsed.name, 120),
    avatar: sanitizeMetadataString(parsed.avatar, 400),
    role: sanitizeMetadataString(parsed.role || fallbackRole, 40),
    status: sanitizeMetadataString(parsed.status, 40)
  };

  if (parsed.mediaState && typeof parsed.mediaState === 'object') {
    person.mediaState = {
      audio: Boolean(parsed.mediaState.audio),
      video: Boolean(parsed.mediaState.video),
      screenShare: Boolean(parsed.mediaState.screenShare),
      recording: Boolean(parsed.mediaState.recording),
      isSpeaking: Boolean(parsed.mediaState.isSpeaking)
    };
  }

  return person;
}

function sanitizeRecordingParticipants(value: any): RecordingPerson[] {
  const parsed = parseJsonField(value);
  if (!Array.isArray(parsed)) {
    return [];
  }

  return parsed
    .slice(0, 100)
    .map((participant) => sanitizeRecordingPerson(participant, 'participant'))
    .filter((participant) => Boolean(participant));
}

function buildFallbackRecordingHost(userId: string | undefined): RecordingPerson | undefined {
  const safeUserId = sanitizeMetadataString(userId, 120);
  if (!safeUserId) {
    return undefined;
  }

  return {
    userId: safeUserId,
    id: safeUserId,
    role: 'host'
  };
}

function readRecordingMetadata(metadataPath: string): RecordingMetadata {
  try {
    if (!fs.existsSync(metadataPath)) {
      return {};
    }

    return JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
  } catch (error) {
    log(LogLevel.warn, 'Failed to read recording metadata:', error);
    return {};
  }
}

function getRecordingMimeType(filename: string, metadata: RecordingMetadata): string {
  const ext = path.extname(filename);
  const mimetype = normalizeMimeType(metadata.mimetype);
  if (mimetype && mimetype !== 'text/plain' && mimetype !== 'application/octet-stream') {
    return mimetype;
  }

  return getRecordingMimeTypeFromExtension(ext);
}

function buildRecordingInfo(recordingRoot: string, meetingId: string, filename: string) {
  const filePath = path.join(recordingRoot, meetingId, filename);
  const metadataPath = path.join(recordingRoot, meetingId, `${filename}.json`);
  const stat = fs.statSync(filePath);
  const metadata = readRecordingMetadata(metadataPath);
  const resolvedMeetingId = metadata.meetingId || meetingId;
  const resolvedFilename = metadata.filename || filename;
  const participants = Array.isArray(metadata.participants) ? metadata.participants : [];

  return {
    id: metadata.id || path.basename(filename, path.extname(filename)),
    meetingId: resolvedMeetingId,
    filename: resolvedFilename,
    originalFilename: metadata.originalFilename || filename,
    mimetype: getRecordingMimeType(filename, metadata),
    size: stat.size,
    userId: metadata.userId || '',
    host: metadata.host || buildFallbackRecordingHost(metadata.userId),
    participants,
    participantCount: participants.length,
    uploadedAt: metadata.uploadedAt || stat.birthtime.toISOString(),
    updatedAt: metadata.updatedAt || stat.mtime.toISOString(),
    modifiedAt: stat.mtime.toISOString(),
    downloadUrl: `/api/recordings/${encodeURIComponent(meetingId)}/${encodeURIComponent(filename)}/download`,
    streamUrl: `/api/recordings/${encodeURIComponent(meetingId)}/${encodeURIComponent(filename)}/stream`
  };
}

function listRecordings(recordingRoot: string) {
  if (!fs.existsSync(recordingRoot)) {
    return [];
  }

  const recordings = [];
  const meetingIds = fs.readdirSync(recordingRoot).filter((name) => {
    const fullPath = path.join(recordingRoot, name);
    return name !== '.tmp' && fs.statSync(fullPath).isDirectory();
  });

  meetingIds.forEach((meetingId) => {
    const meetingDir = path.join(recordingRoot, meetingId);
    fs.readdirSync(meetingDir).forEach((filename) => {
      const ext = path.extname(filename).toLowerCase();
      const filePath = path.join(meetingDir, filename);
      if (!ALLOWED_RECORDING_EXTENSIONS.has(ext) || !fs.statSync(filePath).isFile()) {
        return;
      }

      recordings.push(buildRecordingInfo(recordingRoot, meetingId, filename));
    });
  });

  recordings.sort((a, b) => Date.parse(b.uploadedAt) - Date.parse(a.uploadedAt));
  return recordings;
}

function removeEmptyDirectory(directory: string): void {
  try {
    if (fs.existsSync(directory) && fs.readdirSync(directory).length === 0) {
      fs.rmdirSync(directory);
    }
  } catch (error) {
    log(LogLevel.warn, 'Failed to remove empty recording directory:', error);
  }
}

function removeFileIfExists(filePath: string | undefined): void {
  if (!filePath) {
    return;
  }

  try {
    if (fs.existsSync(filePath)) {
      fs.unlinkSync(filePath);
    }
  } catch (error) {
    log(LogLevel.warn, 'Failed to remove temporary file:', error);
  }
}

function getMergedRecordingExtension(videoExt: string): string {
  return videoExt.toLowerCase() === '.webm' ? '.webm' : '.mp4';
}

function runFfmpeg(args: string[]): Promise<void> {
  return new Promise((resolve, reject) => {
    const ffmpegPath = process.env.FFMPEG_PATH || 'ffmpeg';
    const child = spawn(ffmpegPath, args, { windowsHide: true });
    let stderr = '';

    child.stderr.on('data', (chunk) => {
      stderr += chunk.toString();
    });
    child.on('error', reject);
    child.on('close', (code) => {
      if (code === 0) {
        resolve();
        return;
      }

      reject(new Error(`ffmpeg exited with code ${code}: ${stderr.slice(-2000)}`));
    });
  });
}

function mergeVideoWithServerAudio(videoPath: string, audioPath: string, outputPath: string, outputExt: string): Promise<void> {
  const isWebmOutput = outputExt.toLowerCase() === '.webm';
  const args = isWebmOutput
    ? [
        '-y',
        '-i', videoPath,
        '-i', audioPath,
        '-map', '0:v:0',
        '-map', '1:a:0',
        '-c:v', 'copy',
        '-c:a', 'libopus',
        '-shortest',
        outputPath
      ]
    : [
        '-y',
        '-i', videoPath,
        '-i', audioPath,
        '-map', '0:v:0',
        '-map', '1:a:0',
        '-c:v', 'copy',
        '-c:a', 'aac',
        '-shortest',
        '-movflags', '+faststart',
        outputPath
      ];

  return runFfmpeg(args);
}

export const createServer = (config: Options): express.Express => {
  const app: express.Express = express();
  resetHandler(config.mode);

  if (config.logging !== 'none') {
    app.use(morgan(config.logging));
  }

  app.use(cors({ origin: '*' }));
  app.use(express.urlencoded({ extended: true }));
  app.use(express.json());

  app.get('/config', (_req, res) => res.json({
    useWebSocket: config.type === 'websocket',
    startupMode: config.mode,
    logging: config.logging,
    protocol: config.secure ? 'https' : 'http',
    port: config.port
  }));

  app.use('/signaling', signaling);

  app.get(['/recordings', '/recordings/'], (_req, res) => {
    const recordingsPagePath = path.join(__dirname, '../client/public/recordings/index.html');
    fs.access(recordingsPagePath, (err) => {
      if (err) {
        log(LogLevel.warn, `Can't find file '${recordingsPagePath}'`);
        res.status(404).send(`Can't find file ${recordingsPagePath}`);
        return;
      }

      res.sendFile(recordingsPagePath);
    });
  });

  app.use('/css', express.static(path.join(__dirname, '../client/public/styles')));
  app.use('/images', express.static(path.join(__dirname, '../client/public/assets/images')));
  app.use(express.static(path.join(__dirname, '../client/public')));
  app.use('/module', express.static(path.join(__dirname, '../client/src')));

  app.get(['/', '/index.html'], (_req, res) => {
    const indexPagePath = path.join(__dirname, '../client/public/call/index.html');
    fs.access(indexPagePath, (err) => {
      if (err) {
        log(LogLevel.warn, `Can't find file '${indexPagePath}'`);
        res.status(404).send(`Can't find file ${indexPagePath}`);
        return;
      }

      res.sendFile(indexPagePath);
    });
  });

  app.get('/connect/connect.html', (_req, res) => {
    res.redirect('/connect/');
  });

  app.get('/endcall/endcall.html', (_req, res) => {
    res.redirect('/endcall/');
  });

  initSwagger(app, config);

  const storage = multer.diskStorage({
    destination: (_req: any, _file: any, cb: (error: Error | null, destination: string) => void) => {
      const uploadDir = path.join(__dirname, '../client/public/assets/uploads/avatars');
      if (!fs.existsSync(uploadDir)) {
        fs.mkdirSync(uploadDir, { recursive: true });
      }
      cb(null, uploadDir);
    },
    filename: (_req: any, file: any, cb: (error: Error | null, filename: string) => void) => {
      cb(null, file.originalname);
    }
  });

  const upload = multer({
    storage,
    limits: {
      fileSize: AVATAR_UPLOAD_LIMIT_BYTES
    },
    fileFilter: (_req: express.Request, file: any, cb: (error: Error | null, acceptFile?: boolean) => void) => {
      if (!isAllowedAvatar(file)) {
        cb(new Error('Only jpg, png, webp, or gif avatars are allowed'));
        return;
      }

      cb(null, true);
    }
  });

  app.post('/api/upload/avatar', (req: express.Request, res: express.Response) => {
    upload.single('avatar')(req, res, (error: Error) => {
      if (error) {
        log(LogLevel.warn, 'Avatar upload rejected:', error.message);
        const isSizeLimit = error.name === 'MulterError' && (error as any).code === 'LIMIT_FILE_SIZE';
        res.status(400).json({
          success: false,
          message: isSizeLimit ? 'Avatar file is too large' : error.message
        });
        return;
      }

      const request = req as any;
      if (!request.file) {
        res.status(400).json({ success: false, message: 'No file uploaded' });
        return;
      }

      const ext = safeAvatarExtension(request.file);
      if (!ext) {
        fs.unlink(request.file.path, () => undefined);
        res.status(400).json({ success: false, message: 'Unsupported avatar file type' });
        return;
      }

      const oldPath = request.file.path;
      const newFilename = `avatar_${uuid()}${ext}`;
      const newPath = path.join(path.dirname(oldPath), newFilename);

      fs.rename(oldPath, newPath, (err) => {
        if (err) {
          log(LogLevel.error, 'Error renaming file:', err);
          res.status(500).json({ success: false, message: 'Avatar rename failed' });
          return;
        }

        res.json({ success: true, avatarUrl: `/uploads/avatars/${newFilename}` });
      });
    });
  });

  const recordingRoot = getRecordingRoot();
  const recordingTempDir = path.join(recordingRoot, '.tmp');
  const serverAudioRecordings = new ServerAudioRecorderManager(recordingTempDir);
  const recordingStorage = multer.diskStorage({
    destination: (_req: any, _file: any, cb: (error: Error | null, destination: string) => void) => {
      if (!fs.existsSync(recordingTempDir)) {
        fs.mkdirSync(recordingTempDir, { recursive: true });
      }
      cb(null, recordingTempDir);
    },
    filename: (_req: any, file: any, cb: (error: Error | null, filename: string) => void) => {
      cb(null, `${uuid()}${safeRecordingExtension(file)}`);
    }
  });

  const recordingUpload = multer({
    storage: recordingStorage,
    limits: {
      fileSize: getRecordingUploadLimitBytes()
    },
    fileFilter: (_req: express.Request, file: any, cb: (error: Error | null, acceptFile?: boolean) => void) => {
      if (!isAllowedRecording(file)) {
        log(LogLevel.warn, 'Recording upload rejected by type filter:', {
          originalname: file.originalname,
          mimetype: file.mimetype,
          normalizedMimetype: normalizeMimeType(file.mimetype),
          extension: safeRecordingExtension(file)
        });
        cb(new Error('Only mp4 or webm recordings are allowed'));
        return;
      }

      cb(null, true);
    }
  });

  app.post('/api/server-audio-recordings/start', async (req: express.Request, res: express.Response) => {
    try {
      const offerSdp = req.body.offerSdp || req.body.sdp;
      const meetingId = sanitizePathSegment(req.body.meetingId, 'unknown');
      const started = await serverAudioRecordings.start({
        meetingId,
        offerSdp,
        iceServers: Array.isArray(req.body.iceServers) ? req.body.iceServers : undefined
      });

      res.json({
        success: true,
        recordingId: started.recordingId,
        meetingId: started.meetingId,
        answerSdp: started.answerSdp,
        candidates: started.candidates
      });
    } catch (error) {
      log(LogLevel.error, 'Failed to start server audio recording:', error);
      res.status(400).json({
        success: false,
        message: error instanceof Error ? error.message : 'Failed to start server audio recording'
      });
    }
  });

  app.post('/api/server-audio-recordings/:recordingId/candidate', async (req: express.Request, res: express.Response) => {
    try {
      const recordingId = sanitizePathSegment(req.params.recordingId, '');
      const candidate = req.body.candidate && typeof req.body.candidate === 'object'
        ? req.body.candidate
        : {
            candidate: req.body.candidate,
            sdpMid: req.body.sdpMid,
            sdpMLineIndex: req.body.sdpMLineIndex
          };
      const added = await serverAudioRecordings.addCandidate(recordingId, candidate);

      if (!added) {
        res.status(404).json({ success: false, message: 'Server audio recording not found' });
        return;
      }

      res.json({ success: true });
    } catch (error) {
      log(LogLevel.error, 'Failed to add server audio ICE candidate:', error);
      res.status(400).json({
        success: false,
        message: error instanceof Error ? error.message : 'Failed to add server audio ICE candidate'
      });
    }
  });

  app.delete('/api/server-audio-recordings/:recordingId', async (req: express.Request, res: express.Response) => {
    const recordingId = sanitizePathSegment(req.params.recordingId, '');
    const cancelled = await serverAudioRecordings.cancel(recordingId);
    if (!cancelled) {
      res.status(404).json({ success: false, message: 'Server audio recording not found' });
      return;
    }

    res.json({ success: true });
  });

  app.post('/api/server-audio-recordings/:recordingId/stop', (req: express.Request, res: express.Response) => {
    const recordingId = sanitizePathSegment(req.params.recordingId, '');
    const stopPromise = serverAudioRecordings.stop(recordingId);
    stopPromise.catch(() => undefined);

    recordingUpload.single('video')(req, res, async (error: Error) => {
      const request = req as any;
      let stopped = null;

      try {
        stopped = await stopPromise;

        if (!stopped) {
          removeFileIfExists(request.file && request.file.path);
          res.status(404).json({ success: false, message: 'Server audio recording not found' });
          return;
        }

        if (error) {
          removeFileIfExists(stopped.audioPath);
          log(LogLevel.warn, 'Server audio merge upload rejected:', error.message);
          const isSizeLimit = error.name === 'MulterError' && (error as any).code === 'LIMIT_FILE_SIZE';
          res.status(400).json({
            success: false,
            message: isSizeLimit ? 'Recording file is too large' : error.message
          });
          return;
        }

        if (!stopped.hasAudio) {
          removeFileIfExists(request.file && request.file.path);
          removeFileIfExists(stopped.audioPath);
          res.status(400).json({ success: false, message: 'No server audio was captured' });
          return;
        }

        if (!request.file) {
          res.json({
            success: true,
            recordingId: stopped.recordingId,
            meetingId: stopped.meetingId,
            audioOnly: true,
            audioTrackCount: stopped.audioTrackCount
          });
          return;
        }

        const videoExt = safeRecordingExtension(request.file);
        if (!videoExt) {
          removeFileIfExists(request.file.path);
          removeFileIfExists(stopped.audioPath);
          res.status(400).json({ success: false, message: 'Unsupported recording file type' });
          return;
        }

        const finalExt = getMergedRecordingExtension(videoExt);
        const meetingId = sanitizePathSegment(request.body.meetingId || stopped.meetingId, 'unknown');
        const originalFilename = path.basename(request.body.filename || request.file.originalname || `recording${finalExt}`);
        const userId = sanitizeMetadataString(request.body.userId, 120);
        const host = sanitizeRecordingPerson(request.body.host, 'host') || buildFallbackRecordingHost(userId);
        const participants = sanitizeRecordingParticipants(request.body.participants);
        const finalFilename = `${new Date().toISOString().replace(/[:.]/g, '-')}-${stopped.recordingId}${finalExt}`;
        const meetingDir = path.join(recordingRoot, meetingId);
        const finalPath = path.join(meetingDir, finalFilename);

        if (!isPathInside(recordingRoot, finalPath)) {
          removeFileIfExists(request.file.path);
          removeFileIfExists(stopped.audioPath);
          res.status(400).json({ success: false, message: 'Invalid recording path' });
          return;
        }

        if (!fs.existsSync(meetingDir)) {
          fs.mkdirSync(meetingDir, { recursive: true });
        }

        await mergeVideoWithServerAudio(request.file.path, stopped.audioPath, finalPath, finalExt);
        const stat = fs.statSync(finalPath);
        const metadata = {
          id: stopped.recordingId,
          meetingId,
          filename: finalFilename,
          originalFilename,
          mimetype: getRecordingMimeTypeFromExtension(finalExt),
          size: stat.size,
          userId,
          host,
          participants,
          serverAudio: {
            audioTrackCount: stopped.audioTrackCount,
            startedAt: new Date(stopped.createdAt).toISOString(),
            stoppedAt: new Date(stopped.stoppedAt).toISOString()
          },
          uploadedAt: new Date().toISOString()
        };
        fs.writeFileSync(path.join(meetingDir, `${finalFilename}.json`), JSON.stringify(metadata, null, 2));

        removeFileIfExists(request.file.path);
        removeFileIfExists(stopped.audioPath);

        res.json({
          success: true,
          recordingId: stopped.recordingId,
          meetingId,
          filename: finalFilename,
          originalFilename,
          size: stat.size,
          merged: true,
          url: `/api/recordings/${encodeURIComponent(meetingId)}/${encodeURIComponent(finalFilename)}/download`
        });
      } catch (mergeError) {
        removeFileIfExists(request.file && request.file.path);
        if (stopped) {
          removeFileIfExists(stopped.audioPath);
        }
        log(LogLevel.error, 'Failed to stop server audio recording:', mergeError);
        res.status(500).json({
          success: false,
          message: mergeError instanceof Error ? mergeError.message : 'Failed to stop server audio recording'
        });
      }
    });
  });

  app.get('/api/recordings', (_req: express.Request, res: express.Response) => {
    try {
      const recordings = listRecordings(recordingRoot);
      res.json({
        success: true,
        recordings,
        totalCount: recordings.length,
        root: recordingRoot
      });
    } catch (error) {
      log(LogLevel.error, 'Error listing recordings:', error);
      res.status(500).json({ success: false, message: 'Failed to list recordings' });
    }
  });

  app.post('/api/recordings', (req: express.Request, res: express.Response) => {
    recordingUpload.single('recording')(req, res, (error: Error) => {
      if (error) {
        log(LogLevel.warn, 'Recording upload rejected:', error.message);
        const isSizeLimit = error.name === 'MulterError' && (error as any).code === 'LIMIT_FILE_SIZE';
        res.status(400).json({
          success: false,
          message: isSizeLimit ? 'Recording file is too large' : error.message
        });
        return;
      }

      const request = req as any;
      if (!request.file) {
        res.status(400).json({ success: false, message: 'No recording uploaded' });
        return;
      }

      const ext = safeRecordingExtension(request.file);
      if (!ext) {
        fs.unlink(request.file.path, () => undefined);
        res.status(400).json({ success: false, message: 'Unsupported recording file type' });
        return;
      }

      const recordingId = uuid();
      const meetingId = sanitizePathSegment(request.body.meetingId, 'unknown');
      const originalFilename = path.basename(request.body.filename || request.file.originalname || `recording${ext}`);
      const userId = sanitizeMetadataString(request.body.userId, 120);
      const host = sanitizeRecordingPerson(request.body.host, 'host') || buildFallbackRecordingHost(userId);
      const participants = sanitizeRecordingParticipants(request.body.participants);
      const finalFilename = `${new Date().toISOString().replace(/[:.]/g, '-')}-${recordingId}${ext}`;
      const meetingDir = path.join(recordingRoot, meetingId);
      const finalPath = path.join(meetingDir, finalFilename);

      if (!isPathInside(recordingRoot, finalPath)) {
        fs.unlink(request.file.path, () => undefined);
        res.status(400).json({ success: false, message: 'Invalid recording path' });
        return;
      }

      fs.mkdir(meetingDir, { recursive: true }, (mkdirError) => {
        if (mkdirError) {
          fs.unlink(request.file.path, () => undefined);
          log(LogLevel.error, 'Error creating recording directory:', mkdirError);
          res.status(500).json({ success: false, message: 'Recording directory unavailable' });
          return;
        }

        fs.rename(request.file.path, finalPath, (renameError) => {
          if (renameError) {
            fs.unlink(request.file.path, () => undefined);
            log(LogLevel.error, 'Error saving recording:', renameError);
            res.status(500).json({ success: false, message: 'Recording save failed' });
            return;
          }

          const metadata = {
            id: recordingId,
            meetingId,
            filename: finalFilename,
            originalFilename,
            mimetype: getRecordingMimeTypeFromExtension(ext),
            size: request.file.size,
            userId,
            host,
            participants,
            uploadedAt: new Date().toISOString()
          };
          fs.writeFile(path.join(meetingDir, `${finalFilename}.json`), JSON.stringify(metadata, null, 2), () => undefined);

          res.json({
            success: true,
            recordingId,
            meetingId,
            filename: finalFilename,
            originalFilename,
            size: request.file.size,
            url: `/api/recordings/${encodeURIComponent(meetingId)}/${encodeURIComponent(finalFilename)}/download`
          });
        });
      });
    });
  });

  app.get('/api/recordings/:meetingId/:filename', (req: express.Request, res: express.Response) => {
    const meetingId = sanitizePathSegment(req.params.meetingId, 'unknown');
    const filename = sanitizePathSegment(req.params.filename, '');
    const ext = path.extname(filename).toLowerCase();

    if (!filename || !ALLOWED_RECORDING_EXTENSIONS.has(ext)) {
      res.status(400).json({ success: false, message: 'Invalid recording filename' });
      return;
    }

    const filePath = path.join(recordingRoot, meetingId, filename);
    if (!isPathInside(recordingRoot, filePath) || !fs.existsSync(filePath)) {
      res.status(404).json({ success: false, message: 'Recording not found' });
      return;
    }

    try {
      res.json({ success: true, recording: buildRecordingInfo(recordingRoot, meetingId, filename) });
    } catch (error) {
      log(LogLevel.error, 'Error reading recording:', error);
      res.status(500).json({ success: false, message: 'Failed to read recording' });
    }
  });

  app.patch('/api/recordings/:meetingId/:filename', (req: express.Request, res: express.Response) => {
    const meetingId = sanitizePathSegment(req.params.meetingId, 'unknown');
    const filename = sanitizePathSegment(req.params.filename, '');
    const nextMeetingId = sanitizePathSegment(req.body.meetingId, meetingId);
    const ext = path.extname(filename).toLowerCase();

    if (!filename || !ALLOWED_RECORDING_EXTENSIONS.has(ext)) {
      res.status(400).json({ success: false, message: 'Invalid recording filename' });
      return;
    }

    const sourceDir = path.join(recordingRoot, meetingId);
    const sourcePath = path.join(sourceDir, filename);
    const sourceMetadataPath = path.join(sourceDir, `${filename}.json`);
    const targetDir = path.join(recordingRoot, nextMeetingId);
    const targetPath = path.join(targetDir, filename);
    const targetMetadataPath = path.join(targetDir, `${filename}.json`);

    if (!isPathInside(recordingRoot, sourcePath) || !isPathInside(recordingRoot, targetPath)) {
      res.status(400).json({ success: false, message: 'Invalid recording path' });
      return;
    }

    if (!fs.existsSync(sourcePath)) {
      res.status(404).json({ success: false, message: 'Recording not found' });
      return;
    }

    if (sourcePath !== targetPath && fs.existsSync(targetPath)) {
      res.status(409).json({ success: false, message: 'Recording already exists in target meeting' });
      return;
    }

    try {
      if (!fs.existsSync(targetDir)) {
        fs.mkdirSync(targetDir, { recursive: true });
      }

      if (sourcePath !== targetPath) {
        fs.renameSync(sourcePath, targetPath);
        if (fs.existsSync(sourceMetadataPath)) {
          fs.renameSync(sourceMetadataPath, targetMetadataPath);
        }
      }

      const metadata = readRecordingMetadata(targetMetadataPath);
      const nextUserId = typeof req.body.userId === 'string' ? sanitizeMetadataString(req.body.userId, 120) : metadata.userId || '';
      const shouldSyncHostFromUserId = !metadata.host
        || metadata.host.userId === metadata.userId
        || metadata.host.id === metadata.userId;
      const nextHost = req.body.host !== undefined
        ? sanitizeRecordingPerson(req.body.host, 'host') || buildFallbackRecordingHost(nextUserId)
        : shouldSyncHostFromUserId ? buildFallbackRecordingHost(nextUserId) : metadata.host;
      const nextParticipants = req.body.participants !== undefined
        ? sanitizeRecordingParticipants(req.body.participants)
        : Array.isArray(metadata.participants) ? metadata.participants : [];
      const nextMetadata = {
        ...metadata,
        meetingId: nextMeetingId,
        filename,
        originalFilename: typeof req.body.originalFilename === 'string' && req.body.originalFilename.trim()
          ? path.basename(req.body.originalFilename.trim())
          : metadata.originalFilename || filename,
        userId: nextUserId,
        host: nextHost,
        participants: nextParticipants,
        mimetype: metadata.mimetype || getRecordingMimeTypeFromExtension(ext),
        size: fs.statSync(targetPath).size,
        uploadedAt: metadata.uploadedAt || fs.statSync(targetPath).birthtime.toISOString(),
        updatedAt: new Date().toISOString()
      };

      fs.writeFileSync(targetMetadataPath, JSON.stringify(nextMetadata, null, 2));
      if (sourcePath !== targetPath) {
        removeEmptyDirectory(sourceDir);
      }

      res.json({ success: true, recording: buildRecordingInfo(recordingRoot, nextMeetingId, filename) });
    } catch (error) {
      log(LogLevel.error, 'Error updating recording:', error);
      res.status(500).json({ success: false, message: 'Failed to update recording' });
    }
  });

  app.delete('/api/recordings/:meetingId/:filename', (req: express.Request, res: express.Response) => {
    const meetingId = sanitizePathSegment(req.params.meetingId, 'unknown');
    const filename = sanitizePathSegment(req.params.filename, '');
    const ext = path.extname(filename).toLowerCase();

    if (!filename || !ALLOWED_RECORDING_EXTENSIONS.has(ext)) {
      res.status(400).json({ success: false, message: 'Invalid recording filename' });
      return;
    }

    const meetingDir = path.join(recordingRoot, meetingId);
    const filePath = path.join(meetingDir, filename);
    const metadataPath = path.join(meetingDir, `${filename}.json`);
    if (!isPathInside(recordingRoot, filePath)) {
      res.status(400).json({ success: false, message: 'Invalid recording path' });
      return;
    }

    if (!fs.existsSync(filePath)) {
      res.status(404).json({ success: false, message: 'Recording not found' });
      return;
    }

    try {
      fs.unlinkSync(filePath);
      if (fs.existsSync(metadataPath)) {
        fs.unlinkSync(metadataPath);
      }
      removeEmptyDirectory(meetingDir);
      res.json({ success: true });
    } catch (error) {
      log(LogLevel.error, 'Error deleting recording:', error);
      res.status(500).json({ success: false, message: 'Failed to delete recording' });
    }
  });

  app.get('/api/recordings/:meetingId/:filename/stream', (req: express.Request, res: express.Response) => {
    const meetingId = sanitizePathSegment(req.params.meetingId, 'unknown');
    const filename = sanitizePathSegment(req.params.filename, '');
    const ext = path.extname(filename).toLowerCase();

    if (!filename || !ALLOWED_RECORDING_EXTENSIONS.has(ext)) {
      res.status(400).json({ success: false, message: 'Invalid recording filename' });
      return;
    }

    const filePath = path.join(recordingRoot, meetingId, filename);
    if (!isPathInside(recordingRoot, filePath) || !fs.existsSync(filePath)) {
      res.status(404).json({ success: false, message: 'Recording not found' });
      return;
    }

    const stat = fs.statSync(filePath);
    const range = req.headers.range;
    const contentType = ext === '.mp4' ? 'video/mp4' : 'video/webm';

    if (!range) {
      res.writeHead(200, {
        'Content-Length': stat.size,
        'Content-Type': contentType,
        'Accept-Ranges': 'bytes'
      });
      fs.createReadStream(filePath).pipe(res);
      return;
    }

    const parts = range.replace(/bytes=/, '').split('-');
    const start = parseInt(parts[0], 10);
    const end = parts[1] ? parseInt(parts[1], 10) : stat.size - 1;

    if (Number.isNaN(start) || Number.isNaN(end) || start >= stat.size || end >= stat.size || start > end) {
      res.status(416).set('Content-Range', `bytes */${stat.size}`).end();
      return;
    }

    res.writeHead(206, {
      'Content-Range': `bytes ${start}-${end}/${stat.size}`,
      'Accept-Ranges': 'bytes',
      'Content-Length': end - start + 1,
      'Content-Type': contentType
    });
    fs.createReadStream(filePath, { start, end }).pipe(res);
  });

  app.get('/api/recordings/:meetingId/:filename/download', (req: express.Request, res: express.Response) => {
    const meetingId = sanitizePathSegment(req.params.meetingId, 'unknown');
    const filename = sanitizePathSegment(req.params.filename, '');
    const ext = path.extname(filename).toLowerCase();

    if (!filename || !ALLOWED_RECORDING_EXTENSIONS.has(ext)) {
      res.status(400).json({ success: false, message: 'Invalid recording filename' });
      return;
    }

    const filePath = path.join(recordingRoot, meetingId, filename);
    if (!isPathInside(recordingRoot, filePath)) {
      res.status(400).json({ success: false, message: 'Invalid recording path' });
      return;
    }

    fs.access(filePath, fs.constants.R_OK, (accessError) => {
      if (accessError) {
        res.status(404).json({ success: false, message: 'Recording not found' });
        return;
      }

      res.download(filePath, filename);
    });
  });

  app.use('/uploads', express.static(path.join(__dirname, '../client/public/assets/uploads')));

  return app;
};
