const express = require('express');
const fs = require('fs').promises;
const path = require('path');
const JSONbig = require('json-bigint')({ storeAsString: true });

const app = express();

// Используем json-bigint, чтобы не терять точность больших чисел
app.use(express.text({ type: 'application/json' }));
app.use((req, res, next) => {
  if (req.body && typeof req.body === 'string') {
    try {
      req.body = JSONbig.parse(req.body);
    } catch (e) {
      return res.status(400).json({ error: 'Invalid JSON' });
    }
  }
  next();
});

const UPLOAD_BASE = process.env.UPLOAD_PATH || '/app/uploads';

app.post('/api/files/merge', async (req, res) => {
  try {
    const { userId, fileId, parts, fileName } = req.body;
    
    // Защита от выхода за пределы каталога (path traversal)
    if (!fileName || typeof fileName !== 'string') {
      return res.status(400).json({ error: 'Invalid fileName parameter' });
    }

    // Блокируем попытки обхода пути
    if (fileName.includes('..') || fileName.includes('/') || fileName.includes('\\')) {
      console.error(`Заблокирована попытка обхода пути - fileName: ${fileName}, IP: ${req.ip}`);
      return res.status(400).json({ error: 'Invalid fileName: path traversal not allowed' });
    }

    // Очищаем имя файла: оставляем только буквы, цифры, дефис, подчёркивание и точки
    const sanitizedFileName = fileName.replace(/[^a-zA-Z0-9._-]/g, '_');
    if (sanitizedFileName !== fileName) {
      console.warn(`Имя файла очищено: "${fileName}" -> "${sanitizedFileName}"`);
    }
    
    console.log(`Merging file from local filesystem: userId=${userId}, fileId=${fileId}, parts=${parts}, fileName=${fileName}`);
    
    // Части файла хранятся в формате: {userId}-{actualFileId}-{partNumber}
    // fileId из command-server может отличаться от того, что использует file-server,
    // поэтому реальный fileId находим, перебирая файлы этого пользователя

    console.log(`Searching for files with userId: ${userId}`);
    
    // Получаем список всех файлов этого пользователя
    const allFiles = await fs.readdir(UPLOAD_BASE);
    const userFiles = allFiles.filter(f => f.startsWith(`${userId}-`));
    
    console.log(`Found ${userFiles.length} files for user ${userId}`);
    
    if (userFiles.length === 0) {
      return res.status(404).json({ error: 'No files found for user' });
    }
    
    // Группируем файлы по fileId (берём его из имени: userId-fileId-partNumber)
    const fileGroups = {};
    for (const file of userFiles) {
      const match = file.match(/^(\d+)-(-?\d+)-(\d+)$/);
      if (match) {
        const [, uid, fid, partNum] = match;
        if (!fileGroups[fid]) {
          fileGroups[fid] = [];
        }
        fileGroups[fid].push({ file, partNum: parseInt(partNum) });
      }
    }
    
    console.log(`Found ${Object.keys(fileGroups).length} file groups:`, Object.keys(fileGroups));
    
    // Ищем группу с нужным количеством частей
    let actualFileId = null;
    let partFiles = null;

    for (const [fid, files] of Object.entries(fileGroups)) {
      if (files.length === parts) {
        actualFileId = fid;
        partFiles = files.sort((a, b) => a.partNum - b.partNum);
        console.log(`Found matching file group: fileId=${fid}, parts=${files.length}`);
        break;
      }
    }

    if (!actualFileId || !partFiles) {
      console.error(`No file group with ${parts} parts found`);
      console.log('Available groups:', Object.entries(fileGroups).map(([fid, files]) => `${fid} (${files.length} parts)`));
      return res.status(404).json({ error: `No file group with ${parts} parts found` });
    }
    
    // Проверяем, что все части на месте
    const partPaths = [];
    for (let i = 0; i < parts; i++) {
      const partFile = partFiles.find(f => f.partNum === i);
      if (!partFile) {
        console.error(`Missing part ${i}`);
        return res.status(404).json({ error: `Missing file part ${i}` });
      }
      partPaths.push(path.join(UPLOAD_BASE, partFile.file));
    }
    
    console.log(`All ${parts} parts found, merging...`);
    
    // Склеиваем части
    const mergedDir = path.join(UPLOAD_BASE, 'merged');
    await fs.mkdir(mergedDir, { recursive: true });

    // Используем очищенное имя файла
    const extension = sanitizedFileName.split('.').pop() || '';
    const mergedFileName = `${userId}-${actualFileId}${extension ? '.' + extension : ''}`;

    // Дополнительно убеждаемся, что итоговый путь остаётся внутри разрешённого каталога
    const mergedFilePath = path.join(mergedDir, mergedFileName);
    const resolvedPath = path.resolve(mergedFilePath);
    const resolvedMergedDir = path.resolve(mergedDir);

    if (!resolvedPath.startsWith(resolvedMergedDir)) {
      console.error(`Обнаружен выход за пределы каталога. Запрошенный путь: ${resolvedPath}`);
      return res.status(400).json({ error: 'Invalid file path' });
    }
    
    const writeStream = require('fs').createWriteStream(mergedFilePath);
    let totalSize = 0;
    
    // Ограничение на размер файла (максимум 2 ГБ)
    const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024; // 2 ГБ

    for (const partPath of partPaths) {
      const partData = await fs.readFile(partPath);

      // Проверяем размер перед записью
      if (totalSize + partData.length > MAX_FILE_SIZE) {
        console.error(`Превышен лимит размера файла - запрошенный размер: ${totalSize + partData.length}, лимит: ${MAX_FILE_SIZE}`);

        // Удаляем недописанный файл
        try {
          writeStream.close();
          await fs.unlink(mergedFilePath);
        } catch (e) {}
        
        return res.status(413).json({ error: 'File too large: Maximum size is 2GB' });
      }
      
      writeStream.write(partData);
      totalSize += partData.length;
    }
    
    console.log(`Проверка размера пройдена: ${totalSize} bytes (limit: ${MAX_FILE_SIZE})`);
    
    writeStream.end();
    
    await new Promise((resolve, reject) => {
      writeStream.on('finish', resolve);
      writeStream.on('error', reject);
    });
    
    console.log(`Merged ${parts} parts into ${mergedFileName}, total size: ${totalSize} bytes`);

    // Удаляем части после склейки
    for (const partPath of partPaths) {
      try {
        await fs.unlink(partPath);
      } catch (e) {
        console.error(`Failed to remove part ${partPath}:`, e.message);
      }
    }
    
    res.json({
      filePath: path.join('merged', mergedFileName),
      totalSize: totalSize
    });
    
  } catch (error) {
    console.error('Merge error:', error);
    res.status(500).json({ error: error.message });
  }
});

app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
  console.log(`File merge proxy listening on port ${PORT}`);
  console.log(`Upload base path: ${UPLOAD_BASE}`);
});
