/**
 * Chunked upload route
 *
 * POST /api/upload/init         — start upload session, returns uploadId
 * POST /api/upload/chunk        — send a chunk (multipart/form-data or raw body)
 * POST /api/upload/complete     — assemble chunks into final file
 * GET  /api/upload/:uploadId/status — check progress
 *
 * Each chunk is sent as raw binary body with headers:
 *   X-Upload-Id: <uploadId>
 *   X-Chunk-Index: <0-based index>
 *   X-Total-Chunks: <total>
 *   Content-Type: application/octet-stream
 */
import fs from 'fs';
import { promises as fsp } from 'fs';
import path from 'path';
import { Router, Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { config } from '../config';
import { probeVideo } from '../services/ffmpeg';
import { validateMimeType } from '../middleware/security';

const router = Router();

interface UploadSession {
  uploadId: string;
  filename: string;
  mimeType: string;
  totalChunks: number;
  receivedChunks: Set<number>;
  tmpDir: string;
  createdAt: number;
}

const sessions = new Map<string, UploadSession>();

// POST /api/upload/init
router.post('/init', (req: Request, res: Response) => {
  const { filename, mimeType, totalChunks } = req.body as {
    filename?: string;
    mimeType?: string;
    totalChunks?: number;
  };

  if (!filename || !mimeType || !totalChunks) {
    res.status(400).json({ error: 'Missing: filename, mimeType, totalChunks' });
    return;
  }

  if (!validateMimeType(mimeType)) {
    res.status(400).json({ error: `Unsupported MIME type: ${mimeType}` });
    return;
  }

  const uploadId = uuidv4();
  const tmpDir = path.join(config.videosDir, 'tmp', uploadId);
  fs.mkdirSync(tmpDir, { recursive: true });

  sessions.set(uploadId, {
    uploadId,
    filename,
    mimeType,
    totalChunks: Number(totalChunks),
    receivedChunks: new Set(),
    tmpDir,
    createdAt: Date.now(),
  });

  res.status(201).json({ uploadId });
});

// POST /api/upload/chunk  (raw binary body)
router.post('/chunk', (req: Request, res: Response) => {
  const uploadId = req.headers['x-upload-id'] as string;
  const chunkIndex = parseInt(req.headers['x-chunk-index'] as string, 10);

  if (!uploadId || isNaN(chunkIndex)) {
    res.status(400).json({ error: 'Missing X-Upload-Id or X-Chunk-Index header' });
    return;
  }

  const session = sessions.get(uploadId);
  if (!session) {
    res.status(404).json({ error: 'Upload session not found' });
    return;
  }

  if (chunkIndex < 0 || chunkIndex >= session.totalChunks) {
    res.status(400).json({ error: 'Invalid chunk index' });
    return;
  }

  const chunkPath = path.join(session.tmpDir, `chunk_${chunkIndex.toString().padStart(6, '0')}`);
  const writeStream = fs.createWriteStream(chunkPath);

  req.pipe(writeStream);

  writeStream.on('finish', () => {
    session.receivedChunks.add(chunkIndex);
    res.json({
      chunkIndex,
      received: session.receivedChunks.size,
      total: session.totalChunks,
    });
  });

  writeStream.on('error', (err) => {
    if (!res.headersSent) {
      res.status(500).json({ error: 'Chunk write failed', detail: String(err) });
    }
  });
});

// POST /api/upload/complete
router.post('/complete', async (req: Request, res: Response) => {
  const { uploadId } = req.body as { uploadId?: string };

  if (!uploadId) {
    res.status(400).json({ error: 'Missing uploadId' });
    return;
  }

  const session = sessions.get(uploadId);
  if (!session) {
    res.status(404).json({ error: 'Upload session not found' });
    return;
  }

  if (session.receivedChunks.size !== session.totalChunks) {
    res.status(400).json({
      error: 'Not all chunks received',
      received: session.receivedChunks.size,
      total: session.totalChunks,
    });
    return;
  }

  // Sanitize filename: keep extension, replace unsafe chars
  const ext = path.extname(session.filename).toLowerCase() || '.mp4';
  const baseName = path.basename(session.filename, ext).replace(/[^a-zA-Z0-9_\-]/g, '_');
  const finalName = `${baseName}_${uploadId.slice(0, 8)}${ext}`;
  const finalPath = path.join(config.videosDir, finalName);

  try {
    const writeStream = fs.createWriteStream(finalPath);

    for (let i = 0; i < session.totalChunks; i++) {
      const chunkPath = path.join(session.tmpDir, `chunk_${i.toString().padStart(6, '0')}`);
      const data = await fsp.readFile(chunkPath);
      await new Promise<void>((resolve, reject) => {
        writeStream.write(data, (err) => {
          if (err) reject(err);
          else resolve();
        });
      });
    }

    await new Promise<void>((resolve, reject) => {
      writeStream.end((err?: Error | null) => {
        if (err) reject(err);
        else resolve();
      });
    });

    // Cleanup tmp dir
    await fsp.rm(session.tmpDir, { recursive: true, force: true });
    sessions.delete(uploadId);

    // Probe final file
    try {
      const probe = await probeVideo(finalPath);
      console.log(`[upload] complete: ${finalName}, duration: ${probe.format.duration}s`);
    } catch (err) {
      console.error('[upload] probe failed:', err);
    }

    res.json({ videoId: finalName, filename: finalName });
  } catch (err) {
    res.status(500).json({ error: 'Assembly failed', detail: String(err) });
  }
});

// GET /api/upload/:uploadId/status
router.get('/:uploadId/status', (req: Request, res: Response) => {
  const { uploadId } = req.params;
  const session = sessions.get(uploadId);
  if (!session) {
    res.status(404).json({ error: 'Upload session not found' });
    return;
  }
  res.json({
    uploadId,
    filename: session.filename,
    received: session.receivedChunks.size,
    total: session.totalChunks,
    status: session.receivedChunks.size === session.totalChunks ? 'complete' : 'uploading',
  });
});

export default router;
