// ── Self-service password reset (public, token-gated) ──────────────────────
//
// Three steps, all unauthenticated:
//   1. POST /auth/password/forgot { email }         → email a tokened link
//   2. GET  /auth/password/reset/:token             → validate the link (for the form)
//   3. POST /auth/password/reset/:token { password } → set the new password
//
// Email is intentionally non-unique (SSO emails may repeat), so a request can
// match several accounts; each gets its own link, and the email names the
// username so the recipient knows which account it's for. The request step NEVER
// reveals whether an address exists — it always returns the same generic success
// (no user enumeration). Only the sha256 hash of each opaque token is stored, so a
// DB read never yields a usable link (same pattern as user_invites). Tokens are
// single-use + expire in ~1h. Setting a new password rotates the hash and revokes
// every session (web cookie cutoff + mobile refresh tokens). We do NOT auto-log-in
// afterwards: the user signs in fresh, so a 2FA account still passes TOTP.

const passwordResets = require('../../../model/passwordResets/passwordResets.model')
const users = require('../../../model/users/users.model')
const mobileSessions = require('../../../model/mobileSessions/mobileSessions.model')
const trustedDevices = require('../../../model/trustedDevices/trustedDevices.model')
const recoveryCodes = require('../../../model/recoveryCodes/recoveryCodes.model')
const activity = require('../../../model/activity/activity.model')
const mailer = require('../../../utils/mailer')

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

// Same generic answer whether or not the address matched — never leaks existence.
const GENERIC_OK = { message: 'If an account exists for that email, a reset link has been sent.' }

function baseUrl() {
  return (process.env.APP_BASE_URL || 'http://localhost:5173').replace(/\/+$/, '')
}

function resetUrl(token) {
  return `${baseUrl()}/account/reset/${token}`
}

// POST /auth/password/forgot — request a reset. Always 200 with GENERIC_OK.
async function requestReset(req, res) {
  const email = String(req.body.email || '').trim()
  try {
    // Bad/empty email: answer identically so probing the shape reveals nothing.
    if (email) {
      const accounts = await users.getActiveByEmail(email)
      for (const account of accounts) {
        try {
          const { token } = await passwordResets.create({ userId: account.id, requestedIp: req.ip })
          const result = await mailer.sendPasswordReset({
            to: account.email,
            resetUrl: resetUrl(token),
            username: account.username,
          })
          if (!result.sent) {
            log.warn('password reset email not sent (mail not configured)', { userId: account.id })
          }
        } catch (err) {
          // A send failure for one account must not abort the others, nor change
          // the generic response. The pending token simply expires unused.
          log.error('password reset send error', err)
        }
      }
      await activity.log({ req, action: 'account.password.reset.request', detail: { email, matched: accounts.length } })
      log.info('password reset requested', { email, matched: accounts.length, ip: req.ip })
    }
    return res.json(GENERIC_OK)
  } catch (err) {
    log.error('requestReset', err)
    // Still generic — don't turn an internal error into an enumeration oracle.
    return res.json(GENERIC_OK)
  }
}

// GET /auth/password/reset/:token — validate a link so the form can render. 404
// for anything not currently usable (never distinguishes expired/used/never-was).
async function lookupReset(req, res) {
  try {
    const row = await passwordResets.findValidByToken(req.params.token)
    if (!row) return res.status(404).json({ message: 'This reset link is invalid or has expired.' })
    // Surface only the target username (nice for the form); never the email/token.
    const user = await users.getById(row.user_id)
    if (!user) return res.status(404).json({ message: 'This reset link is invalid or has expired.' })
    return res.json({ username: user.username })
  } catch (err) {
    log.error('lookupReset', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

// POST /auth/password/reset/:token — set the new password. Consumes the token
// atomically (double-use safe), rotates the hash, and revokes every session.
async function confirmReset(req, res) {
  try {
    const row = await passwordResets.findValidByToken(req.params.token)
    if (!row) return res.status(404).json({ message: 'This reset link is invalid or has expired.' })

    // Consume first: if we lost a double-submit race, stop before touching the
    // password so a spent link can't set a password twice.
    const won = await passwordResets.consume(row.id)
    if (!won) return res.status(404).json({ message: 'This reset link has already been used.' })

    // Rotate the hash. users.update bumps tokens_valid_after, revoking every web
    // session issued before now ("reset password → sign out everywhere").
    await users.update(row.user_id, { password: req.body.password })
    // Web sessions are covered by the cutoff bump; mobile bearer sessions live in
    // their own table and must be revoked explicitly.
    await mobileSessions.revokeAllForUser(row.user_id)
    // A reset is a security event (often "I lost access"): drop every trusted
    // device and recovery code so the second-factor bypass can't survive it.
    await trustedDevices.revokeAllForUser(row.user_id)
    await recoveryCodes.clearForUser(row.user_id)
    // Retire any other outstanding links for this user (e.g. duplicate requests).
    await passwordResets.invalidatePendingForUser(row.user_id)

    await activity.log({ req, userId: row.user_id, action: 'account.password.reset.complete' })
    log.info('password reset completed', { userId: row.user_id, ip: req.ip })
    // No auto-login: the user signs in fresh, so a 2FA account still passes TOTP.
    return res.json({ ok: true, message: 'Your password has been reset. You can sign in now.' })
  } catch (err) {
    log.error('confirmReset', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

module.exports = { requestReset, lookupReset, confirmReset }
