"use client";

import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { useAuth } from "@/contexts/auth-context";
import { fetchAllPermissionCatalog, type PermissionCatalogResult } from "@/lib/rbac/catalog";
import {
  clearAllRbacCaches,
  clearCatalogCache,
  clearUserGrantsCache,
  getCachedUserGrants,
  setCachedUserGrants,
} from "@/lib/rbac/cache";
import { RBAC_REFRESH_EVENT, RBAC_UNAUTHORIZED_MESSAGE_AR } from "@/lib/rbac/constants";
import { getMappedActionPermissions } from "@/lib/rbac/action-permissions";
import { normalizePermissionKeys } from "@/lib/rbac/normalize";
import { buildPermissionSet, can, canAll, canAny, canCategory, type PermissionAction } from "@/lib/rbac/permissions";
import { getAllRoutePolicyPermissions } from "@/lib/rbac/route-policies";
import { clearRbacRuntimeState, setRbacRuntimeState } from "@/lib/rbac/runtime";

type RbacContextValue = {
  permissionSet: Set<string>;
  guardPermissionSet: Set<string>;
  catalogPermissionSet: Set<string>;
  catalogCategories: string[];
  loading: boolean;
  catalogLoading: boolean;
  guardReady: boolean;
  can: (key: string) => boolean;
  canAny: (keys: string[]) => boolean;
  canAll: (keys: string[]) => boolean;
  canCategory: (category: string, action?: PermissionAction) => boolean;
  guard: (key: string, message?: string) => boolean;
  refresh: () => Promise<void>;
};

const RbacContext = createContext<RbacContextValue | undefined>(undefined);

function runCatalogSanityChecks(grants: Set<string>, catalog: Set<string>): void {
  if (!catalog.size) {
    return;
  }

  const missingFromCatalog = Array.from(grants).filter((permission) => !catalog.has(permission));
  if (missingFromCatalog.length > 0) {
    console.warn("[RBAC] Granted permissions missing from catalog:", missingFromCatalog);
  }

  const policyPermissions = new Set([
    ...getAllRoutePolicyPermissions(),
    ...getMappedActionPermissions(),
  ]);

  const unusedCatalog = Array.from(catalog).filter((permission) => !policyPermissions.has(permission));
  if (unusedCatalog.length > 0) {
    console.warn("[RBAC] Catalog permissions not used by route/action policies:", unusedCatalog.slice(0, 25));
  }
}

export function PermissionsProvider({ children }: { children: React.ReactNode }) {
  const { user, isLoading: authLoading } = useAuth();

  const [permissionSet, setPermissionSet] = useState<Set<string>>(new Set());
  const [guardPermissionSet, setGuardPermissionSet] = useState<Set<string>>(new Set());
  const [catalogPermissionSet, setCatalogPermissionSet] = useState<Set<string>>(new Set());
  const [catalogCategories, setCatalogCategories] = useState<string[]>([]);
  const [catalogLoading, setCatalogLoading] = useState(false);
  const [permissionsHydrated, setPermissionsHydrated] = useState(false);
  const [guardReady, setGuardReady] = useState(false);

  const permissionSetRef = useRef<Set<string>>(new Set());
  const previousUserIdRef = useRef<number | null>(null);
  const previousPermissionsStateRef = useRef<string>("");
  const previousGuardStateRef = useRef<string>("");

  const userId = user?.id ?? null;

  const loadCatalog = useCallback(async (force = false): Promise<PermissionCatalogResult | null> => {
    if (!userId) {
      setCatalogPermissionSet(new Set());
      setCatalogCategories([]);
      return null;
    }

    setCatalogLoading(true);

    try {
      const catalog = await fetchAllPermissionCatalog(force);
      const catalogSet = buildPermissionSet(catalog.names);
      setCatalogPermissionSet(catalogSet);
      setCatalogCategories(catalog.categories);
      runCatalogSanityChecks(permissionSetRef.current, catalogSet);
      return catalog;
    } catch (error) {
      console.warn("[RBAC] Failed to fetch permissions catalog, continuing with user grants only.", error);
      return null;
    } finally {
      setCatalogLoading(false);
    }
  }, [userId]);

  useEffect(() => {
    permissionSetRef.current = permissionSet;
  }, [permissionSet]);

  useEffect(() => {
    if (!userId) {
      previousPermissionsStateRef.current = "";
      previousGuardStateRef.current = "";
      // During auth bootstrap, keep RBAC in loading mode to avoid false route denies.
      if (authLoading) {
        setPermissionsHydrated(false);
        setGuardReady(false);
      } else {
        setPermissionsHydrated(true);
        if (permissionSetRef.current.size > 0) {
          setPermissionSet(new Set());
        }
        setGuardPermissionSet(new Set());
        setGuardReady(false);
        clearRbacRuntimeState();
        clearAllRbacCaches();
      }
      return;
    }

    const rawAliasKeys = (user?.permissions || [])
      .filter((permission) => typeof permission === "string")
      .map((permission) => permission.trim().toLowerCase())
      .filter((permission) => permission.startsWith("usser."));
    if (rawAliasKeys.length > 0) {
      console.warn("[RBAC] Found legacy permissions alias keys in user grants:", rawAliasKeys);
    }

    const hasFreshGrants = !authLoading && Array.isArray(user?.permissions);
    const authGrants = hasFreshGrants ? normalizePermissionKeys(user?.permissions ?? []) : [];
    const cachedGrants = getCachedUserGrants(userId) || [];
    const uiGrants = hasFreshGrants ? authGrants : cachedGrants;
    const uiPermissionSource =
      hasFreshGrants ? "fresh" : cachedGrants.length > 0 ? "cached" : "none";
    const uiStateKey = `${uiPermissionSource}:${uiGrants.join("|")}`;

    if (uiStateKey !== previousPermissionsStateRef.current) {
      previousPermissionsStateRef.current = uiStateKey;
      setPermissionSet(buildPermissionSet(uiGrants));
    }

    const guardStateKey = hasFreshGrants ? `fresh:${authGrants.join("|")}` : "pending";
    if (guardStateKey !== previousGuardStateRef.current) {
      previousGuardStateRef.current = guardStateKey;
      setGuardPermissionSet(hasFreshGrants ? buildPermissionSet(authGrants) : new Set());
      setGuardReady(hasFreshGrants);
    }

    if (hasFreshGrants) {
      setCachedUserGrants(userId, authGrants);

      if (previousUserIdRef.current === userId) {
        clearCatalogCache();
        void loadCatalog(true);
      }
    }

    // Mark RBAC grants as hydrated once auth has resolved for the current user.
    // This prevents route guards from evaluating with an empty temporary permission set.
    if (!authLoading) {
      setPermissionsHydrated(true);
    }
  }, [authLoading, loadCatalog, user?.permissions, userId]);

  useEffect(() => {
    if (!userId) {
      previousUserIdRef.current = null;
      return;
    }

    if (previousUserIdRef.current && previousUserIdRef.current !== userId) {
      clearUserGrantsCache(previousUserIdRef.current);
      clearCatalogCache();
    }

    previousUserIdRef.current = userId;
    void loadCatalog(false);
  }, [loadCatalog, userId]);

  useEffect(() => {
    const handleRefresh = () => {
      clearCatalogCache();
      void loadCatalog(true);
    };

    if (typeof window !== "undefined") {
      window.addEventListener(RBAC_REFRESH_EVENT, handleRefresh);
    }

    return () => {
      if (typeof window !== "undefined") {
        window.removeEventListener(RBAC_REFRESH_EVENT, handleRefresh);
      }
    };
  }, [loadCatalog]);

  useEffect(() => {
    if (!userId) {
      return;
    }

    const effectivePermissionSet = guardReady ? guardPermissionSet : permissionSet;
    runCatalogSanityChecks(effectivePermissionSet, catalogPermissionSet);
  }, [catalogPermissionSet, guardPermissionSet, guardReady, permissionSet, userId]);

  useEffect(() => {
    if (!userId) {
      clearRbacRuntimeState();
      return;
    }

    setRbacRuntimeState({
      userId,
      grants: guardPermissionSet,
      ready: guardReady,
    });
  }, [guardPermissionSet, guardReady, userId]);

  const canPermission = useCallback((key: string) => can(permissionSet, key), [permissionSet]);
  const canAnyPermission = useCallback((keys: string[]) => canAny(permissionSet, keys), [permissionSet]);
  const canAllPermissions = useCallback((keys: string[]) => canAll(permissionSet, keys), [permissionSet]);
  const canPermissionCategory = useCallback(
    (category: string, action: PermissionAction = "view") => canCategory(permissionSet, category, action),
    [permissionSet],
  );

  const guard = useCallback(
    (key: string, message = RBAC_UNAUTHORIZED_MESSAGE_AR): boolean => {
      if (canPermission(key)) {
        return true;
      }

      toast.error(message);
      return false;
    },
    [canPermission],
  );

  const refresh = useCallback(async () => {
    clearCatalogCache();
    await loadCatalog(true);
  }, [loadCatalog]);

  const value = useMemo<RbacContextValue>(() => ({
    permissionSet,
    guardPermissionSet,
    catalogPermissionSet,
    catalogCategories,
    loading: authLoading || !permissionsHydrated,
    catalogLoading,
    guardReady,
    can: canPermission,
    canAny: canAnyPermission,
    canAll: canAllPermissions,
    canCategory: canPermissionCategory,
    guard,
    refresh,
  }), [
    authLoading,
    canAllPermissions,
    canAnyPermission,
    canPermission,
    canPermissionCategory,
    catalogCategories,
    catalogLoading,
    catalogPermissionSet,
    guardPermissionSet,
    guardReady,
    guard,
    permissionsHydrated,
    permissionSet,
    refresh,
  ]);

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

export function useRbac(): RbacContextValue {
  const context = useContext(RbacContext);
  if (!context) {
    throw new Error("useRbac must be used within PermissionsProvider");
  }

  return context;
}
