export const GATEWAY_ERROR_CODES = [
  'unauthenticated',
  'identity_mismatch',
  'credential_revoked',
  'request_expired',
  'replay_detected',
  'idempotency_conflict',
  'invalid_request',
  'operation_forbidden',
  'approval_required',
  'rate_limited',
  'capacity_exhausted',
  'service_unavailable',
  'operation_not_found',
] as const

export type GatewayErrorCode = (typeof GATEWAY_ERROR_CODES)[number]

export class GatewayError extends Error {
  readonly name = 'GatewayError'

  constructor(readonly code: GatewayErrorCode) {
    super(code)
  }
}

export type JsonPrimitive = boolean | number | string | null
export type JsonValue = JsonPrimitive | readonly JsonValue[] | { readonly [key: string]: JsonValue }
export type JsonObject = { readonly [key: string]: JsonValue }

export interface CallerIdentity {
  readonly subject: string
  readonly email: string
  readonly edge_session_id: string
  readonly application_credential_id: string
}

export interface OperationRequest {
  readonly requestId: string
  readonly idempotencyKey: string
  readonly issuedAt: string
  readonly nonce: string
  readonly operation: string
  readonly payload: JsonObject
}

export type RawOperationRequest = Readonly<Uint8Array>

export type OperationState = 'accepted' | 'running' | 'completed' | 'failed' | 'cancelled'

export interface OperationHandle {
  readonly operationId: string
}

export interface OperationStatus {
  readonly state: OperationState
  readonly completedAt?: string
  readonly outcome?: JsonValue
}

export type AuthorityClass = 'read' | 'mutate' | 'break_glass'

export interface RatePolicy {
  readonly limit: number
  readonly windowMs: number
}

export interface ServiceAdapter<Request, Status> {
  dispatch(identity: CallerIdentity, request: Request): Promise<OperationHandle>
  getStatus(identity: CallerIdentity, operationId: string): Promise<Status>
}

export interface RouteDefinition<Request, Status> {
  readonly operation: string
  readonly service: string
  readonly authorityClass: AuthorityClass
  readonly requestLimitBytes: number
  readonly ratePolicy: RatePolicy
  readonly approvalRequired: boolean
  readonly parseRequest: (payload: JsonObject) => Request
  readonly parseResponse: (value: unknown) => Status
  readonly adapter: ServiceAdapter<Request, Status>
}

export interface RouteRegistry {
  dispatch(identity: CallerIdentity, wireBody: RawOperationRequest): Promise<OperationHandle>
  getStatus(identity: CallerIdentity, operation: string, operationId: string): Promise<unknown>
  getRoute(operation: string): RouteDefinition<unknown, unknown>
}

const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/
const typedArrayByteLength = Object.getOwnPropertyDescriptor(
  Object.getPrototypeOf(Uint8Array.prototype),
  'byteLength',
)?.get

export function parseOperationRequest(wireBody: RawOperationRequest, limit: number): OperationRequest {
  enforceWireBodyLimit(wireBody, limit)
  const value = parseWireJson(wireBody)

  if (!isPlainRecord(value) || !hasOnlyKeys(value, [
    'request_id',
    'idempotency_key',
    'issued_at',
    'nonce',
    'operation',
    'payload',
  ])) {
    throw invalidRequest()
  }

  const requestId = readIdentifier(value.request_id)
  const idempotencyKey = readIdentifier(value.idempotency_key)
  const issuedAt = readTimestamp(value.issued_at)
  const nonce = readIdentifier(value.nonce)
  const operation = readIdentifier(value.operation)

  if (!isPlainRecord(value.payload)) {
    throw invalidRequest()
  }

  return {
    requestId,
    idempotencyKey,
    issuedAt,
    nonce,
    operation,
    payload: value.payload as JsonObject,
  }
}

export function asGatewayError(error: unknown, fallback: GatewayErrorCode): GatewayError {
  return error instanceof GatewayError ? error : new GatewayError(fallback)
}

export function measureJsonValueBytes(value: unknown, limit: number): number {
  const encoder = new TextEncoder()
  const pending: JsonMeasureFrame[] = [{ kind: 'value', value }]
  const seen = new Set<object>()
  let bytes = 0

  try {
    while (pending.length > 0) {
      const frame = pending.pop()
      if (frame === undefined) {
        break
      }
      if (frame.kind === 'token') {
        bytes = addTokenBytes(encoder, bytes, frame.value, limit)
        continue
      }
      const current = frame.value
      if (current === null || typeof current === 'string' || typeof current === 'boolean') {
        bytes = addTokenBytes(encoder, bytes, JSON.stringify(current), limit)
        continue
      }
      if (typeof current === 'number') {
        if (!Number.isFinite(current)) {
          throw invalidRequest()
        }
        bytes = addTokenBytes(encoder, bytes, JSON.stringify(current), limit)
        continue
      }
      if (typeof current !== 'object' || seen.has(current)) {
        throw invalidRequest()
      }
      seen.add(current)
      if (Array.isArray(current)) {
        pending.push({ kind: 'token', value: ']' })
        for (let index = current.length - 1; index >= 0; index -= 1) {
          pending.push({ kind: 'value', value: current[index] })
          if (index > 0) {
            pending.push({ kind: 'token', value: ',' })
          }
        }
        pending.push({ kind: 'token', value: '[' })
        continue
      }
      if (!isPlainRecord(current)) {
        throw invalidRequest()
      }
      const keys = Object.keys(current)
      pending.push({ kind: 'token', value: '}' })
      for (let index = keys.length - 1; index >= 0; index -= 1) {
        const key = keys[index]
        if (key === undefined) {
          throw invalidRequest()
        }
        pending.push({ kind: 'value', value: current[key] })
        pending.push({ kind: 'token', value: ':' })
        pending.push({ kind: 'token', value: JSON.stringify(key) })
        if (index > 0) {
          pending.push({ kind: 'token', value: ',' })
        }
      }
      pending.push({ kind: 'token', value: '{' })
    }
  } catch (error) {
    throw asGatewayError(error, 'invalid_request')
  }

  return bytes
}

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

  try {
    const prototype = Object.getPrototypeOf(value)
    return prototype === Object.prototype || prototype === null
  } catch {
    return false
  }
}

export function isJsonObject(value: unknown): value is JsonObject {
  return isPlainRecord(value) && isJsonValue(value)
}

export function isJsonValue(value: unknown): value is JsonValue {
  const pending: unknown[] = [value]
  const seen = new Set<object>()

  try {
    while (pending.length > 0) {
      const current = pending.pop()
      if (current === null || typeof current === 'string' || typeof current === 'boolean') {
        continue
      }
      if (typeof current === 'number') {
        if (Number.isFinite(current)) {
          continue
        }
        return false
      }
      if (typeof current !== 'object' || seen.has(current)) {
        return false
      }
      seen.add(current)
      if (Array.isArray(current)) {
        pending.push(...current)
        continue
      }
      if (isPlainRecord(current)) {
        pending.push(...Object.values(current))
        continue
      }
      return false
    }
  } catch {
    return false
  }

  return true
}

type JsonMeasureFrame =
  | { readonly kind: 'value'; readonly value: unknown }
  | { readonly kind: 'token'; readonly value: string }

function addTokenBytes(encoder: TextEncoder, bytes: number, token: string | undefined, limit: number): number {
  if (token === undefined) {
    throw invalidRequest()
  }
  const nextBytes = bytes + encoder.encode(token).byteLength
  if (nextBytes > limit) {
    throw invalidRequest()
  }
  return nextBytes
}

function hasOnlyKeys(value: Record<string, unknown>, expectedKeys: readonly string[]): boolean {
  const actualKeys = Object.keys(value)
  return actualKeys.length === expectedKeys.length && actualKeys.every((key) => expectedKeys.includes(key))
}

function readIdentifier(value: unknown): string {
  if (typeof value !== 'string' || !identifierPattern.test(value)) {
    throw invalidRequest()
  }
  return value
}

function readTimestamp(value: unknown): string {
  if (typeof value !== 'string' || Number.isNaN(Date.parse(value))) {
    throw invalidRequest()
  }
  return value
}

function invalidRequest(): GatewayError {
  return new GatewayError('invalid_request')
}

function enforceWireBodyLimit(wireBody: RawOperationRequest, limit: number): void {
  if (!Number.isSafeInteger(limit) || limit < 1) {
    throw invalidRequest()
  }

  let wireBytes: number
  try {
    if (!(wireBody instanceof Uint8Array) || typedArrayByteLength === undefined) {
      throw invalidRequest()
    }
    wireBytes = typedArrayByteLength.call(wireBody)
    Uint8Array.prototype.slice.call(wireBody, 0, 0)
  } catch {
    throw invalidRequest()
  }

  if (wireBytes > limit) {
    throw invalidRequest()
  }

  return
}

function parseWireJson(wireBody: RawOperationRequest): unknown {
  try {
    return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(wireBody))
  } catch {
    throw invalidRequest()
  }
}
