// Admin email invites. A staff member invites someone by email at a pre-chosen
// access level; the invitee accepts via a tokened link that creates their website
// user at that role. The opaque token lives only in the emailed link — the DB
// stores just its sha256 hash (like mobile refresh tokens), so a DB read never
// yields a usable invite. Invites are single-use and expiring.

const crypto = require('crypto')
const db = require('./invites.db')

const DEFAULT_TTL_DAYS = 7

function hashToken(raw) {
  return crypto.createHash('sha256').update(String(raw)).digest('hex')
}

// Public-safe shape (never exposes the token hash).
function toSafe(row) {
  if (!row) return null
  return {
    id: row.id,
    email: row.email,
    role: row.role,
    status: row.status,
    invitedBy: row.invited_by,
    acceptedUserId: row.accepted_user_id,
    expiresAt: row.expires_at,
    createdAt: row.created_at,
    acceptedAt: row.accepted_at,
    expired: new Date(row.expires_at).getTime() < Date.now(),
  }
}

// Create an invite. Returns { invite, token } — the plaintext token is returned
// ONCE (for the email link) and never stored or recoverable afterwards.
async function create({ email, role, invitedBy, ttlDays = DEFAULT_TTL_DAYS }) {
  const token = crypto.randomBytes(32).toString('base64url')
  const expiresAt = new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1000)
  const id = await db.insert({ tokenHash: hashToken(token), email, role, invitedBy, expiresAt })
  return { invite: toSafe(await db.getById(id)), token }
}

// Resolve a pending, unexpired invite from its plaintext token, else null. Returns
// the RAW row (incl. id) for the accept flow; callers sanitize with publicView.
async function findValidByToken(token) {
  if (!token) return null
  const row = await db.findByTokenHash(hashToken(token))
  if (!row || row.status !== 'pending') return null
  if (new Date(row.expires_at).getTime() < Date.now()) return null
  return row
}

// Atomically consume a pending invite (double-accept-safe). Returns true if this
// call won the race and bound the invite to userId.
async function accept(id, userId) {
  return (await db.markAccepted(id, userId)) === 1
}

const revoke = (id) => db.revoke(id)

async function list(limit = 100) {
  const n = Math.min(Math.max(Number(limit) || 100, 1), 500)
  const rows = await db.listRecent(n)
  return rows.map(toSafe)
}

// A minimal, safe view of an invite for the (unauthenticated) accept page —
// only what the form needs, never the token or internal ids.
function publicView(row) {
  if (!row) return null
  return { email: row.email, role: row.role }
}

module.exports = { create, findValidByToken, accept, revoke, list, publicView, toSafe, hashToken }
