/**
 * Slack inbound webhook receiver — system-communications-notifications (Task 14).
 *
 * POST /api/webhooks/slack
 *
 * 1. Handle URL-verification challenge (GET or POST with type="url_verification").
 * 2. Verify Slack signing secret via HMAC-SHA256 (timing-safe — no early-return).
 * 3. Parse event/slash command → InboundMessage.
 * 4. Enqueue { tenantId, source: 'slack', message } to comms.inbound queue.
 * 5. Return 200 fast (processing is async).
 *
 * Security: Slack signature is verified BEFORE reading the body payload.
 * Timing-safe HMAC comparison protects against timing attacks.
 */
import { Hono } from 'hono'
import { timingSafeEqual, decryptCredential } from '@zync/auth'
import { createDb, findSlackCredentialByTeamId } from '@zync/db/queries'
import type { AppEnv } from '../../types'
import type { InboundMessage } from '@zync/types'
import { slackWebhookSchema, type SlackWebhookBody } from '../../schemas/webhooks'
import { claimInboundWebhookEvent } from '../../lib/webhook-idempotency'

const slackRouter = new Hono<AppEnv>()

/**
 * Compute HMAC-SHA256 of `data` using `secret` via WebCrypto.
 * Returns hex digest.
 */
async function hmacSha256Hex(secret: string, data: string): Promise<string> {
  const keyBytes = new TextEncoder().encode(secret)
  const dataBytes = new TextEncoder().encode(data)
  const key = await crypto.subtle.importKey(
    'raw',
    keyBytes,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  )
  const sigBuf = await crypto.subtle.sign('HMAC', key, dataBytes)
  return Array.from(new Uint8Array(sigBuf))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('')
}

slackRouter.post('/', async (c) => {
  const rawBody = await c.req.text()
  const timestamp = c.req.header('X-Slack-Request-Timestamp')
  const slackSignature = c.req.header('X-Slack-Signature')
  const slackRetryNum = c.req.header('X-Slack-Retry-Num')

  // Slack retries on 5xx — skip re-processing duplicate deliveries.
  if (slackRetryNum && Number(slackRetryNum) > 0) {
    return c.json({ ok: true })
  }

  // Replay attack prevention: reject requests older than 5 minutes
  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return c.json({ error: 'Request timestamp too old' }, 400)
  }

  // Parse the body for tenant identification
  let rawParsed: unknown
  try {
    rawParsed = JSON.parse(rawBody)
  } catch {
    return c.json({ error: 'Invalid JSON' }, 400)
  }
  const bodyParsed = slackWebhookSchema.safeParse(rawParsed)
  if (!bodyParsed.success) {
    return c.json({ error: 'Invalid request', issues: bodyParsed.error.issues }, 400)
  }
  const body = bodyParsed.data

  // Handle URL-verification challenge BEFORE checking signature
  // (Slack sends this during initial webhook setup)
  if (body['type'] === 'url_verification') {
    return c.json({ challenge: body['challenge'] })
  }

  // Determine tenantId from team_id in the Slack payload
  // The tenantId is mapped from Slack team_id via adapter_credentials metadata
  const team = body['team'] as { id?: string } | undefined
  const teamId = (body['team_id'] ?? team?.id) as string | undefined
  if (!teamId) {
    return c.json({ error: 'Missing team_id' }, 400)
  }

  // Look up the Slack signing secret for this team
  const db = createDb(c.env)

  // Find the tenant credential for this Slack team
  // Uses the query helper which does a Drizzle-safe lookup by metadata->>'slack_team_id'
  const credRow = await findSlackCredentialByTeamId(db, teamId)

  if (!credRow) {
    // No matching Slack integration — ack without processing
    return c.json({ ok: true })
  }

  const credJson = await decryptCredential(
    { ciphertext: credRow.ciphertext, iv: credRow.iv, authTag: credRow.authTag },
    c.env.INTEGRATION_ENCRYPTION_KEY,
  )
  const cred = JSON.parse(credJson) as { signing_secret: string }
  const tenantId = credRow.tenantId

  // Verify Slack signing secret — signature is MANDATORY (no unsigned bypass).
  // A missing header or unconfigured secret is an auth failure, not a pass-through.
  if (!slackSignature || !cred.signing_secret) {
    return c.json({ error: 'Missing signature' }, 401)
  }
  const sigBaseString = `v0:${timestamp}:${rawBody}`
  const computedHex = await hmacSha256Hex(cred.signing_secret, sigBaseString)
  const computed = `v0=${computedHex}`
  // timingSafeEqual ensures no early-return on partial match
  if (!timingSafeEqual(slackSignature, computed)) {
    return c.json({ error: 'Invalid signature' }, 401)
  }

  // Parse event into InboundMessage
  const inboundMessage = parseSlackEvent(body)
  if (!inboundMessage) {
    return c.json({ ok: true })
  }

  const eventId = (body as { event_id?: string }).event_id
  if (eventId) {
    const claimed = await claimInboundWebhookEvent(c.env.KV, 'slack', eventId)
    if (!claimed) {
      return c.json({ ok: true })
    }
  }

  // Enqueue for async processing
  await c.env.QUEUE.send({
    type: 'comms.inbound',
    source: 'slack',
    tenantId,
    message: inboundMessage,
    receivedAt: new Date().toISOString(),
  })

  return c.json({ ok: true })
})

function parseSlackEvent(body: SlackWebhookBody): InboundMessage | null {
  const event = body.event
  if (!event) return null

  const text = (event['text'] as string | undefined) ?? ''
  const userId = (event['user'] as string | undefined) ?? ''
  const channel = (event['channel'] as string | undefined) ?? ''

  if (!text || !userId || !channel) return null

  return {
    from: userId,
    text,
    chatId: channel,
    metadata: {
      slackEventType: event['type'],
      slackTeamId: body.team_id,
      ts: event['ts'],
    },
  }
}

export { slackRouter }
