import {
  createDb,
  createTaskFromCalendarEvent,
  createTicket,
  getCalendarEvent,
  listCalendarConnections,
  updateCalendarConnectionTokens,
  updateCalendarEvent,
} from '@zync/db/queries'
import { GoogleCalendarProvider, OutlookCalendarProvider } from '@zync/calendar'
import { decryptToken, encryptToken } from '@zync/calendar/server'
import type { Env } from '../env'

const REFRESH_SKEW_MS = 5 * 60 * 1000

type CalendarJob =
  | { type: 'calendar.event.push'; tenantId: string; userId: string; eventId: string }
  | { type: 'calendar.event.update'; tenantId: string; userId: string; eventId: string }
  | {
      type: 'calendar.event.delete'
      tenantId: string
      userId: string
      externalId: string
      externalCalendarId: string | null
      source: string
    }
  | {
      type: 'calendar.booking.create_task'
      tenantId: string
      eventId: string
      customerId: string | null
      title: string
      startAt: string
    }
  | {
      type: 'calendar.booking.create_ticket'
      tenantId: string
      eventId: string
      customerId: string | null
      title: string
      startAt: string
    }

function buildProvider(provider: 'google' | 'outlook', env: Env) {
  return provider === 'google'
    ? new GoogleCalendarProvider(env.GOOGLE_OAUTH_CLIENT_ID ?? '', env.GOOGLE_OAUTH_CLIENT_SECRET ?? '')
    : new OutlookCalendarProvider(
        env.MICROSOFT_OAUTH_CLIENT_ID ?? '',
        env.MICROSOFT_OAUTH_CLIENT_SECRET ?? '',
      )
}

async function maybeRefreshAccessToken(
  env: Env,
  job: CalendarJob,
  connection: Awaited<ReturnType<typeof listCalendarConnections>>[number],
) {
  const encryptionKey = env.INTEGRATION_ENCRYPTION_KEY
  if (!encryptionKey) {
    throw new Error('INTEGRATION_ENCRYPTION_KEY is not configured')
  }

  const provider = buildProvider(connection.provider as 'google' | 'outlook', env)
  const [accessToken, refreshToken] = await Promise.all([
    decryptToken(connection.accessToken, encryptionKey),
    decryptToken(connection.refreshToken, encryptionKey),
  ])

  if (!connection.tokenExpiresAt || connection.tokenExpiresAt.getTime() - Date.now() >= REFRESH_SKEW_MS) {
    return { accessToken, provider }
  }

  const refreshed = await provider.refreshAccessToken(refreshToken)
  const db = createDb(env)
  const encrypted = await encryptToken(refreshed.accessToken, encryptionKey)
  await updateCalendarConnectionTokens(
    db,
    connection.tenantId,
    connection.id,
    encrypted,
    new Date(refreshed.expiresAt),
  )

  return { accessToken: refreshed.accessToken, provider }
}

function firstEligibleConnection(
  connections: Awaited<ReturnType<typeof listCalendarConnections>>,
) {
  return connections.find((connection) => connection.syncEnabled && connection.selectedCalendarId)
}

export async function handleCalendarQueueJob(job: CalendarJob, env: Env): Promise<void> {
  const db = createDb(env)

  if (job.type === 'calendar.booking.create_task') {
    const event = await getCalendarEvent(db, job.tenantId, job.eventId)
    if (!event) return
    await createTaskFromCalendarEvent(db, job.tenantId, event.createdBy, job.eventId, {
      title: `Follow up: ${job.title}`,
      dueDate: job.startAt.slice(0, 10),
    })
    return
  }

  if (job.type === 'calendar.booking.create_ticket') {
    await createTicket(db, job.tenantId, {
      customer_id: job.customerId ?? null,
      contact_id: null,
      category_id: null,
      assignee_id: null,
      title: job.title,
      description: `Booking created for ${job.startAt}`,
      priority: 'medium',
      source: 'web',
      external_id: job.eventId,
      external_thread_id: null,
    })
    return
  }

  const connections = await listCalendarConnections(db, job.tenantId, job.userId)
  const connection = firstEligibleConnection(connections)
  if (!connection || !connection.selectedCalendarId) {
    return
  }

  const { accessToken, provider } = await maybeRefreshAccessToken(env, job, connection)

  if (job.type === 'calendar.event.delete') {
    if (!job.externalId) return
    await provider.deleteEvent(
      accessToken,
      job.externalCalendarId ?? connection.selectedCalendarId,
      job.externalId,
    )
    return
  }

  const event = await getCalendarEvent(db, job.tenantId, job.eventId)
  if (!event) return

  const externalEvent = {
    title: event.title,
    description: event.description ?? null,
    startAt: event.startAt.toISOString(),
    endAt: event.endAt.toISOString(),
    allDay: event.allDay,
    location: event.location ?? null,
  }

  if (job.type === 'calendar.event.push') {
    const pushed = await provider.pushEvent(accessToken, connection.selectedCalendarId, externalEvent)
    await updateCalendarEvent(db, job.tenantId, event.id, {
      externalId: pushed.externalId,
      externalCalendarId: connection.selectedCalendarId,
      syncedAt: new Date(),
      syncStatus: 'synced',
    })
    return
  }

  if (!event.externalId) return
  await provider.updateEvent(
    accessToken,
    event.externalCalendarId ?? connection.selectedCalendarId,
    event.externalId,
    externalEvent,
  )
  await updateCalendarEvent(db, job.tenantId, event.id, {
    syncedAt: new Date(),
    syncStatus: 'synced',
  })
}
