import type { LoginInput, SafeUserProfile, SignupInput } from "@batch-cooking/shared";
import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from "react";
import { apiClient } from "../../api/client";

/** Shape of the auth state/actions exposed via {@link useAuth}. */
interface AuthContextValue {
  /** Currently authenticated profile, or `null` if no active session. */
  user: SafeUserProfile | null;
  /** True only while the initial `/auth/me` check (on app load) is pending — lets route guards avoid a premature redirect. */
  isLoading: boolean;
  /** Creates a profile (+ household) and updates `user` on success. Throws `ApiError` on failure. */
  signup: (input: SignupInput) => Promise<void>;
  /** Verifies credentials and updates `user` on success. Throws `ApiError` on failure. */
  login: (input: LoginInput) => Promise<void>;
  /** Ends the session and clears `user`. */
  logout: () => Promise<void>;
  /** Permanently deletes the current account and clears `user`. Throws `ApiError` (e.g. wrong password) on failure. */
  deleteAccount: (password: string) => Promise<void>;
  /**
   * Re-fetches the current profile and updates `user`. Needed after
   * anything that changes profile fields `user` carries (e.g. `dietId`)
   * outside of `signup`/`login` — `PATCH /profile/diet` (see
   * `PreferencesPage.tsx`) updates the database directly via `apiClient`,
   * which doesn't touch this context on its own.
   */
  refreshUser: () => Promise<void>;
}

/** React context carrying {@link AuthContextValue} — always accessed through {@link useAuth}, never directly. */
const AuthContext = createContext<AuthContextValue | null>(null);

/**
 * Provides authentication state to the whole app. On mount, calls
 * `GET /auth/me` once to restore the session from the httpOnly cookie (if
 * any) — this is what lets a page reload keep the user logged in.
 */
export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<SafeUserProfile | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    apiClient
      .me()
      .then(setUser)
      // No session cookie (or it's invalid/expired) — that's the normal
      // state for a first-time visitor, not an error to surface.
      .catch(() => setUser(null))
      .finally(() => setIsLoading(false));
  }, []);

  const signup = useCallback(async (input: SignupInput) => {
    try {
      setUser(await apiClient.signup(input));
    } catch (err) {
      // Rethrown as-is — the caller (`SignupPage`'s `handleSubmit`) is what
      // actually catches and displays this (see this context value's doc
      // comment, "Throws ApiError on failure"); this callback just isn't
      // allowed a bare `await` per the repo's async/try-catch convention.
      throw err;
    }
  }, []);

  const login = useCallback(async (input: LoginInput) => {
    try {
      setUser(await apiClient.login(input));
    } catch (err) {
      throw err; // see signup()'s catch comment above
    }
  }, []);

  const logout = useCallback(async () => {
    try {
      await apiClient.logout();
      setUser(null);
    } catch (err) {
      throw err; // see signup()'s catch comment above
    }
  }, []);

  const deleteAccount = useCallback(async (password: string) => {
    try {
      await apiClient.deleteAccount(password);
      setUser(null);
    } catch (err) {
      throw err; // see signup()'s catch comment above
    }
  }, []);

  const refreshUser = useCallback(async () => {
    try {
      setUser(await apiClient.me());
    } catch (err) {
      throw err; // see signup()'s catch comment above
    }
  }, []);

  return (
    <AuthContext.Provider
      value={{ user, isLoading, signup, login, logout, deleteAccount, refreshUser }}
    >
      {children}
    </AuthContext.Provider>
  );
}

/** Reads the current auth state/actions. Must be called from within an {@link AuthProvider}. */
export function useAuth(): AuthContextValue {
  const ctx = useContext(AuthContext);
  if (!ctx) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return ctx;
}
