import crypto from 'node:crypto'
import { DateTime } from 'luxon'
import RefreshToken from '#models/refresh_token'
import User from '#models/user'
import env from '#start/env'
import type { HttpContext } from '@adonisjs/core/http'

export const REFRESH_COOKIE_NAME = 'rubis_refresh'

/**
 * Génère un token plain (32 bytes random → base64url ~43 chars), retourne
 * { plain, hashed } pour ne stocker que le hashed côté DB.
 *
 * SHA-256 suffit : le token est un opaque random non humain, pas un mot
 * de passe — pas besoin de bcrypt/argon (contrairement aux passwords).
 */
function generateToken(): { plain: string; hashed: string } {
  const plain = crypto.randomBytes(32).toString('base64url')
  const hashed = crypto.createHash('sha256').update(plain).digest('hex')
  return { plain, hashed }
}

function hashToken(plain: string): string {
  return crypto.createHash('sha256').update(plain).digest('hex')
}

function ttlDays(): number {
  return env.get('REFRESH_TOKEN_TTL_DAYS', 30)
}

/**
 * Pose le cookie httpOnly avec le token plain. Le SPA ne peut pas le lire
 * en JS — c'est ce qui le protège du XSS, contrairement à localStorage.
 *
 * `path: /api/v1/auth` : le browser n'envoie le cookie qu'aux endpoints
 * d'auth, pas sur chaque requête API. Réduit la surface d'attaque CSRF.
 *
 * `sameSite: 'lax'` (et pas 'strict') : `strict` bloque l'envoi du cookie
 * sur les navigations top-level (URL tapée, bookmark, restauration
 * d'onglets) — surtout Safari iOS — et casse le bootstrap SPA après
 * fermeture/réouverture du navigateur. Lax envoie le cookie sur les
 * top-level GET (qui sont safe) tout en bloquant les requêtes cross-site
 * embedded → protection CSRF préservée pour le POST /auth/refresh.
 */
function setRefreshCookie(ctx: HttpContext, plain: string) {
  const maxAgeSeconds = ttlDays() * 24 * 60 * 60
  ctx.response.cookie(REFRESH_COOKIE_NAME, plain, {
    httpOnly: true,
    secure: env.get('COOKIE_SECURE', false),
    sameSite: 'lax',
    path: '/api/v1/auth',
    domain: env.get('COOKIE_DOMAIN') || undefined,
    maxAge: maxAgeSeconds,
  })
}

function clearRefreshCookie(ctx: HttpContext) {
  ctx.response.clearCookie(REFRESH_COOKIE_NAME, {
    path: '/api/v1/auth',
    domain: env.get('COOKIE_DOMAIN') || undefined,
  })
}

/**
 * Crée un refresh token pour un user et pose le cookie correspondant.
 * Appelé après signup et login.
 */
export async function issueRefreshToken(
  user: User,
  ctx: HttpContext
): Promise<{ token: RefreshToken; plain: string }> {
  const { plain, hashed } = generateToken()

  const token = await RefreshToken.create({
    userId: user.id,
    hashedToken: hashed,
    expiresAt: DateTime.now().plus({ days: ttlDays() }),
    lastUsedAt: null,
    revokedAt: null,
    ipAddress: ctx.request.ip(),
    userAgent: ctx.request.header('user-agent') ?? null,
  })

  setRefreshCookie(ctx, plain)
  return { token, plain }
}

/**
 * Fenêtre de grâce après révocation pendant laquelle on tolère la
 * réutilisation d'un token révoqué SI un successeur actif existe — pour
 * accommoder les courses entre onglets (deux refresh parallèles avec le
 * même cookie). Au-delà : présomption de vol → panic mode.
 */
const ROTATION_GRACE_SECONDS = 60

/**
 * Valide le cookie reçu et révoque l'ancien token. Retourne le user
 * authentifié — le contrôleur appelle ensuite `issueRefreshToken` (via
 * emitAuthSession) pour poser un nouveau cookie. Rotation complète.
 *
 * Si le user envoie un token déjà révoqué :
 *  - et qu'un successeur actif a été créé pour le même user dans les
 *    {ROTATION_GRACE_SECONDS} dernières secondes → c'est très probablement
 *    un refresh parallèle d'un autre onglet, pas un vol. On rotate ce
 *    successeur et on retourne la session normalement (les 2 onglets
 *    restent connectés).
 *  - sinon, c'est une réutilisation tardive → présomption de vol, on
 *    coupe TOUS les tokens actifs du user (panic mode). Le vrai
 *    propriétaire devra se re-logger.
 */
export async function consumeRefreshToken(
  ctx: HttpContext
): Promise<{ user: User } | { errorCode: 'no_session' | 'session_expired' }> {
  const cookie = ctx.request.cookie(REFRESH_COOKIE_NAME)
  if (!cookie) return { errorCode: 'no_session' }

  const hashed = hashToken(cookie)
  const stored = await RefreshToken.query().where('hashed_token', hashed).first()

  if (!stored) {
    clearRefreshCookie(ctx)
    return { errorCode: 'session_expired' }
  }

  // Token révoqué : check pour course entre onglets avant de paniquer.
  if (stored.revokedAt) {
    const graceCutoff = stored.revokedAt.plus({ seconds: ROTATION_GRACE_SECONDS })
    const successor = await RefreshToken.query()
      .where('user_id', stored.userId)
      .whereNull('revoked_at')
      .where('expires_at', '>', DateTime.now().toSQL())
      .where('created_at', '>=', stored.revokedAt.toSQL())
      .where('created_at', '<=', graceCutoff.toSQL())
      .orderBy('created_at', 'desc')
      .first()

    if (successor) {
      // Course détectée — rotate le successeur et continue normalement.
      successor.revokedAt = DateTime.now()
      successor.lastUsedAt = DateTime.now()
      await successor.save()
      const user = await User.findOrFail(successor.userId)
      return { user }
    }

    // Vrai vol présumé : tout révoquer.
    await revokeAllForUser(stored.userId)
    clearRefreshCookie(ctx)
    return { errorCode: 'session_expired' }
  }

  if (stored.expiresAt < DateTime.now()) {
    stored.revokedAt = DateTime.now()
    await stored.save()
    clearRefreshCookie(ctx)
    return { errorCode: 'session_expired' }
  }

  stored.revokedAt = DateTime.now()
  stored.lastUsedAt = DateTime.now()
  await stored.save()

  const user = await User.findOrFail(stored.userId)
  return { user }
}

/**
 * Révoque le token courant (utilisé par /account/logout).
 * Pas de panic — l'user demande explicitement la déconnexion.
 */
export async function revokeCurrentRefreshToken(ctx: HttpContext): Promise<void> {
  const cookie = ctx.request.cookie(REFRESH_COOKIE_NAME)
  if (cookie) {
    const hashed = hashToken(cookie)
    await RefreshToken.query()
      .where('hashed_token', hashed)
      .whereNull('revoked_at')
      .update({ revoked_at: DateTime.now().toSQL() })
  }
  clearRefreshCookie(ctx)
}

/**
 * Révoque tous les tokens d'un user (panic mode si vol détecté, ou
 * appelable par "déconnecter toutes mes sessions").
 */
export async function revokeAllForUser(userId: string): Promise<void> {
  await RefreshToken.query()
    .where('user_id', userId)
    .whereNull('revoked_at')
    .update({ revoked_at: DateTime.now().toSQL() })
}
