/**
 * Webhook endpoint CRUD + delivery routes — webhook-endpoint-detail (wave-11 leaf-D).
 * Mounted at /api/webhooks in routes/index.ts.
 *
 * GET    /                         — list endpoints
 * POST   /                         — create endpoint (returns plaintext secret once)
 * GET    /events                   — list available event types
 * GET    /:id                      — endpoint detail
 * PATCH  /:id                      — update endpoint
 * DELETE /:id                      — delete endpoint
 * POST   /:id/rotate-secret        — rotate HMAC secret (OWNER only)
 * GET    /:id/deliveries           — paginated delivery log
 * GET    /:id/deliveries/:did      — single delivery detail
 * POST   /:id/deliveries/:did/retry — retry a delivery
 * POST   /:id/test                 — send a test dispatch
 *
 * All mutation routes: requirePermission('settings:write').
 * All read routes:     requirePermission('settings:read').
 */
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
import { z } from 'zod'
import type { AppEnv } from '../../types'
import { authMiddleware } from '../../middleware/auth'
import { requirePermission } from '../../middleware/guards'
import {
  createDb,
  listWebhookEndpoints,
  getWebhookEndpoint,
  getWebhookEndpointWithSecret,
  createWebhookEndpoint,
  updateWebhookEndpoint,
  updateWebhookSecret,
  deleteWebhookEndpoint,
  listWebhookDeliveries,
  getWebhookDelivery,
  insertWebhookDelivery,
  createWebhookSchema,
  updateWebhookSchema,
} from '@zync/db/queries'
import { encryptCredential, generateOpaqueToken, decryptCredential } from '@zync/auth'
import { assertSafeOutboundUrl, UnsafeOutboundUrlError } from '@zync/utils'
import { WEBHOOK_EVENT_CATALOG, WEBHOOK_EVENT_TYPES, isEndpointSubscribed } from '../../features/webhooks/catalog'

const WEBHOOK_URL_ERROR = 'Webhook URL must be a public HTTPS endpoint'

function assertSafeWebhookUrl(rawUrl: string): void {
  try {
    assertSafeOutboundUrl(rawUrl)
  } catch (err) {
    if (err instanceof UnsafeOutboundUrlError) {
      throw new HTTPException(400, { message: WEBHOOK_URL_ERROR })
    }
    throw err
  }
}

const paginationSchema = z.object({
  status: z.enum(['pending', 'delivered', 'failed', 'test']).optional(),
  from: z.string().datetime({ offset: true }).optional(),
  to: z.string().datetime({ offset: true }).optional(),
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(50),
})

const testDispatchSchema = z.object({
  event_type: z.string().refine((t) => WEBHOOK_EVENT_TYPES.includes(t), {
    message: 'unknown_event_type',
  }),
})

export const webhookEndpointRoutes = new Hono<AppEnv>()

webhookEndpointRoutes.use('*', authMiddleware)

// ── GET /webhooks/events ── (BEFORE /:id to avoid capture) ───────────────────
webhookEndpointRoutes.get(
  '/events',
  requirePermission('settings:read'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }
    return c.json(WEBHOOK_EVENT_CATALOG, 200)
  },
)

// ── GET /webhooks ─────────────────────────────────────────────────────────────
webhookEndpointRoutes.get(
  '/',
  requirePermission('settings:read'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }
    const db = createDb(c.env)
    const endpoints = await listWebhookEndpoints(db, session.tid)
    return c.json({ endpoints }, 200)
  },
)

// ── POST /webhooks ────────────────────────────────────────────────────────────
webhookEndpointRoutes.post(
  '/',
  requirePermission('settings:write'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const body = await c.req.json().catch(() => null)
    const parsed = createWebhookSchema.safeParse(body)
    if (!parsed.success) {
      return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400)
    }

    // SSRF guard — reject private/internal URLs before persisting
    assertSafeWebhookUrl(parsed.data.url)

    // Generate HMAC secret and encrypt it
    const plaintextSecret = generateOpaqueToken()
    const secretEncryptedObj = await encryptCredential(
      plaintextSecret,
      c.env.INTEGRATION_ENCRYPTION_KEY,
    )
    const secretEncrypted = JSON.stringify(secretEncryptedObj)

    const db = createDb(c.env)
    const endpoint = await createWebhookEndpoint(db, session.tid, {
      ...parsed.data,
      secretEncrypted,
    })

    return c.json({ endpoint, secret: plaintextSecret }, 201)
  },
)

// ── GET /webhooks/:id ─────────────────────────────────────────────────────────
webhookEndpointRoutes.get(
  '/:id',
  requirePermission('settings:read'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const { id } = c.req.param()
    const db = createDb(c.env)
    const endpoint = await getWebhookEndpoint(db, session.tid, id)
    if (!endpoint) return c.json({ error: 'Not found' }, 404)

    // Attach recent delivery summary
    const recentDeliveries = await listWebhookDeliveries(db, session.tid, id, {
      limit: 5,
      page: 1,
    })

    return c.json({ endpoint, recent_deliveries: recentDeliveries }, 200)
  },
)

// ── PATCH /webhooks/:id ───────────────────────────────────────────────────────
webhookEndpointRoutes.patch(
  '/:id',
  requirePermission('settings:write'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const { id } = c.req.param()
    const body = await c.req.json().catch(() => null)
    const parsed = updateWebhookSchema.safeParse(body)
    if (!parsed.success) {
      return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400)
    }

    if (parsed.data.url) {
      assertSafeWebhookUrl(parsed.data.url)
    }

    const db = createDb(c.env)
    const endpoint = await updateWebhookEndpoint(db, session.tid, id, parsed.data)
    if (!endpoint) return c.json({ error: 'Not found' }, 404)

    return c.json({ endpoint }, 200)
  },
)

// ── DELETE /webhooks/:id ──────────────────────────────────────────────────────
webhookEndpointRoutes.delete(
  '/:id',
  requirePermission('settings:write'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const { id } = c.req.param()
    const db = createDb(c.env)
    const deleted = await deleteWebhookEndpoint(db, session.tid, id)
    if (!deleted) return c.json({ error: 'Not found' }, 404)

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

// ── POST /webhooks/:id/rotate-secret ─────────────────────────────────────────
webhookEndpointRoutes.post(
  '/:id/rotate-secret',
  requirePermission('settings:write'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    // OWNER role required for secret rotation
    const role = (session as { role?: string }).role ?? ''
    if (role !== 'owner' && role !== 'OWNER') {
      return c.json({ error: 'Owner role required to rotate secret' }, 403)
    }

    const { id } = c.req.param()
    const db = createDb(c.env)

    // Verify endpoint belongs to tenant
    const existing = await getWebhookEndpoint(db, session.tid, id)
    if (!existing) return c.json({ error: 'Not found' }, 404)

    const plaintextSecret = generateOpaqueToken()
    const secretEncryptedObj = await encryptCredential(
      plaintextSecret,
      c.env.INTEGRATION_ENCRYPTION_KEY,
    )
    const secretEncrypted = JSON.stringify(secretEncryptedObj)

    await updateWebhookSecret(db, session.tid, id, secretEncrypted)

    return c.json({ secret: plaintextSecret }, 200)
  },
)

// ── GET /webhooks/:id/deliveries ──────────────────────────────────────────────
webhookEndpointRoutes.get(
  '/:id/deliveries',
  requirePermission('settings:read'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const { id } = c.req.param()
    const query = c.req.query()
    const parsed = paginationSchema.safeParse(query)
    if (!parsed.success) {
      return c.json({ error: 'Invalid query', issues: parsed.error.issues }, 400)
    }

    const db = createDb(c.env)
    const deliveries = await listWebhookDeliveries(db, session.tid, id, parsed.data)

    return c.json({ deliveries }, 200)
  },
)

// ── GET /webhooks/:id/deliveries/:did ─────────────────────────────────────────
webhookEndpointRoutes.get(
  '/:id/deliveries/:did',
  requirePermission('settings:read'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const { id, did } = c.req.param()
    const db = createDb(c.env)
    const delivery = await getWebhookDelivery(db, session.tid, did)
    if (!delivery || delivery.endpoint_id !== id) return c.json({ error: 'Not found' }, 404)

    return c.json({ delivery }, 200)
  },
)

// ── POST /webhooks/:id/deliveries/:did/retry ──────────────────────────────────
webhookEndpointRoutes.post(
  '/:id/deliveries/:did/retry',
  requirePermission('settings:write'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const { id, did } = c.req.param()
    const db = createDb(c.env)

    const delivery = await getWebhookDelivery(db, session.tid, did)
    if (!delivery || delivery.endpoint_id !== id) return c.json({ error: 'Not found' }, 404)
    if (delivery.status !== 'failed') {
      return c.json({ error: 'Only failed deliveries can be retried' }, 409)
    }

    const endpointRaw = await getWebhookEndpointWithSecret(db, session.tid, id)
    if (!endpointRaw) return c.json({ error: 'Endpoint not found' }, 404)

    // Decrypt secret and re-dispatch
    const plaintextSecret = await decryptCredential(
      JSON.parse(endpointRaw.secretEncrypted) as { ciphertext: string; iv: string; authTag: string },
      c.env.INTEGRATION_ENCRYPTION_KEY,
    )

    const body = delivery.request_body ?? '{}'
    const timestamp = Math.floor(Date.now() / 1000)
    const signature = await computeHmacSignature(plaintextSecret, timestamp, body)

    // SSRF guard — defense-in-depth even though registration already blocks bad URLs
    assertSafeWebhookUrl(endpointRaw.url)

    const startMs = Date.now()
    let responseStatus: number | null = null
    let responseBody: string | null = null
    let status = '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': delivery.event_type,
          'X-Zync-Delivery': did,
        },
        body,
      })
      responseStatus = resp.status
      responseBody = (await resp.text()).slice(0, 1000)
      if (resp.ok) status = 'delivered'
    } catch {
      // network error — status stays 'failed'
    }

    const latencyMs = Date.now() - startMs

    const newDelivery = await insertWebhookDelivery(db, {
      tenantId: session.tid,
      endpointId: id,
      eventType: delivery.event_type,
      status,
      requestBody: body,
      responseStatus,
      responseBody,
      latencyMs,
      attempt: delivery.attempt + 1,
    })

    return c.json({ delivery: newDelivery }, 200)
  },
)

// ── POST /webhooks/:id/test ───────────────────────────────────────────────────
webhookEndpointRoutes.post(
  '/:id/test',
  requirePermission('settings:write'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const { id } = c.req.param()
    const body = await c.req.json().catch(() => null)
    const parsed = testDispatchSchema.safeParse(body)
    if (!parsed.success) {
      return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400)
    }

    const db = createDb(c.env)
    const endpointRaw = await getWebhookEndpointWithSecret(db, session.tid, id)
    if (!endpointRaw) return c.json({ error: 'Not found' }, 404)

    const eventDef = WEBHOOK_EVENT_CATALOG.find((e) => e.type === parsed.data.event_type)
    if (!eventDef) return c.json({ error: 'Unknown event type' }, 400)

    if (!isEndpointSubscribed(endpointRaw.events, parsed.data.event_type)) {
      return c.json({ error: 'Endpoint not subscribed to this event' }, 422)
    }

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

    const payload = JSON.stringify({
      event: parsed.data.event_type,
      data: eventDef.payloadShape,
      _test: true,
    })

    const timestamp = Math.floor(Date.now() / 1000)
    const signature = await computeHmacSignature(plaintextSecret, timestamp, payload)

    // SSRF guard — defense-in-depth even though registration already blocks bad URLs
    assertSafeWebhookUrl(endpointRaw.url)

    const startMs = Date.now()
    let responseStatus: number | null = null
    let responseBody: string | null = null
    let status: 'test' | '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': parsed.data.event_type,
          'X-Zync-Delivery': `test-${Date.now()}`,
        },
        body: payload,
      })
      responseStatus = resp.status
      responseBody = (await resp.text()).slice(0, 1000)
      status = 'test'
    } catch {
      // network error
    }

    const latencyMs = Date.now() - startMs

    // Insert test delivery row
    await insertWebhookDelivery(db, {
      tenantId: session.tid,
      endpointId: id,
      eventType: parsed.data.event_type,
      status,
      requestBody: payload,
      responseStatus,
      responseBody,
      latencyMs,
      attempt: 1,
    })

    return c.json({
      status,
      response_status: responseStatus,
      response_body: responseBody,
      latency_ms: latencyMs,
    }, 200)
  },
)

// ── HMAC helper ───────────────────────────────────────────────────────────────

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('')
}
