import { handleErrorsDecorator as handleErrors } from '@botpress/common'
import { RuntimeError } from '@botpress/sdk'
import axios, { type AxiosInstance } from 'axios'
import type { CommonHandlerProps, Result } from '../types'
import { type GetOAuthAccessTokenResp, getOAuthAccessTokenRespSchema } from './schemas'
import * as bp from '.botpress'

const AUTH_BASE_URL = 'https://auth.calendly.com' as const

export class CalendlyAuthClient {
  private _axiosClient: AxiosInstance

  public constructor() {
    const { OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET } = bp.secrets

    this._axiosClient = axios.create({
      baseURL: AUTH_BASE_URL,
      headers: {
        Authorization: `Basic ${Buffer.from(`${OAUTH_CLIENT_ID}:${OAUTH_CLIENT_SECRET}`).toString('base64')}`,
      },
    })
  }

  private async _getAccessToken(params: GetAccessTokenParams): Promise<Result<GetOAuthAccessTokenResp>> {
    // The Calendly API docs states that it only accepts
    // `application/x-www-form-urlencoded` for this endpoint.
    const formData = new FormData()
    Object.entries(params).forEach(([key, value]) => formData.append(key, value))
    const resp = await this._axiosClient.post('/oauth/token', formData)

    if (resp.status < 200 || resp.status >= 300) {
      return {
        success: false,
        error: new RuntimeError(
          `Failed to retrieve access token w/${params.grant_type} | Invalid Status '${resp.status}'`
        ),
      }
    }

    const result = getOAuthAccessTokenRespSchema.safeParse(resp.data)
    if (!result.success) {
      return {
        success: false,
        error: new RuntimeError(`Failed to retrieve access token w/${params.grant_type} | Schema Parse Failure`),
      }
    }

    return { success: true, data: result.data }
  }

  @handleErrors('Failed to obtain Calendly OAuth access token from authorization code')
  public async getAccessTokenWithCode(code: string): Promise<Result<GetOAuthAccessTokenResp>> {
    return this._getAccessToken({
      grant_type: 'authorization_code',
      code,
      redirect_uri: `${process.env.BP_WEBHOOK_URL}/oauth`,
    })
  }

  @handleErrors('Failed to refresh Calendly OAuth access token')
  public async getAccessTokenWithRefreshToken(refreshToken: string): Promise<Result<GetOAuthAccessTokenResp>> {
    return this._getAccessToken({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
    })
  }
}

type GetAccessTokenParams =
  | {
      grant_type: 'authorization_code'
      code: string
      redirect_uri: string
    }
  | {
      grant_type: 'refresh_token'
      refresh_token: string
    }

export const applyOAuthState = async ({ client, ctx }: CommonHandlerProps, resp: GetOAuthAccessTokenResp) => {
  const { access_token, refresh_token, created_at, expires_in, owner: userUri } = resp
  const { state } = await client.setState({
    type: 'integration',
    name: 'configuration',
    id: ctx.integrationId,
    payload: {
      oauth: {
        accessToken: access_token,
        refreshToken: refresh_token,
        expiresAt: (created_at + expires_in) * 1000,
      },
    },
  })

  if (!state.payload.oauth) {
    throw new Error('Failed to store OAuth state')
  }

  return { oauth: state.payload.oauth, userUri }
}

export const exchangeAuthCodeForRefreshToken = async (props: bp.HandlerProps, oAuthCode: string): Promise<void> => {
  const authClient = new CalendlyAuthClient()
  const resp = await authClient.getAccessTokenWithCode(oAuthCode)
  if (!resp.success) throw resp.error

  await applyOAuthState(props, resp.data)

  await props.client.configureIntegration({
    identifier: props.ctx.webhookId,
  })
}
