import {
  asGatewayError,
  GatewayError,
  parseOperationRequest,
  type CallerIdentity,
  type GatewayErrorCode,
  type JsonObject,
  type OperationHandle,
  type RouteDefinition,
  type RouteRegistry,
} from './contracts.js'
import { AdmissionError, RequestAdmission, type AdmissionPolicy } from './admission.js'
import { AuditIntegrityError, AuditStorageError, AuditTrail, type AuditRequest } from './audit.js'

export const READ_ONLY_OPERATIONS = [
  'getOperatorContext',
  'listProjects',
  'getAuditReceipt',
] as const

export type ReadOnlyOperation = (typeof READ_ONLY_OPERATIONS)[number]
export type OpaqueResponse = JsonObject

export interface OperationContext {
  readonly callerSubject: string
  readonly applicationCredentialId: string
  readonly requestId: string
  readonly operation: ReadOnlyOperation
}

export interface ReadOnlyAdapter<Payload> {
  dispatch(payload: Payload, context: OperationContext): Promise<OpaqueResponse>
}

export interface ReadOnlyRoute<Payload> {
  readonly operation: ReadOnlyOperation
  readonly policy: AdmissionPolicy
  readonly parsePayload: (payload: JsonObject) => Payload
  readonly parseResponse: (response: unknown) => OpaqueResponse
  readonly adapter: ReadOnlyAdapter<Payload>
}

export interface ReadOnlyGateway {
  dispatch(identity: CallerIdentity, rawBody: Uint8Array): Promise<OpaqueResponse>
  readonly operations: readonly ReadOnlyOperation[]
}

export function createReadOnlyGateway(
  routes: readonly ReadOnlyRoute<unknown>[],
  admission: RequestAdmission<OpaqueResponse>,
  audit: AuditTrail,
): ReadOnlyGateway {
  const routesByOperation = new Map<ReadOnlyOperation, ReadOnlyRoute<unknown>>()
  if (routes.length !== READ_ONLY_OPERATIONS.length) throw new GatewayError('invalid_request')
  for (const route of routes) {
    validateReadOnlyRoute(route)
    if (routesByOperation.has(route.operation)) throw new GatewayError('invalid_request')
    routesByOperation.set(route.operation, route)
  }
  for (const operation of READ_ONLY_OPERATIONS) {
    if (!routesByOperation.has(operation)) throw new GatewayError('invalid_request')
  }
  const maximumRequestBytes = Math.max(...routes.map((route) => route.policy.maxRequestBytes))

  return {
    operations: READ_ONLY_OPERATIONS,
    async dispatch(identity, rawBody) {
      let request: ReturnType<typeof parseOperationRequest>
      try {
        request = parseOperationRequest(rawBody, maximumRequestBytes)
      } catch (error) {
        throw asGatewayError(error, 'invalid_request')
      }
      const route = routesByOperation.get(request.operation as ReadOnlyOperation)
      if (!route) throw new GatewayError('operation_not_found')
      const auditRequest = toAuditRequest(identity, request.requestId, request.operation, route.policy)
      let payload: unknown
      try {
        payload = route.parsePayload(request.payload)
      } catch (error) {
        await recordRejected(audit, auditRequest, asGatewayError(error, 'invalid_request').code)
        throw asGatewayError(error, 'invalid_request')
      }
      let result: Awaited<ReturnType<typeof admission.admit>>
      try {
        result = await admission.admit(identity, rawBody, route.policy)
      } catch (error) {
        const code = gatewayCode(error)
        await recordRejected(audit, auditRequest, code)
        throw new GatewayError(code)
      }
      if (result.kind === 'replay') {
        const response = route.parseResponse(result.outcome)
        await ensureCompletedAudit(audit, auditRequest, request.requestId)
        return response
      }
      try {
        await audit.recordAccepted(auditRequest)
      } catch (error) {
        const code = gatewayCode(error)
        try {
          await result.abandon()
        } catch {
          throw new GatewayError('service_unavailable')
        }
        await recordRejected(audit, auditRequest, code)
        throw new GatewayError(code)
      }

      let response: OpaqueResponse
      try {
        response = route.parseResponse(await route.adapter.dispatch(payload, {
          callerSubject: identity.subject,
          applicationCredentialId: identity.application_credential_id,
          requestId: request.requestId,
          operation: route.operation,
        }))
      } catch (error) {
        const code = gatewayCode(error)
        try {
          await result.abandon()
        } catch {
          throw new GatewayError('service_unavailable')
        }
        try {
          await audit.recordTerminal({
            ...auditRequest,
            operationId: request.requestId,
            state: 'failed',
            rejectionCode: code,
          })
        } catch {
          throw new GatewayError('service_unavailable')
        }
        throw new GatewayError(code)
      }

      try {
        await result.complete(response)
      } catch {
        throw new GatewayError('service_unavailable')
      }
      await ensureCompletedAudit(audit, auditRequest, request.requestId)
      return response
    },
  }
}

export function createRouteRegistry(routes: readonly RouteDefinition<unknown, unknown>[]): RouteRegistry {
  const routesByOperation = new Map<string, RouteDefinition<unknown, unknown>>()

  for (const route of routes) {
    validateRoute(route)
    if (routesByOperation.has(route.operation)) {
      throw new GatewayError('invalid_request')
    }
    routesByOperation.set(route.operation, route)
  }

  const maximumRequestLimit = Math.max(1, ...routes.map((route) => route.requestLimitBytes))

  return {
    async dispatch(identity, wireBody) {
      const request = parseOperationRequest(wireBody, maximumRequestLimit)
      const route = getRoute(request.operation)
      enforceRequestSize(intrinsicByteLength(wireBody), route.requestLimitBytes)

      let validatedRequest: unknown
      try {
        validatedRequest = route.parseRequest(request.payload)
      } catch (error) {
        throw asGatewayError(error, 'invalid_request')
      }

      try {
        const handle = await route.adapter.dispatch(identity, validatedRequest)
        return parseOperationHandle(handle)
      } catch (error) {
        throw asGatewayError(error, 'service_unavailable')
      }
    },
    async getStatus(identity, operation, operationId) {
      const route = getRoute(operation)
      if (!isOpaqueIdentifier(operationId)) {
        throw new GatewayError('operation_not_found')
      }

      let response: unknown
      try {
        response = await route.adapter.getStatus(identity, operationId)
      } catch (error) {
        throw asGatewayError(error, 'service_unavailable')
      }

      try {
        return route.parseResponse(response)
      } catch (error) {
        throw asGatewayError(error, 'service_unavailable')
      }
    },
    getRoute,
  }

  function getRoute(operation: string): RouteDefinition<unknown, unknown> {
    return routesByOperation.get(operation) ?? failOperationNotFound()
  }
}

function validateReadOnlyRoute(route: ReadOnlyRoute<unknown>): void {
  if (!isPlainRecord(route) || !READ_ONLY_OPERATIONS.includes(route.operation) || !isPlainRecord(route.policy)
    || typeof route.parsePayload !== 'function' || typeof route.parseResponse !== 'function'
    || !isPlainRecord(route.adapter) || typeof route.adapter.dispatch !== 'function') {
    throw new GatewayError('invalid_request')
  }
}

function toAuditRequest(identity: CallerIdentity, requestId: string, operation: string, policy: AdmissionPolicy): AuditRequest {
  return {
    requestId,
    callerSubject: identity.subject,
    applicationCredentialId: identity.application_credential_id,
    operation: auditOperation(operation),
    limits: {
      requestBytes: policy.maxRequestBytes,
      rateLimit: policy.rate.maxRequests,
      concurrencyLimit: policy.maxConcurrent,
    },
  }
}

async function recordRejected(audit: AuditTrail, request: AuditRequest, code: GatewayErrorCode): Promise<void> {
  try {
    await audit.recordRejected(request, code)
  } catch {
    throw new GatewayError('service_unavailable')
  }
}

function gatewayCode(error: unknown): GatewayErrorCode {
  if (error instanceof GatewayError) return error.code
  if (error instanceof AdmissionError) return error.code
  if (error instanceof AuditStorageError || error instanceof AuditIntegrityError) return 'service_unavailable'
  return 'service_unavailable'
}

function auditOperation(operation: string): string {
  return operation.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
}

async function ensureCompletedAudit(audit: AuditTrail, request: AuditRequest, operationId: string): Promise<void> {
  try {
    if (await hasCompletedAudit(audit, request, operationId)) return
    try {
      await audit.recordTerminal({ ...request, operationId, state: 'completed' })
    } catch {
      if (await hasCompletedAudit(audit, request, operationId)) return
      throw new GatewayError('service_unavailable')
    }
  } catch (error) {
    if (error instanceof GatewayError) throw error
    throw new GatewayError('service_unavailable')
  }
}

async function hasCompletedAudit(audit: AuditTrail, request: AuditRequest, operationId: string): Promise<boolean> {
  const terminal = (await audit.getOperationReceipts(request.callerSubject, operationId)).at(-1)
  if (terminal?.state === 'completed') return true
  if (terminal?.state === 'failed') throw new GatewayError('service_unavailable')
  return false
}

function validateRoute(route: RouteDefinition<unknown, unknown>): void {
  if (!isPlainRecord(route)
    || !isOpaqueIdentifier(route.operation)
    || !isOpaqueIdentifier(route.service)
    || !isAuthorityClass(route.authorityClass)
    || typeof route.approvalRequired !== 'boolean'
    || typeof route.parseRequest !== 'function'
    || typeof route.parseResponse !== 'function'
    || !isServiceAdapter(route.adapter)) {
    throw new GatewayError('invalid_request')
  }
  if (!Number.isSafeInteger(route.requestLimitBytes) || route.requestLimitBytes < 1) {
    throw new GatewayError('invalid_request')
  }
  if (!isPlainRecord(route.ratePolicy)
    || !Number.isSafeInteger(route.ratePolicy.limit) || route.ratePolicy.limit < 1
    || !Number.isSafeInteger(route.ratePolicy.windowMs) || route.ratePolicy.windowMs < 1) {
    throw new GatewayError('invalid_request')
  }
}

function isPlainRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function isAuthorityClass(value: unknown): boolean {
  return value === 'read' || value === 'mutate' || value === 'break_glass'
}

function isServiceAdapter(value: unknown): value is { dispatch: Function; getStatus: Function } {
  return isPlainRecord(value) && typeof value.dispatch === 'function' && typeof value.getStatus === 'function'
}

function parseOperationHandle(value: unknown): OperationHandle {
  if (!isPlainRecord(value) || Object.keys(value).length !== 1 || typeof value.operationId !== 'string'
    || !isOpaqueIdentifier(value.operationId)) {
    throw new GatewayError('service_unavailable')
  }
  return { operationId: value.operationId }
}

function intrinsicByteLength(value: Uint8Array): number {
  const getter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), 'byteLength')?.get
  if (!getter) throw new GatewayError('invalid_request')
  try {
    return getter.call(value)
  } catch {
    throw new GatewayError('invalid_request')
  }
}

function enforceRequestSize(wireBytes: number, limit: number): void {
  if (!Number.isSafeInteger(wireBytes) || wireBytes < 0 || wireBytes > limit) {
    throw new GatewayError('invalid_request')
  }
}

function isOpaqueIdentifier(value: string): boolean {
  return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value)
}

function failOperationNotFound(): never {
  throw new GatewayError('operation_not_found')
}
