import { queryClient, queryKeys } from '@lenserfight/data/cache'
import {
  authService,
  lenserService,
  partnerProvisioningRepository,
} from '@lenserfight/data/repositories'
import { AuthState, UserMetadata } from '@lenserfight/types'
import { buildAuthReturnUrl } from '@lenserfight/utils/dom'
import { AUTH_BASE_URL, getEnvMetadata } from '@lenserfight/utils/env'
import { storage } from '@lenserfight/utils/storage'
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'

interface RegisterOptions {
  displayName?: string
  preferredLanguage?: string
}

interface AuthContextType extends AuthState {
  /** Accepts an email address OR a profile handle (with or without a leading @). */
  login: (identifier: string, pass: string, captchaToken?: string) => Promise<void>
  register: (
    email: string,
    pass: string,
    options?: RegisterOptions,
    captchaToken?: string
  ) => Promise<void>
  logout: () => Promise<void>
  requestPasswordReset: (email: string, captchaToken?: string) => Promise<void>
  resetPassword: (password: string) => Promise<void>
  signInWithOAuth: (provider: 'google' | 'github' | 'apple' | 'azure' | 'custom:chainabit') => Promise<void>
  resendSignupConfirmation: (email: string) => Promise<void>
  sendMagicLink: (email: string, captchaToken?: string) => Promise<void>
  /** Redirect to the external auth app login page, preserving the current page as return_url. */
  redirectToLogin: (delayMs?: number) => void
  /** True when the current session was established via a password-reset email link.
   *  Gate the reset-password form on this flag to prevent unauthorized access. */
  isRecoverySession: boolean
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

// Shared promise: survives React Strict Mode's double-mount without letting the
// first throwaway mount starve the live provider of its initial auth result.
let _initialAuthUserPromise: Promise<AuthState['user']> | null = null

const getInitialAuthUser = (): Promise<AuthState['user']> => {
  if (!_initialAuthUserPromise) {
    _initialAuthUserPromise = authService.getCurrentUser().catch((err) => {
      _initialAuthUserPromise = null
      throw err
    })
  }
  return _initialAuthUserPromise
}

if (import.meta.hot) {
  import.meta.hot.accept(() => {
    _initialAuthUserPromise = null
  })
}

// sessionStorage-backed guard: survives React Strict Mode and HMR (unlike module-level booleans
// which reset on every hot-reload in dev). Key is scoped by user ID so switching accounts works.
const _deletionCheckKey = (userId: string) => `acdr_${userId}`

const isEmailLike = (s: string): boolean => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)

const getErrorMessage = (err: unknown): string => {
  if (err instanceof Error) return err.message
  if (typeof err === 'string') return err
  return 'An unexpected error occurred'
}

// Keys cleared on logout (also used in LenserContext)
export const LENSER_CACHE_KEY = 'lenser_profile_data_v1'
export const WAITINGLIST_CACHE_KEY = 'waitinglist_status_v1'
const MOCK_AUTH_KEY = 'mock_auth_user'
const AUTH_PROFILE_GATE_QUERY_KEY = ['lenser', 'auth-profile-gate'] as const

const clearAuthStorage = () => {
  storage.removeItem('lenser_has_profile')
  storage.removeItem(LENSER_CACHE_KEY)
  storage.removeItem(WAITINGLIST_CACHE_KEY)
  storage.removeItem(MOCK_AUTH_KEY)
}

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [state, setState] = useState<AuthState>({
    user: null,
    isAuthenticated: false,
    isLoading: true,
    error: null,
  })

  /** True while the current session was established by a PASSWORD_RECOVERY email link. */
  const [isRecoverySession, setIsRecoverySession] = useState(false)

  const loginTransitionInFlight = useRef(false)
  // Track whether the user was previously authenticated so we only clear caches
  // on actual sign-out transitions (not null → null for anonymous users).
  const wasAuthenticated = useRef(false)

  const restoreLenserAccountIfNeeded = useCallback(async (): Promise<boolean> => {
    try {
      const result = await lenserService.cancelDeletionOnLogin()
      if (result?.restored) {
        queryClient.removeQueries({ queryKey: queryKeys.lenser.authenticated() })
        queryClient.removeQueries({ queryKey: AUTH_PROFILE_GATE_QUERY_KEY })
      }
      return true
    } catch (err) {
      console.warn('Failed to check account recovery on login', err)
      return false
    }
  }, [])

  useEffect(() => {
    let isDisposed = false
    let initialAuthSettled = false

    // 1. Subscribe before initAuth to avoid missing events that fire synchronously
    //    during subscription setup (Supabase fires INITIAL_SESSION immediately).
    const unsubscribe = authService.onAuthStateChange((user, event) => {
      // Track PASSWORD_RECOVERY sessions — the reset-password page gates on this.
      if (event === 'PASSWORD_RECOVERY') {
        setIsRecoverySession(true)
      } else if (event === 'USER_UPDATED') {
        // Password has been updated — clear the recovery flag.
        setIsRecoverySession(false)
      }
      // Skip the early INITIAL_SESSION fire — initAuth owns the first state write.
      // Other events may be meaningful even while the initial user request is in flight.
      if (!initialAuthSettled && event === 'INITIAL_SESSION') return
      if (isDisposed) return
      if (loginTransitionInFlight.current && user) return
      // Clear stale caches on actual sign-out (user was authenticated, now is not).
      // Skip for anonymous users (null → null) to avoid wiping in-flight public queries.
      if (!user && wasAuthenticated.current) {
        queryClient.clear()
        clearAuthStorage()
      }
      wasAuthenticated.current = !!user
      setState((s) => {
        if (s.user?.id === user?.id && !s.isLoading) return s
        return { ...s, user, isAuthenticated: !!user, isLoading: false }
      })
    })

    // 2. Initial load from persisted session or network
    const initAuth = async () => {
      try {
        const user = await getInitialAuthUser()
        initialAuthSettled = true
        if (isDisposed) return
        if (user) {
          if (!sessionStorage.getItem(_deletionCheckKey(user.id))) {
            const restored = await restoreLenserAccountIfNeeded()
            if (isDisposed) return
            if (restored) {
              sessionStorage.setItem(_deletionCheckKey(user.id), '1')
            }
          }
          wasAuthenticated.current = true
          setState((s) => ({ ...s, user, isAuthenticated: true, isLoading: false }))
        } else {
          wasAuthenticated.current = false
          setState((s) => ({ ...s, user: null, isAuthenticated: false, isLoading: false }))
        }
      } catch (err: unknown) {
        const authError = err as { name?: string; message?: string; code?: string; status?: number }
        initialAuthSettled = true
        if (isDisposed) return
        // AuthSessionMissingError is expected when no session exists — not a real error
        if (
          authError.name === 'AuthSessionMissingError' ||
          authError.message?.includes('Auth session missing')
        ) {
          setState((s) => ({ ...s, user: null, isAuthenticated: false, isLoading: false }))
          return
        }
        // Transient network failure (offline, DNS hiccup, service worker intercept) — treat as
        // unauthenticated rather than propagating an error that blocks the whole app.
        if (
          authError.name === 'AuthRetryableFetchError' ||
          authError.name === 'TypeError' ||
          authError.message?.includes('NetworkError') ||
          authError.message?.includes('Failed to fetch') ||
          authError.message?.includes('Load failed')
        ) {
          setState((s) => ({ ...s, user: null, isAuthenticated: false, isLoading: false }))
          return
        }
        console.error('Auth initialization error:', err)
        // User deleted in DB but JWT still valid → force clean logout
        if (
          authError.code === 'user_not_found' ||
          authError.message?.includes('User from sub claim in JWT does not exist')
        ) {
          console.warn('User not found in database, clearing session...')
          await authService.logout()
          clearAuthStorage()
          setState({ user: null, isAuthenticated: false, isLoading: false, error: null })
          return
        }
        // 403 Forbidden: token revoked, expired, or invalid → force clean logout
        if (authError.status === 403) {
          console.warn('Auth token forbidden (403), forcing clean logout...')
          await authService.logout()
          clearAuthStorage()
          queryClient.clear()
          setState({ user: null, isAuthenticated: false, isLoading: false, error: null })
          return
        }
        setState((s) => ({ ...s, isLoading: false, error: 'Failed to restore session' }))
      }
    }

    initAuth()

    return () => {
      isDisposed = true
      unsubscribe()
    }
  }, [restoreLenserAccountIfNeeded])

  const login = useCallback(
    async (identifier: string, pass: string, captchaToken?: string) => {
      setState((s) => ({ ...s, error: null }))
      loginTransitionInFlight.current = true
      try {
        // Resolve handle → email when the identifier is not an email address.
        // The email is used only as the Supabase auth credential and is never
        // surfaced to the user. Return a generic error if the handle is unknown
        // to prevent account-existence enumeration.
        let email = identifier.trim()
        if (!isEmailLike(email)) {
          const handle = email.startsWith('@') ? email.slice(1) : email
          const resolved = await authService.resolveHandleToEmail(handle)
          if (!resolved) {
            throw new Error('Invalid login credentials')
          }
          email = resolved
        }
        const user = await authService.login(email, pass, captchaToken)
        if (!sessionStorage.getItem(_deletionCheckKey(user.id))) {
          const restored = await restoreLenserAccountIfNeeded()
          if (restored) {
            sessionStorage.setItem(_deletionCheckKey(user.id), '1')
          }
        }
        wasAuthenticated.current = true
        setState({ user, isAuthenticated: true, isLoading: false, error: null })

        // Update environment metadata in the background — do not block the login flow
        getEnvMetadata().then((env) => {
          const metadata: Partial<UserMetadata> = {
            detected_language: env.detected_language,
            timezone: env.timezone,
            country: env.country,
          }
          authService
            .updateMetadata(metadata)
            .catch((e) => console.warn('Failed to update user metadata on login', e))
        })
      } catch (err: unknown) {
        const message = getErrorMessage(err)
        setState((s) => ({ ...s, isLoading: false, error: message }))
        throw err
      } finally {
        loginTransitionInFlight.current = false
      }
    },
    [restoreLenserAccountIfNeeded]
  )

  const register = useCallback(
    async (email: string, pass: string, options?: RegisterOptions, captchaToken?: string) => {
      setState((s) => ({ ...s, error: null }))
      try {
        const env = await getEnvMetadata()
        const metadata: UserMetadata = {
          display_name: options?.displayName,
          preferred_language: options?.preferredLanguage,
          ui_language: options?.preferredLanguage || env.detected_language,
          detected_language: env.detected_language,
          timezone: env.timezone,
          country: env.country,
        }
        const user = await authService.register(email, pass, metadata, captchaToken)
        wasAuthenticated.current = true
        setState({ user, isAuthenticated: true, isLoading: false, error: null })
      } catch (err: unknown) {
        const message = getErrorMessage(err)
        setState((s) => ({ ...s, isLoading: false, error: message }))
        throw err
      }
    },
    []
  )

  const logout = useCallback(async () => {
    // Clear all per-user deletion check keys from this session
    Object.keys(sessionStorage)
      .filter((k) => k.startsWith('acdr_'))
      .forEach((k) => sessionStorage.removeItem(k))
    // Best-effort: revoke Chainabit developer token before signing out
    partnerProvisioningRepository.revokeToken('chainabit').catch(() => {})
    try {
      await authService.logout()
    } catch (e) {
      console.error('Logout error', e)
    }
    wasAuthenticated.current = false
    queryClient.clear()
    clearAuthStorage()
    setState({ user: null, isAuthenticated: false, isLoading: false, error: null })
  }, [])

  const requestPasswordReset = useCallback(async (email: string, captchaToken?: string) => {
    await authService.requestPasswordReset(email, captchaToken)
  }, [])

  const resetPassword = useCallback(async (password: string) => {
    await authService.resetPassword(password)
    setIsRecoverySession(false)
  }, [])

  const signInWithOAuth = useCallback(
    async (provider: 'google' | 'github' | 'apple' | 'azure' | 'custom:chainabit') => {
      try {
        await authService.signInWithOAuth(provider)
      } catch (err: unknown) {
        const message = getErrorMessage(err)
        setState((s) => ({ ...s, error: message }))
        throw err
      }
    },
    []
  )

  const resendSignupConfirmation = useCallback(async (email: string) => {
    await authService.resendSignupConfirmation(email)
  }, [])

  const sendMagicLink = useCallback(async (email: string, captchaToken?: string) => {
    try {
      await authService.sendMagicLink(email, captchaToken)
    } catch (err: unknown) {
      const message = getErrorMessage(err)
      setState((s) => ({ ...s, error: message }))
      throw err
    }
  }, [])

  const redirectToLogin = useCallback((delayMs = 0) => {
    const returnUrl = encodeURIComponent(buildAuthReturnUrl(window.location.href))
    const target = `${AUTH_BASE_URL}/login?return_url=${returnUrl}`
    if (delayMs > 0) {
      setTimeout(() => {
        window.location.href = target
      }, delayMs)
    } else {
      window.location.href = target
    }
  }, [])

  return (
    <AuthContext.Provider
      value={{
        ...state,
        isRecoverySession,
        login,
        register,
        logout,
        requestPasswordReset,
        resetPassword,
        signInWithOAuth,
        resendSignupConfirmation,
        sendMagicLink,
        redirectToLogin,
      }}
    >
      {children}
    </AuthContext.Provider>
  )
}

export const useAuth = () => {
  const context = useContext(AuthContext)
  if (!context) throw new Error('useAuth must be used within AuthProvider')
  return context
}
