require('dotenv').config();

const crypto = require('crypto');
const express = require('express');
const path = require('path');
const fs = require('fs');
const cors = require('cors');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');
const passport = require('./auth/google');
const authMiddleware = require('./middleware/auth');
const db = require('./db');
const { getUserRank, upsertLeaderboardEntry } = require('./services/leaderboard');
const { calculateElo, DEFAULT_ELO } = require('./services/elo');
const { generateStage } = require('./services/questions');

const VALID_GRADES = new Set(['G3', 'G6', 'G9']);

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'too_many_attempts' },
  skipSuccessfulRequests: true,
});

const registerLimiter = rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 5,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'too_many_attempts' },
});

const authLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 20,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'too_many_attempts' },
});
const USERNAME_RE = /^[a-zA-Z0-9_]{3,20}$/;
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

function issueJwt(user) {
  return jwt.sign(
    { userId: user.id, email: user.email, name: user.displayName, picture: user.avatarUrl ?? null },
    process.env.JWT_SECRET,
    { algorithm: 'HS256', expiresIn: '7d' },
  );
}

async function generateUniqueUsernameFromEmail(email) {
  const base = (email.split('@')[0].replace(/\./g, '_').replace(/[^a-zA-Z0-9_]/g, '').slice(0, 15).toLowerCase()) || 'user';
  if (!await db.user.findFirst({ where: { username: base } })) return base;
  for (let i = 0; i < 20; i++) {
    const candidate = `${base}_${String(Math.floor(1000 + Math.random() * 9000))}`;
    if (!await db.user.findFirst({ where: { username: candidate } })) return candidate;
  }
  return `${base}_${Date.now()}`;
}
const STAGE_SIZE = 11;
const MIN_GAME_MS = 8000;   // fastest a human can answer 11 questions

// ── In-memory matchmaking ──────────────────────────────────────
const queue = {};          // { userId: { grade, joinedAt, displayName, avatarUrl } }
const playerGames = {};    // { userId: gameId }
const activeGames = {};    // { gameId: { ... } }

function generateGameId() {
  return crypto.randomBytes(8).toString('hex');
}

function computeGameResult(game) {
  const players = Object.keys(game.players);
  const p0 = game.players[players[0]];
  const p1 = game.players[players[1]];

  if (p0.finished && p1.finished) {
    if (p0.timeSpentMs === p1.timeSpentMs) return players[Math.random() < 0.5 ? 0 : 1];
    return p0.timeSpentMs < p1.timeSpentMs ? players[0] : players[1];
  }
  if (p0.finished) return players[0];
  if (p1.finished) return players[1];
  return null;
}

async function completeGame(game, forcedWinnerId = null) {
  if (game.completing || game.status !== 'active') return;
  game.completing = true;

  if (game.abandonTimeout) {
    clearTimeout(game.abandonTimeout);
    game.abandonTimeout = null;
  }

  const players = Object.keys(game.players);
  const winnerId = forcedWinnerId ?? computeGameResult(game);
  game.winner = winnerId;
  game.completedAt = Date.now();

  // No winner (e.g. both disconnected) — skip ELO, just clean up
  if (!winnerId) {
    for (const uid of players) delete playerGames[uid];
    game.status = 'completed';
    setTimeout(() => { delete activeGames[game.id]; }, 30000);
    return;
  }

  // Snapshot both ELOs before any writes, then write atomically-as-possible
  try {
    const eloSnapshot = {};
    for (const uid of players) {
      const entry = await db.leaderboardEntry.findUnique({
        where: { userId_grade: { userId: uid, grade: game.grade } },
      });
      eloSnapshot[uid] = entry?.elo ?? DEFAULT_ELO;
    }

    const loserId = players.find(id => id !== winnerId);
    const { winnerChange, loserChange } = calculateElo(eloSnapshot[winnerId], eloSnapshot[loserId]);

    for (const userId of players) {
      const p = game.players[userId];
      const isWinner = userId === winnerId;
      const change = isWinner ? winnerChange : loserChange;
      const newElo = eloSnapshot[userId] + change;

      await upsertLeaderboardEntry(userId, game.grade, {
        elo: newElo,
        bestTime: p.completedQuiz ? (p.timeSpentMs ?? undefined) : undefined,
        correctAnswers: p.correctAnswers,
      });

      p.eloChange = change;
      p.newElo = newElo;

      delete playerGames[userId];
    }

    // Record per-player match results for dashboard history
    await Promise.all(players.map(userId => {
      const p = game.players[userId];
      return db.matchResult.create({
        data: {
          userId,
          grade: game.grade,
          won: userId === winnerId,
          eloChange: p.eloChange,
          eloAfter: p.newElo,
          timeSpentMs: p.timeSpentMs ?? null,
        },
      });
    }));
  } catch (err) {
    console.error('[completeGame] DB error during ELO write:', err);
    // Clean up playerGames refs even on failure so players aren't stuck
    for (const uid of players) delete playerGames[uid];
  }

  // Set completed only after ELO is written so polls see final eloChange
  game.status = 'completed';

  setTimeout(() => {
    delete activeGames[game.id];
  }, 30000);
}

// Cleanup stale queue entries (30s no heartbeat) and old/ghost games every 30s
setInterval(() => {
  const now = Date.now();
  for (const [userId, q] of Object.entries(queue)) {
    if (now - q.lastSeen > 30000) delete queue[userId];
  }
  for (const [id, game] of Object.entries(activeGames)) {
    if (game.status === 'completed' && game.completedAt && now - game.completedAt > 60000) {
      delete activeGames[id];
      continue;
    }
    // Ghost game: all players have been silent for 60+ seconds — purge without ELO changes
    if (game.status === 'active' && !game.completing) {
      const allInactive = Object.values(game.players).every(
        p => p.lastSeen && now - p.lastSeen > 60000,
      );
      if (allInactive) {
        console.log(`[cleanup] purging ghost game ${id} — all players inactive >60s`);
        for (const uid of Object.keys(game.players)) delete playerGames[uid];
        delete activeGames[id];
      }
    }
  }
}, 30000);

const app = express();

app.use(express.json({ limit: '3mb' }));
app.use(cors({ origin: process.env.FRONTEND_URL, credentials: true }));
app.use(passport.initialize());

const UPLOADS_DIR = path.join(__dirname, '..', 'uploads');
if (!fs.existsSync(UPLOADS_DIR)) fs.mkdirSync(UPLOADS_DIR, { recursive: true });
app.use('/uploads', express.static(UPLOADS_DIR));

app.get('/auth/google', authLimiter, passport.authenticate('google', {
  scope: ['profile', 'email'],
  session: false,
}));

app.get('/auth/google/callback', authLimiter, (req, res, next) => {
  passport.authenticate('google', { session: false }, (err, result) => {
    if (err || !result) {
      console.error('[auth/google/callback] failure:', err);
      return res.redirect(`${process.env.FRONTEND_URL}/?error=auth_failed`);
    }
    res.redirect(`${process.env.FRONTEND_URL}/dashboard?token=${result.token}`);
  })(req, res, next);
});

app.post('/auth/register', registerLimiter, async (req, res) => {
  const { email, password, username } = req.body;
  if (!email || !EMAIL_RE.test(email)) return res.status(400).json({ error: 'invalid_email' });
  if (!password || password.length < 8) return res.status(400).json({ error: 'password_too_short' });

  let finalUsername = username ? username.trim() : null;
  if (finalUsername) {
    if (!USERNAME_RE.test(finalUsername)) return res.status(400).json({ error: 'invalid_username' });
    finalUsername = finalUsername.toLowerCase();
  }

  try {
    if (await db.user.findUnique({ where: { email } })) return res.status(409).json({ error: 'email_taken' });
    if (!finalUsername) finalUsername = await generateUniqueUsernameFromEmail(email);
    if (await db.user.findFirst({ where: { username: finalUsername } })) return res.status(409).json({ error: 'username_taken' });

    const passwordHash = await bcrypt.hash(password, 12);
    const user = await db.user.create({
      data: { email, displayName: finalUsername, username: finalUsername, passwordHash, avatarUrl: null },
    });
    res.json({ token: issueJwt(user) });
  } catch (err) {
    console.error('[auth/register]', err);
    if (err.code === 'P2002') {
      return res.status(409).json({ error: err.meta?.target?.includes('email') ? 'email_taken' : 'username_taken' });
    }
    res.status(500).json({ error: 'internal' });
  }
});

app.post('/auth/login', loginLimiter, async (req, res) => {
  const { email, password } = req.body;
  if (!email || !password) return res.status(400).json({ error: 'missing_fields' });
  try {
    const user = await db.user.findUnique({ where: { email } });
    if (!user) return res.status(401).json({ error: 'invalid_credentials' });
    if (!user.passwordHash) return res.status(401).json({ error: 'use_google' });
    if (!await bcrypt.compare(password, user.passwordHash)) return res.status(401).json({ error: 'invalid_credentials' });
    res.json({ token: issueJwt(user) });
  } catch (err) {
    console.error('[auth/login]', err);
    res.status(500).json({ error: 'internal' });
  }
});

app.get('/auth/me', authMiddleware, async (req, res) => {
  try {
    const user = await db.user.findUnique({ where: { id: req.user.userId } });
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.json({
      id: user.id,
      displayName: user.displayName,
      avatarUrl: user.avatarUrl,
      email: user.email,
      username: user.username,
    });
  } catch (err) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.get('/leaderboard', async (req, res) => {
  const { grade, type = 'time' } = req.query;
  if (!VALID_GRADES.has(grade)) {
    return res.status(400).json({ error: 'grade must be one of G3, G6, G9' });
  }
  const byElo = type === 'elo';
  try {
    const entries = await db.leaderboardEntry.findMany({
      where: byElo ? { grade, gamesPlayed: { gt: 0 } } : { grade, bestTime: { not: null } },
      orderBy: byElo ? { elo: 'desc' } : { bestTime: 'asc' },
      take: 10,
      include: {
        user: { select: { displayName: true, username: true, avatarUrl: true } },
      },
    });
    res.json(entries.map((e, i) => ({
      rank: i + 1,
      displayName: e.user.displayName,
      username: e.user.username,
      avatarUrl: e.user.avatarUrl,
      bestTime: e.bestTime,
      elo: e.elo,
    })));
  } catch {
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.get('/me/stats', authMiddleware, async (req, res) => {
  try {
    const entries = await Promise.all(
      ['G3', 'G6', 'G9'].map(async grade => {
        const [eloResult, timeResult, totalPlayers, recentMatches] = await Promise.all([
          getUserRank(req.user.userId, grade, 'elo'),
          getUserRank(req.user.userId, grade, 'bestTime'),
          db.leaderboardEntry.count({ where: { grade, gamesPlayed: { gt: 0 } } }),
          db.matchResult.findMany({
            where: { userId: req.user.userId, grade },
            orderBy: { playedAt: 'desc' },
            take: 20,
          }),
        ]);
        const entry = eloResult?.entry ?? null;
        let winStreak = 0;
        for (const m of recentMatches) {
          if (m.won) winStreak++;
          else break;
        }
        return {
          grade,
          elo:          entry?.elo          ?? 800,
          bestTime:     entry?.bestTime      ?? null,
          gamesPlayed:  entry?.gamesPlayed   ?? 0,
          totalCorrect: entry?.totalCorrect  ?? 0,
          eloRank:      eloResult?.rank      ?? null,
          timeRank:     timeResult?.rank     ?? null,
          totalPlayers,
          winStreak,
        };
      })
    );
    res.json({ entries });
  } catch {
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.get('/me/recent-matches', authMiddleware, async (req, res) => {
  const { grade } = req.query;
  if (grade && !VALID_GRADES.has(grade)) {
    return res.status(400).json({ error: 'invalid grade' });
  }
  try {
    const matches = await db.matchResult.findMany({
      where: { userId: req.user.userId, ...(grade ? { grade } : {}) },
      orderBy: { playedAt: 'desc' },
      take: 5,
    });
    res.json(matches);
  } catch {
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.post('/game/complete', authMiddleware, async (req, res) => {
  const { grade, correctAnswers, totalQuestions, timeSpentMs, isRanked, won } = req.body;

  if (!VALID_GRADES.has(grade)) {
    return res.status(400).json({ error: 'grade must be one of G3, G6, G9' });
  }
  if (typeof correctAnswers !== 'number' || typeof totalQuestions !== 'number') {
    return res.status(400).json({ error: 'correctAnswers and totalQuestions required' });
  }

  try {
    const userId = req.user.userId;
    const currentEntry = await db.leaderboardEntry.findUnique({
      where: { userId_grade: { userId, grade } },
    });

    await upsertLeaderboardEntry(userId, grade, {
      bestTime: typeof timeSpentMs === 'number' ? timeSpentMs : undefined,
      correctAnswers,
    });

    res.json({
      elo: currentEntry?.elo ?? DEFAULT_ELO,
      eloChange: 0,
      bestTime: currentEntry?.bestTime ?? null,
      gamesPlayed: (currentEntry?.gamesPlayed ?? 0) + 1,
      totalCorrect: (currentEntry?.totalCorrect ?? 0) + correctAnswers,
    });
  } catch (err) {
    console.error('[game/complete] error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.put('/me/username', authMiddleware, async (req, res) => {
  const { username } = req.body;
  if (!username || !USERNAME_RE.test(username)) {
    return res.status(400).json({ error: 'invalid' });
  }
  const lower = username.toLowerCase();
  try {
    const conflict = await db.user.findFirst({
      where: { username: lower, id: { not: req.user.userId } },
    });
    if (conflict) return res.status(409).json({ error: 'taken' });
    const updated = await db.user.update({
      where: { id: req.user.userId },
      data: { username: lower },
    });
    res.json({ username: updated.username });
  } catch (err) {
    console.error('[me/username]', err);
    if (err.code === 'P2002') return res.status(409).json({ error: 'taken' });
    res.status(500).json({ error: 'Internal server error' });
  }
});

const VALID_IMAGE_RE = /^data:image\/(png|jpeg|jpg|gif|webp);base64,/;

app.post('/me/avatar', authMiddleware, async (req, res) => {
  const { image } = req.body;
  if (!image || typeof image !== 'string' || !VALID_IMAGE_RE.test(image)) {
    return res.status(400).json({ error: 'Invalid image data' });
  }
  const base64Data = image.replace(/^data:image\/\w+;base64,/, '');
  const buf = Buffer.from(base64Data, 'base64');
  if (buf.length > 2 * 1024 * 1024) {
    return res.status(400).json({ error: 'Image too large (max 2MB)' });
  }
  const mime = image.match(/^data:(image\/\w+);/)[1];
  const ext = mime.split('/')[1].replace('jpeg', 'jpg');
  const filename = `avatar_${req.user.userId}_${Date.now()}.${ext}`;
  const filepath = path.join(UPLOADS_DIR, filename);
  try {
    await fs.promises.writeFile(filepath, buf);
    const base = process.env.BACKEND_URL ?? `${req.protocol}://${req.get('host')}`;
    const avatarUrl = `${base}/uploads/${filename}`;
    await db.user.update({ where: { id: req.user.userId }, data: { avatarUrl } });
    res.json({ avatarUrl });
  } catch (err) {
    console.error('[me/avatar]', err);
    res.status(500).json({ error: 'Failed to save image' });
  }
});

app.get('/questions', authMiddleware, (req, res) => {
  const { grade } = req.query;
  if (!VALID_GRADES.has(grade)) {
    return res.status(400).json({ error: 'grade must be one of G3, G6, G9' });
  }
  res.json(generateStage(grade, 1));
});

app.post('/session/start', authMiddleware, async (req, res) => {
  const { grade, subject } = req.body;
  const VALID_SUBJECTS = new Set(['MATH', 'READING', 'WRITING']);
  if (!VALID_GRADES.has(grade) || !VALID_SUBJECTS.has(subject)) {
    return res.status(400).json({ error: 'invalid grade or subject' });
  }
  try {
    const session = await db.gameSession.create({
      data: { userId: req.user.userId, grade, subject, totalQuestions: 22, status: 'IN_PROGRESS' },
    });
    res.json({ sessionId: session.id });
  } catch (err) {
    console.error('[session/start]', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.post('/session/complete', authMiddleware, async (req, res) => {
  const { sessionId, correctAnswers, totalQuestions } = req.body;
  if (!sessionId) return res.status(400).json({ error: 'sessionId required' });
  try {
    const session = await db.gameSession.findUnique({ where: { id: sessionId } });
    if (!session || session.userId !== req.user.userId) {
      return res.status(404).json({ error: 'Session not found' });
    }
    const total = totalQuestions ?? session.totalQuestions;
    await db.gameSession.update({
      where: { id: sessionId },
      data: {
        status: 'COMPLETED',
        correctAnswers: correctAnswers ?? 0,
        totalQuestions: total,
        score: Math.round(((correctAnswers ?? 0) / (total || 1)) * 100),
        completedAt: new Date(),
      },
    });
    res.json({ ok: true });
  } catch (err) {
    console.error('[session/complete]', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.post('/me/best-time', authMiddleware, async (req, res) => {
  const { grade, timeMs } = req.body;
  if (!VALID_GRADES.has(grade) || typeof timeMs !== 'number' || timeMs <= 0) {
    return res.status(400).json({ error: 'Invalid grade or timeMs' });
  }
  try {
    const entry = await db.leaderboardEntry.findUnique({
      where: { userId_grade: { userId: req.user.userId, grade } },
    });
    const isNewBest = !entry || entry.bestTime === null || timeMs < entry.bestTime;
    console.log('[best-time] userId:', req.user.userId, 'grade:', grade, 'timeMs:', timeMs, 'isNewBest:', isNewBest);
    const updated = await db.leaderboardEntry.upsert({
      where: { userId_grade: { userId: req.user.userId, grade } },
      create: { userId: req.user.userId, grade, elo: 800, bestTime: timeMs, gamesPlayed: 1 },
      update: {
        ...(isNewBest ? { bestTime: timeMs } : {}),
        gamesPlayed: { increment: 1 },
      },
    });
    res.json({ updated: isNewBest, bestTime: updated.bestTime });
  } catch {
    res.status(500).json({ error: 'Internal server error' });
  }
});

// ── Matchmaking ────────────────────────────────────────────────

app.post('/queue/join', authMiddleware, async (req, res) => {
  const { grade } = req.body;
  if (!VALID_GRADES.has(grade)) {
    return res.status(400).json({ error: 'grade must be one of G3, G6, G9' });
  }

  try {
    if (playerGames[req.user.userId]) {
      return res.json({ status: 'matched', gameId: playerGames[req.user.userId] });
    }

    if (queue[req.user.userId]) {
      queue[req.user.userId].grade = grade;
      queue[req.user.userId].lastSeen = Date.now();
      return res.json({ status: 'waiting' });
    }

    const user = await db.user.findUnique({ where: { id: req.user.userId } });
    if (!user) return res.status(404).json({ error: 'User not found' });

    console.log(`[queue/join] user=${req.user.userId} grade=${grade} queue_size=${Object.keys(queue).length} queue_keys=${JSON.stringify(Object.keys(queue))}`);
    const existing = Object.entries(queue).find(([id, q]) => id !== req.user.userId && q.grade === grade);

    if (existing) {
      const [opponentId, opponentQueue] = existing;
      delete queue[opponentId];
      const gameGrade = opponentQueue.grade;
      console.log(`[queue] matched ${req.user.userId} vs ${opponentId} for ${gameGrade}`);

      const stage1 = generateStage(gameGrade, 1);
      const stage2 = generateStage(gameGrade, 2);
      const gameId = generateGameId();
      const game = {
        id: gameId,
        grade: gameGrade,
        stage1Questions: stage1,
        stage2Questions: stage2,
        players: {
          [opponentId]: { displayName: opponentQueue.displayName, username: opponentQueue.username, avatarUrl: opponentQueue.avatarUrl, currentQuestion: 0, correctAnswers: 0, finished: false, finishedAt: null, timeSpentMs: null, eloChange: 0, newElo: 800, lastSeen: Date.now() },
          [req.user.userId]: { displayName: user.displayName, username: user.username, avatarUrl: user.avatarUrl, currentQuestion: 0, correctAnswers: 0, finished: false, finishedAt: null, timeSpentMs: null, eloChange: 0, newElo: 800, lastSeen: Date.now() },
        },
        status: 'active',
        winner: null,
        startedAt: Date.now(),
        completedAt: null,
        chatMessages: [],
      };
      activeGames[gameId] = game;
      playerGames[opponentId] = gameId;
      playerGames[req.user.userId] = gameId;

      return res.json({ status: 'matched', gameId, questions: { stage1: game.stage1Questions, stage2: game.stage2Questions } });
    }

    queue[req.user.userId] = { grade, joinedAt: Date.now(), displayName: user.displayName, username: user.username, avatarUrl: user.avatarUrl, lastSeen: Date.now() };
    res.json({ status: 'waiting' });
  } catch (err) {
    console.error('[queue/join] error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.delete('/queue/leave', authMiddleware, async (req, res) => {
  const userId = req.user.userId;
  delete queue[userId];

  // If the player was matched into a game during the cancel window, forfeit it
  // immediately so the opponent isn't stuck waiting for a ghost.
  const gameId = playerGames[userId];
  if (gameId) {
    const game = activeGames[gameId];
    if (game && game.status === 'active') {
      const player = game.players[userId];
      if (player && !player.finished) {
        const opponentId = Object.keys(game.players).find(id => id !== userId);
        // Leave timeSpentMs null so completeGame doesn't record a bestTime for
        // someone who cancelled before the game even started.
        player.correctAnswers = 0;
        player.finished = true;
        player.finishedAt = Date.now();
        try {
          await completeGame(game, opponentId);
        } catch (err) {
          console.error('[queue/leave] completeGame error:', err);
        }
      }
    }
  }

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

app.get('/queue/debug', authMiddleware, (_req, res) => {
  res.json({
    queueSize: Object.keys(queue).length,
    queue: Object.entries(queue).map(([id, q]) => ({ id: id.slice(0,8), grade: q.grade, joinedAgo: Date.now() - q.joinedAt })),
    activeGames: Object.keys(activeGames).length,
    playerGames: Object.keys(playerGames).length,
  });
});

app.get('/queue/status', authMiddleware, (req, res) => {
  const gameId = playerGames[req.user.userId];

  if (gameId) {
    const game = activeGames[gameId];
    if (!game) {
      delete playerGames[req.user.userId];
      return res.json({ status: 'not_in_queue' });
    }

    game.players[req.user.userId].lastSeen = Date.now();

    const opponentId = Object.keys(game.players).find(id => id !== req.user.userId);
    const opponent = game.players[opponentId];

    if (!opponent) {
      delete playerGames[req.user.userId];
      delete activeGames[gameId];
      return res.json({ status: 'not_in_queue' });
    }

    return res.json({
      status: 'matched',
      gameId,
      questions: { stage1: game.stage1Questions, stage2: game.stage2Questions },
      opponent: {
        username: opponent.username ?? opponent.displayName,
        avatarUrl: opponent.avatarUrl ?? null,
      },
    });
  }

  if (queue[req.user.userId]) {
    queue[req.user.userId].lastSeen = Date.now();
    return res.json({ status: 'waiting' });
  }

  res.json({ status: 'not_in_queue' });
});

// ── Game management ────────────────────────────────────────────

app.post('/game/forfeit', authMiddleware, async (req, res) => {
  const { gameId } = req.body;
  if (!gameId) return res.status(400).json({ error: 'gameId required' });

  const game = activeGames[gameId];
  if (!game || game.status !== 'active') {
    return res.status(404).json({ error: 'Game not found or already completed' });
  }

  const player = game.players[req.user.userId];
  if (!player) {
    return res.status(403).json({ error: 'Not a participant in this game' });
  }

  if (player.finished) return res.json({ success: true });

  const elapsed = Date.now() - game.startedAt;
  player.timeSpentMs = Math.max(MIN_GAME_MS, elapsed + 5000);
  player.correctAnswers = 0;
  player.finished = true;
  player.finishedAt = Date.now();

  game.abandonTimeout = null;

  const opponentId = Object.keys(game.players).find(id => id !== req.user.userId);
  try {
    await completeGame(game, opponentId);
  } catch (err) {
    console.error('[game/forfeit] completeGame error:', err);
  }

  res.json({ success: true, eloChange: player.eloChange ?? 0, newElo: player.newElo ?? 800 });
});

app.post('/game/progress', authMiddleware, async (req, res) => {
  const { gameId, currentQuestion, correctAnswers, finished, timeSpentMs, stage } = req.body;
  const game = activeGames[gameId];

  if (!game || game.status !== 'active') {
    return res.status(404).json({ error: 'Game not found or already completed' });
  }

  const player = game.players[req.user.userId];
  if (!player) {
    return res.status(403).json({ error: 'Not a participant in this game' });
  }

  player.lastSeen = Date.now();

  if (player.finished) return res.json({ success: true }); // idempotent

  if (currentQuestion !== undefined) {
    player.currentQuestion = Math.max(0, Math.min(STAGE_SIZE, Math.floor(Number(currentQuestion))));
  }
  if (correctAnswers !== undefined) {
    player.correctAnswers = Math.max(0, Math.min(STAGE_SIZE, Math.floor(Number(correctAnswers))));
  }
  if (stage !== undefined) player.currentStage = Number(stage) || 1;

  if (finished && stage === 1) {
    player.stage1Finished = true;
    player.stage1CorrectCount = player.correctAnswers ?? 0;
  }

  if (finished && stage === 2) {
    const elapsed = Date.now() - game.startedAt;
    const claimed = typeof timeSpentMs === 'number' ? timeSpentMs : elapsed;
    // Clamp: no faster than minimum human time, no slower than actual elapsed + small buffer
    player.timeSpentMs = Math.max(MIN_GAME_MS, Math.min(claimed, elapsed + 5000));
    player.stage2Finished = true;
    player.finished = true;
    player.finishedAt = Date.now();
    player.completedQuiz = true;

    const winner = computeGameResult(game);
    if (winner) {
      try {
        await completeGame(game);
      } catch (err) {
        console.error('[game/progress] completeGame error:', err);
      }
    } else {
      // Opponent hasn't finished — award win to this player after 2 minutes if game still active
      const finishedUserId = req.user.userId;
      game.abandonTimeout = setTimeout(async () => {
        if (game.status === 'active') {
          try {
            await completeGame(game, finishedUserId);
          } catch (err) {
            console.error('[game/progress] abandonTimeout completeGame error:', err);
          }
        }
      }, 2 * 60 * 1000);
    }
  }

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

app.post('/game/emote', authMiddleware, (req, res) => {
  const { gameId, emote } = req.body;
  if (!gameId) return res.status(400).json({ error: 'gameId required' });
  if (!emote || typeof emote !== 'string' || !emote.trim() || emote.length > 20) {
    return res.status(400).json({ error: 'invalid emote' });
  }
  const game = activeGames[gameId];
  if (!game || game.status !== 'active') {
    return res.status(404).json({ error: 'Game not found or already completed' });
  }
  const player = game.players[req.user.userId];
  if (!player) return res.status(403).json({ error: 'Not a participant' });
  player.lastEmote = { text: emote.trim(), sentAt: Date.now() };
  res.json({ ok: true });
});

app.post('/game/chat', authMiddleware, (req, res) => {
  const { gameId, message } = req.body;
  if (!gameId) return res.status(400).json({ error: 'gameId required' });
  if (!message || typeof message !== 'string' || !message.trim()) {
    return res.status(400).json({ error: 'invalid message' });
  }
  const trimmed = message.trim().slice(0, 120);
  const game = activeGames[gameId];
  if (!game || game.status !== 'active') {
    return res.status(404).json({ error: 'Game not found or already completed' });
  }
  const player = game.players[req.user.userId];
  if (!player) return res.status(403).json({ error: 'Not a participant' });

  if (!player.chatMsgTimes) player.chatMsgTimes = [];
  const now = Date.now();
  player.chatMsgTimes = player.chatMsgTimes.filter(t => now - t < 1000);
  if (player.chatMsgTimes.length >= 4) return res.status(429).json({ error: 'slow_down' });
  player.chatMsgTimes.push(now);

  game.chatMessages.push({
    userId: req.user.userId,
    username: player.username ?? player.displayName,
    text: trimmed,
    sentAt: now,
    elapsedMs: now - game.startedAt,
  });
  if (game.chatMessages.length > 50) game.chatMessages.shift();

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

app.get('/game/status/:gameId', authMiddleware, async (req, res) => {
  const game = activeGames[req.params.gameId];
  if (!game) {
    return res.status(404).json({ error: 'Game not found' });
  }

  const player = game.players[req.user.userId];
  if (!player) {
    return res.status(403).json({ error: 'Not a participant in this game' });
  }

  player.lastSeen = Date.now();

  const opponentId = Object.keys(game.players).find(id => id !== req.user.userId);
  const opponent = game.players[opponentId];

  // Detect opponent disconnect — award win after 20s of inactivity
  const opponentInactive = opponent.lastSeen && Date.now() - opponent.lastSeen > 20000;
  if (game.status === 'active' && !player.finished && opponentInactive) {
    game.disconnectedPlayer = opponentId;
    try {
      await completeGame(game, req.user.userId);
    } catch (err) {
      console.error('[game/status] completeGame error:', err);
    }
  }

  // Use cached name — avoids a DB query on every 1-second poll
  const opponentName = opponent.username ?? opponent.displayName;

  const opponentDced = game.disconnectedPlayer === opponentId;
  const youDced = game.disconnectedPlayer === req.user.userId;
  // Show DC badge early (8s inactivity) before the game officially ends at 20s
  const opponentConnected = !opponentDced && (!opponent.lastSeen || Date.now() - opponent.lastSeen < 8000);

  res.json({
    status: game.status,
    youWon: game.winner !== null && game.winner === req.user.userId,
    opponentDced,
    youDced,
    opponentConnected,
    players: {
      you: {
        currentQuestion: player.currentQuestion,
        correctAnswers: player.correctAnswers,
        finished: player.finished,
        timeSpentMs: player.timeSpentMs,
        eloChange: player.eloChange,
        newElo: player.newElo,
        currentStage: player.currentStage ?? 1,
        stage1Finished: player.stage1Finished ?? false,
        stage: player.currentStage ?? 1,
        savedProgress: {
          currentQuestion: player.currentQuestion ?? 0,
          correctAnswers: player.correctAnswers ?? 0,
          stage: player.currentStage ?? 1,
        },
      },
      opponent: {
        username: opponentName,
        currentQuestion: opponent.currentQuestion,
        correctAnswers: opponent.correctAnswers,
        finished: opponent.finished,
        timeSpentMs: opponent.timeSpentMs,
        currentStage: opponent.currentStage ?? 1,
        stage1Finished: opponent.stage1Finished ?? false,
        lastEmote: (opponent.lastEmote && Date.now() - opponent.lastEmote.sentAt < 4000) ? opponent.lastEmote.text : null,
      },
    },
    chatMessages: game.chatMessages ?? [],
  });
});

app.get('/online-count', (_req, res) => {
  const now = Date.now();
  const inQueue = Object.values(queue).filter(q => now - q.lastSeen <= 30000).length;
  const inGame = Object.entries(playerGames).filter(([uid, gameId]) => {
    const game = activeGames[gameId];
    if (!game || game.status !== 'active') return false;
    const p = game.players[uid];
    return p && p.lastSeen && now - p.lastSeen <= 30000;
  }).length;
  res.json({ online: inQueue + inGame });
});

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

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));