import { createAction, option } from '@typebot.io/forge'
import { isDefined, isEmpty } from '@typebot.io/lib'
import { auth } from '../auth'
import { ClientOptions, OpenAI } from 'openai'
import { baseOptions } from '../baseOptions'

export const askAssistant = createAction({
  auth,
  baseOptions,
  name: 'Ask Assistant',
  options: option.object({
    assistantId: option.string.layout({
      label: 'Assistant ID',
      placeholder: 'Select an assistant',
      moreInfoTooltip: 'The OpenAI assistant you want to ask question to.',
      fetcher: 'fetchAssistants',
    }),
    threadId: option.string.layout({
      label: 'Thread ID',
      moreInfoTooltip:
        'Used to remember the conversation with the user. If empty, a new thread is created.',
    }),
    message: option.string.layout({
      label: 'Message',
      inputType: 'textarea',
    }),
    responseMapping: option
      .saveResponseArray(['Message', 'Thread ID'] as const)
      .layout({
        accordion: 'Save response',
      }),
  }),
  fetchers: [
    {
      id: 'fetchAssistants',
      fetch: async ({ options, credentials }) => {
        const config = {
          apiKey: credentials.apiKey,
          baseURL: options.baseUrl,
          defaultHeaders: {
            'api-key': credentials.apiKey,
          },
          defaultQuery: options.apiVersion
            ? {
                'api-version': options.apiVersion,
              }
            : undefined,
        } satisfies ClientOptions

        const openai = new OpenAI(config)

        const response = await openai.beta.assistants.list()

        return response.data
          .map((assistant) =>
            assistant.name
              ? {
                  label: assistant.name,
                  value: assistant.id,
                }
              : undefined
          )
          .filter(isDefined)
      },
      dependencies: ['baseUrl', 'apiVersion'],
    },
  ],
  getSetVariableIds: ({ responseMapping }) =>
    responseMapping?.map((r) => r.variableId).filter(isDefined) ?? [],
  run: {
    server: async ({
      credentials: { apiKey },
      options: {
        baseUrl,
        apiVersion,
        assistantId,
        message,
        responseMapping,
        threadId,
      },
      variables,
      logs,
    }) => {
      if (isEmpty(assistantId)) {
        logs.add('Assistant ID is empty')
        return
      }
      if (isEmpty(message)) {
        logs.add('Message is empty')
        return
      }
      const config = {
        apiKey,
        baseURL: baseUrl,
        defaultHeaders: {
          'api-key': apiKey,
        },
        defaultQuery: apiVersion
          ? {
              'api-version': apiVersion,
            }
          : undefined,
      } satisfies ClientOptions

      const openai = new OpenAI(config)

      // Create a thread if needed
      const currentThreadId = isEmpty(threadId)
        ? (await openai.beta.threads.create({})).id
        : threadId

      // Add a message to the thread
      const createdMessage = await openai.beta.threads.messages.create(
        currentThreadId,
        {
          role: 'user',
          content: message,
        }
      )

      const run = await openai.beta.threads.runs.create(currentThreadId, {
        assistant_id: assistantId,
      })

      async function waitForRun(run: OpenAI.Beta.Threads.Runs.Run) {
        // Poll for status change
        while (run.status === 'queued' || run.status === 'in_progress') {
          await new Promise((resolve) => setTimeout(resolve, 500))

          run = await openai.beta.threads.runs.retrieve(currentThreadId, run.id)
        }

        // Check the run status
        if (
          run.status === 'cancelled' ||
          run.status === 'cancelling' ||
          run.status === 'failed' ||
          run.status === 'expired'
        ) {
          throw new Error(run.status)
        }
      }

      await waitForRun(run)

      const responseMessages = (
        await openai.beta.threads.messages.list(currentThreadId, {
          after: createdMessage.id,
          order: 'asc',
        })
      ).data

      responseMapping?.forEach((mapping) => {
        if (!mapping.variableId) return
        if (!mapping.item || mapping.item === 'Message') {
          let message = ''
          const messageContents = responseMessages[0].content
          for (const content of messageContents) {
            switch (content.type) {
              case 'text':
                message += (message !== '' ? '\n\n' : '') + content.text.value
                break
            }
          }
          variables.set(mapping.variableId, message)
        }
        if (mapping.item === 'Thread ID')
          variables.set(mapping.variableId, currentThreadId)
      })
    },
  },
})
