"use client";

import {
  createContext,
  useContext,
  useState,
  useEffect,
  useCallback,
  useRef,
  type ReactNode,
} from "react";
import type { AuthUser, AuthSession } from "@mosaic/shared";
import { apiGet, apiPost } from "../api/client";
import { fetchUserWorkspaces } from "../api/workspaces";
import { IS_MOCK_AUTH_MODE } from "../config";
import { parseAuthError } from "./auth-errors";

/**
 * Error types for auth session checks
 */
export type AuthErrorType = "network" | "backend" | "session_expired" | null;

/** Threshold in minutes before session expiry to start warning */
const SESSION_EXPIRY_WARNING_MINUTES = 5;

/** Interval in milliseconds to check session expiry */
const SESSION_CHECK_INTERVAL_MS = 60_000;

/**
 * localStorage key for the active workspace ID.
 * Must match the WORKSPACE_KEY constant in useLayout.ts and the key read
 * by apiRequest in client.ts.
 */
const WORKSPACE_STORAGE_KEY = "mosaic-workspace-id";

/**
 * Persist the workspace ID to localStorage so it is available to
 * useWorkspaceId and apiRequest on the next render / request cycle.
 * Silently ignores localStorage errors (private browsing, storage full).
 */
function persistWorkspaceId(workspaceId: string | undefined): void {
  if (typeof window === "undefined") return;
  try {
    if (workspaceId) {
      localStorage.setItem(WORKSPACE_STORAGE_KEY, workspaceId);
    }
  } catch {
    // localStorage unavailable — not fatal
  }
}

/**
 * Remove the workspace ID from localStorage on sign-out so stale workspace
 * context is not sent on subsequent unauthenticated requests.
 */
function clearWorkspaceId(): void {
  if (typeof window === "undefined") return;
  try {
    localStorage.removeItem(WORKSPACE_STORAGE_KEY);
  } catch {
    // localStorage unavailable — not fatal
  }
}

const MOCK_AUTH_USER: AuthUser = {
  id: "dev-user-local",
  email: "dev@localhost",
  name: "Local Dev User",
};

interface AuthContextValue {
  user: AuthUser | null;
  isLoading: boolean;
  isAuthenticated: boolean;
  authError: AuthErrorType;
  sessionExpiring: boolean;
  sessionMinutesRemaining: number;
  signOut: () => Promise<void>;
  refreshSession: () => Promise<void>;
}

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

/**
 * Classify an error into an {@link AuthErrorType} using the centralised
 * {@link parseAuthError} utility.
 *
 * Normal authentication failures (401 Unauthorized, session expired) return
 * `null` so the UI simply shows the logged-out state without a banner.
 *
 * Defaults unrecognised `Error` instances to `"backend"` rather than `null`
 * so that unexpected failures surface a "having trouble connecting" banner
 * instead of silently logging the user out.
 */
function classifyAuthError(error: unknown): AuthErrorType {
  const parsed = parseAuthError(error);
  if (parsed.code === "network_error") return "network";
  if (parsed.code === "server_error") return "backend";
  // Normal auth failures (not logged in, session expired) are not errors —
  // return null so the UI shows logged-out state without a banner
  if (parsed.code === "invalid_credentials" || parsed.code === "session_expired") return null;
  // For truly unrecognised errors, default to "backend" rather than null
  // (safer to show "having trouble connecting" than silently log out)
  if (error instanceof Error) return "backend";
  return null;
}

/**
 * Log auth errors — always logs, including production.
 * Auth failures are operational issues, not debug noise.
 */
function logAuthError(message: string, error: unknown): void {
  console.error(`[Auth] ${message}:`, error);
}

export function AuthProvider({ children }: { children: ReactNode }): React.JSX.Element {
  if (IS_MOCK_AUTH_MODE) {
    return <MockAuthProvider>{children}</MockAuthProvider>;
  }

  return <RealAuthProvider>{children}</RealAuthProvider>;
}

function RealAuthProvider({ children }: { children: ReactNode }): React.JSX.Element {
  const [user, setUser] = useState<AuthUser | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [authError, setAuthError] = useState<AuthErrorType>(null);
  const [sessionExpiring, setSessionExpiring] = useState(false);
  const [sessionMinutesRemaining, setSessionMinutesRemaining] = useState(0);
  const expiresAtRef = useRef<Date | null>(null);

  const checkSession = useCallback(async () => {
    try {
      const session = await apiGet<AuthSession>("/auth/session");
      setUser(session.user);
      setAuthError(null);

      // Fetch the user's workspace memberships and persist the default.
      // Workspace context is an application concern, not an auth concern —
      // BetterAuth does not return workspace fields on the session user.
      try {
        const workspaces = await fetchUserWorkspaces();
        const defaultWorkspace = workspaces[0];
        if (defaultWorkspace) {
          persistWorkspaceId(defaultWorkspace.id);
        }
      } catch (wsError) {
        logAuthError("Failed to fetch workspaces after session check", wsError);
      }

      // Track session expiry timestamp
      expiresAtRef.current = new Date(session.session.expiresAt);

      // Reset expiring state on successful session check
      setSessionExpiring(false);
    } catch (error) {
      const errorType = classifyAuthError(error);

      if (errorType) {
        logAuthError("Session check failed due to backend/network issue", error);
      }
      setAuthError(errorType);

      setUser(null);
      expiresAtRef.current = null;
      setSessionExpiring(false);
    } finally {
      setIsLoading(false);
    }
  }, []);

  const signOut = useCallback(async () => {
    try {
      await apiPost("/auth/sign-out");
    } catch (error) {
      logAuthError("Sign out request did not complete", error);
      setAuthError(classifyAuthError(error) ?? "backend");
    } finally {
      setUser(null);
      expiresAtRef.current = null;
      setSessionExpiring(false);
      // Clear persisted workspace ID so stale context is not sent on
      // subsequent unauthenticated API requests.
      clearWorkspaceId();
    }
  }, []);

  const refreshSession = useCallback(async () => {
    await checkSession();
  }, [checkSession]);

  useEffect(() => {
    void checkSession();
  }, [checkSession]);

  // Periodically check whether the session is approaching expiry
  useEffect((): (() => void) => {
    if (!user || !expiresAtRef.current) {
      return (): void => {
        /* no-op cleanup */
      };
    }

    const checkExpiry = (): void => {
      if (!expiresAtRef.current) return;

      const remainingMs = expiresAtRef.current.getTime() - Date.now();
      const minutes = Math.ceil(remainingMs / 60_000);

      if (minutes <= 0) {
        // Session has expired — set explicit state so the UI can react
        setUser(null);
        setSessionExpiring(false);
        setSessionMinutesRemaining(0);
        expiresAtRef.current = null;
        setAuthError("session_expired");
      } else if (minutes <= SESSION_EXPIRY_WARNING_MINUTES) {
        setSessionExpiring(true);
        setSessionMinutesRemaining(minutes);
      } else {
        setSessionExpiring(false);
        setSessionMinutesRemaining(minutes);
      }
    };

    checkExpiry();
    const interval = setInterval(checkExpiry, SESSION_CHECK_INTERVAL_MS);
    return (): void => {
      clearInterval(interval);
    };
  }, [user]);

  const value: AuthContextValue = {
    user,
    isLoading,
    isAuthenticated: user !== null,
    authError,
    sessionExpiring,
    sessionMinutesRemaining,
    signOut,
    refreshSession,
  };

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

function MockAuthProvider({ children }: { children: ReactNode }): React.JSX.Element {
  const [user, setUser] = useState<AuthUser | null>(MOCK_AUTH_USER);

  const signOut = useCallback((): Promise<void> => {
    setUser(null);
    return Promise.resolve();
  }, []);

  const refreshSession = useCallback((): Promise<void> => {
    setUser(MOCK_AUTH_USER);
    return Promise.resolve();
  }, []);

  const value: AuthContextValue = {
    user,
    isLoading: false,
    isAuthenticated: user !== null,
    authError: null,
    sessionExpiring: false,
    sessionMinutesRemaining: 0,
    signOut,
    refreshSession,
  };

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

export function useAuth(): AuthContextValue {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("useAuth must be used within AuthProvider");
  }
  return context;
}
