/**
 * routes/twofa.js
 * TOTP enrollment, verification, backup codes.
 *
 * Flow:
 *   1. POST /2fa/start       — generates a secret, returns QR + otpauth URL
 *   2. POST /2fa/verify      — confirms the user can produce a valid code, then enables
 *   3. POST /2fa/disable     — disable (requires current code)
 *   4. POST /2fa/backup-codes — regenerate (requires current code)
 *   5. POST /auth/login/2fa   — second factor after password is correct
 */

'use strict';

const express = require('express');
const { db } = require('../config/db');
const User    = require('../models/user');
const totp    = require('../services/totp');
const sec     = require('../config/security');
const Audit   = require('../models/auditLog');
const { requireLogin } = require('../middleware/auth');

const router = express.Router();

function getRow(userId) {
  return db.prepare('SELECT * FROM user_2fa WHERE user_id = ?').get(userId);
}
function setRow(userId, secret, enabled, backupCodes) {
  db.prepare(`
    INSERT INTO user_2fa (user_id, secret, enabled, backup_codes, enrolled_at)
    VALUES (?, ?, ?, ?, ?)
    ON CONFLICT(user_id) DO UPDATE SET
      secret = excluded.secret,
      enabled = excluded.enabled,
      backup_codes = excluded.backup_codes,
      enrolled_at = excluded.enrolled_at
  `).run(userId, secret, enabled ? 1 : 0, JSON.stringify(backupCodes || []), enabled ? Date.now() : null);
}

function handleStart(req, res) {
  if (!req.user) return res.status(401).json({ error: 'UNAUTHENTICATED' });
  const existing = getRow(req.user.id);
  // If 2FA is already enabled, refuse to re-enroll without disable
  if (existing && existing.enabled) {
    return res.status(400).json({ error: 'ALREADY_ENABLED' });
  }
  const secret = totp.generateSecret();
  // Pre-generate backup codes (so they're ready to display at verify time)
  const backupCodes = totp.generateBackupCodes();
  setRow(req.user.id, secret, false, backupCodes.map(totp.hashBackupCode));
  const url = totp.otpauthUrl({ secret, label: req.user.email, issuer: sec.TOTP_ISSUER });
  res.json({ secret, otpauth_url: url, message: 'Scan the QR or paste the secret into your authenticator, then POST /2fa/verify with the 6-digit code to enable.' });
}

function handleVerify(req, res) {
  if (!req.user) return res.status(401).json({ error: 'UNAUTHENTICATED' });
  const { token, remember_device, remember_days } = req.body || {};
  const row = getRow(req.user.id);
  if (!row) return res.status(400).json({ error: 'NOT_STARTED' });
  if (!totp.verifyToken(row.secret, token)) {
    return res.status(400).json({ error: 'INVALID_CODE' });
  }
  // Enable + return backup codes ONCE
  setRow(req.user.id, row.secret, true, JSON.parse(row.backup_codes || '[]'));
  // Re-derive the plaintext backup codes from their hashes by looking at the
  // original generation. (We only have the hashes, so re-issue new codes
  // and return them to the user — at the cost of invalidating any pre-shown
  // codes. That's a fair tradeoff since /start is the only way to see them.)
  const newCodes = totp.generateBackupCodes();
  setRow(req.user.id, row.secret, true, newCodes.map(totp.hashBackupCode));
  // Mark session 2FA-verified so the admin gate lets us through for 1h
  if (req.session) req.session.twofa_verified_at = Date.now();
  // Trusted-device cookie: opt-in via `remember_device: true` in the
  // body, or auto-issue when `remember_days` is set. Default to ON
  // for the superadmin / single-tenant install so the operator
  // doesn't have to re-verify every deploy. Other roles can still
  // opt out by passing `remember_device: false`.
  const wantsRemember = remember_device !== false && req.user.role === 'superadmin';
  if (wantsRemember) {
    try {
      const trustedDevice = require('../services/trustedDevice');
      trustedDevice.issue(req, res, { user_id: req.user.id, ttlDays: remember_days || trustedDevice.DEFAULT_TTL_DAYS });
    } catch (e) { console.warn('[2fa] trusted-device issue failed:', e.message); }
  }
  Audit.log(req.user, '2fa.enable', `user:${req.user.id}`, { remember_device: wantsRemember });
  res.json({ ok: true, backup_codes: newCodes, message: 'Save these backup codes. Each can be used once if you lose access to your authenticator.', trusted_device_issued: wantsRemember });
}

router.post('/start', requireLogin, handleStart);
router.post('/verify', requireLogin, handleVerify);

router.post('/disable', requireLogin, (req, res) => {
  if (!req.user) return res.status(401).json({ error: 'UNAUTHENTICATED' });
  const { token } = req.body || {};
  const row = getRow(req.user.id);
  if (!row || !row.enabled) return res.status(400).json({ error: 'NOT_ENABLED' });
  if (!totp.verifyToken(row.secret, token)) {
    return res.status(400).json({ error: 'INVALID_CODE' });
  }
  db.prepare('DELETE FROM user_2fa WHERE user_id = ?').run(req.user.id);
  Audit.log(req.user, '2fa.disable', `user:${req.user.id}`);
  res.json({ ok: true });
});

router.post('/backup-codes', requireLogin, (req, res) => {
  if (!req.user) return res.status(401).json({ error: 'UNAUTHENTICATED' });
  const { token } = req.body || {};
  const row = getRow(req.user.id);
  if (!row || !row.enabled) return res.status(400).json({ error: 'NOT_ENABLED' });
  if (!totp.verifyToken(row.secret, token)) {
    return res.status(400).json({ error: 'INVALID_CODE' });
  }
  const newCodes = totp.generateBackupCodes();
  setRow(req.user.id, row.secret, true, newCodes.map(totp.hashBackupCode));
  res.json({ backup_codes: newCodes });
});

/**
 * Verify a TOTP / backup code during login.
 * Body: { email, password, token }
 * On success: sets req.session.userId and returns the user.
 */
const { twofa: twofaLimiter } = require('../middleware/rateLimit');
router.post('/auth/login/2fa', twofaLimiter, (req, res) => {
  const { token } = req.body || {};
  if (!token) return res.status(400).json({ error: 'MISSING_TOKEN' });
  // The session itself is the source of truth for the pending 2FA state.
  const sess = req.session;
  if (!sess.pending_2fa_user_id || !sess.pending_2fa_expires || sess.pending_2fa_expires < Date.now()) {
    return res.status(401).json({ error: 'PENDING_EXPIRED' });
  }
  const u = User.getById(sess.pending_2fa_user_id);
  if (!u) return res.status(401).json({ error: 'INVALID_PENDING' });
  const row = getRow(u.id);
  if (!row || !row.enabled) return res.status(400).json({ error: 'NO_2FA' });

  let ok = false;
  if (token && /^\d{6}$/.test(token)) {
    ok = totp.verifyToken(row.secret, token);
  } else if (token) {
    // Try as backup code
    const hashed = totp.hashBackupCode(token);
    const codes = JSON.parse(row.backup_codes || '[]');
    const idx = codes.indexOf(hashed);
    if (idx >= 0) {
      // Consume the backup code
      codes.splice(idx, 1);
      db.prepare('UPDATE user_2fa SET backup_codes = ? WHERE user_id = ?')
        .run(JSON.stringify(codes), u.id);
      ok = true;
    }
  }
  if (!ok) return res.status(401).json({ error: 'INVALID_CODE' });

  // Promote session to authenticated
  delete sess.pending_2fa_user_id;
  delete sess.pending_2fa_expires;
  sess.userId = u.id;
  sess.password_at = u.password_changed_at || Date.now();
  sess.email_at    = u.email_changed_at    || Date.now();
  sess.created_ip = req.ip;
  sess.created_ua = (req.get('user-agent') || '').slice(0, 256);
  sess.created_at = Date.now();
  try {
    const geo = require('../services/geo');
    sess.created_geo = geo.lookup(req.ip);
  } catch (_) {}
  // Mark this session as 2FA-verified so the superadmin-2FA gate lets
  // the admin through for the next hour (rolling on every admin call).
  sess.twofa_verified_at = Date.now();
  // Trusted-device cookie — auto-issue for superadmin so a single-
  // tenant operator doesn't have to re-verify 2FA after every
  // deploy (which wipes data/sessions.sqlite). Skip if the client
  // explicitly says no.
  const wantsRemember = (req.body && req.body.remember_device !== false) && u.role === 'superadmin';
  if (wantsRemember) {
    try {
      const trustedDevice = require('../services/trustedDevice');
      trustedDevice.issue(req, res, { user_id: u.id, ttlDays: (req.body && req.body.remember_days) || trustedDevice.DEFAULT_TTL_DAYS });
    } catch (e) { console.warn('[2fa login] trusted-device issue failed:', e.message); }
  }
  Audit.log(u, 'user.login.2fa', `user:${u.id}`, { trusted_device_issued: wantsRemember });
  // Persist the session before responding so Set-Cookie is included
  sess.save((err) => {
    if (err) console.warn('[twofa] session save error:', err.message);
    res.json({ ok: true, user: User.safeUser(u), trusted_device_issued: wantsRemember });
  });
});

/**
 * Public status: does this session have a pending 2FA challenge?
 */
router.get('/status', (req, res) => {
  if (req.session && req.session.pending_2fa_user_id && req.session.pending_2fa_expires > Date.now()) {
    return res.json({ pending_2fa: true, expires_at: req.session.pending_2fa_expires });
  }
  return res.json({ pending_2fa: false });
});

module.exports = router;
module.exports.handleStart = handleStart;
module.exports.handleVerify = handleVerify;
