import { useQueryClient } from '@tanstack/react-query'
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
  type ReactNode,
} from 'react'

import { ApiError, UNAUTHENTICATED_EVENT, api } from '@/lib/api'
import type { DataResponse, User } from '@/types/api'

interface LoginCredentials {
  email: string
  password: string
  remember?: boolean
  two_factor_code?: string
  recovery_code?: string
}

interface RegisterDetails {
  name: string
  email: string
  password: string
  password_confirmation: string
  accept_terms: boolean
  /**
   * Optional. Omitted entirely rather than sent empty — the API treats a phone
   * number as unique among live accounts, and an empty string is a value that
   * only the first account could ever hold.
   */
  phone?: string
  marketing_opt_in?: boolean
}

interface AuthContextValue {
  user: User | null
  /** True until the initial session check settles, so guards do not flash. */
  isLoading: boolean
  isAuthenticated: boolean
  login: (credentials: LoginCredentials) => Promise<void>
  register: (details: RegisterDetails) => Promise<void>
  logout: () => Promise<void>
  /** Re-reads the account, e.g. after email verification or a profile edit. */
  refresh: () => Promise<void>
  /** Replaces the cached account with a known-good copy from a mutation. */
  setUser: (user: User) => void
}

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

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUserState] = useState<User | null>(null)
  const [isLoading, setIsLoading] = useState(true)
  const queryClient = useQueryClient()

  const loadSession = useCallback(async () => {
    try {
      const response = await api.get<DataResponse<User>>('/account')
      setUserState(response.data)
    } catch (error) {
      /*
       * A 401 here is the ordinary case for a visitor: the marketplace is
       * browsable without an account, so no session is not an error worth
       * surfacing. Anything else is left to bubble into the console rather than
       * silently pretending the user is signed out.
       */
      if (!(error instanceof ApiError && error.isUnauthenticated)) {
        console.error('Could not load the current session.', error)
      }

      setUserState(null)
    } finally {
      setIsLoading(false)
    }
  }, [])

  useEffect(() => {
    void loadSession()
  }, [loadSession])

  /*
   * Any request that comes back 401 clears local auth state. Without this, a
   * session that expired server-side would leave the UI showing a signed-in
   * shell whose every action fails.
   */
  useEffect(() => {
    const onUnauthenticated = () => {
      setUserState(null)
      queryClient.clear()
    }

    window.addEventListener(UNAUTHENTICATED_EVENT, onUnauthenticated)

    return () => window.removeEventListener(UNAUTHENTICATED_EVENT, onUnauthenticated)
  }, [queryClient])

  const login = useCallback(
    async (credentials: LoginCredentials) => {
      const response = await api.post<{ user: User }>('/auth/login', credentials)
      setUserState(response.user)

      // Anything cached for the previous visitor must not survive a sign-in.
      queryClient.clear()
    },
    [queryClient],
  )

  const register = useCallback(
    async (details: RegisterDetails) => {
      const response = await api.post<{ user: User }>('/auth/register', details)
      setUserState(response.user)
      queryClient.clear()
    },
    [queryClient],
  )

  const logout = useCallback(async () => {
    try {
      await api.post('/auth/logout')
    } catch (error) {
      /*
       * Swallowed on purpose, not merely tolerated.
       *
       * Pressing sign out has one guarantee: you end up signed out of this
       * browser. Local state is cleared below whatever the network did, so the
       * promise must also RESOLVE -- rejecting here would mean callers that do
       * `await logout(); navigate('/')` never reach the redirect, leaving
       * someone on a signed-in-looking page they have in fact been signed out
       * of. The server-side token is revoked on its next use or at expiry.
       */
      console.warn('Sign-out request failed; the local session was cleared anyway.', error)
    } finally {
      setUserState(null)
      queryClient.clear()
    }
  }, [queryClient])

  const value = useMemo<AuthContextValue>(
    () => ({
      user,
      isLoading,
      isAuthenticated: user !== null,
      login,
      register,
      logout,
      refresh: loadSession,
      setUser: setUserState,
    }),
    [user, isLoading, login, register, logout, loadSession],
  )

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

export function useAuth(): AuthContextValue {
  const context = useContext(AuthContext)

  if (context === null) {
    throw new Error('useAuth must be used inside an AuthProvider.')
  }

  return context
}
