'use client';

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
} from 'react';
import {useRouter} from 'next/navigation';
import {authService} from '@/features/auth/services/authService';
import type {
  AuthSession,
  AuthUser,
  LoginCredentials,
  LoginResult,
} from '@/features/auth/types/auth';

/**
 * Who is signed in, according to the SERVER.
 *
 * ── What changed, and why it matters ──────────────────────────────────────
 * This used to read a `loyaly.session` object out of localStorage, which meant
 * the client both stored and validated its own identity — anything with a
 * devtools console could grant itself a session. Now the credential is an
 * httpOnly cookie the client cannot read, and this provider's only job is to
 * ASK (`GET /api/auth/session`) and cache the answer for rendering.
 *
 * The consequence to keep in mind: nothing here is a security boundary. It
 * decides what to draw, never what to permit. Permission is decided in
 * src/proxy.ts before the route renders, and again in every route handler.
 *
 * ── The three states ──────────────────────────────────────────────────────
 *   'loading'          the bootstrap request is in flight; render nothing
 *                      identity-shaped or the UI flashes "signed out"
 *   'authenticated'    user present
 *   'unauthenticated'  server says no session
 *
 * `loading` is a first-class state rather than `user === null`, because those
 * two mean opposite things to a guard: one should wait, the other redirect.
 */

export type SessionStatus = 'loading' | 'authenticated' | 'unauthenticated';

interface SessionValue {
  status: SessionStatus;
  session: AuthSession | null;
  user: AuthUser | null;
  isAuthenticated: boolean;
  login: (credentials: LoginCredentials) => Promise<LoginResult>;
  logout: () => Promise<void>;
  /** Re-ask the server. Used after anything that could change identity. */
  refresh: () => Promise<void>;
}

const SessionContext = createContext<SessionValue | null>(null);

export function SessionProvider({
  children,
  /**
   * The server-resolved session, from the signed cookie in the root layout.
   *
   * REQUIRED, not optional. Making it optional would mean a mount path where
   * the client has to ask who it is before it can draw anything, and that
   * costs a guard spinner on every full page load for an answer the server
   * already had in the same request.
   */
  initialSession,
}: {
  children: React.ReactNode;
  initialSession: AuthSession | null;
}) {
  const router = useRouter();
  const [session, setSession] = useState<AuthSession | null>(initialSession);
  const [status, setStatus] = useState<SessionStatus>(
    initialSession ? 'authenticated' : 'unauthenticated',
  );

  const refresh = useCallback(async () => {
    const next = await authService.currentSession();
    setSession(next);
    setStatus(next ? 'authenticated' : 'unauthenticated');
  }, []);

  /**
   * Re-check on focus.
   *
   * A workspace tab can sit open for hours. In that time the session can
   * expire, or the merchant can sign out in another tab — and without this the
   * shell keeps rendering an identity that no longer exists until something
   * happens to hit the network. Re-asking when the tab is looked at again is
   * cheap (one request, only on a real focus change) and it is what makes
   * AuthGuard's redirect fire at the moment the user comes back.
   *
   * This is the subscribe-to-an-external-system shape an effect is for: the
   * setState happens in the event callback, not in the effect body.
   */
  useEffect(() => {
    const onFocus = () => {
      if (document.visibilityState === 'visible') void refresh();
    };
    window.addEventListener('visibilitychange', onFocus);
    return () => window.removeEventListener('visibilitychange', onFocus);
  }, [refresh]);

  const login = useCallback(
    async (credentials: LoginCredentials): Promise<LoginResult> => {
      const result = await authService.login(credentials);
      if (result.ok) {
        setSession(result.session);
        setStatus('authenticated');
      }
      return result;
    },
    [],
  );

  const logout = useCallback(async () => {
    await authService.logout();
    setSession(null);
    setStatus('unauthenticated');

    /**
     * Belt and braces on the client side of the door.
     *
     * The cookie is already gone server-side, which is what actually ends the
     * session. This clears the *other* things a signed-in user leaves behind —
     * a stored sidebar preference, a cached scope — so a shared machine does
     * not hand the next person a workspace shaped like the last one. Wrapped
     * because storage throws in private mode, and a failed cleanup must not
     * strand someone in a session they asked to leave.
     */
    try {
      window.localStorage.clear();
      window.sessionStorage.clear();
    } catch {
      /* ignore */
    }

    // replace, not push: Back must not return to a workspace the session no
    // longer authorises. The proxy would bounce it anyway; this keeps the
    // history clean rather than relying on that.
    router.replace('/login');
    // Drop the client router cache too, or a Back gesture can repaint the
    // previous authenticated render from memory before the proxy is consulted.
    router.refresh();
  }, [router]);

  const value = useMemo<SessionValue>(
    () => ({
      status,
      session,
      user: session?.user ?? null,
      isAuthenticated: status === 'authenticated',
      login,
      logout,
      refresh,
    }),
    [status, session, login, logout, refresh],
  );

  return <SessionContext value={value}>{children}</SessionContext>;
}

export function useSession(): SessionValue {
  const ctx = useContext(SessionContext);
  if (!ctx) {
    throw new Error('useSession must be used inside <SessionProvider>');
  }
  return ctx;
}
