/** @platform-modules/auth core — zero runtime dependency seam. */

export type Principal = {
  userId: string
  sessionId: string
  roles: string[]
  capabilities?: string[]
  tenantId?: string
}

export type Session = {
  id: string
  userId: string
  expiresAt: Date
}

export type SignInCredentials = {
  email: string
  password: string
}

export type SignInResult = {
  principal: Principal
  accessToken: string
  refreshToken: string
}

export type RefreshResult = {
  principal: Principal
  accessToken: string
  refreshToken?: string
}

export type CreateUserInput = {
  email: string
  password: string
  roles?: string[]
}

export type AuthEngine = {
  signIn(credentials: SignInCredentials): Promise<SignInResult>
  signOut(sessionId: string, refreshToken?: string): Promise<void>
  verifySession(token: string): Promise<Principal | null>
  refresh(refreshToken: string): Promise<RefreshResult | null>
  createUser(input: CreateUserInput): Promise<{ userId: string }>
  setPassword(userId: string, password: string): Promise<void>
  verifyPassword(password: string, storedHash: string): Promise<boolean>
}

export interface UserAdminEngine {
  listUsers(opts?: { limit?: number; offset?: number }): Promise<{
    users: {
      id: string
      email: string
      roles: string[]
      status: 'active' | 'disabled'
      createdAt: string
    }[]
    total: number
  }>
  setUserRoles(userId: string, roles: string[]): Promise<void>
  disableUser(userId: string): Promise<void>
}

export class AuthError extends Error {}

export class InvalidSessionError extends AuthError {
  override readonly name = 'InvalidSessionError'
  constructor(
    message: string,
    readonly reason?: string,
  ) {
    super(message)
  }
}

export class RevokedSessionError extends AuthError {
  override readonly name = 'RevokedSessionError'
  constructor(
    message: string,
    readonly userId?: string,
  ) {
    super(message)
  }
}

export class RateLimitedError extends AuthError {
  override readonly name = 'RateLimitedError'
  constructor(
    message: string,
    readonly scope?: string,
  ) {
    super(message)
  }
}

export class EngineMismatchError extends AuthError {
  override readonly name = 'EngineMismatchError'
  constructor(message: string) {
    super(message)
  }
}

export class PermissionDeniedError extends AuthError {
  override readonly name = 'PermissionDeniedError'
  constructor(
    message: string,
    readonly permission: string,
    readonly principal: Principal | null,
  ) {
    super(message)
  }
}

export class RoleDeniedError extends AuthError {
  override readonly name = 'RoleDeniedError'
  constructor(
    message: string,
    readonly role: string,
    readonly principal: Principal | null,
  ) {
    super(message)
  }
}

export class UserNotFoundError extends AuthError {
  override readonly name = 'UserNotFoundError'
  constructor(
    message: string,
    readonly userId: string,
  ) {
    super(message)
  }
}

export type GetSessionOptions = {
  accessCookieName?: string
}

const _cookieReCache = new Map<string, RegExp>()

function cookieRe(name: string): RegExp {
  const cached = _cookieReCache.get(name)
  if (cached) return cached
  const re = new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=([^;]+)`)
  _cookieReCache.set(name, re)
  return re
}

export async function getSession(
  headers: HeadersInit,
  engine: AuthEngine,
  opts?: GetSessionOptions,
): Promise<Principal | null> {
  try {
    const h = headers instanceof Headers ? headers : new Headers(headers)
    const cookieName = opts?.accessCookieName ?? 'access_token'
    let token: string | null = null

    const auth = h.get('authorization')
    if (auth?.startsWith('Bearer ')) {
      token = auth.slice(7).trim() || null
    }

    if (!token) {
      const cookie = h.get('cookie') ?? ''
      const m = cookie.match(cookieRe(cookieName))
      token = m?.[1]?.trim() ?? null
    }

    if (!token) return null
    return await engine.verifySession(token)
  } catch {
    return null
  }
}

export function createAuth(engine: AuthEngine) {
  return {
    getSession: (headers: HeadersInit, opts?: GetSessionOptions) =>
      getSession(headers, engine, opts),
    signIn: (credentials: SignInCredentials) => engine.signIn(credentials),
    signOut: (sessionId: string, refreshToken?: string) => engine.signOut(sessionId, refreshToken),
    refresh: (refreshToken: string) => engine.refresh(refreshToken),
  }
}

export function hasPermission(principal: Principal, permission: string): boolean {
  return principal.capabilities?.includes(permission) ?? false
}

export function isAtLeastRole(
  principal: Principal,
  role: string,
  hierarchy: readonly string[],
): boolean {
  const targetIdx = hierarchy.indexOf(role)
  if (targetIdx < 0) return false
  for (const r of principal.roles) {
    const idx = hierarchy.indexOf(r)
    if (idx >= 0 && idx >= targetIdx) return true
  }
  return false
}

export function requirePermission(permission: string) {
  return (principal: Principal | null): Principal => {
    if (!principal) {
      throw new InvalidSessionError('no session', 'missing_principal')
    }
    if (!hasPermission(principal, permission)) {
      throw new PermissionDeniedError(`permission denied: ${permission}`, permission, principal)
    }
    return principal
  }
}

/**
 * Role gate — the role-based twin of `requirePermission`. Returns a resolver:
 *   - `principal == null`              → throws `InvalidSessionError`
 *   - `!isAtLeastRole(p, role, hier)`  → throws `RoleDeniedError`
 *   - else                             → returns the principal
 *
 * Use with engines that populate `Principal.roles` (the custom engine does; `requirePermission`
 * is capability-based and inert there). Host admin guard:
 * `requireRole('admin', HIERARCHY)(await getSession(headers, engine))`.
 */
export function requireRole(role: string, hierarchy: readonly string[]) {
  return (principal: Principal | null): Principal => {
    if (!principal) {
      throw new InvalidSessionError('no session', 'missing_principal')
    }
    if (!isAtLeastRole(principal, role, hierarchy)) {
      throw new RoleDeniedError(`role denied: ${role}`, role, principal)
    }
    return principal
  }
}
