import type { Viewer } from '@authentication/auth.guard'
import type { ViewerResult } from '@authentication/authentication.types'
import { useQuery } from '@tanstack/react-query'

import Keycloak from 'keycloak-js'
import * as React from 'react'
import { createContext, useCallback, useEffect, useMemo, useState } from 'react'

import { APP_CONFIG } from '@lib/config'
import { createUseContext } from '@lib/react'
import { useTRPC } from '@lib/trpc'
import { Result } from '@postxl/utils'

type FrontendViewer = Viewer

export const authErrorTypes = ['errorInit', 'errorLoadUser', 'errorTokenRefresh'] as const
export type AuthErrorType = (typeof authErrorTypes)[number]
export type AuthState = 'inProgress' | 'loggedIn' | 'loggedOut'

export type AuthContext = {
  loginState: AuthState | AuthErrorType
  login: () => Promise<void>
  logout: (options?: { redirectUri?: string }) => Promise<void>
  viewerData: FrontendViewer | null
}

const AuthContext = createContext<AuthContext | null>(null)

let _keycloakInitializationStarted = false
let _authToken: string | undefined

export function setAuthToken(token?: string) {
  _authToken = token
}

export function getAuthToken() {
  return _authToken
}

export function getAuthHeaders(options?: RequestInit) {
  const token = getAuthToken()
  const headers = new Headers(options?.headers)

  if (token) {
    headers.set('Authorization', `Bearer ${token}`)
  }
  return headers
}

const keycloak = new Keycloak({
  url: APP_CONFIG.keycloakURL,
  realm: APP_CONFIG.keycloakRealm,
  clientId: APP_CONFIG.keycloakClientId,
})

keycloak.onAuthLogout = () => {
  console.log('KEYCLOAK AUTH LOGOUT')
  setAuthToken()
}

keycloak.onTokenExpired = () => {
  console.log('KEYCLOAK TOKEN EXPIRED')
}
keycloak.onAuthSuccess = () => {
  console.log('KEYCLOAK AUTH SUCCESS')
}
keycloak.onAuthRefreshSuccess = () => {
  console.log('KEYCLOAK AUTH REFRESH SUCCESS')
}
keycloak.onAuthError = () => {
  console.log('KEYCLOAK AUTH ERROR')
}

export function AuthProvider({ children }: Readonly<{ children: React.ReactNode }>) {
  const [viewerData, setViewerData] = React.useState<FrontendViewer | null>(null)
  const [loginState, setLoginState] = useState<AuthState | AuthErrorType>('inProgress')

  const trpc = useTRPC()

  const {
    data: userData,
    isSuccess: isUserSuccessfullyLoaded,
    isError: isUserDataError,
  } = useQuery(
    trpc.viewer.viewer.queryOptions(
      { pageUrl: location.href },
      // load user data when keycloak init complete and user is authenticated
      { enabled: loginState === 'loggedIn' },
    ),
  )

  const logout = useCallback(async (options?: { redirectUri?: string }) => {
    setAuthToken()
    await keycloak.logout(options)
  }, [])

  const login = useCallback(async () => {
    await keycloak.login()
  }, [])

  /**
   * #1 - initialize the keycloak adapter on a page-load (only once)
   */
  useEffect(() => {
    const initKeycloak = async () => {
      try {
        if (!_keycloakInitializationStarted) {
          _keycloakInitializationStarted = true

          const authenticated = await keycloak.init({
            onLoad: 'check-sso',
            silentCheckSsoRedirectUri: `${location.origin}/silent-check-sso.html`,
            enableLogging: true,
            // SSO-check via iFrame doesn't work for Brave browser, if
            // the domain of the auth keycloak url differs from the app domain
            // see: https://www.keycloak.org/securing-apps/javascript-adapter#_session_status_iframe
            // This is no problem in the USP setup as Keycloak runs on the same domain,
            // but should be addressed in PXL
            checkLoginIframe: APP_CONFIG.checkLoginIframe,
          })

          console.log(
            'Keycloak initialized COMPLETE, user authenticated:',
            authenticated,
            'token:',
            keycloak.token ? 'present' : 'missing',
          )

          if (authenticated && keycloak.token) {
            setAuthToken(keycloak.token)
            setLoginState('loggedIn')
          } else {
            // if keycloak initialized and the user is not authenticated,
            // the user will be redirected to the keycloak login page (see "/pages/Login")
            setLoginState('loggedOut')
          }
        }
      } catch (error) {
        console.error('Failed to initialize adapter:', error)
        setLoginState('errorInit')
      }
    }

    if (APP_CONFIG.AUTH) {
      void initKeycloak()
    } else {
      setLoginState('loggedIn')
    }
  }, [])

  /**
   * #2 - user data loading
   * when user authenticated by keycloak, the user data is loaded from the backend
   */
  useEffect(() => {
    if (loginState === 'loggedIn' && isUserDataError) {
      setLoginState('errorLoadUser')
      return
    }

    if (userData && isUserSuccessfullyLoaded) {
      const userDataResult: ViewerResult = Result.fromObject(userData as any) as ViewerResult
      if (userDataResult.isOk()) {
        const value = userDataResult.unwrap()
        console.log('User data loaded successfully')
        setViewerData(value)
      } else if (userDataResult.isErr()) {
        console.log('User data loaded with an error => logout and redirect to auth-error', {
          error: userDataResult.unwrapErr(),
        })
        setLoginState('errorLoadUser')
      }
    }
  }, [isUserDataError, isUserSuccessfullyLoaded, loginState, userData])

  /**
   * #3 - token refreshing
   * when the user is authenticated, the token is refreshed every 5 seconds
   */
  useEffect(() => {
    if (loginState !== 'loggedIn' || !APP_CONFIG.AUTH) {
      return
    }

    const refreshIntervalId = setInterval(async () => {
      try {
        // refresh the token if it expires within 60 seconds
        const refreshed = await keycloak.updateToken(60)

        if (refreshed) {
          console.log('Token was successfully refreshed')
          // Update your application state if needed
          setAuthToken(keycloak.token)
        }
      } catch (error) {
        console.error('Failed to refresh token', error)
        setLoginState('errorTokenRefresh')
      }
    }, 5000)

    return () => clearInterval(refreshIntervalId)
  }, [loginState])

  const memoizedValues = useMemo(
    () => ({
      loginState,
      login,
      logout,
      viewerData,
    }),
    [loginState, login, logout, viewerData],
  )

  return <AuthContext.Provider value={memoizedValues}>{children}</AuthContext.Provider>
}

export const useAuth = createUseContext(AuthContext, 'AuthContext')
