const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const passwords = require('../utils/passwords');
const jwt = require('jsonwebtoken');
const { isMobileClient } = require('../utils/platform');
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
const crypto = require('crypto');
const db = require('../db/database');
const { JWT_SECRET, authMiddleware } = require('../middleware/auth');
const { hashToken, parseUserAgent, generateSessionId } = require('../utils/sessions');
const { notifyNewLogin, notifyPasswordChanged, notifyNewRegistration } = require('../utils/notify');
var logger = require('../utils/logger');

// Check password against Have I Been Pwned (k-anonymity — only first 5 chars of SHA-1 sent)
async function checkPwnedPassword(password) {
  try {
    var sha1 = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
    var prefix = sha1.substring(0, 5);
    var suffix = sha1.substring(5);
    var resp = await fetch('https://api.pwnedpasswords.com/range/' + prefix, {
      headers: { 'User-Agent': 'PediatricAIScribe-PasswordCheck' }
    });
    if (!resp.ok) return 0;
    var text = await resp.text();
    var match = text.split('\n').find(function(line) { return line.startsWith(suffix); });
    return match ? parseInt(match.split(':')[1]) : 0;
  } catch (e) { return 0; } // fail open — don't block registration if HIBP is down
}

// Public endpoint — check password before form submission
router.post('/check-password', async (req, res) => {
  var { password } = req.body;
  if (!password) return res.json({ breached: false });
  var count = await checkPwnedPassword(password);
  res.json({ breached: count > 0, count: count });
});

// ── Cookie helper ─────────────────────────────────────────────
// Sets the JWT as a secure, httpOnly cookie so JS cannot read it.
// maxAge is a safety net — real timeout is 24h sliding via middleware.
function setAuthCookie(res, token) {
  var isProduction = process.env.NODE_ENV === 'production' || process.env.APP_URL;
  res.cookie('ped_auth', token, {
    httpOnly: true,
    secure: !!isProduction,
    sameSite: 'lax',
    maxAge: 30 * 24 * 60 * 60 * 1000,  // 30 days absolute; middleware enforces 24h sliding idle
    path: '/'
  });
}

// Issue a JWT whose absolute expiry matches the client platform:
//   web    → 30 days (sliding 24h idle handled in middleware)
//   mobile → 365 days (persistent, token sits in Keychain / Keystore)
function signAuthToken(userId, req) {
  var expiresIn = isMobileClient(req) ? '365d' : '30d';
  return jwt.sign({ userId: userId }, JWT_SECRET, { expiresIn: expiresIn });
}

function clearAuthCookie(res) {
  res.clearCookie('ped_auth', { path: '/' });
}

// ============================================================
// HTML HELPERS
// ============================================================
function escHtml(str) {
  if (!str) return '';
  return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function safeAppUrl() {
  return escHtml((process.env.APP_URL || 'http://localhost:3000').replace(/\/$/, ''));
}

// ============================================================
// EMAIL TEMPLATES — markdown-style, Resend/Linear aesthetic
// Plain, spacious, no decorative chrome. Reads like a document.
// ============================================================
function emailWrapper(body) {
  var siteName = process.env.SITE_NAME || 'Pediatric AI Scribe';
  var F = '-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif';
  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>${escHtml(siteName)}</title>
  <style>
    body,table,td,p,a{font-family:${F};}
    @media only screen and (max-width:600px){
      .outer{padding:24px 16px !important;}
    }
  </style>
</head>
<body style="margin:0;padding:0;background:#ffffff;-webkit-text-size-adjust:100%;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
  <tr><td class="outer" align="center" style="padding:48px 24px;">
    <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="max-width:520px;width:100%;">

      <!-- Wordmark -->
      <tr><td style="padding-bottom:32px;border-bottom:1px solid #e5e7eb;">
        <p style="margin:0;font-size:14px;font-weight:600;color:#111827;letter-spacing:-0.1px;">${escHtml(siteName)}</p>
      </td></tr>

      <!-- Body -->
      <tr><td style="padding:32px 0;">
        ${body}
      </td></tr>

      <!-- Footer -->
      <tr><td style="padding-top:24px;border-top:1px solid #e5e7eb;">
        <p style="margin:0;font-size:12px;color:#9ca3af;line-height:1.7;">
          ${escHtml(siteName)} &mdash; AI-powered clinical documentation<br>
          If you didn&rsquo;t request this, you can safely ignore it.
        </p>
      </td></tr>

    </table>
  </td></tr>
</table>
</body>
</html>`;
}

function btnHtml(href, label) {
  return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0;">
    <tr><td style="border-radius:5px;background:#111827;">
      <a href="${escHtml(href)}" target="_blank"
         style="display:inline-block;color:#ffffff;padding:11px 22px;border-radius:5px;
                text-decoration:none;font-weight:500;font-size:14px;line-height:1;
                font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;">${escHtml(label)}</a>
    </td></tr>
  </table>`;
}

function linkFallback(url) {
  return `<p style="margin:16px 0 4px;font-size:13px;color:#6b7280;line-height:1.5;">If the button doesn&rsquo;t work, copy this link:</p>
  <p style="margin:0;padding:10px 12px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:4px;
            font-size:12px;font-family:'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace;
            word-break:break-all;color:#374151;line-height:1.6;">${escHtml(url)}</p>`;
}

// Email helper — DB settings override env vars
async function getSmtpTransport() {
  var nodemailer = require('nodemailer');
  var host = await db.getSetting('smtp.host').catch(function() { return null; }) || process.env.SMTP_HOST;
  if (!host) return null;
  var port = parseInt(await db.getSetting('smtp.port').catch(function() { return null; }) || process.env.SMTP_PORT || '587', 10);
  var user = await db.getSetting('smtp.user').catch(function() { return null; }) || process.env.SMTP_USER || '';
  var pass = await db.getSetting('smtp.pass').catch(function() { return null; }) || process.env.SMTP_PASS || '';
  var from = await db.getSetting('smtp.from').catch(function() { return null; }) || process.env.SMTP_FROM || user;
  var secure = (await db.getSetting('smtp.secure').catch(function() { return null; }) || process.env.SMTP_SECURE || 'false') === 'true';
  return { transport: nodemailer.createTransport({ host: host, port: port, secure: secure, auth: user ? { user: user, pass: pass } : undefined }), from: from };
}

async function sendEmail(to, subject, html) {
  try {
    var smtp = await getSmtpTransport();
    if (!smtp) {
      console.log('[Email] SMTP not configured. Would send to:', to);
      return false;
    }
    await smtp.transport.sendMail({ from: smtp.from, to: to, subject: subject, html: html });
    return true;
  } catch (err) {
    console.error('[Email] Failed:', err.message);
    return false;
  }
}

// ============================================================
// REGISTER (checks if registration is enabled)
// ============================================================
router.post('/register', async (req, res) => {
  try {
    var regEnabled = await db.getSetting('registration_enabled');
    if (regEnabled === 'false') {
      return res.status(403).json({ error: 'Registration is currently disabled. Contact an administrator.' });
    }

    var { email, password, name, turnstileToken } = req.body;
    if (!email || !password || !name) return res.status(400).json({ error: 'All fields required' });
    if (password.length < 8) return res.status(400).json({ error: 'Password must be 8+ characters' });

    // Cloudflare Turnstile verification
    if (process.env.TURNSTILE_SECRET_KEY) {
      if (!turnstileToken) return res.status(400).json({ error: 'Please complete the verification challenge' });
      var turnstileRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ secret: process.env.TURNSTILE_SECRET_KEY, response: turnstileToken, remoteip: req.ip })
      });
      var turnstileData = await turnstileRes.json();
      if (!turnstileData.success) {
        console.error('[Auth] Turnstile verification failed:', turnstileData['error-codes']);
        return res.status(400).json({ error: 'Bot verification failed. Please try again.' });
      }
    }

    var existing = await db.get('SELECT id FROM users WHERE email = ?', [email.toLowerCase()]);
    if (existing) return res.status(400).json({ error: 'Email already registered' });

    var hash = await passwords.hash(password);
    var verifyToken = crypto.randomBytes(32).toString('hex');
    var verifyExpires = Date.now() + 24 * 60 * 60 * 1000;

    var userCount = await db.get('SELECT COUNT(*) as count FROM users', []);
    var role = (userCount && parseInt(userCount.count) === 0) ? 'admin' : 'user';

    var result = await db.run(
      'INSERT INTO users (email, password, name, role, verify_token, verify_expires, email_verified) VALUES (?, ?, ?, ?, ?, ?, false)',
      [email.toLowerCase(), hash, name, role, verifyToken, verifyExpires]
    );

    var userId = result.lastInsertRowid;
    var verifyUrl = safeAppUrl() + '/api/auth/verify-email?token=' + verifyToken;
    var verifySubject = await db.getSetting('email.verify.subject') || 'Verify your Pediatric AI Scribe account';
    var verifyBody    = await db.getSetting('email.verify.body')    || 'Great to have you on Pediatric AI Scribe. Please verify your email address by clicking the button below.';

    await sendEmail(email, verifySubject, emailWrapper(
      `<p style="margin:0 0 8px;font-size:20px;font-weight:600;">Welcome aboard, ${escHtml(name)}!</p>
       <p style="color:#4b5563;margin:12px 0 20px;line-height:1.6;font-size:14px;">${escHtml(verifyBody).replace(/\n/g, '<br>')}</p>
       ${btnHtml(verifyUrl, 'Verify My Email')}
       ${linkFallback(verifyUrl)}
       <p style="color:#9ca3af;font-size:11px;margin:16px 0 0;">This link expires in 24 hours.</p>`
    ));

    await db.run('INSERT INTO audit_log (user_id, action, ip_address, details) VALUES (?, ?, ?, ?)',
      [userId, 'register', req.ip, role === 'admin' ? 'First user — auto admin' : 'standard user']);
    logger.audit(userId, 'register', role === 'admin' ? 'First user — auto admin' : 'standard user', req, { category: 'auth' });
    notifyNewRegistration(email, name);

    var smtpHost = await db.getSetting('smtp.host').catch(function() { return null; }) || process.env.SMTP_HOST;
    if (!smtpHost) {
      await db.run('UPDATE users SET email_verified = true, verify_token = NULL WHERE id = ?', [userId]);
      var token = signAuthToken(userId, req);
      var regSessionId = generateSessionId();
      try {
        await db.run('INSERT INTO user_sessions (id, user_id, token_hash, ip_address, user_agent, device_label) VALUES (?, ?, ?, ?, ?, ?)',
          [regSessionId, userId, hashToken(token), req.ip, req.headers['user-agent'] || '', parseUserAgent(req.headers['user-agent'])]);
      } catch (e) { /* table may not exist yet */ }
      setAuthCookie(res, token);
      return res.json({
        success: true, token: token, sessionId: regSessionId,
        user: { id: userId, email: email.toLowerCase(), name: name, role: role, email_verified: true },
        message: role === 'admin' ? 'Account created as ADMIN (first user). Auto-verified.' : 'Account created (auto-verified).'
      });
    }

    res.json({ success: true, needsVerification: true, message: 'Check your email for verification link.' + (role === 'admin' ? ' You are the first user and have admin privileges.' : '') });
  } catch (err) {
    console.error('[Auth] Register error:', err.message);
    res.status(500).json({ error: 'Registration failed. Please try again.' });
  }
});

// ============================================================
// VERIFY EMAIL
// ============================================================
router.get('/verify-email', async (req, res) => {
  try {
    var token = req.query.token;
    if (!token) return res.status(400).send('Missing token');
    var user = await db.get('SELECT id, name FROM users WHERE verify_token = ? AND verify_expires > ?', [token, Date.now()]);
    if (!user) {
      return res.send('<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Link Expired</title></head><body style="margin:0;padding:40px 24px;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;background:#fafafa;text-align:center;"><p style="font-size:15px;font-weight:600;color:#111827;margin:0 0 8px;">Link expired or invalid</p><p style="color:#6b7280;font-size:14px;margin:0 0 24px;">This verification link has expired. Request a new one from the app.</p><a href="' + safeAppUrl() + '" style="display:inline-block;background:#111827;color:#fff;padding:10px 22px;border-radius:6px;text-decoration:none;font-size:14px;font-weight:500;">Go to app</a></body></html>');
    }
    await db.run('UPDATE users SET email_verified = true, verify_token = NULL, verify_expires = NULL WHERE id = ?', [user.id]);
    await db.run('INSERT INTO audit_log (user_id, action) VALUES (?, ?)', [user.id, 'email_verified']);
    res.send('<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Email Verified</title></head><body style="margin:0;padding:40px 24px;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;background:#fafafa;text-align:center;"><p style="font-size:15px;font-weight:600;color:#111827;margin:0 0 8px;">Email verified</p><p style="color:#6b7280;font-size:14px;margin:0 0 24px;">Welcome, ' + escHtml(user.name) + '. You\'re all set.</p><a href="' + safeAppUrl() + '" style="display:inline-block;background:#111827;color:#fff;padding:10px 22px;border-radius:6px;text-decoration:none;font-size:14px;font-weight:500;">Open app</a></body></html>');
  } catch (err) { console.error('[Auth] Verify error:', err.message); res.status(500).send('Verification failed. Please try again.'); }
});

// RESEND VERIFICATION
router.post('/resend-verification', async (req, res) => {
  try {
    var user = await db.get('SELECT id, name, email_verified FROM users WHERE email = ?', [req.body.email.toLowerCase()]);
    if (!user) return res.json({ success: true, message: 'If account exists, verification email sent' });
    if (user.email_verified) return res.json({ success: true, message: 'Already verified.' });
    var verifyToken = crypto.randomBytes(32).toString('hex');
    await db.run('UPDATE users SET verify_token = ?, verify_expires = ? WHERE id = ?', [verifyToken, Date.now() + 86400000, user.id]);
    var verifyUrl = safeAppUrl() + '/api/auth/verify-email?token=' + verifyToken;
    await sendEmail(req.body.email, 'Verify your email — Pediatric AI Scribe', emailWrapper(
      `<p style="margin:0 0 8px;font-size:20px;font-weight:600;">Verify your email</p>
       <p style="color:#4b5563;margin:12px 0 20px;line-height:1.6;font-size:14px;">Here's a fresh verification link for your account. Click below to confirm your email and get started.</p>
       ${btnHtml(verifyUrl, 'Verify My Email')}
       ${linkFallback(verifyUrl)}
       <p style="color:#9ca3af;font-size:11px;margin:16px 0 0;">This link expires in 24 hours.</p>`
    ));
    res.json({ success: true, message: 'Verification email sent' });
  } catch (err) { console.error('[Auth] Resend error:', err.message); res.status(500).json({ error: 'Failed to send email' }); }
});

// ============================================================
// LOGIN (checks disabled status)
// ============================================================
router.post('/login', async (req, res) => {
  try {
    var { email, password, totpCode } = req.body;
    if (!email || !password) return res.status(400).json({ error: 'Email and password required' });

    // No Turnstile on login. The widget could not reliably complete a
    // challenge inside the Capacitor WebView, which locked mobile users out.
    // Brute-force cover here comes from the 10-per-15-min per-IP rate limit
    // (server.js), the constant-time bcrypt comparison below, and TOTP 2FA.
    // Registration and password reset — the endpoints that actually attract
    // bots — are still gated.

    var user = await db.get('SELECT * FROM users WHERE email = ?', [email.toLowerCase()]);
    // Enumeration-resistant: always run bcrypt to keep timing constant, and return
    // the same generic message for unknown-user, wrong-password, disabled, unverified.
    var DUMMY_HASH = '$2b$12$CwTycUXWue0Thq9StjUM0uJ8.aDLA18dY6nB5xuWz5M6l8lR6rYS.';
    if (!user) {
      // Server-side only — no email in the message, so operators can watch
      // lookup-miss rates in Grafana without leaking which addresses exist.
      console.warn('[Auth] login: user not found (ip=' + req.ip + ')');
      await bcrypt.compare(password, DUMMY_HASH).catch(function(){});
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    var valid;
    try {
      valid = await passwords.verify(password, user.password);
    } catch (verifyErr) {
      console.error('[Auth] passwords.verify threw:', verifyErr.message);
      return res.status(500).json({ error: 'Request failed' });
    }
    if (!valid) {
      await db.run('INSERT INTO audit_log (user_id, action, ip_address) VALUES (?, ?, ?)', [user.id, 'login_failed', req.ip]).catch(function(){});
      logger.access(user.id, 'login_failed', req, false);
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    // Transparent migration: bcrypt → argon2id on next successful login.
    passwords.maybeRehash(password, user.password).then(function(newHash) {
      if (newHash) db.run('UPDATE users SET password = ? WHERE id = ?', [newHash, user.id]).catch(function(){});
    }).catch(function(){});

    if (user.disabled) {
      await db.run('INSERT INTO audit_log (user_id, action, ip_address, details) VALUES (?, ?, ?, ?)',
        [user.id, 'login_blocked', req.ip, 'Account disabled']).catch(function(){});
      logger.access(user.id, 'login_blocked', req, false);
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    if (!user.email_verified) {
      return res.status(403).json({ error: 'Email not verified', needsVerification: true });
    }

    if (user.totp_enabled) {
      if (!totpCode) return res.json({ requires2FA: true });
      var totpInput = String(totpCode).trim();
      var verified = speakeasy.totp.verify({ secret: user.totp_secret, encoding: 'base32', token: totpInput, window: 1 });
      if (!verified) {
        // Backup-code fallback. Backup codes are 10-char alphanumeric and
        // each hash is bcrypt — consumed on use.
        var consumed = await tryConsumeBackupCode(user.id, totpInput);
        if (!consumed) return res.status(401).json({ error: 'Invalid 2FA code' });
        await db.run('INSERT INTO audit_log (user_id, action, ip_address, details) VALUES (?, ?, ?, ?)',
          [user.id, '2fa_backup_code_used', req.ip, 'Backup code consumed at login']).catch(function(){});
        logger.audit(user.id, '2fa_backup_code_used', 'Backup code consumed at login', req, { category: 'auth' });
      }
    }

    var token = signAuthToken(user.id, req);
    await db.run('INSERT INTO audit_log (user_id, action, ip_address) VALUES (?, ?, ?)', [user.id, 'login', req.ip]);
    logger.access(user.id, 'login', req, true);

    // Create session record
    var sessionId = generateSessionId();
    try {
      await db.run('INSERT INTO user_sessions (id, user_id, token_hash, ip_address, user_agent, device_label) VALUES (?, ?, ?, ?, ?, ?)',
        [sessionId, user.id, hashToken(token), req.ip, req.headers['user-agent'] || '', parseUserAgent(req.headers['user-agent'])]);
    } catch (e) { /* table may not exist yet */ }

    // Notify user of new login (fire-and-forget)
    notifyNewLogin(user.id, parseUserAgent(req.headers['user-agent']), req.ip);

    setAuthCookie(res, token);
    res.json({
      success: true, token: token, sessionId: sessionId,
      user: { id: user.id, email: user.email, name: user.name, role: user.role, totp_enabled: user.totp_enabled, email_verified: user.email_verified }
    });
  } catch (err) { console.error('[Auth] Login error:', err.message); res.status(500).json({ error: 'Login failed' }); }
});

// ── 2FA backup codes helpers ──────────────────────────────
// 10 single-use codes, 10 chars each (alphanumeric, readable). Stored as
// JSON array of bcrypt hashes in users.totp_backup_codes.
function generateBackupCodes(n) {
  n = n || 10;
  var alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no 0/O/1/I to avoid confusion
  var out = [];
  for (var i = 0; i < n; i++) {
    var code = '';
    var buf = crypto.randomBytes(10);
    for (var j = 0; j < 10; j++) code += alphabet[buf[j] % alphabet.length];
    // Render as XXXXX-XXXXX for readability
    out.push(code.slice(0, 5) + '-' + code.slice(5));
  }
  return out;
}

async function hashBackupCodes(codes) {
  var hashes = [];
  for (var i = 0; i < codes.length; i++) {
    hashes.push(await bcrypt.hash(codes[i].replace('-', '').toUpperCase(), 10));
  }
  return hashes;
}

async function tryConsumeBackupCode(userId, submitted) {
  if (!submitted) return false;
  var normalized = String(submitted).replace(/[-\s]/g, '').toUpperCase();
  if (!/^[A-Z0-9]{10}$/.test(normalized)) return false; // wrong format — skip

  // Atomic read-modify-write: wrap the SELECT + UPDATE in a transaction
  // with SELECT ... FOR UPDATE to serialize concurrent consume attempts.
  // Without this, two parallel logins using the same code could both
  // pass the bcrypt compare and both succeed. The row lock funnels them
  // one at a time — second attempt sees the already-consumed array.
  var client = await db.pool.connect();
  try {
    await client.query('BEGIN');
    var rowRes = await client.query(
      'SELECT totp_backup_codes FROM users WHERE id = $1 FOR UPDATE',
      [userId]
    );
    var row = rowRes.rows[0];
    if (!row || !row.totp_backup_codes) { await client.query('ROLLBACK'); return false; }
    var hashes;
    try { hashes = JSON.parse(row.totp_backup_codes); } catch (e) { await client.query('ROLLBACK'); return false; }
    if (!Array.isArray(hashes)) { await client.query('ROLLBACK'); return false; }
    for (var i = 0; i < hashes.length; i++) {
      if (await bcrypt.compare(normalized, hashes[i])) {
        hashes.splice(i, 1);
        await client.query(
          'UPDATE users SET totp_backup_codes = $1 WHERE id = $2',
          [JSON.stringify(hashes), userId]
        );
        await client.query('COMMIT');
        return true;
      }
    }
    await client.query('ROLLBACK');
    return false;
  } catch (e) {
    try { await client.query('ROLLBACK'); } catch (_) {}
    console.error('[Auth] backup-code consume error:', e.message);
    return false;
  } finally {
    client.release();
  }
}

// Generate (or regenerate) backup codes. Requires current password to authorise.
router.post('/2fa/backup-codes', authMiddleware, async (req, res) => {
  try {
    var { password } = req.body;
    if (!password) return res.status(400).json({ error: 'Current password required' });
    var user = await db.get('SELECT password, totp_enabled FROM users WHERE id = ?', [req.user.id]);
    if (!user || !user.totp_enabled) return res.status(400).json({ error: '2FA is not enabled' });
    var valid = await passwords.verify(password, user.password);
    if (!valid) return res.status(401).json({ error: 'Wrong password' });

    var codes = generateBackupCodes(10);
    var hashes = await hashBackupCodes(codes);
    await db.run('UPDATE users SET totp_backup_codes = ? WHERE id = ?', [JSON.stringify(hashes), req.user.id]);
    await db.run('INSERT INTO audit_log (user_id, action, ip_address) VALUES (?, ?, ?)', [req.user.id, '2fa_backup_codes_regenerated', req.ip]).catch(function(){});
    logger.audit(req.user.id, '2fa_backup_codes_regenerated', 'Backup codes regenerated', req, { category: 'auth' });
    // Return plaintext codes — this is the ONLY time they are visible.
    res.json({ success: true, codes: codes, message: 'Save these somewhere safe — they cannot be shown again.' });
  } catch (err) { console.error('[Auth] 2FA backup codes error:', err.message); res.status(500).json({ error: 'Request failed' }); }
});

// How many backup codes remain (for UI badge).
router.get('/2fa/backup-codes/count', authMiddleware, async (req, res) => {
  try {
    var row = await db.get('SELECT totp_backup_codes FROM users WHERE id = ?', [req.user.id]);
    var n = 0;
    if (row && row.totp_backup_codes) {
      try { var arr = JSON.parse(row.totp_backup_codes); if (Array.isArray(arr)) n = arr.length; } catch(e){}
    }
    res.json({ success: true, remaining: n });
  } catch (err) { res.status(500).json({ error: 'Request failed' }); }
});

// 2FA
router.post('/setup-2fa', authMiddleware, async (req, res) => {
  try {
    // SSO-only accounts never go through the local password flow, so TOTP
    // would sit dormant. Reject up front with a clear message.
    var row = await db.get('SELECT password FROM users WHERE id = ?', [req.user.id]);
    if (!row || !hasLocalPassword(row.password)) {
      return res.status(400).json({ error: 'This account uses SSO. Two-factor authentication is managed by your identity provider.' });
    }
    var secret = speakeasy.generateSecret({ name: 'PedScribe (' + req.user.email + ')', issuer: 'Pediatric AI Scribe' });
    await db.run('UPDATE users SET totp_secret = ? WHERE id = ?', [secret.base32, req.user.id]);
    var qrUrl = await QRCode.toDataURL(secret.otpauth_url);
    res.json({ success: true, secret: secret.base32, qrCode: qrUrl });
  } catch (err) { console.error('[Auth] 2FA setup error:', err.message); res.status(500).json({ error: '2FA setup failed' }); }
});

router.post('/verify-2fa', authMiddleware, async (req, res) => {
  try {
    var user = await db.get('SELECT totp_secret, totp_enabled FROM users WHERE id = ?', [req.user.id]);
    var verified = speakeasy.totp.verify({ secret: user.totp_secret, encoding: 'base32', token: req.body.code, window: 1 });
    if (!verified) return res.status(400).json({ error: 'Invalid code' });

    // First time enabling — generate backup codes and show them once.
    var wasFirstEnable = !user.totp_enabled;
    var backupCodes = null;
    if (wasFirstEnable) {
      backupCodes = generateBackupCodes(10);
      var hashes = await hashBackupCodes(backupCodes);
      await db.run('UPDATE users SET totp_enabled = true, totp_backup_codes = ? WHERE id = ?', [JSON.stringify(hashes), req.user.id]);
    } else {
      await db.run('UPDATE users SET totp_enabled = true WHERE id = ?', [req.user.id]);
    }
    res.json({ success: true, backupCodes: backupCodes });
  } catch (err) { console.error('[Auth] 2FA verify error:', err.message); res.status(500).json({ error: '2FA verification failed' }); }
});

router.post('/disable-2fa', authMiddleware, async (req, res) => {
  try {
    var user = await db.get('SELECT password FROM users WHERE id = ?', [req.user.id]);
    var valid = await passwords.verify(req.body.password, user.password);
    if (!valid) return res.status(401).json({ error: 'Wrong password' });
    await db.run('UPDATE users SET totp_enabled = false, totp_secret = NULL, totp_backup_codes = NULL WHERE id = ?', [req.user.id]);
    res.json({ success: true });
  } catch (err) { console.error('[Auth] 2FA disable error:', err.message); res.status(500).json({ error: '2FA disable failed' }); }
});

// Password reset
//
// Timing-safe: response is sent after Turnstile verification, BEFORE the DB
// lookup and email send. Hit and miss therefore take the same wall-clock
// time (no SMTP RTT on hit, no extra latency on miss), closing the
// user-enumeration oracle that previously let an attacker distinguish
// registered emails by response time.
router.post('/forgot-password', async (req, res) => {
  try {
    // Cloudflare Turnstile verification (runs for every request regardless
    // of whether the account exists, so timing is equal)
    if (process.env.TURNSTILE_SECRET_KEY) {
      var turnstileToken = req.body.turnstileToken;
      if (!turnstileToken) return res.status(400).json({ error: 'Please complete the verification' });
      var tsRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ secret: process.env.TURNSTILE_SECRET_KEY, response: turnstileToken, remoteip: req.ip })
      });
      var tsData = await tsRes.json();
      if (!tsData.success) return res.status(400).json({ error: 'Verification failed. Please try again.' });
    }

    var email = String(req.body.email || '').toLowerCase();

    // Return the identical response immediately — do not await anything
    // account-specific below this point.
    res.json({ success: true, message: 'If account exists, reset email sent' });

    // Fire-and-forget: DB lookup + token + email happen after the response
    // is sent, so their latency cannot be measured by the caller.
    async function sendResetEmailInBackground() {
      try {
        var user = await db.get('SELECT id FROM users WHERE email = ?', [email]);
        if (!user) return;
        var token = crypto.randomBytes(32).toString('hex');
        await db.run('UPDATE users SET reset_token = ?, reset_expires = ? WHERE id = ?', [token, Date.now() + 3600000, user.id]);
        var resetUrl     = safeAppUrl() + '/reset-password?token=' + token;
        var resetSubject = await db.getSetting('email.reset.subject') || 'Reset your password — Pediatric AI Scribe';
        var resetBody    = await db.getSetting('email.reset.body')    || 'Someone requested a password reset for your Pediatric AI Scribe account. If that was you, click the button below to choose a new password. This link expires in 1 hour. If you did not request a password reset, no action is needed.';
        await sendEmail(email, resetSubject, emailWrapper(
          `<p style="margin:0 0 8px;font-size:20px;font-weight:600;">Password reset request</p>
           <p style="color:#4b5563;margin:12px 0 20px;line-height:1.6;font-size:14px;">${escHtml(resetBody).replace(/\n/g, '<br>')}</p>
           ${btnHtml(resetUrl, 'Reset My Password')}
           ${linkFallback(resetUrl)}`
        ));
      } catch (bgErr) {
        console.error('[Auth] forgot-password background send failed:', bgErr.message);
      }
    }
    sendResetEmailInBackground();
  } catch (err) { console.error('[Auth] Forgot password error:', err.message); res.status(500).json({ error: 'Password reset request failed' }); }
});

router.post('/reset-password', async (req, res) => {
  try {
    var { token, newPassword } = req.body;
    if (!token || !newPassword || newPassword.length < 8) return res.status(400).json({ error: 'Valid token and 8+ char password required' });
    var user = await db.get('SELECT id FROM users WHERE reset_token = ? AND reset_expires > ?', [token, Date.now()]);
    if (!user) return res.status(400).json({ error: 'Invalid or expired token' });
    var pwnedCount = await checkPwnedPassword(newPassword);
    var hash = await passwords.hash(newPassword);
    await db.run('UPDATE users SET password = ?, reset_token = NULL, reset_expires = NULL WHERE id = ?', [hash, user.id]);
    // Force logout — destroy all sessions for this user
    try { await db.run('DELETE FROM user_sessions WHERE user_id = ?', [user.id]); } catch (e) { /* best effort */ }
    var resp = { success: true };
    if (pwnedCount > 0) resp.passwordWarning = 'This password has appeared in ' + pwnedCount.toLocaleString() + ' data breaches. Consider changing it to something unique.';
    res.json(resp);
  } catch (err) { console.error('[Auth] Reset password error:', err.message); res.status(500).json({ error: 'Password reset failed' }); }
});

// Logout — destroy session and clear cookie
router.post('/logout', async function(req, res) {
  try {
    var token = null;
    var authHeader = req.headers.authorization;
    if (authHeader && authHeader.startsWith('Bearer ')) token = authHeader.substring(7) || null;
    if (!token && req.cookies && req.cookies.ped_auth) token = req.cookies.ped_auth;
    if (token) {
      await db.run('DELETE FROM user_sessions WHERE token_hash = ?', [hashToken(token)]);
    }
  } catch (e) { /* best effort */ }
  clearAuthCookie(res);
  res.json({ success: true });
});

// Returns true if the user has a real password hash and can change it.
// SSO-auto-created users have a random hex blob in `password` — changing
// it is meaningless because they never authenticate locally.
function hasLocalPassword(hash) {
  return !!(hash && (/^\$2[aby]\$/.test(hash) || hash.indexOf('$argon2') === 0));
}

// Change password (requires current password)
router.post('/change-password', authMiddleware, async (req, res) => {
  try {
    var { currentPassword, newPassword } = req.body;
    if (!currentPassword || !newPassword) return res.status(400).json({ error: 'Current and new password required' });
    if (newPassword.length < 8) return res.status(400).json({ error: 'New password must be 8+ characters' });

    var user = await db.get('SELECT password FROM users WHERE id = ?', [req.user.id]);
    if (!hasLocalPassword(user.password)) {
      return res.status(400).json({ error: 'This account uses SSO. Password changes are managed by your identity provider.' });
    }
    var valid = await passwords.verify(currentPassword, user.password);
    if (!valid) return res.status(401).json({ error: 'Current password is incorrect' });

    var pwnedCount = await checkPwnedPassword(newPassword);
    var hash = await passwords.hash(newPassword);
    await db.run('UPDATE users SET password = ? WHERE id = ?', [hash, req.user.id]);

    // Destroy all OTHER sessions (keep current one)
    if (req.sessionId) {
      try { await db.run('DELETE FROM user_sessions WHERE user_id = ? AND id != ?', [req.user.id, req.sessionId]); } catch (e) { /* best effort */ }
    }

    await db.run('INSERT INTO audit_log (user_id, action, ip_address) VALUES (?, ?, ?)', [req.user.id, 'password_changed', req.ip]);
    logger.audit(req.user.id, 'password_changed', 'Password changed', req, { category: 'auth' });
    notifyPasswordChanged(req.user.id);

    var resp = { success: true, message: 'Password changed. All other sessions have been logged out.' };
    if (pwnedCount > 0) resp.passwordWarning = 'This password has appeared in ' + pwnedCount.toLocaleString() + ' data breaches. Consider choosing a different one.';
    res.json(resp);
  } catch (err) { console.error('[Auth] Change password error:', err.message); res.status(500).json({ error: 'Password change failed' }); }
});

// Get current user
router.get('/me', authMiddleware, async (req, res) => {
  try {
    var user = await db.get(
      'SELECT id, email, name, role, totp_enabled, email_verified, nextcloud_url, nextcloud_user, nextcloud_folder, oidc_sub, password, created_at FROM users WHERE id = ?',
      [req.user.id]
    );
    // canLocalAuth: true if the user has a real password hash (can log in
    // locally, so password change / 2FA are meaningful). SSO-auto-created
    // users have a random hex blob in `password` which can't be verified
    // — hide those UIs for them.
    var canLocalAuth = !!(user && user.password && (/^\$2[aby]\$/.test(user.password) || user.password.indexOf('$argon2') === 0));
    // Don't leak the password hash in the response.
    if (user) delete user.password;
    if (user) user.canLocalAuth = canLocalAuth;
    res.json({ user: user });
  } catch (err) { console.error('[Auth] Me error:', err.message); res.status(500).json({ error: 'Failed to load user' }); }
});

// Check if registration is enabled (public endpoint)
router.get('/registration-status', async (req, res) => {
  try {
    var enabled = await db.getSetting('registration_enabled');
    res.json({ registrationEnabled: enabled !== 'false' });
  } catch (err) { res.json({ registrationEnabled: true }); }
});

// Expose helpers for adminConfig test-email and email template loading
module.exports = router;
module.exports.__sendEmail    = sendEmail;
module.exports.__emailWrapper = emailWrapper;
module.exports.__btnHtml      = btnHtml;
