import * as sdk from '@botpress/sdk'
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
import axiosRetry from 'axios-retry'
import type { ZendeskUser, ZendeskTicket, ZendeskWebhook } from './definitions/schemas'
import { summarizeAxiosError } from './misc/axios-utils'
import { ConditionsData, getTriggerTemplate, type TriggerNames } from './triggers'
import type { ZendeskEventType } from './webhookEvents'
import * as bp from '.botpress'

export type TicketRequester =
  | {
      name?: string
      email?: string
    }
  | {
      id: string
    }

export type Trigger = {
  url: string
  id: string
}

const _makeBaseUrl = (organizationDomain: string) => {
  return organizationDomain.startsWith('https') ? organizationDomain : `https://${organizationDomain}.zendesk.com`
}

type AxiosRetryClient = Parameters<typeof axiosRetry>[0]
type ZendeskConfig = {
  type: 'OAuth'
  accessToken: string
  subdomain: string
}

class ZendeskApi {
  private _client: AxiosInstance
  private _logger: bp.Logger
  public constructor(config: ZendeskConfig, logger: bp.Logger) {
    this._logger = logger
    this._client = axios.create({
      baseURL: _makeBaseUrl(config.subdomain),
      headers: {
        Authorization: `Bearer ${config.accessToken}`,
      },
    })

    axiosRetry(this._client as AxiosRetryClient, {
      retries: 3,
      retryDelay: axiosRetry.exponentialDelay,
      retryCondition: (error) => {
        const rateLimitReached = error.response?.status === 429
        return axiosRetry.isNetworkOrIdempotentRequestError(error) || rateLimitReached
      },
    })
  }

  public async findCustomers(query: string): Promise<ZendeskUser[]> {
    const { data } = await this._client
      .get<{ users: ZendeskUser[] }>(`/api/v2/users/search.json?query=${query}`)
      .catch(this._summarizeAxiosError)
    return data.users
  }

  public async getTicket(ticketId: string) {
    const { data } = await this._client
      .get<{ ticket: ZendeskTicket }>(`/api/v2/tickets/${ticketId}.json`)
      .catch(this._summarizeAxiosError)
    return data.ticket
  }

  public async createTicket(
    subject: string,
    comment: string,
    requester: TicketRequester,
    extraFields: Partial<ZendeskTicket> = {}
  ): Promise<ZendeskTicket> {
    const requesterPayload = 'id' in requester ? { requester_id: requester.id } : { requester }

    const { data } = await this._client
      .post<{ ticket: ZendeskTicket }>('/api/v2/tickets.json', {
        ticket: {
          subject,
          comment: { body: comment },
          ...requesterPayload,
          ...extraFields,
        },
      })
      .catch(this._summarizeAxiosError)

    return data.ticket
  }

  public async subscribeWebhook(webhookUrl: string): Promise<string> {
    const { data } = await this._client
      .post<{ webhook: { id: string } }>('/api/v2/webhooks', {
        webhook: {
          name: 'bpc_integration_webhook',
          status: 'active',
          endpoint: webhookUrl,
          http_method: 'POST',
          request_format: 'json',
          subscriptions: ['conditional_ticket_events'],
        },
      })
      .catch(this._summarizeAxiosError)

    return data.webhook?.id
  }

  public async createTrigger(name: TriggerNames, subscriptionId: string, conditions: ConditionsData): Promise<string> {
    const { data } = await this._client
      .post<{ trigger: Trigger }>('/api/v2/triggers.json', {
        trigger: {
          actions: [
            {
              field: 'notification_webhook',
              value: [subscriptionId, JSON.stringify(getTriggerTemplate(name), null, 2)],
            },
          ],
          conditions,
          title: `bpc_${name}`,
        },
      })
      .catch(this._summarizeAxiosError)

    return `${data.trigger.id}`
  }

  public async deleteTrigger(triggerId: string): Promise<void> {
    await this._client.delete(`/api/v2/triggers/${triggerId}.json`).catch(this._summarizeAxiosError)
  }

  public async unsubscribeWebhook(subscriptionId: string): Promise<void> {
    await this._client.delete(`/api/v2/webhooks/${subscriptionId}`).catch(this._summarizeAxiosError)
  }

  public async createPlaintextComment(ticketId: string, authorId: string, content: string) {
    const response = await this.updateTicket(ticketId, {
      comment: {
        body: content,
        author_id: authorId,
      },
    }).catch(this._summarizeAxiosError)
    const zendeskCommentId = this._extractCommentId(response)
    return { zendeskCommentId }
  }

  private _extractCommentId(response: Awaited<ReturnType<typeof this.updateTicket>>) {
    const commentId = response.audit.events.find((event) => event.type === 'Comment' && 'id' in event)?.id

    if (!commentId) {
      throw new sdk.RuntimeError('Failed to retrieve comment ID')
    }

    return commentId
  }

  public async updateTicket(ticketId: string | number, updateFields: Partial<ZendeskTicket>) {
    const response = await this._client
      .put<{
        ticket: ZendeskTicket
        audit: {
          events: (
            | {
                id: number
                type: 'Comment'
              }
            | { type: string }
          )[]
        }
      }>(`/api/v2/tickets/${ticketId}.json`, {
        ticket: updateFields,
      })
      .catch(this._summarizeAxiosError)
    return response.data
  }

  public async getAgents(online?: boolean): Promise<ZendeskUser[]> {
    const { data } = await this._client
      .get<{ users: ZendeskUser[] }>('/api/v2/users.json?role[]=agent&role[]=admin')
      .catch(this._summarizeAxiosError)
    return online ? data.users.filter((user) => user.user_fields?.availability === 'online') : data.users
  }

  public async createOrUpdateUser(fields: Partial<ZendeskUser>): Promise<ZendeskUser> {
    const { data } = await this._client
      .post<{ user: ZendeskUser }>('/api/v2/users/create_or_update', {
        user: fields,
      })
      .catch(this._summarizeAxiosError)
    return data.user
  }

  public async updateUser(userId: number | string, fields: Partial<ZendeskUser>): Promise<ZendeskUser> {
    const { data } = await this._client
      .put<{ user: ZendeskUser }>(`/api/v2/users/${userId}.json`, {
        user: fields,
      })
      .catch(this._summarizeAxiosError)
    return data.user
  }

  public async getUser(userId: number | string): Promise<ZendeskUser> {
    const { data } = await this._client
      .get<{ user: ZendeskUser }>(`/api/v2/users/${userId}.json`)
      .catch(this._summarizeAxiosError)
    return data.user
  }

  public async createArticleWebhook(webhookUrl: string, webhookId: string): Promise<void> {
    const subscriptions: ZendeskEventType[] = ['zen:event-type:article.published', 'zen:event-type:article.unpublished']

    await this._client
      .post('/api/v2/webhooks', {
        webhook: {
          endpoint: `${webhookUrl}/article-event`,
          http_method: 'POST',
          name: `bpc_article_event_${webhookId}`,
          request_format: 'json',
          status: 'active',
          subscriptions,
        },
      })
      .catch(this._summarizeAxiosError)
  }

  public async deleteWebhook(webhookId: string): Promise<void> {
    await this._client.delete(`/api/v2/webhooks/${webhookId}`).catch(this._summarizeAxiosError)
  }

  public async findWebhooks(params?: Record<string, string>): Promise<ZendeskWebhook[]> {
    const { data } = await this._client.get('/api/v2/webhooks', { params }).catch(this._summarizeAxiosError)

    return data.webhooks
  }

  public async makeRequest(requestConfig: AxiosRequestConfig) {
    const { data, headers, status } = await this._client.request(requestConfig).catch(this._summarizeAxiosError)

    return {
      data,
      headers: headers as Record<string, string>,
      status,
    }
  }

  private _summarizeAxiosError = (thrown: unknown) => summarizeAxiosError(thrown, this._logger)
}

export type ZendeskClient = InstanceType<typeof ZendeskApi>

export const getZendeskClient = async (client: bp.Client, ctx: bp.Context, logger: bp.Logger): Promise<ZendeskApi> => {
  const { accessToken, subdomain } = await client
    .getState({ type: 'integration', name: 'credentials', id: ctx.integrationId })
    .then((result) => result.state.payload)
  if (accessToken === undefined) {
    throw new sdk.RuntimeError('Failed to get the OAuth accessToken')
  }
  if (subdomain === undefined) {
    throw new sdk.RuntimeError('Failed to get the subdomain')
  }

  return new ZendeskApi({ type: 'OAuth', accessToken, subdomain }, logger)
}
