/**
 * webhook.deliver queue consumer — white-label-api / S6-i2-001.
 *
 * Delivers tenant webhook events to registered endpoints. Re-validates each
 * outbound URL with assertSafeOutboundUrl immediately before fetch() to defend
 * against DNS rebinding between registration and delivery time.
 */
import { decryptCredential } from '@zync/auth'
import {
  createDb,
  getTenantById,
  listWebhookEndpoints,
  getWebhookEndpointWithSecret,
  insertWebhookDelivery,
} from '@zync/db/queries'
import type { TenantId } from '@zync/types'
import type { Env } from '@zync/types'
import { assertSafeOutboundUrl } from '@zync/utils'
import { isEndpointSubscribed } from '../features/webhooks/catalog'

export interface WebhookDeliverJob {
  type: 'webhook.deliver'
  tenantId: string
  event: string
  payload: Record<string, unknown>
  deliveryId?: string
  endpointId?: string
  attempt?: number
}

async function computeHmacSignature(
  secret: string,
  timestamp: number,
  body: string,
): Promise<string> {
  const enc = new TextEncoder()
  const key = await crypto.subtle.importKey(
    'raw',
    enc.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  )
  const data = enc.encode(`${timestamp}.${body}`)
  const sig = await crypto.subtle.sign('HMAC', key, data)
  return Array.from(new Uint8Array(sig))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('')
}

async function deliverToEndpoint(
  env: Env,
  db: ReturnType<typeof createDb>,
  job: {
    tenantId: string
    endpointId: string
    eventType: string
    payload: Record<string, unknown>
    deliveryId?: string
    attempt?: number
  },
): Promise<void> {
  const endpointRaw = await getWebhookEndpointWithSecret(db, job.tenantId, job.endpointId)
  if (!endpointRaw || !endpointRaw.isActive) return

  if (!isEndpointSubscribed(endpointRaw.events, job.eventType)) return

  // Delivery-time SSRF re-check — defense against DNS rebinding.
  assertSafeOutboundUrl(endpointRaw.url)

  const plaintextSecret = await decryptCredential(
    JSON.parse(endpointRaw.secretEncrypted) as {
      ciphertext: string
      iv: string
      authTag: string
    },
    env.INTEGRATION_ENCRYPTION_KEY,
  )

  const body = JSON.stringify({
    event: job.eventType,
    tenantId: job.tenantId,
    data: job.payload,
    timestamp: Math.floor(Date.now() / 1000),
  })

  const timestamp = Math.floor(Date.now() / 1000)
  const signature = await computeHmacSignature(plaintextSecret, timestamp, body)
  const deliveryId = job.deliveryId ?? crypto.randomUUID()

  const startMs = Date.now()
  let responseStatus: number | null = null
  let responseBody: string | null = null
  let status: 'delivered' | 'failed' = 'failed'

  try {
    const resp = await fetch(endpointRaw.url, {
      method: 'POST',
      redirect: 'manual',
      headers: {
        'Content-Type': 'application/json',
        'X-Zync-Signature': `sha256=${signature}`,
        'X-Zync-Timestamp': String(timestamp),
        'X-Zync-Event': job.eventType,
        'X-Zync-Delivery': deliveryId,
      },
      body,
    })
    responseStatus = resp.status
    responseBody = (await resp.text()).slice(0, 1000)
    if (resp.ok) status = 'delivered'
  } catch (err) {
    console.error('[webhook-deliver] fetch failed', {
      endpointId: job.endpointId,
      err,
    })
  }

  const latencyMs = Date.now() - startMs

  await insertWebhookDelivery(db, {
    tenantId: job.tenantId,
    endpointId: job.endpointId,
    eventType: job.eventType,
    status,
    requestBody: body,
    responseStatus,
    responseBody,
    latencyMs,
    attempt: job.attempt ?? 1,
  })
}

export async function handleWebhookDeliverBatch(
  batch: MessageBatch<unknown>,
  env: Env,
): Promise<void> {
  const db = createDb(env)

  for (const queueMsg of batch.messages) {
    const job = queueMsg.body as WebhookDeliverJob

    if (job?.type !== 'webhook.deliver') {
      queueMsg.ack()
      continue
    }

    try {
      const tenant = await getTenantById(db, job.tenantId as TenantId)
      if (!tenant) {
        queueMsg.ack()
        continue
      }

      if (job.endpointId) {
        await deliverToEndpoint(env, db, {
          tenantId: job.tenantId,
          endpointId: job.endpointId,
          eventType: job.event,
          payload: job.payload,
          deliveryId: job.deliveryId,
          attempt: job.attempt,
        })
        queueMsg.ack()
        continue
      }

      const endpoints = await listWebhookEndpoints(db, job.tenantId)
      for (const ep of endpoints) {
        if (!ep.is_active) continue
        if (!isEndpointSubscribed(ep.events, job.event)) continue

        await deliverToEndpoint(env, db, {
          tenantId: job.tenantId,
          endpointId: ep.id,
          eventType: job.event,
          payload: job.payload,
        })
      }

      queueMsg.ack()
    } catch (err) {
      console.error('[webhook-deliver] batch item failed', err)
      queueMsg.retry()
    }
  }
}
