/**
 * Session issuing and verification.
 *
 * Sessions are stateless JWTs carried in an httpOnly cookie, but every token also has a
 * row in `sessions` keyed by its `jti`. That combination gives fast verification without
 * a database round-trip on every request *and* the ability to revoke a session
 * immediately — a plain stateless JWT cannot be revoked, which is unacceptable for a
 * console that can dispatch grid actions.
 */

import 'server-only'

import { SignJWT, jwtVerify } from 'jose'
import { cookies } from 'next/headers'

import { env } from '../env'
import type { Locale } from '../domain/enums'

export const SESSION_COOKIE = 'nabdh_session'
export const CSRF_COOKIE = 'nabdh_csrf'
export const CSRF_HEADER = 'x-csrf-token'

const ISSUER = 'nabdh'
const AUDIENCE = 'nabdh-console'

export interface SessionPayload {
  /** User id. */
  sub: string
  email: string
  name: string
  nameAr: string
  locale: Locale
  roles: string[]
  permissions: string[]
  /** Token id, matched against the `sessions` table for revocation. */
  jti: string
}

function secretKey(): Uint8Array {
  return new TextEncoder().encode(env.authSecret)
}

export async function signSession(payload: SessionPayload): Promise<{ token: string; expiresAt: Date }> {
  const expiresAt = new Date(Date.now() + env.sessionTtlHours * 3_600_000)
  const token = await new SignJWT({
    email: payload.email,
    name: payload.name,
    nameAr: payload.nameAr,
    locale: payload.locale,
    roles: payload.roles,
    permissions: payload.permissions,
  })
    .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
    .setSubject(payload.sub)
    .setJti(payload.jti)
    .setIssuer(ISSUER)
    .setAudience(AUDIENCE)
    .setIssuedAt()
    .setExpirationTime(Math.floor(expiresAt.getTime() / 1000))
    .sign(secretKey())

  return { token, expiresAt }
}

export async function verifySession(token: string): Promise<SessionPayload | null> {
  try {
    const { payload } = await jwtVerify(token, secretKey(), {
      issuer: ISSUER,
      audience: AUDIENCE,
      algorithms: ['HS256'],
    })
    if (!payload.sub || !payload.jti) return null
    return {
      sub: payload.sub,
      jti: payload.jti,
      email: String(payload.email ?? ''),
      name: String(payload.name ?? ''),
      nameAr: String(payload.nameAr ?? ''),
      locale: (payload.locale === 'en' ? 'en' : 'ar') as Locale,
      roles: Array.isArray(payload.roles) ? (payload.roles as string[]) : [],
      permissions: Array.isArray(payload.permissions) ? (payload.permissions as string[]) : [],
    }
  } catch {
    // Expired, tampered with, or signed by a different secret — all mean "no session".
    return null
  }
}

export async function setSessionCookie(token: string, expiresAt: Date) {
  const store = await cookies()
  store.set(SESSION_COOKIE, token, {
    httpOnly: true,
    sameSite: 'lax',
    secure: env.isProduction,
    path: '/',
    expires: expiresAt,
  })
}

export async function clearSessionCookie() {
  const store = await cookies()
  store.delete(SESSION_COOKIE)
  store.delete(CSRF_COOKIE)
}

export async function readSessionCookie(): Promise<string | null> {
  const store = await cookies()
  return store.get(SESSION_COOKIE)?.value ?? null
}

/**
 * Issues the CSRF token. Deliberately *not* httpOnly: the double-submit pattern requires
 * client JavaScript to read it and echo it in a header, which an attacker on another
 * origin cannot do because the same-origin policy blocks them from reading the cookie.
 */
export async function setCsrfCookie(token: string, expiresAt: Date) {
  const store = await cookies()
  store.set(CSRF_COOKIE, token, {
    httpOnly: false,
    sameSite: 'lax',
    secure: env.isProduction,
    path: '/',
    expires: expiresAt,
  })
}

export function generateToken(): string {
  const bytes = new Uint8Array(32)
  crypto.getRandomValues(bytes)
  return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')
}

/** Constant-time string comparison, for CSRF tokens. */
export function timingSafeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false
  let diff = 0
  for (let i = 0; i < a.length; i += 1) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
  return diff === 0
}
