import * as React from 'react';
import { createContext, useContext, useCallback, useMemo, useEffect, useState, useRef } from 'react';
import type { User, AuthState, LoginCredentials, RegisterCredentials, OTPVerifyCredentials, MagicLinkVerifyCredentials } from './types';

const TOKEN_STORAGE_KEY = 'auth_token';
const USER_STORAGE_KEY = 'auth_user';

/**
 * Authentication context value.
 */
export interface AuthContextValue extends AuthState {
  /** Log in with email and password */
  login: (credentials: LoginCredentials) => Promise<void>;
  /** Log in with a token directly */
  loginWithToken: (token: string, user?: User) => void;
  /** Register a new account */
  register: (credentials: RegisterCredentials) => Promise<void>;
  /** Log out the current user */
  logout: () => Promise<void>;
  /** Get the current access token */
  getToken: () => string | null;
  /** Check if user has a specific role */
  hasRole: (role: string) => boolean;
  /** Check if user has a specific scope */
  hasScope: (scope: string) => boolean;
  /** Send an OTP code to an email */
  sendOTP: (email: string) => Promise<void>;
  /** Log in with an OTP code */
  loginWithOTP: (credentials: OTPVerifyCredentials) => Promise<void>;
  /** Send a magic link to an email */
  sendMagicLink: (email: string) => Promise<void>;
  /** Log in with a magic link token */
  loginWithMagicLink: (credentials: MagicLinkVerifyCredentials) => Promise<void>;
  /** Refresh the access token */
  refreshToken: () => Promise<void>;
}

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

/**
 * Auth provider configuration.
 */
export interface AuthProviderProps {
  children: React.ReactNode;
  /** API base URL for auth endpoints (e.g. "/api/my-service") */
  authBaseUrl?: string;
  /** API endpoint for login (defaults to authBaseUrl + "/auth/login") */
  loginUrl?: string;
  /** API endpoint for logout */
  logoutUrl?: string;
  /** API endpoint for registration */
  registerUrl?: string;
  /** API endpoint for token refresh */
  refreshUrl?: string;
  /** Custom login handler */
  onLogin?: (credentials: LoginCredentials) => Promise<{ token: string; user: User }>;
  /** Custom logout handler */
  onLogout?: () => Promise<void>;
  /** Storage type for persisting auth state */
  storage?: 'localStorage' | 'sessionStorage' | 'none';
}

/**
 * AuthProvider manages authentication state and provides auth methods.
 */
export function AuthProvider({
  children,
  authBaseUrl,
  loginUrl,
  logoutUrl,
  registerUrl,
  refreshUrl,
  onLogin,
  onLogout,
  storage = 'localStorage',
}: AuthProviderProps) {
  // Derive URLs from authBaseUrl if individual URLs not provided
  const resolvedLoginUrl = loginUrl || (authBaseUrl ? `${authBaseUrl}/auth/login` : '/api/auth/login');
  const resolvedLogoutUrl = logoutUrl || (authBaseUrl ? `${authBaseUrl}/auth/logout` : '/api/auth/logout');
  const resolvedRegisterUrl = registerUrl || (authBaseUrl ? `${authBaseUrl}/auth/register` : '/api/auth/register');
  const resolvedRefreshUrl = refreshUrl || (authBaseUrl ? `${authBaseUrl}/auth/refresh` : '/api/auth/refresh');
  const resolvedOtpSendUrl = authBaseUrl ? `${authBaseUrl}/auth/otp/send` : '/api/auth/otp/send';
  const resolvedOtpVerifyUrl = authBaseUrl ? `${authBaseUrl}/auth/otp/verify` : '/api/auth/otp/verify';
  const resolvedMagicLinkUrl = authBaseUrl ? `${authBaseUrl}/auth/magic-link` : '/api/auth/magic-link';
  const resolvedMagicLinkVerifyUrl = authBaseUrl ? `${authBaseUrl}/auth/magic-link/verify` : '/api/auth/magic-link/verify';

  const [state, setState] = useState<AuthState>({
    user: null,
    isLoading: true,
    isAuthenticated: false,
    error: null,
  });

  const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  // Get storage implementation
  const getStorage = useCallback(() => {
    if (storage === 'none') return null;
    return storage === 'sessionStorage' ? sessionStorage : localStorage;
  }, [storage]);

  // Store token and user
  const persistAuth = useCallback((token: string, user: User) => {
    const store = getStorage();
    if (store) {
      store.setItem(TOKEN_STORAGE_KEY, token);
      store.setItem(USER_STORAGE_KEY, JSON.stringify(user));
    }
  }, [getStorage]);

  // Clear stored auth
  const clearAuth = useCallback(() => {
    const store = getStorage();
    if (store) {
      store.removeItem(TOKEN_STORAGE_KEY);
      store.removeItem(USER_STORAGE_KEY);
    }
  }, [getStorage]);

  // Schedule token refresh (at 80% of token lifetime)
  const scheduleRefresh = useCallback((token: string) => {
    if (refreshTimerRef.current) {
      clearTimeout(refreshTimerRef.current);
    }
    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      const exp = payload.exp * 1000;
      const iat = payload.iat * 1000;
      const lifetime = exp - iat;
      const refreshAt = iat + lifetime * 0.8;
      const delay = refreshAt - Date.now();

      if (delay > 0) {
        refreshTimerRef.current = setTimeout(async () => {
          try {
            const store = getStorage();
            const currentToken = store?.getItem(TOKEN_STORAGE_KEY);
            if (!currentToken) return;

            const response = await fetch(resolvedRefreshUrl, {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${currentToken}`,
              },
            });

            if (response.ok) {
              const data = await response.json();
              const newToken = data.data?.token || data.token;
              const newUser = data.data?.user || data.user;
              persistAuth(newToken, newUser);
              setState(s => ({ ...s, user: newUser }));
              scheduleRefresh(newToken);
            } else if (response.status === 401) {
              // Session revoked or token invalid — force logout.
              clearAuth();
              setState({ user: null, isLoading: false, isAuthenticated: false, error: null });
            }
          } catch {
            // Refresh failed (network error) — clear auth to prevent silent expiry.
            clearAuth();
            setState({ user: null, isLoading: false, isAuthenticated: false, error: null });
          }
        }, delay);
      }
    } catch {
      // Corrupted token in storage — clear it and force logout.
      clearAuth();
      setState({ user: null, isLoading: false, isAuthenticated: false, error: null });
    }
  }, [getStorage, persistAuth, clearAuth, resolvedRefreshUrl]);

  // Initialize auth state from storage
  useEffect(() => {
    const store = getStorage();
    if (!store) {
      setState((s) => ({ ...s, isLoading: false }));
      return;
    }

    const token = store.getItem(TOKEN_STORAGE_KEY);
    const userJson = store.getItem(USER_STORAGE_KEY);

    if (token && userJson) {
      try {
        const user = JSON.parse(userJson) as User;

        // Check if the stored token is already expired
        let tokenExpired = false;
        let deeplyExpired = false;
        try {
          const payload = JSON.parse(atob(token.split('.')[1]));
          const exp = payload.exp * 1000;
          tokenExpired = exp < Date.now();
          // Session is deeply expired if token expired over 30 days ago
          deeplyExpired = exp + 30 * 24 * 60 * 60 * 1000 < Date.now();
        } catch {
          // Corrupted token — treat as expired
          tokenExpired = true;
          deeplyExpired = true;
        }

        if (deeplyExpired) {
          // Session expired beyond recovery — clear auth
          store.removeItem(TOKEN_STORAGE_KEY);
          store.removeItem(USER_STORAGE_KEY);
          setState({ user: null, isLoading: false, isAuthenticated: false, error: null });
        } else if (tokenExpired) {
          // Token expired but session may still be valid — attempt refresh
          setState((s) => ({ ...s, isLoading: true }));
          fetch(resolvedRefreshUrl, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Authorization': `Bearer ${token}`,
            },
          })
            .then((response) => {
              if (response.ok) {
                return response.json().then((data) => {
                  const newToken = data.data?.token || data.token;
                  const newUser = data.data?.user || data.user;
                  store.setItem(TOKEN_STORAGE_KEY, newToken);
                  store.setItem(USER_STORAGE_KEY, JSON.stringify(newUser));
                  setState({ user: newUser, isLoading: false, isAuthenticated: true, error: null });
                  scheduleRefresh(newToken);
                });
              }
              // Refresh failed (401, etc.) — clear auth
              store.removeItem(TOKEN_STORAGE_KEY);
              store.removeItem(USER_STORAGE_KEY);
              setState({ user: null, isLoading: false, isAuthenticated: false, error: null });
            })
            .catch(() => {
              // Network error during refresh — clear auth
              store.removeItem(TOKEN_STORAGE_KEY);
              store.removeItem(USER_STORAGE_KEY);
              setState({ user: null, isLoading: false, isAuthenticated: false, error: null });
            });
        } else {
          // Token is still valid — restore authenticated state
          setState({
            user,
            isLoading: false,
            isAuthenticated: true,
            error: null,
          });
          scheduleRefresh(token);
        }
      } catch {
        store.removeItem(TOKEN_STORAGE_KEY);
        store.removeItem(USER_STORAGE_KEY);
        setState((s) => ({ ...s, isLoading: false }));
      }
    } else {
      setState((s) => ({ ...s, isLoading: false }));
    }
  }, [getStorage, scheduleRefresh, resolvedRefreshUrl]);

  // Cleanup timer
  useEffect(() => {
    return () => {
      if (refreshTimerRef.current) {
        clearTimeout(refreshTimerRef.current);
      }
    };
  }, []);

  // Helper to handle auth response (login, register, OTP verify, magic link verify)
  const handleAuthResponse = useCallback(
    (token: string, user: User) => {
      persistAuth(token, user);
      setState({
        user,
        isLoading: false,
        isAuthenticated: true,
        error: null,
      });
      scheduleRefresh(token);
    },
    [persistAuth, scheduleRefresh]
  );

  // Login with credentials
  const login = useCallback(
    async (credentials: LoginCredentials) => {
      setState((s) => ({ ...s, isLoading: true, error: null }));

      try {
        let token: string;
        let user: User;

        if (onLogin) {
          const result = await onLogin(credentials);
          token = result.token;
          user = result.user;
        } else {
          const response = await fetch(resolvedLoginUrl, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(credentials),
          });

          if (!response.ok) {
            const errBody = await response.json().catch(() => ({}));
            const errMsg = errBody.error?.message || errBody.message || 'Login failed';
            throw new Error(errMsg);
          }

          const data = await response.json();
          token = data.data?.token || data.token;
          user = data.data?.user || data.user;
        }

        handleAuthResponse(token, user);
      } catch (error) {
        setState({
          user: null,
          isLoading: false,
          isAuthenticated: false,
          error: error instanceof Error ? error : new Error('Login failed'),
        });
        throw error;
      }
    },
    [resolvedLoginUrl, onLogin, handleAuthResponse]
  );

  // Login with token directly
  const loginWithToken = useCallback(
    (token: string, user?: User) => {
      const store = getStorage();
      if (store) {
        store.setItem(TOKEN_STORAGE_KEY, token);
        if (user) {
          store.setItem(USER_STORAGE_KEY, JSON.stringify(user));
        }
      }

      setState({
        user: user || null,
        isLoading: false,
        isAuthenticated: true,
        error: null,
      });
      scheduleRefresh(token);
    },
    [getStorage, scheduleRefresh]
  );

  // Register
  const register = useCallback(
    async (credentials: RegisterCredentials) => {
      setState((s) => ({ ...s, isLoading: true, error: null }));

      try {
        const response = await fetch(resolvedRegisterUrl, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(credentials),
        });

        if (!response.ok) {
          const errBody = await response.json().catch(() => ({}));
          const errMsg = errBody.error?.message || errBody.message || 'Registration failed';
          throw new Error(errMsg);
        }

        const data = await response.json();
        const token = data.data?.token || data.token;
        const user = data.data?.user || data.user;

        handleAuthResponse(token, user);
      } catch (error) {
        setState({
          user: null,
          isLoading: false,
          isAuthenticated: false,
          error: error instanceof Error ? error : new Error('Registration failed'),
        });
        throw error;
      }
    },
    [resolvedRegisterUrl, handleAuthResponse]
  );

  // Logout
  const logout = useCallback(async () => {
    if (refreshTimerRef.current) {
      clearTimeout(refreshTimerRef.current);
    }

    try {
      if (onLogout) {
        await onLogout();
      } else {
        const store = getStorage();
        const token = store?.getItem(TOKEN_STORAGE_KEY);
        if (token) {
          await fetch(resolvedLogoutUrl, {
            method: 'POST',
            headers: { 'Authorization': `Bearer ${token}` },
          }).catch(() => {});
        }
      }
    } finally {
      clearAuth();
      setState({
        user: null,
        isLoading: false,
        isAuthenticated: false,
        error: null,
      });
    }
  }, [resolvedLogoutUrl, onLogout, getStorage, clearAuth]);

  // Send OTP
  const sendOTP = useCallback(
    async (email: string) => {
      const response = await fetch(resolvedOtpSendUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email }),
      });
      if (!response.ok) {
        const errBody = await response.json().catch(() => ({}));
        throw new Error(errBody.error?.message || errBody.message || 'Failed to send code');
      }
    },
    [resolvedOtpSendUrl]
  );

  // Login with OTP
  const loginWithOTP = useCallback(
    async (credentials: OTPVerifyCredentials) => {
      setState((s) => ({ ...s, isLoading: true, error: null }));

      try {
        const response = await fetch(resolvedOtpVerifyUrl, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(credentials),
        });

        if (!response.ok) {
          const errBody = await response.json().catch(() => ({}));
          throw new Error(errBody.error?.message || errBody.message || 'Invalid code');
        }

        const data = await response.json();
        const token = data.data?.token || data.token;
        const user = data.data?.user || data.user;

        handleAuthResponse(token, user);
      } catch (error) {
        setState((s) => ({
          ...s,
          isLoading: false,
          error: error instanceof Error ? error : new Error('OTP verification failed'),
        }));
        throw error;
      }
    },
    [resolvedOtpVerifyUrl, handleAuthResponse]
  );

  // Send magic link
  const sendMagicLink = useCallback(
    async (email: string) => {
      const response = await fetch(resolvedMagicLinkUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email }),
      });
      if (!response.ok) {
        const errBody = await response.json().catch(() => ({}));
        throw new Error(errBody.error?.message || errBody.message || 'Failed to send link');
      }
    },
    [resolvedMagicLinkUrl]
  );

  // Login with magic link token
  const loginWithMagicLink = useCallback(
    async (credentials: MagicLinkVerifyCredentials) => {
      setState((s) => ({ ...s, isLoading: true, error: null }));

      try {
        const response = await fetch(resolvedMagicLinkVerifyUrl, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(credentials),
        });

        if (!response.ok) {
          const errBody = await response.json().catch(() => ({}));
          throw new Error(errBody.error?.message || errBody.message || 'Invalid link');
        }

        const data = await response.json();
        const token = data.data?.token || data.token;
        const user = data.data?.user || data.user;

        handleAuthResponse(token, user);
      } catch (error) {
        setState((s) => ({
          ...s,
          isLoading: false,
          error: error instanceof Error ? error : new Error('Magic link verification failed'),
        }));
        throw error;
      }
    },
    [resolvedMagicLinkVerifyUrl, handleAuthResponse]
  );

  // Refresh token
  const refreshTokenFn = useCallback(async () => {
    const store = getStorage();
    const currentToken = store?.getItem(TOKEN_STORAGE_KEY);
    if (!currentToken) return;

    const response = await fetch(resolvedRefreshUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${currentToken}`,
      },
    });

    if (!response.ok) {
      throw new Error('Token refresh failed');
    }

    const data = await response.json();
    const newToken = data.data?.token || data.token;
    const newUser = data.data?.user || data.user;

    persistAuth(newToken, newUser);
    setState(s => ({ ...s, user: newUser }));
    scheduleRefresh(newToken);
  }, [getStorage, resolvedRefreshUrl, persistAuth, scheduleRefresh]);

  // Get token
  const getToken = useCallback(() => {
    const store = getStorage();
    return store ? store.getItem(TOKEN_STORAGE_KEY) : null;
  }, [getStorage]);

  // Role check
  const hasRole = useCallback(
    (role: string) => {
      return state.user?.roles?.includes(role) ?? false;
    },
    [state.user]
  );

  // Scope check
  const hasScope = useCallback(
    (scope: string) => {
      return state.user?.scopes?.includes(scope) ?? false;
    },
    [state.user]
  );

  const value = useMemo(
    (): AuthContextValue => ({
      ...state,
      login,
      loginWithToken,
      register,
      logout,
      getToken,
      hasRole,
      hasScope,
      sendOTP,
      loginWithOTP,
      sendMagicLink,
      loginWithMagicLink,
      refreshToken: refreshTokenFn,
    }),
    [state, login, loginWithToken, register, logout, getToken, hasRole, hasScope, sendOTP, loginWithOTP, sendMagicLink, loginWithMagicLink, refreshTokenFn]
  );

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

/**
 * Hook to access authentication state and methods.
 *
 * @example
 * function Profile() {
 *   const { user, logout, isAuthenticated } = useAuth();
 *
 *   if (!isAuthenticated) {
 *     return <LoginForm />;
 *   }
 *
 *   return (
 *     <div>
 *       <p>Welcome, {user?.name}</p>
 *       <button onClick={logout}>Logout</button>
 *     </div>
 *   );
 * }
 */
export function useAuth(): AuthContextValue {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}
