const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const { PDFDocument } = require('pdf-lib');
const { WebSocketServer } = require('ws');
const http = require('http');

const app = express();
const server = http.createServer(app);
const PORT = process.env.PORT || 3000;

// Trust the reverse proxy (Nginx) so req.ip returns the real user IP
app.set('trust proxy', 1);

// ── Authentication ─────────────────────────────────────────

let currentAppPassword = process.env.APP_PASSWORD || null;
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || null;

// Generate unique session tokens every time the server starts
const USER_TOKEN = uuidv4(); 
const ADMIN_TOKEN = uuidv4(); 

function requireAuth(req, res, next) {
  if (!currentAppPassword && !ADMIN_PASSWORD) {
    req.userRole = 'admin';
    return next();
  }

  const token = req.headers['x-auth-token'];
  
  if (ADMIN_PASSWORD && token === ADMIN_TOKEN) {
    req.userRole = 'admin';
    return next();
  }
  
  if (currentAppPassword && token === USER_TOKEN) {
    req.userRole = 'user';
    return next();
  }
  
  res.status(401).json({ error: 'Unauthorized' });
}

function requireAdmin(req, res, next) {
  if (req.userRole === 'admin') return next();
  res.status(403).json({ error: 'Forbidden: Admins only' });
}

// ── Brute Force Protection ─────────────────────────────────

const loginAttempts = new Map();

function checkBruteForce(req, res, next) {
  const ip = req.ip;
  const record = loginAttempts.get(ip);
  if (record && record.lockoutUntil && Date.now() < record.lockoutUntil) {
    const remaining = Math.ceil((record.lockoutUntil - Date.now()) / 1000);
    return res.status(429).json({ error: `Too many attempts. Try again in ${remaining} seconds.` });
  }
  next();
}

function handleFailedLogin(ip) {
  let record = loginAttempts.get(ip) || { attempts: 0, lockoutUntil: 0 };
  record.attempts += 1;
  
  if (record.attempts >= 5) {
    // 5th attempt = 1 min, 6th = 2 mins, 7th = 4 mins, etc.
    const lockMinutes = Math.pow(2, record.attempts - 5);
    record.lockoutUntil = Date.now() + (lockMinutes * 60 * 1000);
  }
  loginAttempts.set(ip, record);
  return record;
}

function handleSuccessfulLogin(ip) {
  loginAttempts.delete(ip);
}

// ── Upload Storage ─────────────────────────────────────────

const UPLOAD_DIR = path.join(__dirname, 'uploads');
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });

const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, UPLOAD_DIR),
  filename: (req, file, cb) => {
    const sessionId = uuidv4();
    const ext = path.extname(file.originalname);
    cb(null, `${sessionId}${ext}`);
  }
});

const upload = multer({
  storage,
  limits: { fileSize: 500 * 1024 * 1024 }, // 500 MB
  fileFilter: (req, file, cb) => {
    if (file.mimetype === 'application/pdf') cb(null, true);
    else cb(new Error('Only PDF files are allowed'));
  }
});

// ── Session Tracking ───────────────────────────────────────
// Map sessionId -> { filePath, originalName, connectedClients, lastAccess }

const sessions = new Map();

function touchSession(sessionId) {
  const s = sessions.get(sessionId);
  if (s) s.lastAccess = Date.now();
}

function cleanupSession(sessionId) {
  const session = sessions.get(sessionId);
  if (!session) return;

  // Only delete if no clients are connected
  if (session.connectedClients > 0) return;

  try {
    if (fs.existsSync(session.filePath)) {
      fs.unlinkSync(session.filePath);
      console.log(`[cleanup] Deleted: ${session.originalName} (${sessionId})`);
    }
  } catch (e) {
    console.warn(`[cleanup] Failed to delete ${sessionId}:`, e.message);
  }
  sessions.delete(sessionId);
}

// Periodic cleanup: delete sessions idle for > 10 minutes
setInterval(() => {
  const now = Date.now();
  for (const [id, session] of sessions) {
    if (session.connectedClients <= 0 && now - session.lastAccess > 10 * 60 * 1000) {
      cleanupSession(id);
    }
  }
}, 60 * 1000); // check every minute

// ── WebSocket for Connection Tracking ──────────────────────

const wss = new WebSocketServer({ server });

wss.on('connection', (ws) => {
  let wsSessionId = null;

  ws.on('message', (msg) => {
    try {
      const data = JSON.parse(msg);
      if (data.type === 'register' && data.sessionId) {
        // Enforce auth on websocket
        if (currentAppPassword || ADMIN_PASSWORD) {
          const isAdmin = ADMIN_PASSWORD && data.token === ADMIN_TOKEN;
          const isUser = currentAppPassword && data.token === USER_TOKEN;
          if (!isAdmin && !isUser) return;
        }

        wsSessionId = data.sessionId;
        const session = sessions.get(wsSessionId);
        if (session) {
          session.connectedClients++;
          touchSession(wsSessionId);
          console.log(`[ws] Client connected to session ${wsSessionId} (${session.connectedClients} clients)`);
        }
      }
    } catch (e) { /* ignore */ }
  });

  ws.on('close', () => {
    if (wsSessionId) {
      const session = sessions.get(wsSessionId);
      if (session) {
        session.connectedClients--;
        console.log(`[ws] Client disconnected from session ${wsSessionId} (${session.connectedClients} remaining)`);
        // Schedule cleanup after a short delay (in case of page refresh)
        setTimeout(() => {
          const s = sessions.get(wsSessionId);
          if (s && s.connectedClients <= 0) {
            cleanupSession(wsSessionId);
          }
        }, 30 * 1000); // 30 second grace period for refreshes
      }
    }
  });
});

// ── Static Files ───────────────────────────────────────────

app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());

// ── API: Auth ──────────────────────────────────────────────

app.get('/api/check-auth', requireAuth, (req, res) => {
  res.json({ ok: true, role: req.userRole });
});

app.post('/api/login', checkBruteForce, (req, res) => {
  if (!currentAppPassword && !ADMIN_PASSWORD) {
    return res.json({ token: 'no-auth', role: 'admin' });
  }

  const { password } = req.body;
  const ip = req.ip;
  
  if (ADMIN_PASSWORD && password === ADMIN_PASSWORD) {
    handleSuccessfulLogin(ip);
    return res.json({ token: ADMIN_TOKEN, role: 'admin' });
  }
  
  if (currentAppPassword && password === currentAppPassword) {
    handleSuccessfulLogin(ip);
    return res.json({ token: USER_TOKEN, role: 'user' });
  }
  
  const record = handleFailedLogin(ip);
  
  if (record.lockoutUntil > Date.now()) {
    const remaining = Math.ceil((record.lockoutUntil - Date.now()) / 1000);
    return res.status(429).json({ error: `Too many attempts. Locked out for ${remaining} seconds.` });
  }
  
  res.status(401).json({ error: 'Invalid password' });
});

// ── API: Admin ─────────────────────────────────────────────

app.get('/api/admin/stats', requireAuth, requireAdmin, (req, res) => {
  const mem = process.memoryUsage();
  
  const blockedIps = [];
  for (const [ip, record] of loginAttempts) {
    if (record.lockoutUntil > Date.now()) {
      blockedIps.push({ ip, attempts: record.attempts, lockoutRemaining: Math.ceil((record.lockoutUntil - Date.now()) / 1000) });
    }
  }

  res.json({
    uptime: process.uptime(),
    memoryUsedMB: Math.round(mem.rss / 1024 / 1024),
    activeSessions: sessions.size,
    loginAttemptsTracked: loginAttempts.size,
    blockedIps
  });
});

app.post('/api/admin/clear-cache', requireAuth, requireAdmin, (req, res) => {
  let deletedCount = 0;
  for (const [id, session] of sessions) {
    try {
      if (fs.existsSync(session.filePath)) {
        fs.unlinkSync(session.filePath);
        deletedCount++;
      }
    } catch (e) { /* ignore */ }
  }
  sessions.clear();
  res.json({ success: true, deletedCount });
});

app.post('/api/admin/clear-logins', requireAuth, requireAdmin, (req, res) => {
  loginAttempts.clear();
  res.json({ success: true });
});

app.post('/api/admin/change-password', requireAuth, requireAdmin, (req, res) => {
  const { newPassword } = req.body;
  currentAppPassword = newPassword || null;
  res.json({ success: true });
});

// ── API: Upload PDF ────────────────────────────────────────

app.post('/api/upload', requireAuth, upload.single('pdf'), (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file uploaded' });

  const sessionId = path.basename(req.file.filename, path.extname(req.file.filename));

  sessions.set(sessionId, {
    filePath: req.file.path,
    originalName: req.file.originalname,
    connectedClients: 0,
    lastAccess: Date.now()
  });

  console.log(`[upload] ${req.file.originalname} -> session ${sessionId}`);

  res.json({
    sessionId,
    originalName: req.file.originalname,
    fileSize: req.file.size
  });
});

// ── API: Serve PDF for client-side rendering ───────────────

app.get('/api/pdf/:sessionId', requireAuth, (req, res) => {
  const session = sessions.get(req.params.sessionId);
  if (!session || !fs.existsSync(session.filePath)) {
    return res.status(404).json({ error: 'Session not found' });
  }
  touchSession(req.params.sessionId);
  res.setHeader('Content-Type', 'application/pdf');
  res.sendFile(session.filePath);
});

// ── API: Split PDF ─────────────────────────────────────────

app.post('/api/split', requireAuth, async (req, res) => {
  const { sessionId, pageNumbers } = req.body;

  const session = sessions.get(sessionId);
  if (!session || !fs.existsSync(session.filePath)) {
    return res.status(404).json({ error: 'Session not found' });
  }
  touchSession(sessionId);

  try {
    const inputBytes = fs.readFileSync(session.filePath);
    const srcDoc = await PDFDocument.load(inputBytes, {
      ignoreEncryption: true,
      updateMetadata: false
    });
    const outDoc = await PDFDocument.create();

    const sorted = [...pageNumbers].sort((a, b) => a - b);
    const indices = sorted.map(p => p - 1).filter(i => i >= 0 && i < srcDoc.getPageCount());

    const copiedPages = await outDoc.copyPages(srcDoc, indices);
    copiedPages.forEach(page => outDoc.addPage(page));

    const outBytes = await outDoc.save();

    const outputName = session.originalName.replace(/\.pdf$/i, '') + '_split.pdf';
    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader('Content-Disposition', `attachment; filename="${outputName}"`);
    res.send(Buffer.from(outBytes));
  } catch (err) {
    console.error('[split] Error:', err);
    res.status(500).json({ error: err.message });
  }
});

// ── Start Server ───────────────────────────────────────────

server.listen(PORT, '0.0.0.0', () => {
  console.log(`PDF Splitter running at http://0.0.0.0:${PORT}`);
});
