const users = require('../../../model/users/users.model')
const activity = require('../../../model/activity/activity.model')
const { setAuthCookie, clearAuthCookie } = require('../../../auth/token')
const sessionService = require('../../../auth/session.service')
const totp = require('../../../utils/totp')
const botScore = require('../../../middleware/botScore')
const loginProtection = require('../../../middleware/loginProtection')

const log = require('../../../utils/logger')('auth')

// Honeypot input name — must match the hidden field rendered on the login form.
// Chosen to look like a real field so naive bots fill it; real users never see it.
const HONEYPOT_FIELD = 'company'

// One generic failure response for every "you don't get in" case (wrong user,
// wrong password, tripped honeypot). Never reveals which was wrong.
const GENERIC_FAIL = { message: 'Incorrect username or password.' }

// True when this user must complete a second factor before getting a session.
function needsTotp(user) {
  return Boolean(user && user.totp_enabled)
}

// Issue the real session: create the session token via the session service, set
// the cookie, clear the IP's failure backoff, and record the login. authMethod
// records how this session was authenticated ('local' password, or 'totp' after
// the second factor) — carried in the session token for downstream visibility.
async function issueSession(req, res, user, authMethod = 'local') {
  loginProtection.recordSuccess(req.ip)
  await users.recordLogin(user.id)
  const { token } = sessionService.createSession(user, authMethod)
  setAuthCookie(req, res, token)
  await activity.log({ req, userId: user.id, action: 'auth.login' })
  log.info('login success', { username: user.username, id: user.id, ip: req.ip, authMethod })
  return res.json({ user: { id: user.id, username: user.username, role: user.role } })
}

async function login(req, res) {
  const { username, password } = req.body

  // Honeypot: a populated hidden field means a bot. Fail generically, but score
  // it hard — this is an unambiguous signal, unlike a mistyped password.
  if (req.body[HONEYPOT_FIELD]) {
    botScore.recordHoneypot(req.ip)
    loginProtection.recordFailure(req.ip)
    log.warn('honeypot login hit', { ip: req.ip, username })
    return res.status(401).json(GENERIC_FAIL)
  }

  try {
    const user = await users.getRawByUsername(username)
    const ok = user && (await users.validatePassword(user, password))
    if (!ok) {
      botScore.recordLoginFailure(req.ip)
      loginProtection.recordFailure(req.ip)
      log.warn('login failed', { username, ip: req.ip })
      return res.status(401).json(GENERIC_FAIL)
    }

    // Password is correct. If this user has TOTP on, do NOT issue a session yet —
    // hand back a short-lived, signed "password verified" challenge and require
    // the code. If TOTP is off, log them straight in.
    if (needsTotp(user)) {
      const challenge = sessionService.createPartialSession(user)
      log.info('password ok, awaiting TOTP', { username: user.username, id: user.id, ip: req.ip })
      return res.json({ totpRequired: true, challenge })
    }

    return issueSession(req, res, user, 'local')
  } catch (err) {
    log.error('login error', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

// Second step for TOTP users: verify the challenge token + code, then issue the
// session. A wrong code counts as a failed attempt (backoff + bot score).
async function loginTotp(req, res) {
  const { challenge, code } = req.body
  const decoded = sessionService.upgradeSessionAfterTotp(challenge)
  if (!decoded) {
    return res.status(401).json({ message: 'Your verification session expired. Please sign in again.' })
  }
  try {
    const user = await users.getRawById(decoded.id)
    if (!user || !user.totp_enabled || !totp.verifyCode(user.totp_secret, code)) {
      botScore.recordLoginFailure(req.ip)
      loginProtection.recordFailure(req.ip)
      log.warn('TOTP verify failed', { id: decoded.id, ip: req.ip })
      return res.status(401).json({ message: 'Invalid verification code.' })
    }
    return issueSession(req, res, user, 'totp')
  } catch (err) {
    log.error('loginTotp error', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

// Clear the caller's cookie AND revoke this session server-side, so a copy of the
// token (proxy log, shared machine, XSS-exfiltrated cookie) can't keep being used
// after logout. attachSession populated req.session (best-effort) with the jti +
// expiry; if there was no valid session, there's simply nothing to revoke.
async function logout(req, res) {
  clearAuthCookie(req, res)
  try {
    if (req.session?.sessionId) {
      await sessionService.revokeSession(req.session.sessionId, {
        userId: req.session.userId,
        expiresAt: req.session.expiresAt,
      })
      await activity.log({ req, userId: req.session.userId, action: 'auth.logout' })
    }
  } catch (err) {
    // Never fail the logout on a revocation/logging hiccup — the cookie is cleared.
    log.error('logout revoke error', err)
  }
  return res.json({ message: 'Logged out.' })
}

async function me(req, res) {
  try {
    const user = await users.getById(req.user.id)
    if (!user) return res.status(401).json({ message: 'Unauthorized' })
    return res.json({ user })
  } catch (err) {
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

module.exports = { login, loginTotp, logout, me, needsTotp, HONEYPOT_FIELD }
