const bcrypt = require('bcryptjs')
const usersDb = require('./users.db')

const SALT_ROUNDS = 10

// Strip secrets (password hash, TOTP secret) before sending a user anywhere.
function sanitize(user) {
  if (!user) return null
  const { password_hash, totp_secret, ...safe } = user
  return safe
}

// password may be omitted/null — an SSO-provisioned player has no password until
// they set one (a null hash makes password login impossible, see validatePassword).
async function createUser({ username, password, role = 'admin', email = null, status = 'active', emailVerified = false }) {
  const passwordHash = password ? await bcrypt.hash(password, SALT_ROUNDS) : null
  const id = await usersDb.insertUser({ username, passwordHash, role, email, status, emailVerified })
  return sanitize(await usersDb.findById(id))
}

// True when a DB error is the unique-index violation on username (the atomic
// backstop for the uniqueness race). Callers translate this into a 409 rather
// than doing a check-then-write.
function isDuplicateUsername(err) {
  return Boolean(err && (err.code === 'ER_DUP_ENTRY' || err.errno === 1062))
}

// Returns the raw row (incl. hash) — used by login only.
async function getRawByUsername(username) {
  return usersDb.findByUsername(username)
}

async function getById(id) {
  return sanitize(await usersDb.findById(id))
}

// Raw rows (incl. email/status) for every active account on an email address.
// Server-side only (password-reset request); email is non-unique so this may
// return several. Never sent to a client.
async function getActiveByEmail(email) {
  if (!email) return []
  return usersDb.findActiveByEmail(String(email).trim())
}

// Raw row incl. totp_secret — server-side only (TOTP setup/verify). Never sent
// to a client; sanitize() strips the secret from anything user-facing.
async function getRawById(id) {
  return usersDb.findById(id)
}

async function setTotpSecret(id, secret) {
  return usersDb.setTotpSecret(id, secret)
}

async function enableTotp(id) {
  return usersDb.enableTotp(id)
}

async function disableTotp(id) {
  return usersDb.disableTotp(id)
}

async function validatePassword(user, password) {
  if (!user || !user.password_hash) return false
  return bcrypt.compare(password, user.password_hash)
}

async function list() {
  return usersDb.listUsers()
}

async function update(id, { username, password, role, email, status, emailVerified }) {
  const fields = {}
  if (username !== undefined) fields.username = username
  if (role !== undefined) fields.role = role
  if (email !== undefined) fields.email = email
  if (status !== undefined) fields.status = status
  if (emailVerified !== undefined) fields.email_verified = emailVerified ? 1 : 0
  if (password) fields.password_hash = await bcrypt.hash(password, SALT_ROUNDS)
  await usersDb.updateUser(id, fields)
  // A password change must revoke existing sessions ("change password to log
  // everyone out"), so bump the cutoff whenever the hash was rotated.
  if (password) await usersDb.bumpTokensValidAfter(id)
  return getById(id)
}

// Invalidate every session token this user currently holds ("log out everywhere")
// by advancing their tokens_valid_after cutoff to now.
async function invalidateSessions(id) {
  return usersDb.bumpTokensValidAfter(id)
}

// Set the session cutoff to an explicit instant. Used by the self password-change
// flow to keep the caller's freshly re-issued session alive (see users.db).
async function setSessionCutoff(id, when) {
  return usersDb.setTokensValidAfter(id, when)
}

async function remove(id) {
  return usersDb.deleteUser(id)
}

async function count() {
  return usersDb.countUsers()
}

async function countAdmins() {
  return usersDb.countAdmins()
}

async function recordLogin(id, ip = null) {
  return usersDb.touchLastLogin(id, ip)
}

module.exports = {
  createUser,
  isDuplicateUsername,
  getRawByUsername,
  getById,
  getActiveByEmail,
  getRawById,
  validatePassword,
  list,
  update,
  invalidateSessions,
  setSessionCutoff,
  remove,
  count,
  countAdmins,
  recordLogin,
  setTotpSecret,
  enableTotp,
  disableTotp,
}
