/**
 * Calendar sync webhook routes — calendar-module.
 * Handles push notifications from Google and Outlook.
 *
 * POST /webhooks/calendar/google/:connId   Google Calendar push channel
 * POST /webhooks/calendar/outlook/:connId  Outlook Graph subscription
 *
 * Rate-limited via RATE_LIMITER_WEBHOOK binding.
 * Never overwrites Zync-sourced (task/project) events.
 */
import { Hono } from 'hono'
import { z } from 'zod'
import { createDb } from '@zync/db/queries'
import type { AppEnv } from '../../types'
import {
  outlookClientStateKey,
  parseOutlookNotifications,
  outlookNotificationsAuthorized,
} from '../../lib/outlook-calendar-webhook'
import {
  getCalendarConnectionById,
  upsertExternalCalendarEvent,
  updateCalendarConnectionLastSynced,
  updateCalendarConnectionTokens,
} from '@zync/db/queries'
import {
  GoogleCalendarProvider,
  OutlookCalendarProvider,
} from '@zync/calendar'
import { decryptToken, encryptToken } from '@zync/calendar/server'
import type { ExternalEvent } from '@zync/calendar'
import type { Db } from '@zync/db/queries'

export const syncWebhooksRoute = new Hono<AppEnv>()

const REFRESH_SKEW_MS = 5 * 60 * 1000 // 5 minutes

// ── Google push channel ───────────────────────────────────────────────────────

syncWebhooksRoute.post('/webhooks/calendar/google/:connId', async (c) => {
  // Rate limit inbound webhook
  const rateLimiter = c.env.RATE_LIMITER_WEBHOOK
  if (rateLimiter) {
    const result = await rateLimiter.limit({ key: 'calendar-google-webhook' })
    if (!result.success) {
      return c.text('Rate limit exceeded', 429)
    }
  }

  const connId = c.req.param('connId')
  const channelId = c.req.header('X-Goog-Channel-Id')
  const resourceState = c.req.header('X-Goog-Resource-State')

  // 'sync' is a handshake notification — acknowledge and return
  if (resourceState === 'sync') {
    return c.text('OK', 200)
  }

  if (!channelId) {
    return c.text('Missing channel id', 400)
  }
  if (!z.string().uuid().safeParse(channelId).success) {
    return c.text('Invalid channel id', 400)
  }

  const encryptionKey = c.env.INTEGRATION_ENCRYPTION_KEY
  const googleClientId = c.env.GOOGLE_OAUTH_CLIENT_ID
  const googleClientSecret = c.env.GOOGLE_OAUTH_CLIENT_SECRET

  if (!encryptionKey || !googleClientId || !googleClientSecret) {
    return c.text('Server misconfiguration', 500)
  }

  const db = createDb(c.env)

  // Validate channel mapping in KV
  const kv = c.env.KV
  if (!kv) {
    return c.text('Server misconfiguration', 503)
  }
  const stored = await kv.get(`google_channel:${channelId}`)
  if (!stored || stored !== connId) {
    return c.text('Invalid channel', 401)
  }

  // Cross-tenant lookup by id — tenantId is not known until the row is loaded.
  // KV channel map above provides the security gate.
  const conn = await getCalendarConnectionById(db, connId)

  if (!conn) {
    return c.text('Connection not found', 404)
  }

  const [accessTokenPlain, refreshTokenPlain] = await Promise.all([
    decryptToken(conn.accessToken, encryptionKey),
    decryptToken(conn.refreshToken, encryptionKey),
  ])

  const provider = new GoogleCalendarProvider(googleClientId, googleClientSecret)

  let currentAccessToken = accessTokenPlain
  if (conn.tokenExpiresAt && conn.tokenExpiresAt.getTime() - Date.now() < REFRESH_SKEW_MS) {
    const refreshed = await provider.refreshAccessToken(refreshTokenPlain)
    const newEncrypted = await encryptToken(refreshed.accessToken, encryptionKey)
    await updateCalendarConnectionTokens(db, conn.tenantId, conn.id, newEncrypted, new Date(refreshed.expiresAt))
    currentAccessToken = refreshed.accessToken
  }

  if (!conn.selectedCalendarId) {
    return c.text('No calendar selected', 200)
  }

  const changes = await provider.fetchChanges(
    currentAccessToken,
    conn.selectedCalendarId,
    conn.lastSyncedAt?.toISOString() ?? null,
  )

  await processExternalEvents(db, conn.tenantId, conn.userId, 'google', conn.selectedCalendarId, changes)
  await updateCalendarConnectionLastSynced(db, conn.tenantId, conn.id)

  return c.text('OK', 200)
})

// ── Outlook Graph subscription ─────────────────────────────────────────────────

syncWebhooksRoute.post('/webhooks/calendar/outlook/:connId', async (c) => {
  const rateLimiter = c.env.RATE_LIMITER_WEBHOOK
  if (rateLimiter) {
    const result = await rateLimiter.limit({ key: 'calendar-outlook-webhook' })
    if (!result.success) {
      return c.text('Rate limit exceeded', 429)
    }
  }

  // Outlook validation handshake: echo validationToken as plain text
  const url = new URL(c.req.url)
  const validationToken = url.searchParams.get('validationToken')
  if (validationToken) {
    return new Response(validationToken, {
      status: 200,
      headers: { 'Content-Type': 'text/plain' },
    })
  }

  const connId = c.req.param('connId')
  const kv = c.env.KV
  if (!kv) {
    return c.text('Server misconfiguration', 503)
  }

  const rawBody = await c.req.text()
  const notifications = parseOutlookNotifications(rawBody)
  if (notifications === null) {
    return c.text('Invalid JSON', 400)
  }

  const storedClientState = await kv.get(outlookClientStateKey(connId))
  if (!outlookNotificationsAuthorized(notifications, storedClientState)) {
    console.error('[outlook-webhook] clientState validation failed', { connId })
    return c.text('Unauthorized', 401)
  }

  const encryptionKey = c.env.INTEGRATION_ENCRYPTION_KEY
  const outlookClientId = c.env.MICROSOFT_OAUTH_CLIENT_ID
  const outlookClientSecret = c.env.MICROSOFT_OAUTH_CLIENT_SECRET

  if (!encryptionKey || !outlookClientId || !outlookClientSecret) {
    return c.text('Server misconfiguration', 500)
  }

  const db = createDb(c.env)

  const conn = await getCalendarConnectionById(db, connId)

  if (!conn) {
    return c.text('Connection not found', 404)
  }

  const [accessTokenPlain, refreshTokenPlain] = await Promise.all([
    decryptToken(conn.accessToken, encryptionKey),
    decryptToken(conn.refreshToken, encryptionKey),
  ])

  const provider = new OutlookCalendarProvider(outlookClientId, outlookClientSecret)

  let currentAccessToken = accessTokenPlain
  if (conn.tokenExpiresAt && conn.tokenExpiresAt.getTime() - Date.now() < REFRESH_SKEW_MS) {
    const refreshed = await provider.refreshAccessToken(refreshTokenPlain)
    const newEncrypted = await encryptToken(refreshed.accessToken, encryptionKey)
    await updateCalendarConnectionTokens(db, conn.tenantId, conn.id, newEncrypted, new Date(refreshed.expiresAt))
    currentAccessToken = refreshed.accessToken
  }

  if (!conn.selectedCalendarId) {
    return c.text('No calendar selected', 200)
  }

  const changes = await provider.fetchChanges(
    currentAccessToken,
    conn.selectedCalendarId,
    conn.lastSyncedAt?.toISOString() ?? null,
  )

  await processExternalEvents(db, conn.tenantId, conn.userId, 'outlook', conn.selectedCalendarId, changes)
  await updateCalendarConnectionLastSynced(db, conn.tenantId, conn.id)

  return c.text('OK', 200)
})

// ── Shared event processing ───────────────────────────────────────────────────

async function processExternalEvents(
  db: Db,
  tenantId: string,
  userId: string,
  source: 'google' | 'outlook',
  calendarId: string,
  events: ExternalEvent[],
): Promise<void> {
  for (const ev of events) {
    await upsertExternalCalendarEvent(db, {
      tenantId,
      createdBy: userId,
      title: ev.title,
      description: ev.description,
      startAt: new Date(ev.startAt),
      endAt: new Date(ev.endAt),
      allDay: ev.allDay,
      location: ev.location,
      source,
      externalId: ev.externalId,
      externalCalendarId: calendarId,
      syncedAt: new Date(),
      syncStatus: 'synced',
    })
  }
}
