import type { calendar_v3 } from '@googleapis/calendar'
import type { BaseIntegrationAdapter } from '../../../base-integration-adapter'
import { ToolingError } from '../../../../types'
import type { ExecutionContext } from '../../../../../base/types'
import { getOAuthCredentials } from '../../../../../../../integrations/oauth/server/credentials'

/**
 * Google Calendar credentials format
 * Stored in credentials table, encrypted
 */
interface GoogleCalendarCredentials {
  accessToken: string
  refreshToken?: string
  expiresAt?: string
}

/**
 * Date/time value from Google Calendar API
 */
export interface CalendarDateTime {
  dateTime?: string // ISO 8601 datetime (when time is specified)
  date?: string // YYYY-MM-DD (for all-day events)
  timeZone?: string
}

/**
 * A Google Calendar event (plausible-use fields only)
 */
export interface CalendarEvent {
  id: string
  summary?: string
  description?: string
  location?: string
  start: CalendarDateTime
  end: CalendarDateTime
  status?: 'confirmed' | 'tentative' | 'cancelled'
  htmlLink?: string
  created?: string
  updated?: string
  organizer?: { email?: string; displayName?: string }
  attendees?: Array<{ email?: string; displayName?: string; responseStatus?: string }>
  recurringEventId?: string
}

/**
 * listEvents parameters.
 *
 * `credentialName` is required when calling the adapter directly (HTTP-handler
 * path). It is ignored on the dispatch path, where credentials are injected
 * by the execution-engine pipeline.
 */
interface ListEventsParams {
  organizationId: string
  credentialName?: string
  calendarId?: string
  timeMin?: string
  timeMax?: string
  maxResults?: number
  singleEvents?: boolean
  orderBy?: 'startTime' | 'updated'
}

/**
 * listEvents result
 */
export interface ListEventsResult {
  items: CalendarEvent[]
  nextPageToken?: string
  summary?: string
  timeZone?: string
}

/**
 * getEvent parameters
 */
interface GetEventParams {
  organizationId: string
  credentialName?: string
  calendarId?: string
  eventId: string
}

/**
 * Google Calendar adapter for calendar and event operations via Google Calendar API v3
 *
 * Read-only methods:
 * - listEvents: List events from a calendar
 * - getEvent: Get a single event by ID
 *
 * @example
 * const adapter = new GoogleCalendarAdapter()
 * const result = await adapter.listEvents({
 *   organizationId: 'org-uuid',
 *   calendarId: 'primary',
 *   maxResults: 10
 * })
 */
export class GoogleCalendarAdapter implements BaseIntegrationAdapter {
  readonly name = 'google-calendar'

  /**
   * Call Google Calendar API method
   * @param method - Method name ('listEvents' | 'getEvent')
   * @param params - Method parameters (must include organizationId)
   * @param credentials - OAuth2 access token
   * @param context - Execution context for logging and tracing
   */
  async call(
    method: string,
    params: unknown,
    credentials: Record<string, unknown>,
    context?: ExecutionContext
  ): Promise<unknown> {
    if (!this.validateCredentials(credentials)) {
      throw new ToolingError('credentials_invalid', 'Invalid Google Calendar credentials', {
        integration: 'google-calendar',
        method
      })
    }
    const calCreds = credentials as unknown as GoogleCalendarCredentials

    const client = await this.createClient(calCreds)

    switch (method) {
      case 'listEvents':
        return this.listEvents(client, params as ListEventsParams, context)
      case 'getEvent':
        return this.getEvent(client, params as GetEventParams, context)
      default:
        throw new ToolingError('method_not_found', `Unknown method: ${method}`, {
          integration: 'google-calendar',
          method
        })
    }
  }

  /**
   * List events from a calendar — callable directly (no method dispatch overhead)
   */
  async listEvents(
    clientOrParams: calendar_v3.Calendar | ListEventsParams,
    paramsOrContext?: ListEventsParams | ExecutionContext,
    maybeContext?: ExecutionContext
  ): Promise<ListEventsResult> {
    // Support both direct call (listEvents(params, context)) and
    // dispatch call (listEvents(client, params, context))
    let client: calendar_v3.Calendar
    let params: ListEventsParams
    let context: ExecutionContext | undefined

    if ('calendarId' in (clientOrParams as object) || 'organizationId' in (clientOrParams as object)) {
      // Called directly: listEvents(params, context?)
      params = clientOrParams as ListEventsParams
      context = paramsOrContext as ExecutionContext | undefined
      if (!params.credentialName) {
        throw new ToolingError('validation_error', 'Missing required field: credentialName', { params })
      }
      const creds = await this.getCredentialsForOrg(params.organizationId, params.credentialName)
      client = await this.createClient(creds)
    } else {
      // Called via dispatch: listEvents(client, params, context?)
      client = clientOrParams as calendar_v3.Calendar
      params = paramsOrContext as ListEventsParams
      context = maybeContext
    }

    return this._listEvents(client, params, context)
  }

  /**
   * Get a single event by ID — callable directly
   */
  async getEvent(
    clientOrParams: calendar_v3.Calendar | GetEventParams,
    paramsOrContext?: GetEventParams | ExecutionContext,
    maybeContext?: ExecutionContext
  ): Promise<CalendarEvent> {
    let client: calendar_v3.Calendar
    let params: GetEventParams
    let context: ExecutionContext | undefined

    if ('eventId' in (clientOrParams as object) || 'organizationId' in (clientOrParams as object)) {
      // Called directly: getEvent(params, context?)
      params = clientOrParams as GetEventParams
      context = paramsOrContext as ExecutionContext | undefined
      if (!params.credentialName) {
        throw new ToolingError('validation_error', 'Missing required field: credentialName', { params })
      }
      const creds = await this.getCredentialsForOrg(params.organizationId, params.credentialName)
      client = await this.createClient(creds)
    } else {
      // Called via dispatch: getEvent(client, params, context?)
      client = clientOrParams as calendar_v3.Calendar
      params = paramsOrContext as GetEventParams
      context = maybeContext
    }

    return this._getEvent(client, params, context)
  }

  /**
   * Create authenticated Google Calendar client (lazy loads @googleapis/calendar SDK)
   * CRITICAL: Use dynamic import to prevent OOM and module-level crashes
   */
  private async createClient(creds: GoogleCalendarCredentials): Promise<calendar_v3.Calendar> {
    const { calendar } = await import('@googleapis/calendar')
    const { OAuth2Client } = await import('google-auth-library')

    // Get client credentials from environment for token refresh
    const { clientId, clientSecret } = getOAuthCredentials('google-calendar')

    const oauth2Client = new OAuth2Client(clientId, clientSecret)
    oauth2Client.setCredentials({
      access_token: creds.accessToken,
      refresh_token: creds.refreshToken
    })

    return calendar({ version: 'v3', auth: oauth2Client })
  }

  /**
   * Retrieve credentials for an organization from the credentials table.
   * Used when the adapter is called directly (not via the dispatch pipeline).
   * The caller passes the credential name explicitly -- names are user-chosen
   * at OAuth-modal time, so they cannot be hardcoded.
   */
  private async getCredentialsForOrg(
    organizationId: string,
    credentialName: string
  ): Promise<GoogleCalendarCredentials> {
    // Dynamic import avoids pulling Supabase into browser bundles
    const { createClient } = await import('@supabase/supabase-js')
    const { decryptCredentialValue } = await import(
      '../../../../../../../auth/multi-tenancy/credentials/server/service'
    )

    const supabaseUrl = process.env.SUPABASE_URL
    const supabaseKey = process.env.SUPABASE_SERVICE_KEY
    if (!supabaseUrl || !supabaseKey) {
      throw new ToolingError('credentials_invalid', 'Missing Supabase environment variables', { organizationId })
    }

    const supabase = createClient(supabaseUrl, supabaseKey)
    const { data, error } = await supabase
      .from('credentials')
      .select('encrypted_value')
      .eq('organization_id', organizationId)
      .eq('name', credentialName)
      .single()

    if (error || !data) {
      throw new ToolingError('credentials_invalid', 'Google Calendar credential not found', {
        organizationId,
        credentialName
      })
    }

    return decryptCredentialValue(data.encrypted_value) as unknown as GoogleCalendarCredentials
  }

  /**
   * Internal list events implementation
   */
  private async _listEvents(
    client: calendar_v3.Calendar,
    params: ListEventsParams,
    context?: ExecutionContext
  ): Promise<ListEventsResult> {
    if (!params.organizationId) {
      throw new ToolingError('validation_error', 'Missing required field: organizationId', { params })
    }

    const calendarId = params.calendarId ?? 'primary'
    const maxResults = Math.min(params.maxResults ?? 10, 250)
    const singleEvents = params.singleEvents ?? true
    const orderBy = params.orderBy ?? 'startTime'
    const timeMin = params.timeMin ?? new Date().toISOString()

    try {
      const response = await client.events.list({
        calendarId,
        timeMin,
        timeMax: params.timeMax,
        maxResults,
        singleEvents,
        orderBy
      })

      const items = (response.data.items ?? []).map(this.transformEvent)

      if (context?.logger) {
        context.logger.info(
          `[GoogleCalendarAdapter] Events listed: organizationId=${context.organizationId} executionId=${context.executionId} calendarId=${calendarId} count=${items.length}`
        )
      }

      return {
        items,
        nextPageToken: response.data.nextPageToken ?? undefined,
        summary: response.data.summary ?? undefined,
        timeZone: response.data.timeZone ?? undefined
      }
    } catch (error: any) {
      throw this.handleApiError(error, calendarId)
    }
  }

  /**
   * Internal get event implementation
   */
  private async _getEvent(
    client: calendar_v3.Calendar,
    params: GetEventParams,
    context?: ExecutionContext
  ): Promise<CalendarEvent> {
    if (!params.organizationId) {
      throw new ToolingError('validation_error', 'Missing required field: organizationId', { params })
    }
    if (!params.eventId) {
      throw new ToolingError('validation_error', 'Missing required field: eventId', { params })
    }

    const calendarId = params.calendarId ?? 'primary'

    try {
      const response = await client.events.get({
        calendarId,
        eventId: params.eventId
      })

      const event = this.transformEvent(response.data)

      if (context?.logger) {
        context.logger.info(
          `[GoogleCalendarAdapter] Event retrieved: organizationId=${context.organizationId} executionId=${context.executionId} calendarId=${calendarId} eventId=${params.eventId}`
        )
      }

      return event
    } catch (error: any) {
      throw this.handleApiError(error, calendarId)
    }
  }

  /**
   * Transform a raw Google Calendar API event into the typed CalendarEvent shape
   */
  private transformEvent(raw: calendar_v3.Schema$Event): CalendarEvent {
    return {
      id: raw.id ?? '',
      summary: raw.summary ?? undefined,
      description: raw.description ?? undefined,
      location: raw.location ?? undefined,
      start: {
        dateTime: raw.start?.dateTime ?? undefined,
        date: raw.start?.date ?? undefined,
        timeZone: raw.start?.timeZone ?? undefined
      },
      end: {
        dateTime: raw.end?.dateTime ?? undefined,
        date: raw.end?.date ?? undefined,
        timeZone: raw.end?.timeZone ?? undefined
      },
      status: (raw.status as CalendarEvent['status']) ?? undefined,
      htmlLink: raw.htmlLink ?? undefined,
      created: raw.created ?? undefined,
      updated: raw.updated ?? undefined,
      organizer: raw.organizer
        ? {
            email: raw.organizer.email ?? undefined,
            displayName: raw.organizer.displayName ?? undefined
          }
        : undefined,
      attendees: raw.attendees?.map((a) => ({
        email: a.email ?? undefined,
        displayName: a.displayName ?? undefined,
        responseStatus: a.responseStatus ?? undefined
      })),
      recurringEventId: raw.recurringEventId ?? undefined
    }
  }

  /**
   * Handle Google Calendar API errors with meaningful messages
   */
  private handleApiError(error: any, calendarId: string): ToolingError {
    const code = error.code || error.status || error.response?.status

    if (code === 404) {
      return new ToolingError('api_error', `Calendar or event not found: ${calendarId}`, {
        calendarId,
        statusCode: 404
      })
    }
    if (code === 403) {
      return new ToolingError('permission_denied', 'Access denied. Check calendar sharing permissions.', {
        calendarId,
        statusCode: 403
      })
    }
    if (code === 401) {
      return new ToolingError('credentials_invalid', 'Authentication expired. Token refresh required.', {
        calendarId,
        statusCode: 401
      })
    }
    if (code === 429) {
      return new ToolingError('rate_limit_exceeded', 'Rate limited. Retry after delay.', {
        calendarId,
        statusCode: 429
      })
    }

    return new ToolingError('api_error', `Google Calendar API error: ${error.message}`, {
      calendarId,
      statusCode: code,
      details: error
    })
  }

  /**
   * Validate credentials structure
   * Required by BaseIntegrationAdapter interface
   */
  validateCredentials(creds: Record<string, unknown>): boolean {
    return typeof creds.accessToken === 'string' && creds.accessToken.length > 0
  }
}
