import { CloudServiceError } from '../cloud-service-error.ts'
import type {
  ChannelDeliveryRecord,
  ChannelProviderId,
} from '../control-plane-store.ts'
import {
  publicChannelDelivery,
  type PublicChannelDeliveryRecord,
} from '../public-channel-records.ts'
import type { CloudPrincipal } from '../session-service.ts'
import { resolveGatewayChannelBindingScope as deliveryOperatorScope } from './channel-binding-scope.ts'
import {
  assertGatewayAccess,
  CHANNEL_HOUR_MS,
  type CloudChannelDomainServiceOptions,
} from './channel-domain-context.ts'

type ChannelDeliveryTargetInput = {
  agentId: string
  channelBindingId: string
  sessionBindingId?: string | null
  provider: ChannelProviderId
}

async function validateChannelDeliveryTarget(
  options: CloudChannelDomainServiceOptions,
  principal: CloudPrincipal,
  input: ChannelDeliveryTargetInput,
) {
  await options.ensurePrincipal(principal)
  assertGatewayAccess(principal)
  const orgId = options.principalOrgId(principal)
  const agent = await options.store.getHeadlessAgent(orgId, input.agentId)
  if (!agent) throw new CloudServiceError(404, 'Headless agent was not found.')
  const channelBinding = await options.store.getChannelBinding(orgId, input.channelBindingId)
  if (!channelBinding) throw new CloudServiceError(404, 'Channel binding was not found.')
  if (channelBinding.agentId !== agent.agentId) {
    throw new CloudServiceError(403, 'Channel delivery binding does not belong to the selected headless agent.')
  }
  if (channelBinding.provider !== input.provider) {
    throw new CloudServiceError(400, 'Channel delivery provider does not match the channel binding.')
  }
  await deliveryOperatorScope(options, principal, [channelBinding.bindingId])
  if (input.sessionBindingId) {
    const sessionBinding = await options.store.getChannelSessionBinding(orgId, input.sessionBindingId)
    if (!sessionBinding) throw new CloudServiceError(404, 'Channel session binding was not found.')
    if (
      sessionBinding.agentId !== agent.agentId
      || sessionBinding.channelBindingId !== channelBinding.bindingId
      || sessionBinding.provider !== input.provider
    ) {
      throw new CloudServiceError(403, 'Channel delivery session binding does not belong to the selected channel binding.')
    }
  }
  return { orgId, agent, channelBinding }
}

export async function assertChannelDeliveryTarget(
  options: CloudChannelDomainServiceOptions,
  principal: CloudPrincipal,
  input: ChannelDeliveryTargetInput,
) {
  await validateChannelDeliveryTarget(options, principal, input)
}

export async function createChannelDelivery(
  options: CloudChannelDomainServiceOptions,
  principal: CloudPrincipal,
  input: {
    agentId: string
    channelBindingId: string
    sessionBindingId?: string | null
    provider: ChannelProviderId
    target: Record<string, unknown>
    eventType: string
    payload: Record<string, unknown>
    status?: ChannelDeliveryRecord['status']
    nextAttemptAt?: Date | null
    deliveryId?: string | null
  },
): Promise<PublicChannelDeliveryRecord> {
  const { orgId, agent, channelBinding } = await validateChannelDeliveryTarget(options, principal, input)
  const delivery = await options.store.createChannelDelivery({
    deliveryId: input.deliveryId || options.ids.randomUUID(),
    orgId,
    agentId: agent.agentId,
    channelBindingId: channelBinding.bindingId,
    sessionBindingId: input.sessionBindingId,
    provider: input.provider,
    target: input.target,
    eventType: input.eventType,
    payload: input.payload,
    status: input.status,
    nextAttemptAt: input.nextAttemptAt || undefined,
  })
  return publicChannelDelivery(delivery)
}

export async function listChannelDeliveries(
  options: CloudChannelDomainServiceOptions,
  principal: CloudPrincipal,
  input: {
    deliveryId?: string | null
    status?: ChannelDeliveryRecord['status'] | null
    channelBindingId?: string | null
    limit?: number | null
  } = {},
): Promise<PublicChannelDeliveryRecord[]> {
  await options.ensurePrincipal(principal)
  const scope = await deliveryOperatorScope(options, principal, input.channelBindingId ? [input.channelBindingId] : null)
  return (await options.store.listChannelDeliveries({
    orgId: options.principalOrgId(principal),
    deliveryId: input.deliveryId || null,
    status: input.status || null,
    channelBindingIds: scope.channelBindingIds,
    lastClaimedBy: scope.gatewayTokenId,
    limit: input.limit || null,
  })).map(publicChannelDelivery)
}

export async function retryChannelDelivery(
  options: CloudChannelDomainServiceOptions,
  principal: CloudPrincipal,
  deliveryId: string,
): Promise<PublicChannelDeliveryRecord | null> {
  await options.ensurePrincipal(principal)
  const scope = await deliveryOperatorScope(options, principal)
  const delivery = await options.store.ackChannelDelivery({
    orgId: options.principalOrgId(principal),
    deliveryId,
    channelBindingIds: scope.channelBindingIds,
    lastClaimedBy: scope.gatewayTokenId,
    status: 'failed',
    lastError: null,
    nextAttemptAt: new Date(),
  })
  return delivery ? publicChannelDelivery(delivery) : null
}

export async function deadLetterChannelDelivery(
  options: CloudChannelDomainServiceOptions,
  principal: CloudPrincipal,
  input: { deliveryId: string, lastError?: string | null },
): Promise<PublicChannelDeliveryRecord | null> {
  await options.ensurePrincipal(principal)
  const scope = await deliveryOperatorScope(options, principal)
  const delivery = await options.store.ackChannelDelivery({
    orgId: options.principalOrgId(principal),
    deliveryId: input.deliveryId,
    channelBindingIds: scope.channelBindingIds,
    lastClaimedBy: scope.gatewayTokenId,
    status: 'dead',
    lastError: input.lastError || 'Manually dead-lettered by gateway operator.',
    nextAttemptAt: null,
  })
  return delivery ? publicChannelDelivery(delivery) : null
}

export async function claimNextChannelDelivery(
  options: CloudChannelDomainServiceOptions,
  principal: CloudPrincipal,
  input: { claimedBy: string, ttlMs?: number, now?: Date, channelBindingIds?: readonly string[] | null },
): Promise<ChannelDeliveryRecord | null> {
  await options.ensurePrincipal(principal)
  assertGatewayAccess(principal)
  const scope = await deliveryOperatorScope(options, principal, input.channelBindingIds)
  try {
    const gatewayDeliveryLimit = await options.usageGovernance.effectiveQuotaLimit(
      options.principalOrgId(principal),
      options.abuse.maxGatewayDeliveriesPerHour,
      'maxGatewayDeliveriesPerHour',
    )
    const delivery = await options.store.claimNextChannelDelivery({
      orgId: options.principalOrgId(principal),
      claimedBy: input.claimedBy,
      lastClaimedBy: scope.gatewayTokenId,
      channelBindingIds: scope.channelBindingIds,
      ttlMs: input.ttlMs,
      now: input.now,
      quota: options.usageGovernance.quotaLimit(gatewayDeliveryLimit)
        ? {
            quotaKey: 'gateway_deliveries:hour',
            limit: gatewayDeliveryLimit!,
            windowMs: CHANNEL_HOUR_MS,
            policyCode: 'quota.gateway_deliveries_per_hour_exceeded',
          }
        : null,
    })
    if (delivery) {
      await options.usageGovernance.recordUsage({
        orgId: options.principalOrgId(principal),
        accountId: principal.accountId || null,
        eventType: 'gateway.delivery.claimed',
        unit: 'count',
        metadata: {
          deliveryId: delivery.deliveryId,
          provider: delivery.provider,
          claimedBy: input.claimedBy,
        },
      })
    }
    return delivery
  } catch (error) {
    options.usageGovernance.translateQuotaError(error, 'Gateway delivery quota exceeded.', 'quota.gateway_deliveries_per_hour_exceeded')
  }
}

export async function ackChannelDelivery(
  options: CloudChannelDomainServiceOptions,
  principal: CloudPrincipal,
  input: {
    deliveryId: string
    claimedBy?: string | null
    status: Extract<ChannelDeliveryRecord['status'], 'sent' | 'failed' | 'dead'>
    lastError?: string | null
    nextAttemptAt?: Date | null
  },
): Promise<PublicChannelDeliveryRecord | null> {
  await options.ensurePrincipal(principal)
  assertGatewayAccess(principal)
  const scope = await deliveryOperatorScope(options, principal)
  const delivery = await options.store.ackChannelDelivery({
    orgId: options.principalOrgId(principal),
    deliveryId: input.deliveryId,
    channelBindingIds: scope.channelBindingIds,
    lastClaimedBy: scope.gatewayTokenId,
    claimedBy: input.claimedBy,
    status: input.status,
    lastError: input.lastError,
    nextAttemptAt: input.nextAttemptAt,
  })
  return delivery ? publicChannelDelivery(delivery) : null
}
