import { Router } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { rateLimit, MemoryStore } from 'express-rate-limit';
import { RedisStore } from 'rate-limit-redis';
import * as OTPAuth from 'otpauth';
import { getDb } from '../config/database.js';
import redis from '../config/redis.js';
import { authenticateToken, generateToken, getBaseUrl } from '../middleware/auth.js';
import { isMailerConfigured, sendVerificationEmail, sendPasswordResetEmail } from '../config/mailer.js';
import { getOAuthConfig, discoverOIDC } from '../config/oauth.js';
import { getAppName } from '../config/appName.js';
import { log } from '../config/logger.js';

if (!process.env.JWT_SECRET) {
  log.auth.error('FATAL: JWT_SECRET environment variable is not set.');
  process.exit(1);
}
const JWT_SECRET = process.env.JWT_SECRET;

// ── Rate Limiting ────────────────────────────────────────────────────────────
// Shared Redis store with in-memory fallback. Without the fallback, a dead
// Redis connection makes every rate-limited endpoint (incl. login) return 500 —
// rate limiting must degrade, not take auth down with it.
class ResilientStore {
  constructor(prefix) {
    try {
      this.redisStore = new RedisStore({
        sendCommand: (...args) => redis.call(...args),
        prefix,
      });
    } catch {
      this.redisStore = null;
    }
    this.memoryStore = new MemoryStore();
  }
  init(options) {
    try {
      // RedisStore.init returns the SCRIPT LOAD promise; catch it so a dead
      // Redis connection surfaces as a warning instead of an unhandled
      // rejection that kills the process.
      const initResult = this.redisStore?.init?.(options);
      if (typeof initResult?.catch === 'function') {
        initResult.catch(err => log.auth.warn(`Rate-limit Redis init failed: ${err.message}`));
      }
    } catch (err) {
      log.auth.warn(`Rate-limit Redis init failed: ${err.message}`);
    }
    this.memoryStore.init(options);
  }
  async increment(key) {
    if (this.redisStore && redis.status === 'ready') {
      try {
        return await this.redisStore.increment(key);
      } catch (err) {
        log.auth.warn(`Rate-limit Redis store failed, using in-memory fallback: ${err.message}`);
      }
    }
    return this.memoryStore.increment(key);
  }
  async decrement(key) {
    if (this.redisStore && redis.status === 'ready') {
      try {
        return await this.redisStore.decrement(key);
      } catch { /* fall through to memory */ }
    }
    return this.memoryStore.decrement(key);
  }
  async resetKey(key) {
    if (this.redisStore && redis.status === 'ready') {
      try {
        return await this.redisStore.resetKey(key);
      } catch { /* fall through to memory */ }
    }
    return this.memoryStore.resetKey(key);
  }
}

function makeRedisStore(prefix) {
  return new ResilientStore(prefix);
}

// ── Validation helpers ─────────────────────────────────────────────────────
const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{2,}$/;

// Simple format check for theme/language IDs (actual validation happens on the frontend)
const SAFE_ID_RE = /^[a-zA-Z0-9_-]{1,50}$/;

// Allowlist for CSS color values - only permits hsl(), hex (#rgb/#rrggbb) and plain names
const SAFE_COLOR_RE = /^(?:#[0-9a-fA-F]{3,8}|hsl\(\d{1,3},\s*\d{1,3}%,\s*\d{1,3}%\)|[a-zA-Z]{1,30})$/;

const MIN_PASSWORD_LENGTH = 8;
// bcrypt only uses the first 72 bytes; cap input to prevent CPU-DoS on hashing.
const MAX_PASSWORD_LENGTH = 64;

// Pre-computed bcrypt hash of a random string used as a dummy comparison
// target when the requested account does not exist. Keeps login timing
// roughly constant so we do not leak whether an email is registered.
const DUMMY_BCRYPT_HASH = bcrypt.hashSync('dummy-password-for-timing-' + Math.random(), 12);

// ── Rate Limiters ────────────────────────────────────────────────────────────
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 20,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many login attempts. Please try again in 15 minutes.' },
  store: makeRedisStore('rl:login:'),
});

const registerLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 10,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many registration attempts. Please try again later.' },
  store: makeRedisStore('rl:register:'),
});

const profileLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 30,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many profile update attempts. Please try again later.' },
  store: makeRedisStore('rl:profile:'),
});

const passwordLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many password change attempts. Please try again later.' },
  store: makeRedisStore('rl:password:'),
});

const avatarLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 20,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many avatar upload attempts. Please try again later.' },
  store: makeRedisStore('rl:avatar:'),
});

// S1: rate limit resend-verification to prevent SMTP abuse
const resendVerificationLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests. Please try again later.' },
  store: makeRedisStore('rl:resend:'),
});

const twoFaLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many 2FA attempts. Please try again later.' },
  store: makeRedisStore('rl:2fa:'),
});

// Rate limit forgot-password to prevent SMTP abuse / email enumeration spam
const forgotPasswordLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests. Please try again later.' },
  store: makeRedisStore('rl:forgot:'),
});

// Rate limit reset-password submissions to slow token brute-forcing
const resetPasswordLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many attempts. Please try again later.' },
  store: makeRedisStore('rl:reset:'),
});

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const uploadsDir = path.join(__dirname, '..', '..', 'uploads', 'avatars');

// Ensure uploads directory exists
if (!fs.existsSync(uploadsDir)) {
  fs.mkdirSync(uploadsDir, { recursive: true });
}

const router = Router();

// POST /api/auth/register
router.post('/register', registerLimiter, async (req, res) => {
  try {
    const { username, display_name, email, password, invite_token } = req.body;

    // Check registration mode
    const db = getDb();
    const regModeSetting = await db.get("SELECT value FROM settings WHERE key = 'registration_mode'");
    const registrationMode = regModeSetting?.value || 'open';

    let validatedInvite = null;
    if (registrationMode === 'invite') {
      if (!invite_token) {
        return res.status(403).json({ error: 'Registration is currently invite-only. You need an invitation link to register.' });
      }
      // Validate the invite token
      validatedInvite = await db.get(
        'SELECT * FROM user_invites WHERE token = ? AND used_at IS NULL AND expires_at > CURRENT_TIMESTAMP',
        [invite_token]
      );
      if (!validatedInvite) {
        return res.status(403).json({ error: 'Invalid or expired invitation link.' });
      }
    }

    if (!username || !display_name || !email || !password) {
      return res.status(400).json({ error: 'All fields are required' });
    }

    // L3: display_name length limit (consistent with profile update)
    if (display_name.length > 100) {
      return res.status(400).json({ error: 'Display name must not exceed 100 characters' });
    }

    const usernameRegex = /^[a-zA-Z0-9_-]{3,30}$/;
    if (!usernameRegex.test(username)) {
      return res.status(400).json({ error: 'Username may only contain letters, numbers, _ and - (3-30 chars)' });
    }

    // M1: email format
    if (!EMAIL_RE.test(email)) {
      return res.status(400).json({ error: 'Invalid email address' });
    }

    // M4: minimum password length
    if (password.length < MIN_PASSWORD_LENGTH) {
      return res.status(400).json({ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters long` });
    }
    if (password.length > MAX_PASSWORD_LENGTH) {
      return res.status(400).json({ error: `Password must not exceed ${MAX_PASSWORD_LENGTH} characters` });
    }

    // Emails are stored lowercased, so compare lowercased to catch case-variant duplicates
    const existing = await db.get('SELECT id FROM users WHERE email = ?', [email.toLowerCase()]);
    if (existing) {
      return res.status(409).json({ error: 'Email is already in use' });
    }

    const existingUsername = await db.get('SELECT id FROM users WHERE LOWER(name) = LOWER(?)', [username]);
    if (existingUsername) {
      return res.status(409).json({ error: 'Username is already taken' });
    }

    const hash = await bcrypt.hash(password, 12);

    // If SMTP is configured, require email verification
    if (isMailerConfigured()) {
      const verificationToken = uuidv4();
      const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();

      await db.run(
        'INSERT INTO users (name, display_name, email, password_hash, email_verified, verification_token, verification_token_expires) VALUES (?, ?, ?, ?, 0, ?, ?)',
        [username, display_name, email.toLowerCase(), hash, verificationToken, expires]
      );

      // Mark invite as used if applicable
      if (validatedInvite) {
        const newUser = await db.get('SELECT id FROM users WHERE email = ?', [email.toLowerCase()]);
        if (newUser) {
          await db.run('UPDATE user_invites SET used_by = ?, used_at = CURRENT_TIMESTAMP WHERE id = ?', [newUser.id, validatedInvite.id]);
        }
      }

      // Build verification URL
      const baseUrl = getBaseUrl(req);
      const verifyUrl = `${baseUrl}/verify-email?token=${verificationToken}`;

      // Load configured app name (admin setting → env → default)
      const appName = await getAppName();

      try {
        await sendVerificationEmail(email.toLowerCase(), display_name, verifyUrl, appName, 'en');
      } catch (mailErr) {
        log.auth.error(`Verification mail failed: ${mailErr.message}`);
        // Account is created but email failed — user can resend from login page
        return res.status(201).json({ needsVerification: true, emailFailed: true, message: 'Account created but verification email could not be sent. Please try resending.' });
      }

      return res.status(201).json({ needsVerification: true, message: 'Verification email has been sent' });
    }

    // No SMTP configured - register and login immediately (legacy behaviour)
    const result = await db.run(
      'INSERT INTO users (name, display_name, email, password_hash, email_verified) VALUES (?, ?, ?, ?, 1)',
      [username, display_name, email.toLowerCase(), hash]
    );

    // Mark invite as used if applicable
    if (validatedInvite) {
      await db.run('UPDATE user_invites SET used_by = ?, used_at = CURRENT_TIMESTAMP WHERE id = ?', [result.lastInsertRowid, validatedInvite.id]);
    }

    const token = generateToken(result.lastInsertRowid);
    const user = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image, email_verified FROM users WHERE id = ?', [result.lastInsertRowid]);

    res.status(201).json({ token, user });
  } catch (err) {
    log.auth.error(`Register error: ${err.message}`);
    res.status(500).json({ error: 'Registration failed' });
  }
});

// GET /api/auth/verify-email?token=...
router.get('/verify-email', async (req, res) => {
  try {
    const { token } = req.query;
    if (!token) {
      return res.status(400).json({ error: 'Token is missing' });
    }

    const db = getDb();
    const user = await db.get(
      'SELECT id, verification_token_expires FROM users WHERE verification_token = ? AND email_verified = 0',
      [token]
    );

    if (!user) {
      return res.status(400).json({ error: 'Invalid or already used token' });
    }

    if (new Date(user.verification_token_expires) < new Date()) {
      return res.status(400).json({ error: 'Token has expired. Please register again.' });
    }

    await db.run(
      'UPDATE users SET email_verified = 1, verification_token = NULL, verification_token_expires = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
      [user.id]
    );

    res.json({ verified: true, message: 'Email verified successfully' });
  } catch (err) {
    log.auth.error(`Verify email error: ${err.message}`);
    res.status(500).json({ error: 'Verification failed' });
  }
});

// POST /api/auth/resend-verification
router.post('/resend-verification', resendVerificationLimiter, async (req, res) => {
  try {
    const { email } = req.body;
    if (!email) {
      return res.status(400).json({ error: 'Email is required' });
    }

    if (!isMailerConfigured()) {
      return res.status(400).json({ error: 'SMTP is not configured' });
    }

    const db = getDb();
    const user = await db.get('SELECT id, name, display_name, language, email_verified, verification_resend_at FROM users WHERE email = ?', [email.toLowerCase()]);

    if (!user || user.email_verified) {
      // Don't reveal whether account exists
      return res.json({ message: 'If an account exists, a new email has been sent.' });
    }

    // Server-side 60s rate limit
    if (user.verification_resend_at) {
      const secondsAgo = (Date.now() - new Date(user.verification_resend_at).getTime()) / 1000;
      if (secondsAgo < 60) {
        const waitSeconds = Math.ceil(60 - secondsAgo);
        return res.status(429).json({ error: `Please wait ${waitSeconds} seconds before requesting another email.`, waitSeconds });
      }
    }

    const verificationToken = uuidv4();
    const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
    const now = new Date().toISOString();

    await db.run(
      'UPDATE users SET verification_token = ?, verification_token_expires = ?, verification_resend_at = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
      [verificationToken, expires, now, user.id]
    );

    const baseUrl = getBaseUrl(req);
    const verifyUrl = `${baseUrl}/verify-email?token=${verificationToken}`;

    const appName = await getAppName();

    try {
      await sendVerificationEmail(email.toLowerCase(), user.display_name || user.name, verifyUrl, appName, user.language || 'en');
    } catch (mailErr) {
      log.auth.error(`Resend verification mail failed: ${mailErr.message}`);
      return res.status(502).json({ error: 'Email could not be sent. Please check your SMTP configuration.' });
    }

    res.json({ message: 'If an account exists, a new email has been sent.' });
  } catch (err) {
    log.auth.error(`Resend verification error: ${err.message}`);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Hash a reset token for storage/lookup (never store the raw token)
function hashResetToken(token) {
  return crypto.createHash('sha256').update(token).digest('hex');
}

// POST /api/auth/forgot-password
router.post('/forgot-password', forgotPasswordLimiter, async (req, res) => {
  try {
    const { email } = req.body;
    if (!email || typeof email !== 'string' || !EMAIL_RE.test(email)) {
      return res.status(400).json({ error: 'A valid email address is required' });
    }

    // Generic response used in every branch so we never reveal whether an
    // account exists, whether it uses SSO, or whether SMTP is configured.
    const genericResponse = { message: 'If an account exists for that address, a password reset email has been sent.' };

    if (!isMailerConfigured()) {
      return res.json(genericResponse);
    }

    const db = getDb();
    const user = await db.get('SELECT id, name, display_name, email, language, oauth_provider, reset_requested_at FROM users WHERE email = ?', [email.toLowerCase()]);

    // No account, or an SSO-only account that has no local password to reset
    if (!user || user.oauth_provider) {
      return res.json(genericResponse);
    }

    // Server-side 60s throttle per account to limit repeated emails
    if (user.reset_requested_at) {
      const secondsAgo = (Date.now() - new Date(user.reset_requested_at).getTime()) / 1000;
      if (secondsAgo < 60) {
        return res.json(genericResponse);
      }
    }

    const resetToken = uuidv4();
    const tokenHash = hashResetToken(resetToken);
    const expires = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // 1 hour
    const now = new Date().toISOString();

    await db.run(
      'UPDATE users SET reset_token_hash = ?, reset_token_expires = ?, reset_requested_at = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
      [tokenHash, expires, now, user.id]
    );

    const baseUrl = getBaseUrl(req);
    const resetUrl = `${baseUrl}/reset-password?token=${resetToken}`;
    const appName = await getAppName();

    try {
      await sendPasswordResetEmail(email.toLowerCase(), user.display_name || user.name, resetUrl, appName, user.language || 'en');
    } catch (mailErr) {
      log.auth.error(`Password reset mail failed: ${mailErr.message}`);
      // Still return generic success — don't leak SMTP state to the client
    }

    res.json(genericResponse);
  } catch (err) {
    log.auth.error(`Forgot password error: ${err.message}`);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// POST /api/auth/reset-password
router.post('/reset-password', resetPasswordLimiter, async (req, res) => {
  try {
    const { token, password } = req.body;
    if (!token || typeof token !== 'string') {
      return res.status(400).json({ error: 'Invalid or expired reset link.' });
    }
    if (!password || typeof password !== 'string') {
      return res.status(400).json({ error: 'A new password is required' });
    }
    if (password.length < MIN_PASSWORD_LENGTH) {
      return res.status(400).json({ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters long` });
    }
    if (password.length > MAX_PASSWORD_LENGTH) {
      return res.status(400).json({ error: `Password must not exceed ${MAX_PASSWORD_LENGTH} characters` });
    }

    const db = getDb();
    const tokenHash = hashResetToken(token);
    const user = await db.get('SELECT id, reset_token_expires FROM users WHERE reset_token_hash = ?', [tokenHash]);

    if (!user) {
      return res.status(400).json({ error: 'Invalid or expired reset link.' });
    }
    if (!user.reset_token_expires || new Date(user.reset_token_expires) < new Date()) {
      return res.status(400).json({ error: 'Invalid or expired reset link.' });
    }

    const hash = await bcrypt.hash(password, 12);

    // Set the new password, verify the email (proves mailbox ownership), and
    // clear the reset token so it can't be reused.
    await db.run(
      'UPDATE users SET password_hash = ?, email_verified = 1, reset_token_hash = NULL, reset_token_expires = NULL, reset_requested_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
      [hash, user.id]
    );

    res.json({ message: 'Password has been reset successfully. You can now sign in.' });
  } catch (err) {
    log.auth.error(`Reset password error: ${err.message}`);
    res.status(500).json({ error: 'Password could not be reset' });
  }
});

// POST /api/auth/login
router.post('/login', loginLimiter, async (req, res) => {
  try {
    const { email, password } = req.body;

    if (!email || !password) {
      return res.status(400).json({ error: 'Email and password are required' });
    }

    // M1: basic email format check - invalid format can never match a real account
    if (!EMAIL_RE.test(email)) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    // Cap password length to keep bcrypt CPU work bounded.
    if (typeof password !== 'string' || password.length > MAX_PASSWORD_LENGTH) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    const db = getDb();
    const user = await db.get('SELECT * FROM users WHERE email = ?', [email.toLowerCase()]);

    // Always run bcrypt against either the real hash or a dummy hash so login
    // timing does not reveal whether the email is registered.
    const passwordOk = bcrypt.compareSync(password, user?.password_hash || DUMMY_BCRYPT_HASH);
    if (!user || !passwordOk) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    if (!user.email_verified && isMailerConfigured()) {
      return res.status(403).json({ error: 'Email address not yet verified. Please check your inbox.', needsVerification: true });
    }

    // ── 2FA check ────────────────────────────────────────────────────────
    if (user.totp_enabled) {
      const tempToken = jwt.sign({ userId: user.id, purpose: '2fa' }, JWT_SECRET, { expiresIn: '5m' });
      return res.json({ requires2FA: true, tempToken });
    }

    const token = generateToken(user.id);
    const { password_hash, verification_token, verification_token_expires, verification_resend_at, totp_secret, ...safeUser } = user;

    res.json({ token, user: safeUser });
  } catch (err) {
    log.auth.error(`Login error: ${err.message}`);
    res.status(500).json({ error: 'Login failed' });
  }
});

// POST /api/auth/login/2fa - Verify TOTP code and complete login
router.post('/login/2fa', twoFaLimiter, async (req, res) => {
  try {
    const { tempToken, code } = req.body;
    if (!tempToken || !code) {
      return res.status(400).json({ error: 'Token and code are required' });
    }

    let decoded;
    try {
      decoded = jwt.verify(tempToken, JWT_SECRET);
    } catch {
      return res.status(401).json({ error: 'Invalid or expired token. Please log in again.' });
    }

    if (decoded.purpose !== '2fa') {
      return res.status(401).json({ error: 'Invalid token' });
    }

    const db = getDb();
    const user = await db.get('SELECT * FROM users WHERE id = ?', [decoded.userId]);
    if (!user || !user.totp_enabled || !user.totp_secret) {
      return res.status(401).json({ error: 'Invalid token' });
    }

    const totp = new OTPAuth.TOTP({
      secret: OTPAuth.Secret.fromBase32(user.totp_secret),
      algorithm: 'SHA1',
      digits: 6,
      period: 30,
    });

    const delta = totp.validate({ token: code.replace(/\s/g, ''), window: 1 });
    if (delta === null) {
      return res.status(401).json({ error: 'Invalid 2FA code' });
    }

    const token = generateToken(user.id);
    const { password_hash, verification_token, verification_token_expires, verification_resend_at, totp_secret, ...safeUser } = user;

    res.json({ token, user: safeUser });
  } catch (err) {
    log.auth.error(`2FA login error: ${err.message}`);
    res.status(500).json({ error: '2FA verification failed' });
  }
});

// POST /api/auth/logout - revoke JWT via DragonflyDB blacklist
router.post('/logout', authenticateToken, async (req, res) => {
  try {
    const authHeader = req.headers.authorization;
    const token = authHeader && authHeader.split(' ')[1];
    const decoded = jwt.decode(token);

    if (decoded?.jti && decoded?.exp) {
      const ttl = decoded.exp - Math.floor(Date.now() / 1000);
      if (ttl > 0) {
        try {
          await redis.setex(`blacklist:${decoded.jti}`, ttl, '1');
        } catch (redisErr) {
          log.auth.warn(`Redis blacklist write failed: ${redisErr.message}`);
        }
      }
    }

    // ── RP-Initiated Logout for OIDC/Keycloak users ──────────────────────
    let keycloakLogoutUrl = null;
    if (req.user.oauth_provider === 'oidc') {
      try {
        const config = await getOAuthConfig();
        if (config) {
          const oidc = await discoverOIDC(config.issuer);
          if (oidc.end_session_endpoint) {
            const idToken = await redis.get(`oidc:id_token:${req.user.id}`);
            await redis.del(`oidc:id_token:${req.user.id}`);
            const baseUrl = getBaseUrl(req);
            const params = new URLSearchParams({
              post_logout_redirect_uri: `${baseUrl}/`,
              client_id: config.clientId,
            });
            if (idToken) params.set('id_token_hint', idToken);
            keycloakLogoutUrl = `${oidc.end_session_endpoint}?${params.toString()}`;
          }
        }
      } catch (oidcErr) {
        log.auth.warn(`Could not build Keycloak logout URL: ${oidcErr.message}`);
      }
    }

    res.json({ message: 'Logged out successfully', keycloakLogoutUrl });
  } catch (err) {
    log.auth.error(`Logout error: ${err.message}`);
    res.status(500).json({ error: 'Logout failed' });
  }
});

// GET /api/auth/me
router.get('/me', authenticateToken, (req, res) => {
  res.json({ user: req.user });
});

// PUT /api/auth/profile
router.put('/profile', authenticateToken, profileLimiter, async (req, res) => {
  try {
    const { name, display_name, email, theme, language, avatar_color } = req.body;
    const db = getDb();

    // M1: validate new email format
    if (email && !EMAIL_RE.test(email)) {
      return res.status(400).json({ error: 'Invalid email address' });
    }

    if (email && email !== req.user.email) {
      const existing = await db.get('SELECT id FROM users WHERE email = ? AND id != ?', [email.toLowerCase(), req.user.id]);
      if (existing) {
        return res.status(409).json({ error: 'Email is already in use' });
      }
    }

    // M2: display_name length limit
    if (display_name !== undefined && display_name !== null && display_name.length > 100) {
      return res.status(400).json({ error: 'Display name must not exceed 100 characters' });
    }

    // Theme and language: basic format validation (frontend handles actual ID matching)
    if (theme !== undefined && theme !== null && (typeof theme !== 'string' || !SAFE_ID_RE.test(theme))) {
      return res.status(400).json({ error: 'Invalid theme' });
    }
    if (language !== undefined && language !== null && (typeof language !== 'string' || !SAFE_ID_RE.test(language))) {
      return res.status(400).json({ error: 'Invalid language' });
    }

    // L5: validate avatar_color format/length
    if (avatar_color !== undefined && avatar_color !== null) {
      if (typeof avatar_color !== 'string' || !SAFE_COLOR_RE.test(avatar_color)) {
        return res.status(400).json({ error: 'Invalid avatar color' });
      }
    }

    if (name && name !== req.user.name) {
      const usernameRegex = /^[a-zA-Z0-9_-]{3,30}$/;
      if (!usernameRegex.test(name)) {
        return res.status(400).json({ error: 'Username may only contain letters, numbers, _ and - (3-30 chars)' });
      }
      const existingUsername = await db.get('SELECT id FROM users WHERE LOWER(name) = LOWER(?) AND id != ?', [name, req.user.id]);
      if (existingUsername) {
        return res.status(409).json({ error: 'Username is already taken' });
      }
    }

    await db.run(`
      UPDATE users SET
        name = COALESCE(?, name),
        display_name = COALESCE(?, display_name),
        email = COALESCE(?, email),
        theme = COALESCE(?, theme),
        language = COALESCE(?, language),
        avatar_color = COALESCE(?, avatar_color),
        updated_at = CURRENT_TIMESTAMP
      WHERE id = ?
    `, [name, display_name, email?.toLowerCase(), theme, language, avatar_color, req.user.id]);

    const updated = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image, email_verified FROM users WHERE id = ?', [req.user.id]);
    res.json({ user: updated });
  } catch (err) {
    log.auth.error(`Profile update error: ${err.message}`);
    res.status(500).json({ error: 'Profile could not be updated' });
  }
});

// PUT /api/auth/password
router.put('/password', authenticateToken, passwordLimiter, async (req, res) => {
  try {
    const { currentPassword, newPassword } = req.body;

    // M6: guard against missing/non-string body values
    if (!currentPassword || !newPassword) {
      return res.status(400).json({ error: 'currentPassword and newPassword are required' });
    }
    if (typeof currentPassword !== 'string' || typeof newPassword !== 'string') {
      return res.status(400).json({ error: 'Invalid input' });
    }
    if (currentPassword.length > MAX_PASSWORD_LENGTH || newPassword.length > MAX_PASSWORD_LENGTH) {
      return res.status(400).json({ error: `Password must not exceed ${MAX_PASSWORD_LENGTH} characters` });
    }

    const db = getDb();

    const user = await db.get('SELECT password_hash FROM users WHERE id = ?', [req.user.id]);
    if (!bcrypt.compareSync(currentPassword, user.password_hash)) {
      return res.status(401).json({ error: 'Current password is incorrect' });
    }

    // M4: minimum password length
    if (newPassword.length < MIN_PASSWORD_LENGTH) {
      return res.status(400).json({ error: `New password must be at least ${MIN_PASSWORD_LENGTH} characters long` });
    }

    const hash = await bcrypt.hash(newPassword, 12);
    await db.run('UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [hash, req.user.id]);

    res.json({ message: 'Password changed successfully' });
  } catch (err) {
    log.auth.error(`Password change error: ${err.message}`);
    res.status(500).json({ error: 'Password could not be changed' });
  }
});

// POST /api/auth/avatar - Upload avatar image
router.post('/avatar', authenticateToken, avatarLimiter, async (req, res) => {
  try {
    // Validate file content by checking magic bytes (file signatures)
    const contentType = req.headers['content-type'];
    if (!contentType || !contentType.startsWith('image/')) {
      return res.status(400).json({ error: 'Only image files are allowed' });
    }

    // M15: stream-level size limit - abort as soon as 2 MB is exceeded
    const MAX_AVATAR_SIZE = 2 * 1024 * 1024;
    const buffer = await new Promise((resolve, reject) => {
      const chunks = [];
      let totalSize = 0;
      req.on('data', chunk => {
        totalSize += chunk.length;
        if (totalSize > MAX_AVATAR_SIZE) {
          req.destroy();
          return reject(new Error('LIMIT_EXCEEDED'));
        }
        chunks.push(chunk);
      });
      req.on('end', () => resolve(Buffer.concat(chunks)));
      req.on('error', reject);
    }).catch(err => {
      if (err.message === 'LIMIT_EXCEEDED') return null;
      throw err;
    });

    if (!buffer) {
      return res.status(400).json({ error: 'Image must not exceed 2MB' });
    }

    // Validate magic bytes to prevent Content-Type spoofing
    const magicBytes = buffer.slice(0, 8);
    const isJPEG = magicBytes[0] === 0xFF && magicBytes[1] === 0xD8 && magicBytes[2] === 0xFF;
    const isPNG = magicBytes[0] === 0x89 && magicBytes[1] === 0x50 && magicBytes[2] === 0x4E && magicBytes[3] === 0x47;
    const isGIF = magicBytes[0] === 0x47 && magicBytes[1] === 0x49 && magicBytes[2] === 0x46;
    const isWEBP = magicBytes[0] === 0x52 && magicBytes[1] === 0x49 && magicBytes[2] === 0x46 && magicBytes[3] === 0x46
      && buffer.length > 11 && buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50;
    if (!isJPEG && !isPNG && !isGIF && !isWEBP) {
      return res.status(400).json({ error: 'File content does not match a supported image format (JPEG, PNG, GIF, WebP)' });
    }

    const ext = isPNG ? 'png' : isGIF ? 'gif' : isWEBP ? 'webp' : 'jpg';
    const filename = `${req.user.id}_${Date.now()}.${ext}`;
    const filepath = path.join(uploadsDir, filename);

    // Remove old avatar if exists
    const db = getDb();
    const current = await db.get('SELECT avatar_image FROM users WHERE id = ?', [req.user.id]);
    if (current?.avatar_image) {
      // S8: defense-in-depth path traversal check on DB-stored filename
      const oldPath = path.resolve(uploadsDir, current.avatar_image);
      if (oldPath.startsWith(uploadsDir + path.sep) && fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
    }

    fs.writeFileSync(filepath, buffer);

    await db.run('UPDATE users SET avatar_image = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [filename, req.user.id]);
    const updated = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image, email_verified FROM users WHERE id = ?', [req.user.id]);
    res.json({ user: updated });
  } catch (err) {
    log.auth.error(`Avatar upload error: ${err.message}`);
    res.status(500).json({ error: 'Avatar could not be uploaded' });
  }
});

// DELETE /api/auth/avatar - Remove avatar image
router.delete('/avatar', authenticateToken, avatarLimiter, async (req, res) => {
  try {
    const db = getDb();
    const current = await db.get('SELECT avatar_image FROM users WHERE id = ?', [req.user.id]);
    if (current?.avatar_image) {
      // S8: defense-in-depth path traversal check on DB-stored filename
      const oldPath = path.resolve(uploadsDir, current.avatar_image);
      if (oldPath.startsWith(uploadsDir + path.sep) && fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
    }
    await db.run('UPDATE users SET avatar_image = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.user.id]);
    const updated = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image, email_verified FROM users WHERE id = ?', [req.user.id]);
    res.json({ user: updated });
  } catch (err) {
    log.auth.error(`Avatar delete error: ${err.message}`);
    res.status(500).json({ error: 'Avatar could not be removed' });
  }
});

// Escape XML special characters to prevent XSS in SVG text/attribute contexts
function escapeXml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

// GET /api/auth/avatar/initials/:name - Generate SVG avatar from initials (public, BBB fetches this)
router.get('/avatar/initials/:name', (req, res) => {
  const name = decodeURIComponent(req.params.name).trim();

  // C1 fix: validate color against a strict allowlist before embedding in SVG attribute
  const rawColor = req.query.color || '';
  const color = SAFE_COLOR_RE.test(rawColor) ? rawColor : generateColorFromName(name);

  // C2 fix: XML-escape initials before embedding in SVG text node
  const rawInitials = name
    .split(' ')
    .map(n => n[0])
    .join('')
    .toUpperCase()
    .slice(0, 2) || '?';
  const initials = escapeXml(rawInitials);

  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
    <rect width="128" height="128" rx="64" fill="${escapeXml(color)}"/>
    <text x="64" y="64" dy=".35em" text-anchor="middle" fill="white" font-family="Arial, sans-serif" font-size="52" font-weight="bold">${initials}</text>
  </svg>`;

  res.setHeader('Content-Type', 'image/svg+xml');
  res.setHeader('Cache-Control', 'public, max-age=3600');
  res.send(svg);
});

function generateColorFromName(name) {
  let hash = 0;
  for (let i = 0; i < name.length; i++) {
    hash = name.charCodeAt(i) + ((hash << 5) - hash);
  }
  const hue = Math.abs(hash) % 360;
  return `hsl(${hue}, 55%, 45%)`;
}

// GET /api/auth/avatar/:filename - Serve avatar image
router.get('/avatar/:filename', (req, res) => {
  // H1 fix: resolve the path and ensure it stays inside uploadsDir (prevent path traversal)
  const filepath = path.resolve(uploadsDir, req.params.filename);
  if (!filepath.startsWith(uploadsDir + path.sep)) {
    return res.status(400).json({ error: 'Invalid filename' });
  }
  if (!fs.existsSync(filepath)) {
    return res.status(404).json({ error: 'Avatar not found' });
  }
  const ext = path.extname(filepath).slice(1);
  const mimeMap = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp' };
  res.setHeader('Content-Type', mimeMap[ext] || 'image/jpeg');
  res.setHeader('Cache-Control', 'public, max-age=86400');
  fs.createReadStream(filepath).pipe(res);
});

// ── 2FA Management ──────────────────────────────────────────────────────────

// GET /api/auth/2fa/status
router.get('/2fa/status', authenticateToken, async (req, res) => {
  const db = getDb();
  const user = await db.get('SELECT totp_enabled FROM users WHERE id = ?', [req.user.id]);
  res.json({ enabled: !!user?.totp_enabled });
});

// POST /api/auth/2fa/setup - Generate TOTP secret + provisioning URI
router.post('/2fa/setup', authenticateToken, twoFaLimiter, async (req, res) => {
  try {
    const db = getDb();
    const user = await db.get('SELECT totp_enabled FROM users WHERE id = ?', [req.user.id]);
    if (user?.totp_enabled) {
      return res.status(400).json({ error: '2FA is already enabled' });
    }

    const secret = new OTPAuth.Secret({ size: 20 });

    // Use the configured app name as the TOTP issuer (admin setting → env → default)
    const issuer = await getAppName();

    const totp = new OTPAuth.TOTP({
      issuer,
      label: req.user.email,
      algorithm: 'SHA1',
      digits: 6,
      period: 30,
      secret,
    });

    // Store the secret (but don't enable yet — user must verify first)
    await db.run('UPDATE users SET totp_secret = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [secret.base32, req.user.id]);

    res.json({ secret: secret.base32, uri: totp.toString() });
  } catch (err) {
    log.auth.error(`2FA setup error: ${err.message}`);
    res.status(500).json({ error: '2FA setup failed' });
  }
});

// POST /api/auth/2fa/enable - Verify code and activate 2FA
router.post('/2fa/enable', authenticateToken, twoFaLimiter, async (req, res) => {
  try {
    const { code } = req.body;
    if (!code) {
      return res.status(400).json({ error: 'Code is required' });
    }

    const db = getDb();
    const user = await db.get('SELECT totp_secret, totp_enabled FROM users WHERE id = ?', [req.user.id]);
    if (!user?.totp_secret) {
      return res.status(400).json({ error: 'Please run 2FA setup first' });
    }
    if (user.totp_enabled) {
      return res.status(400).json({ error: '2FA is already enabled' });
    }

    const totp = new OTPAuth.TOTP({
      secret: OTPAuth.Secret.fromBase32(user.totp_secret),
      algorithm: 'SHA1',
      digits: 6,
      period: 30,
    });

    const delta = totp.validate({ token: code.replace(/\s/g, ''), window: 1 });
    if (delta === null) {
      return res.status(401).json({ error: 'Invalid code. Please try again.' });
    }

    await db.run('UPDATE users SET totp_enabled = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.user.id]);
    res.json({ enabled: true, message: '2FA has been enabled' });
  } catch (err) {
    log.auth.error(`2FA enable error: ${err.message}`);
    res.status(500).json({ error: '2FA could not be enabled' });
  }
});

// POST /api/auth/2fa/disable - Disable 2FA (requires password + TOTP code)
router.post('/2fa/disable', authenticateToken, twoFaLimiter, async (req, res) => {
  try {
    const { password, code } = req.body;
    if (!password || !code) {
      return res.status(400).json({ error: 'Password and code are required' });
    }
    if (typeof password !== 'string' || password.length > MAX_PASSWORD_LENGTH) {
      return res.status(400).json({ error: 'Invalid password' });
    }

    const db = getDb();
    const user = await db.get('SELECT password_hash, totp_secret, totp_enabled FROM users WHERE id = ?', [req.user.id]);
    if (!user?.totp_enabled) {
      return res.status(400).json({ error: '2FA is not enabled' });
    }

    if (!bcrypt.compareSync(password, user.password_hash)) {
      return res.status(401).json({ error: 'Invalid password' });
    }

    const totp = new OTPAuth.TOTP({
      secret: OTPAuth.Secret.fromBase32(user.totp_secret),
      algorithm: 'SHA1',
      digits: 6,
      period: 30,
    });

    const delta = totp.validate({ token: code.replace(/\s/g, ''), window: 1 });
    if (delta === null) {
      return res.status(401).json({ error: 'Invalid 2FA code' });
    }

    await db.run('UPDATE users SET totp_enabled = 0, totp_secret = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.user.id]);
    res.json({ enabled: false, message: '2FA has been disabled' });
  } catch (err) {
    log.auth.error(`2FA disable error: ${err.message}`);
    res.status(500).json({ error: '2FA could not be disabled' });
  }
});

export default router;
