import type { MiddlewareHandler } from 'hono'

import { getSession, type AuthEngine, type Principal as SessionPrincipal } from '@platform-modules/auth'
import { verifyApiKey, type VerifiedApiKey } from '@platform-modules/auth/api-keys'
import type { Querier } from '@platform-modules/db'
import type { Tenancy } from '@platform-modules/tenancy'

import { fail } from '../http.js'

export type AuthPrincipal = {
  account: string
  userId: string
  scopes: string[]
}

declare module 'hono' {
  interface ContextVariableMap {
    capabilities: string[]
    principal: AuthPrincipal
  }
}

type AccountResolver = (
  principal: SessionPrincipal,
) => Promise<string | null> | string | null

type ApiKeyPrincipalResolver = (
  verified: VerifiedApiKey,
) => Promise<AuthPrincipal | null> | AuthPrincipal | null

export interface RequireAuthOptions<
  TBindings extends object,
  S extends Record<string, unknown>,
> {
  accessCookieName?: string
  authEngine: AuthEngine
  db: Querier<S>
  resolveApiKeyPrincipal?: ApiKeyPrincipalResolver
  resolveSessionAccount?: AccountResolver
  tenancy: Pick<Tenancy, 'resolveCapabilities'>
}

function unauthorized(): Response {
  return fail('UNAUTHORIZED', 'Authentication required', 401)
}

function getBearerToken(authorization: string | undefined): string | null {
  if (!authorization?.startsWith('Bearer ')) {
    return null
  }

  const token = authorization.slice(7).trim()
  return token.length > 0 ? token : null
}

function restrictCapabilities(capabilities: string[], scopes: string[]): string[] {
  const allowed = new Set(scopes)
  return capabilities.filter((capability) => allowed.has(capability))
}

async function defaultResolveApiKeyPrincipal(verified: VerifiedApiKey): Promise<AuthPrincipal> {
  return {
    account: verified.owner,
    userId: verified.owner,
    scopes: [...verified.scopes],
  }
}

async function defaultResolveSessionAccount(
  principal: SessionPrincipal,
): Promise<string | null> {
  return principal.tenantId ?? null
}

export function requireAuth<
  TBindings extends object,
  S extends Record<string, unknown> = Record<string, never>,
>(
  options: RequireAuthOptions<TBindings, S>,
): MiddlewareHandler<{ Bindings: TBindings }> {
  const resolveApiKeyPrincipal =
    options.resolveApiKeyPrincipal ?? defaultResolveApiKeyPrincipal
  const resolveSessionAccount =
    options.resolveSessionAccount ?? defaultResolveSessionAccount

  return async (context, next) => {
    const bearer = getBearerToken(context.req.header('authorization'))

    let principal: AuthPrincipal | null

    if (bearer) {
      const verified = await verifyApiKey(options.db, bearer)
      if (!verified) {
        return unauthorized()
      }

      principal = await resolveApiKeyPrincipal(verified)
      if (!principal) {
        return unauthorized()
      }
    } else {
      const session = await getSession(context.req.raw.headers, options.authEngine, {
        accessCookieName: options.accessCookieName,
      })
      if (!session) {
        return unauthorized()
      }

      const account = await resolveSessionAccount(session)
      if (!account) {
        return unauthorized()
      }

      const capabilities = await options.tenancy.resolveCapabilities(session.userId, account)
      principal = {
        account,
        userId: session.userId,
        scopes: capabilities,
      }

      context.set('principal', principal)
      context.set('capabilities', capabilities)
      await next()
      return
    }

    const resolvedCapabilities = await options.tenancy.resolveCapabilities(
      principal.userId,
      principal.account,
    )
    const capabilities = restrictCapabilities(resolvedCapabilities, principal.scopes)
    principal = {
      ...principal,
      scopes: capabilities,
    }

    context.set('principal', principal)
    context.set('capabilities', capabilities)
    await next()
  }
}
