// ── Mobile (Android) bearer-token auth ─────────────────────────────────────
//
// Purely additive alongside the web cookie flow. Native clients POST credentials
// here and receive a short-lived access token (a normal session JWT, validated
// on every route by the shared requireAuth middleware) plus a long-lived,
// server-stored, revocable refresh token. This controller reuses the exact same
// brute-force defenses as web login (bot scoring + login backoff), and handles
// TOTP in a single stateless request: if 2FA is on and no/invalid code is given,
// it replies { totpRequired: true } and the app retries with the code.
//
// It does NOT touch the web login/loginTotp handlers or the TOTP staged-challenge
// flow — those are unchanged.

const users = require('../../../model/users/users.model')
const activity = require('../../../model/activity/activity.model')
const mobileSessions = require('../../../model/mobileSessions/mobileSessions.model')
const recoveryCodes = require('../../../model/recoveryCodes/recoveryCodes.model')
const sessionService = require('../../../auth/session.service')
const { establishTrust } = require('./trustDevice.helper')
const totp = require('../../../utils/totp')
const botScore = require('../../../middleware/botScore')
const loginProtection = require('../../../middleware/loginProtection')

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

// Same generic failure text as web — never reveals which credential was wrong.
const GENERIC_FAIL = { message: 'Incorrect username or password.' }

// Shape returned to the client on a successful login/refresh. Access + refresh
// tokens, the access lifetime, and the safe (secret-stripped) user.
function tokenResponse(out, user) {
  return {
    accessToken: out.accessToken,
    refreshToken: out.refreshToken,
    expiresIn: out.expiresIn,
    user: { id: user.id, username: user.username, role: user.role },
  }
}

// Persist a freshly minted refresh token (by hash) and record the login. Shared
// by login and refresh so the storage/side-effect logic lives in one place.
// `deviceName` is the friendly label for the Active Devices list — supplied by
// the client at login and carried forward across rotations on refresh.
async function persistAndFinish(req, user, out, action, deviceName = null) {
  await mobileSessions.store({
    userId: user.id,
    tokenHash: out.refreshHash,
    deviceHash: out.deviceHash,
    deviceName,
    userAgent: out.userAgent,
    expiresAt: out.refreshExpiresAt,
  })
  await users.recordLogin(user.id)
  await activity.log({ req, userId: user.id, action })
}

// POST /auth/mobile/login  { username, password, code?, recoveryCode?, trustDevice? }
async function login(req, res) {
  const { username, password, code, recoveryCode, trustDevice } = req.body
  const deviceName = req.body.device_name || null
  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('mobile login failed', { username, ip: req.ip })
      return res.status(401).json(GENERIC_FAIL)
    }

    // Second factor, single-request style: if 2FA is enabled it must be satisfied
    // by (a) a trusted-device token (X-Trust-Token) bound to this user, (b) a valid
    // TOTP code, or (c) a single-use recovery code. Otherwise tell the app to prompt
    // + retry. A wrong code/recovery code is a real failed attempt (scored + backed
    // off like web); a missing factor is not (it's the expected first round-trip).
    let viaRecovery = false
    if (user.totp_enabled) {
      const device = await sessionService.resolveTrustedDevice(req)
      const trusted = Boolean(device && device.user_id === user.id)
      if (trusted) {
        await sessionService.honorTrustedDevice(device.id)
        await activity.log({ req, userId: user.id, action: 'auth.login.trusted_device' })
      } else {
        let verified = Boolean(code) && totp.verifyCode(user.totp_secret, code)
        if (!verified && recoveryCode) {
          verified = await recoveryCodes.consumeForUser(user.id, recoveryCode)
          viaRecovery = verified
        }
        if (!verified) {
          if (code || recoveryCode) {
            botScore.recordLoginFailure(req.ip)
            loginProtection.recordFailure(req.ip)
            log.warn('mobile TOTP verify failed', { id: user.id, ip: req.ip, recovery: Boolean(recoveryCode) })
          }
          return res.status(401).json({ totpRequired: true, message: 'A verification code is required.' })
        }
      }
    }

    loginProtection.recordSuccess(req.ip)
    const meta = sessionService.sessionMeta(req)
    const out = sessionService.createMobileSession(user, meta)
    await persistAndFinish(req, user, out, 'auth.mobile.login', deviceName)
    if (viaRecovery) {
      await activity.log({ req, userId: user.id, action: 'account.recovery_code.consume' })
      log.info('mobile login via recovery code', { id: user.id, ip: req.ip })
    }

    // Optionally remember this device so future logins skip the second factor.
    const body = tokenResponse(out, user)
    if (trustDevice) {
      const result = await establishTrust(req, user, { platform: 'mobile', deviceName })
      if (result.ok) body.trustToken = result.trustToken
      else if (result.capReached) { body.trustLimitReached = true; body.devices = result.devices }
    }

    log.info('mobile login success', { username: user.username, id: user.id, ip: req.ip })
    return res.json(body)
  } catch (err) {
    log.error('mobile login error', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

// POST /auth/mobile/refresh  { refreshToken }
// Validates the presented refresh token, rotates it (revoke old + issue new),
// and returns a fresh access + refresh pair. Rotation means a stolen-and-used
// refresh token is single-use: the legitimate client's next refresh fails and
// surfaces the compromise.
async function refresh(req, res) {
  const { refreshToken } = req.body
  try {
    const hash = sessionService.hashRefreshToken(refreshToken)
    const row = await mobileSessions.findValidByHash(hash)
    if (!row) {
      log.warn('mobile refresh rejected (unknown/expired/revoked)', { ip: req.ip })
      return res.status(401).json({ message: 'Invalid or expired session. Please sign in again.' })
    }
    const user = await users.getById(row.user_id) // fresh row; 401 if user gone
    if (!user) {
      await mobileSessions.revokeByHash(hash)
      return res.status(401).json({ message: 'Invalid or expired session. Please sign in again.' })
    }

    await mobileSessions.revokeByHash(hash) // rotate: old token is now dead
    const meta = sessionService.sessionMeta(req)
    const out = sessionService.refreshMobileSession(user, meta)
    // Carry the device label forward across rotation so the Active Devices list
    // stays labeled for the life of the session.
    await persistAndFinish(req, user, out, 'auth.mobile.refresh', row.device_name || null)
    log.info('mobile session refreshed', { id: user.id, ip: req.ip })
    return res.json(tokenResponse(out, user))
  } catch (err) {
    log.error('mobile refresh error', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

// POST /auth/mobile/logout  { refreshToken?, all? }
// Runs behind requireAuth (bearer), so req.user is the caller. Revokes the given
// refresh token, or every token for the user when { all: true }. Idempotent.
async function logout(req, res) {
  const { refreshToken, all } = req.body
  try {
    if (all) {
      const n = await mobileSessions.revokeAllForUser(req.user.id)
      log.info('mobile logout (all devices)', { id: req.user.id, revoked: n })
    } else if (refreshToken) {
      await mobileSessions.revokeByHash(sessionService.hashRefreshToken(refreshToken))
    }
    await activity.log({ req, userId: req.user.id, action: 'auth.mobile.logout' })
    return res.json({ message: 'Logged out.' })
  } catch (err) {
    log.error('mobile logout error', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

module.exports = { login, refresh, logout }
