import { RuntimeError } from '@botpress/sdk'
import { CommonHandlerProps } from '../types'
import { DocusignAuthClient } from './auth'
import { GetAccessTokenResp, UserAccount } from './schemas'
import * as bp from '.botpress'

export const MS_PER_MINUTE = 60000
export const MS_PER_HOUR = MS_PER_MINUTE * 60

export const applyOAuthState = async ({ client, ctx }: CommonHandlerProps, tokenResp: GetAccessTokenResp) => {
  const { accessToken, refreshToken, expiresAt, tokenType } = tokenResp

  const { state } = await client.setState({
    type: 'integration',
    name: 'configuration',
    id: ctx.integrationId,
    payload: {
      oauth: {
        accessToken,
        tokenType,
        refreshToken,
        expiresAt,
      },
    },
  })

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

  return state.payload.oauth
}

const OAUTH_TIMEOUT_BUFFER = MS_PER_MINUTE * 5
export const getOAuthState = async (props: CommonHandlerProps, authClient?: DocusignAuthClient) => {
  const { state } = await props.client.getOrSetState({
    type: 'integration',
    name: 'configuration',
    id: props.ctx.integrationId,
    payload: {
      oauth: null,
    },
  })
  let oauthState = state.payload.oauth

  if (!oauthState) {
    throw new RuntimeError('User authentication has not been completed')
  }

  const { expiresAt, refreshToken } = oauthState
  if (expiresAt - OAUTH_TIMEOUT_BUFFER <= Date.now()) {
    authClient ??= DocusignAuthClient.create(props)
    const tokenResp = await authClient.getAccessTokenWithRefreshToken(refreshToken)
    if (!tokenResp.success) throw tokenResp.error

    oauthState = await applyOAuthState(props, tokenResp.data)
  }

  return oauthState
}

const _findAccount = (accountsList: UserAccount[], explicitAccountId: string | undefined): UserAccount => {
  let account: UserAccount | null = null

  if (explicitAccountId) {
    account = accountsList.find(({ account_id }) => account_id === explicitAccountId) ?? null

    if (!account) {
      throw new RuntimeError('An account with the specified API Account ID does not exist or is not owned by this user')
    }
  } else {
    account = accountsList.find(({ is_default }: UserAccount) => is_default) ?? accountsList[0]!
  }

  return account
}

const ACCOUNT_REFRESH_AFTER = MS_PER_HOUR * 24
export const refreshAccountState = async (props: CommonHandlerProps) => {
  const authClient = DocusignAuthClient.create(props)

  const { accessToken, tokenType } = await getOAuthState(props, authClient)

  const userInfoResp = await authClient.getUserInfo(accessToken, tokenType)
  if (!userInfoResp.success) throw userInfoResp.error

  const { client, ctx } = props
  const { accounts } = userInfoResp.data

  const explicitAccountId = ctx.configuration.accountId
  const account = _findAccount(accounts, explicitAccountId)

  const refreshAt = !explicitAccountId?.trim() ? Date.now() + ACCOUNT_REFRESH_AFTER : null
  const { state } = await client.setState({
    type: 'integration',
    name: 'account',
    id: ctx.integrationId,
    payload: {
      account: {
        id: account.account_id,
        baseUri: account.base_uri,
        refreshAt,
      },
    },
  })

  if (!state.payload.account) {
    throw new RuntimeError('Failed to store account state')
  }

  return state.payload.account
}

export const getAccountState = async (props: CommonHandlerProps) => {
  const { state } = await props.client.getOrSetState({
    type: 'integration',
    name: 'account',
    id: props.ctx.integrationId,
    payload: {
      account: null,
    },
  })

  let hasAccountChanged = false
  let accountState = state.payload.account
  if (!accountState || (accountState.refreshAt && accountState.refreshAt <= Date.now())) {
    const prevAccountState = accountState
    accountState = await refreshAccountState(props)

    if (accountState.id !== prevAccountState?.id) {
      hasAccountChanged = true
    }
  }

  return { account: accountState, hasChanged: hasAccountChanged }
}

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

  const userInfoResp = await authClient.getUserInfo(tokenResp.data.accessToken, tokenResp.data.tokenType)
  if (!userInfoResp.success) throw userInfoResp.error

  await applyOAuthState(props, tokenResp.data)

  await props.client.configureIntegration({
    identifier: userInfoResp.data.sub,
  })
}
