/**
 * Authentication context and provider.
 *
 * Manages the user's authenticated state and exposes `login`, `logout`, and
 * `isAuthenticated` through `useAuth()`.  The session token is persisted in
 * `sessionStorage` so it survives page refreshes within the browser tab but
 * is automatically cleared when the tab is closed.
 */

import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
} from "react";
import * as authApi from "../api/auth";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

interface AuthState {
  token: string | null;
  expiresAt: string | null;
}

interface AuthContextValue {
  /** `true` when a valid session token is held in state. */
  isAuthenticated: boolean;
  /**
   * Authenticate with the master password.
   * Throws an `ApiError` on failure.
   */
  login: (password: string) => Promise<void>;
  /** Revoke the current session and clear local state. */
  logout: () => Promise<void>;
}

// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------

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

const SESSION_KEY = "bangui_token";
const SESSION_EXPIRES_KEY = "bangui_expires_at";

// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------

/**
 * Wraps the application and provides authentication state to all children.
 *
 * Place this inside `<FluentProvider>` and `<BrowserRouter>` so all
 * descendants can call `useAuth()`.
 */
export function AuthProvider({
  children,
}: {
  children: React.ReactNode;
}): JSX.Element {
  const [auth, setAuth] = useState<AuthState>(() => ({
    token: sessionStorage.getItem(SESSION_KEY),
    expiresAt: sessionStorage.getItem(SESSION_EXPIRES_KEY),
  }));

  const isAuthenticated = useMemo<boolean>(() => {
    if (!auth.token || !auth.expiresAt) return false;
    // Treat the session as expired if the expiry time has passed.
    return new Date(auth.expiresAt) > new Date();
  }, [auth]);

  const login = useCallback(async (password: string): Promise<void> => {
    const response = await authApi.login(password);
    sessionStorage.setItem(SESSION_KEY, response.token);
    sessionStorage.setItem(SESSION_EXPIRES_KEY, response.expires_at);
    setAuth({ token: response.token, expiresAt: response.expires_at });
  }, []);

  const logout = useCallback(async (): Promise<void> => {
    try {
      await authApi.logout();
    } finally {
      // Always clear local state even if the API call fails (e.g. expired session).
      sessionStorage.removeItem(SESSION_KEY);
      sessionStorage.removeItem(SESSION_EXPIRES_KEY);
      setAuth({ token: null, expiresAt: null });
    }
  }, []);

  const value = useMemo<AuthContextValue>(
    () => ({ isAuthenticated, login, logout }),
    [isAuthenticated, login, logout],
  );

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

// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------

/**
 * Access authentication state and actions.
 *
 * Must be called inside a component rendered within `<AuthProvider>`.
 *
 * @throws {Error} When called outside of `<AuthProvider>`.
 */
export function useAuth(): AuthContextValue {
  const ctx = useContext(AuthContext);
  if (ctx === null) {
    throw new Error("useAuth must be used within <AuthProvider>.");
  }
  return ctx;
}
