import {
  ApolloLink,
  ApolloClient,
  from,
  HttpLink,
  InMemoryCache,
  Observable,
} from '@apollo/client'
import { ErrorLink } from '@apollo/client/link/error'
import { CombinedGraphQLErrors } from '@apollo/client/errors'

import {
  clearAuthStorage,
  getStoredAccessToken,
  getStoredRefreshToken,
  saveAuthTokens,
} from './authStorage'

const graphqlApiUrl = import.meta.env.VITE_GRAPHQL_API_URL ?? '/api/graphql'

const httpLink = new HttpLink({
  uri: graphqlApiUrl,
  credentials: 'include',
})

const authLink = new ApolloLink((operation, forward) => {
  const accessToken = getStoredAccessToken()

  operation.setContext(({ headers = {} }) => ({
    headers: accessToken
      ? {
          ...headers,
          Authorization: `Bearer ${accessToken}`,
        }
      : headers,
  }))

  return forward(operation)
})

let isRefreshing = false
let pendingRequests: Array<() => void> = []

function resolvePendingRequests() {
  pendingRequests.forEach((callback) => callback())
  pendingRequests = []
}

function isUnauthorizedError(error: unknown) {
  if (!CombinedGraphQLErrors.is(error)) {
    return false
  }

  return error.errors.some((graphQLError) => {
    const code = String(graphQLError.extensions?.code ?? '').toLowerCase()
    const classification = String(
      graphQLError.extensions?.classification ?? '',
    ).toLowerCase()
    const message = String(graphQLError.message ?? '').toLowerCase()

    return (
      code === 'unauthorized' ||
      code === 'unauthenticated' ||
      classification === 'unauthorized' ||
      message.includes('unauthorized') ||
      message.includes('unauthenticated') ||
      message.includes('required any role') ||
      message.includes('required role') ||
      message.includes('access denied') ||
      message.includes('forbidden')
    )
  })
}

async function refreshSession() {
  const refreshToken = getStoredRefreshToken()

  if (!refreshToken) {
    throw new Error('Refresh token is missing')
  }

  const response = await fetch(graphqlApiUrl, {
    method: 'POST',
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: `
        mutation RefreshSession($payload: RefreshSessionPayload!) {
          refreshSession(payload: $payload) {
            accessToken
            refreshToken
          }
        }
      `,
      variables: {
        payload: {
          refreshToken,
        },
      },
    }),
  })

  const result = await response.json()

  if (result.errors?.length || !result.data?.refreshSession) {
    throw new Error('Refresh session failed')
  }

  saveAuthTokens(
    result.data.refreshSession.accessToken,
    result.data.refreshSession.refreshToken,
  )

  return result.data.refreshSession
}

const errorLink = new ErrorLink(({ error, operation, forward }) => {
  const isUnauthorized = isUnauthorizedError(error)

  if (!isUnauthorized) {
    return
  }

  if (operation.operationName === 'RefreshSession') {
    window.dispatchEvent(new Event('auth:logout'))
    return
  }

  const alreadyRetried = operation.getContext().alreadyRetriedAfterRefresh

  if (alreadyRetried) {
    window.dispatchEvent(new Event('auth:logout'))
    return
  }

  operation.setContext({
    ...operation.getContext(),
    alreadyRetriedAfterRefresh: true,
  })

  return new Observable((observer) => {
    const retryRequest = () => {
      forward(operation).subscribe({
        next: observer.next.bind(observer),
        error: observer.error.bind(observer),
        complete: observer.complete.bind(observer),
      })
    }

    if (isRefreshing) {
      pendingRequests.push(retryRequest)
      return
    }

    isRefreshing = true

    refreshSession()
      .then(() => {
        isRefreshing = false
        resolvePendingRequests()
        retryRequest()
      })
      .catch((refreshError) => {
        isRefreshing = false
        pendingRequests = []

        clearAuthStorage()
        window.dispatchEvent(new Event('auth:logout'))

        observer.error(refreshError)
      })
  })
})

export const apolloClient = new ApolloClient({
  link: from([errorLink, authLink, httpLink]),
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          getPhonesPage: {
            keyArgs: ['key'],
          },
        },
      },
    },
  }),
})
