import type * as ClientTypes from '../gen/client'
import * as sdk from '../gen/client/sdk.gen'
import { client as baseClient } from '../gen/client/client.gen'
import { EventSource } from 'eventsource'
import type { Signal } from './types/signals'
import { throwErrorInterceptor } from './interceptor'
import { createEventEmitter, EventEmitter } from './emitter'

interface EventSourceLike {
  close(): void
  addEventListener(event: string, handler: (event: any) => void): void
}

type ConversationIdParam = {
  conversationId: string
}

type NextTokenParam = {
  nextToken?: string
}

type UserIdParam = {
  userId: string
}

type MessageIdParam = {
  messageId: string
}

type EventIdParam = {
  eventId: string
}

const extractData = <T>(res: { data: T }): T => res.data

type Event =
  | Signal
  | {
      type: 'error'
      data: any
    }
  | {
      type: 'open'
      data: any
    }

type EventMap = {
  [E in Event as E['type']]: E['data']
}

const MAX_RETRIES = 10
const RETRY_INTERVAL = 3000 // ms
const TIMEOUT = '60000' // ms

type SubscribeFn<T extends Record<string, unknown>> = EventEmitter<T>['on']

export type Subscribable<T extends Record<string, unknown>> = SubscribeFn<T> & {
  on: SubscribeFn<T>
  disconnect: () => void
  cleanup: () => void
}

const createSubscribable = <T extends Record<string, unknown>>(
  eventEmitter: EventEmitter<T>,
  disconnect: () => void
): Subscribable<T> => {
  const fn = (...args: Parameters<SubscribeFn<T>>) => eventEmitter.on(...args)
  return new Proxy(fn, {
    get: (_target, prop) => {
      if (prop === 'on') return eventEmitter.on
      if (prop === 'disconnect') return disconnect
      if (prop === 'cleanup') return eventEmitter.cleanup
      return Reflect.get(_target, prop)
    },
  }) as Subscribable<T>
}

export type Client = {
  createConversation: () => Promise<ClientTypes.CreateConversationResponse>
  getConversation: (params: ConversationIdParam) => Promise<ClientTypes.GetConversationResponse>
  deleteConversation: (params: ConversationIdParam) => Promise<ClientTypes.DeleteConversationResponse>
  listConversations: (params: NextTokenParam) => Promise<ClientTypes.ListConversationsResponse>
  listConversationMessages: (
    params: ConversationIdParam & NextTokenParam
  ) => Promise<ClientTypes.ListConversationMessagesResponse>
  addParticipant: (params: ConversationIdParam & UserIdParam) => Promise<ClientTypes.AddParticipantResponse>
  removeParticipant: (params: ConversationIdParam & UserIdParam) => Promise<ClientTypes.RemoveParticipantResponse>
  getParticipant: (params: ConversationIdParam & UserIdParam) => Promise<ClientTypes.GetParticipantResponse>
  listParticipants: (params: ConversationIdParam & NextTokenParam) => Promise<ClientTypes.ListParticipantsResponse>
  createMessage: (params: ClientTypes.CreateMessageBody) => Promise<ClientTypes.CreateMessageResponse>
  updateMessage: (params: MessageIdParam & ClientTypes.UpdateMessageBody) => Promise<ClientTypes.UpdateMessageResponse>
  getMessage: (params: MessageIdParam) => Promise<ClientTypes.GetMessageResponse>
  deleteMessage: (params: MessageIdParam) => Promise<ClientTypes.DeleteMessageResponse>
  createFile: (params: ClientTypes.CreateFileBody) => Promise<ClientTypes.CreateFileResponse>
  getUser: () => Promise<ClientTypes.GetUserResponse>
  updateUser: (params: ClientTypes.UpdateUserBody) => Promise<ClientTypes.UpdateUserResponse>
  deleteUser: (params: UserIdParam) => Promise<ClientTypes.DeleteUserResponse>
  createEvent: (params: ClientTypes.CreateEventBody) => Promise<ClientTypes.CreateEventResponse>
  getEvent: (params: EventIdParam) => Promise<ClientTypes.GetEventResponse>
  listenConversation: (params: ConversationIdParam) => Subscribable<EventMap>
  addMessageFeedback: (
    params: MessageIdParam & ClientTypes.AddMessageFeedbackBody
  ) => Promise<ClientTypes.AddMessageFeedbackResponse>
  removeMessageFeedback: (params: MessageIdParam) => Promise<ClientTypes.RemoveMessageFeedbackResponse>
  generateUserKey: (
    params: ClientTypes.GenerateUserKeyBody & { adminSecret: string }
  ) => Promise<ClientTypes.GenerateUserKeyResponse>
}

const baseConfigs = {
  throwOnError: true,
} as const satisfies sdk.Options

export const createUser = async ({
  clientId,
  apiUrl,
  adminSecret,
  customHeaders,
  ...body
}: {
  clientId: string
  apiUrl?: string
  adminSecret?: string
  customHeaders?: Record<string, string>
} & ClientTypes.CreateUserBody) => {
  const baseUrl = `${apiUrl || baseClient.getConfig().baseUrl}/${clientId}`

  // @ts-expect-error - this is a private property
  if (baseClient.interceptors.response._fns.length === 0) {
    baseClient.interceptors.response.use(throwErrorInterceptor)
  }

  const configs = {
    ...baseConfigs,
    baseUrl,
  }

  const headers = {
    ...(adminSecret ? { 'x-admin-secret': adminSecret } : {}),
    ...customHeaders,
  }

  return sdk
    .createUser({
      ...configs,
      headers: Object.keys(headers).length > 0 ? headers : undefined,
      body,
    })
    .then(extractData)
}

export const initialize = ({
  conversationId,
  userToken,
  clientId,
  apiUrl,
  adminSecret,
}: {
  conversationId?: string
  userToken?: string
  apiUrl?: string
  clientId: string
  adminSecret?: string
}) => {
  let retryCount = 0
  let retryTimeout: ReturnType<typeof setTimeout> | null = null
  let eventSource: EventSourceLike | null = null
  const eventEmitter = createEventEmitter<EventMap>()
  let resolvedConversationId = conversationId
  let resolvedUserToken = userToken

  const disconnect = () => {
    if (retryTimeout) {
      clearTimeout(retryTimeout)
      retryTimeout = null
    }
    if (eventSource) {
      eventSource.close()
      eventSource = null
    }
  }

  const connect = () => {
    if (eventSource) {
      eventSource.close()
    }

    const baseUrl = `${apiUrl || baseClient.getConfig().baseUrl}/${clientId}`
    const url = `${baseUrl}/initialize${resolvedConversationId ? `?conversationId=${resolvedConversationId}` : ''}`

    const headers = {
      ...(adminSecret ? { 'x-admin-secret': adminSecret } : {}),
      ...(resolvedUserToken ? { 'x-user-key': resolvedUserToken } : {}),
    }

    eventSource = createNewEventSource(url, headers)

    eventSource.addEventListener('open', () => eventEmitter.emit('open'))
    eventSource.addEventListener('error', (err) => {
      if (retryCount < MAX_RETRIES) {
        retryCount++
        retryTimeout = setTimeout(() => connect(), RETRY_INTERVAL)
      } else {
        eventEmitter.emit('error', err)
      }
    })
    eventSource.addEventListener('message', (ev) => {
      const event = safeJsonParse(ev.data) as Signal

      // Save the Conversation ID and User Token from the init event so that internal
      // retries reconnect as the same user to the same conversation.
      if (event.type === 'init') {
        resolvedConversationId = event.data.conversation.id ?? resolvedConversationId
        resolvedUserToken = event.data.user.userKey ?? resolvedUserToken
      }

      eventEmitter.emit(event.type, event.data)
    })
  }

  connect()
  return createSubscribable(eventEmitter, disconnect)
}

export const generateUserKey = async ({
  adminSecret,
  expiresAt,
  userId,
  apiUrl,
  clientId,
}: {
  adminSecret: string
  expiresAt: number
  userId: string
  apiUrl?: string
  clientId: string
}) => {
  const headers = { 'x-admin-secret': adminSecret }

  const baseUrl = `${apiUrl || baseClient.getConfig().baseUrl}/${clientId}`
  const configs = {
    ...baseConfigs,
    baseUrl,
  }
  return sdk.generateUserKey({ ...configs, headers, body: { id: userId, expiresAt } }).then(extractData)
}

export const sendPerfMetrics = async (
  clientId: string,
  body: ClientTypes.SendPerfMetricsBody,
  apiUrl?: string,
  customHeaders?: Record<string, string>
) => {
  const baseUrl = `${apiUrl || baseClient.getConfig().baseUrl}/${clientId}`
  const configs = {
    ...baseConfigs,
    baseUrl,
  }
  return sdk.sendPerfMetrics({ ...configs, body, headers: customHeaders }).then(extractData)
}

export const createClient = ({
  userKey,
  clientId,
  apiUrl,
  customHeaders,
  advancedOptions,
}: {
  userKey: string
  clientId: string
  apiUrl?: string
  customHeaders?: Record<string, string>
  advancedOptions?: {
    eventSource?: (params: { url: string; headers: Record<string, string> }) => EventSourceLike
  }
}): Client => {
  const headers = {
    'x-user-key': userKey,
    ...customHeaders,
  }

  const baseUrl = `${apiUrl || baseClient.getConfig().baseUrl}/${clientId}`
  const configs = {
    ...baseConfigs,
    baseUrl,
  }

  // @ts-expect-error - this is a private property
  if (baseClient.interceptors.response._fns.length === 0) {
    baseClient.interceptors.response.use(throwErrorInterceptor)
  }

  const client: Client = {
    createConversation: () => sdk.createConversation({ ...configs, headers, body: {} }).then(extractData),
    getConversation: ({ conversationId }) =>
      sdk.getConversation({ ...configs, headers, path: { id: conversationId } }).then(extractData),
    deleteConversation: ({ conversationId }) =>
      sdk.deleteConversation({ ...configs, headers, path: { id: conversationId } }).then(extractData),
    listConversations: ({ nextToken }) =>
      sdk.listConversations({ ...configs, headers, query: { nextToken } }).then(extractData),
    listConversationMessages: ({ conversationId, nextToken }) =>
      sdk
        .listConversationMessages({
          ...configs,
          headers,
          path: { id: conversationId },
          query: { nextToken },
        })
        .then(extractData),
    addParticipant: ({ conversationId, userId }) =>
      sdk.addParticipant({ ...configs, headers, path: { id: conversationId }, body: { userId } }).then(extractData),
    removeParticipant: ({ conversationId, userId }) =>
      sdk.removeParticipant({ ...configs, headers, path: { id: conversationId, userId } }).then(extractData),
    getParticipant: ({ conversationId, userId }) =>
      sdk.getParticipant({ ...configs, headers, path: { id: conversationId, userId } }).then(extractData),
    listParticipants: ({ conversationId, nextToken }) =>
      sdk
        .listParticipants({ ...configs, headers, path: { id: conversationId }, query: { nextToken } })
        .then(extractData),
    createMessage: (body) => sdk.createMessage({ ...configs, headers, body }).then(extractData),
    updateMessage: ({ messageId, ...body }) =>
      sdk.updateMessage({ ...configs, headers, path: { id: messageId }, body }).then(extractData),
    getMessage: ({ messageId }) => sdk.getMessage({ ...configs, headers, path: { id: messageId } }).then(extractData),
    deleteMessage: ({ messageId }) =>
      sdk.deleteMessage({ ...configs, headers, path: { id: messageId } }).then(extractData),
    createFile: (body) => sdk.createFile({ ...configs, headers, body }).then(extractData),
    getUser: () => sdk.getUser({ ...configs, headers }).then(extractData),
    updateUser: (body) => sdk.updateUser({ ...configs, headers, body }).then(extractData),
    deleteUser: () => sdk.deleteUser({ ...configs, headers }).then(extractData),
    createEvent: (body) => sdk.createEvent({ ...configs, headers, body }).then(extractData),
    getEvent: ({ eventId }) => sdk.getEvent({ ...configs, headers, path: { id: eventId } }).then(extractData),
    addMessageFeedback: ({ messageId, ...body }) =>
      sdk.addMessageFeedback({ ...configs, headers, path: { id: messageId }, body }).then(extractData),
    removeMessageFeedback: ({ messageId }) =>
      sdk.removeMessageFeedback({ ...configs, headers, path: { id: messageId } }).then(extractData),
    generateUserKey: ({ adminSecret, ...body }) =>
      sdk.generateUserKey({ ...configs, headers: { 'x-admin-secret': adminSecret }, body }).then(extractData),
    listenConversation: ({ conversationId }) => {
      let retryCount = 0
      let retryTimeout: ReturnType<typeof setTimeout> | null = null
      let eventSource: EventSourceLike | null = null
      const eventEmitter = createEventEmitter<EventMap>()

      const disconnect = () => {
        if (retryTimeout) {
          clearTimeout(retryTimeout)
          retryTimeout = null
        }
        if (eventSource) {
          eventSource.close()
          eventSource = null
        }
      }

      const connect = () => {
        if (eventSource) {
          eventSource.close()
        }

        const url = `${baseUrl}/conversations/${conversationId}/listen`

        const providedEventSource = advancedOptions?.eventSource
        eventSource = providedEventSource ? providedEventSource({ url, headers }) : createNewEventSource(url, headers)

        eventSource.addEventListener('open', () => eventEmitter.emit('open'))
        eventSource.addEventListener('error', (err) => {
          if (retryCount < MAX_RETRIES) {
            retryCount++
            retryTimeout = setTimeout(() => connect(), RETRY_INTERVAL)
          } else {
            eventEmitter.emit('error', err)
          }
        })
        eventSource.addEventListener('message', (ev) => {
          const event = safeJsonParse(ev.data) as Signal
          eventEmitter.emit(event.type, event.data)
        })
      }
      connect()
      return createSubscribable(eventEmitter, disconnect)
    },
  }

  return client
}

const safeJsonParse = (data: any) => {
  try {
    return JSON.parse(data)
  } catch {
    return data
  }
}

const createNewEventSource = (url: string, headers: Record<string, string>): EventSourceLike => {
  return new EventSource(url, {
    fetch: (input, init) =>
      fetch(input, {
        ...init,
        headers: {
          ...init?.headers,
          ...headers,
          timeout: TIMEOUT,
        },
      }),
  })
}
