import {
  createContext,
  useContext,
  useState,
  useEffect,
  useCallback,
  useMemo,
  useRef,
  type ReactNode,
} from "react";
import { setSessionExpired, setTokenGetter, setRefreshFn } from "../utils/api";
import { queryClient } from "../lib/queryClient";

const API_BASE = "/api/admin";

interface User {
  id: number;
  username: string;
  email: string;
  fullName: string;
  roleDisplay: string;
  isAdmin: boolean;
  totpEnabled: boolean;
  require2FA: boolean;
  permissions: string[];
  [key: string]: unknown;
}

interface AuthState {
  user: User | null;
  loading: boolean;
  error: string | null;
  isAuthenticated: boolean;
  isAdmin: boolean;
  permissions: string[];
  hasPermission: (permission: string) => boolean;
}

interface AuthActions {
  login: (
    username: string,
    password: string,
    remember?: boolean,
  ) => Promise<{
    success: boolean;
    requires2FA?: boolean;
    loginToken?: string;
    error?: string;
    remember?: boolean;
  }>;
  verify2FA: (
    loginToken: string,
    code: string,
    remember?: boolean,
    isBackup?: boolean,
  ) => Promise<{ success: boolean; error?: string }>;
  logout: () => Promise<void>;
  checkSession: () => Promise<boolean>;
  getAccessToken: () => string | null;
  apiRequest: (endpoint: string, options?: RequestInit) => Promise<Response>;
  silentRefresh: () => Promise<boolean>;
  updateUser: (updates: Partial<User>) => void;
}

const AuthStateContext = createContext<AuthState | null>(null);
const AuthActionsContext = createContext<AuthActions | null>(null);

function mapUser(u: Record<string, unknown> | null): User | null {
  if (!u) return null;
  const id = (u.userId ?? u.id) as number;
  const firstName = (u.firstName ?? u.first_name ?? "") as string;
  const lastName = (u.lastName ?? u.last_name ?? "") as string;
  const roleName = (u.roleName ?? u.role_name ?? "") as string;
  return {
    ...u,
    id,
    fullName: (u.fullName ??
      u.full_name ??
      `${firstName} ${lastName}`.trim()) as string,
    roleDisplay: (u.roleDisplay ?? u.role_display ?? roleName) as string,
    isAdmin: (u.isAdmin ?? u.is_admin ?? roleName === "admin") as boolean,
    totpEnabled: (u.totpEnabled ?? u.totp_enabled ?? false) as boolean,
    require2FA: (u.require2FA ?? u.require_2fa ?? false) as boolean,
    permissions: (u.permissions ?? []) as string[],
  } as User;
}

export function AuthProvider({ children }: { children: ReactNode }) {
  const accessTokenRef = useRef<string | null>(null);
  const tokenExpiresAtRef = useRef<number | null>(null);
  const cachedUserRef = useRef<User | null>(null);
  const sessionFetchedRef = useRef(false);
  const silentRefreshInFlightRef = useRef<Promise<boolean> | null>(null);
  const hadValidSessionRef = useRef(false);
  const [user, setUser] = useState<User | null>(cachedUserRef.current);
  const [loading, setLoading] = useState(!sessionFetchedRef.current);
  const [error, setError] = useState<string | null>(null);
  const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const getAccessTokenFn = useCallback((): string | null => {
    if (
      !tokenExpiresAtRef.current ||
      Date.now() > tokenExpiresAtRef.current - 30000
    )
      return null;
    return accessTokenRef.current;
  }, []);

  const setAccessTokenFn = useCallback(
    (token: string | null, expiresIn?: number) => {
      const ttl = expiresIn ?? 900; // default 15 min matching backend config
      accessTokenRef.current = token;
      tokenExpiresAtRef.current = token ? Date.now() + ttl * 1000 : null;
      if (refreshTimeoutRef.current) {
        clearTimeout(refreshTimeoutRef.current);
        refreshTimeoutRef.current = null;
      }
      if (token && ttl > 60) {
        refreshTimeoutRef.current = setTimeout(
          () => silentRefresh(),
          (ttl - 60) * 1000,
        );
      }
    },
    // silentRefresh and setAccessTokenFn reference each other; listing
    // silentRefresh here would recreate both callbacks on every render. The
    // scheduled silentRefresh is safe to call as a stable closure because all
    // of its state lives in refs.
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [],
  );

  const silentRefresh = useCallback(async (): Promise<boolean> => {
    // Deduplicate concurrent refresh calls — token rotation means only one call can succeed
    if (silentRefreshInFlightRef.current)
      return silentRefreshInFlightRef.current;

    const promise = (async (): Promise<boolean> => {
      try {
        const response = await fetch(`${API_BASE}/refresh`, {
          method: "POST",
          credentials: "include",
        });
        const data = await response.json();
        if (data.success && data.data?.access_token) {
          setAccessTokenFn(data.data.access_token, data.data.expires_in);
          setUser(mapUser(data.data.user));
          hadValidSessionRef.current = true;
          return true;
        }
        accessTokenRef.current = null;
        tokenExpiresAtRef.current = null;
        setUser(null);
        cachedUserRef.current = null;
        if (hadValidSessionRef.current) setSessionExpired();
        return false;
      } catch {
        // Network error — don't kick the user out, just return false
        return false;
      } finally {
        silentRefreshInFlightRef.current = null;
      }
    })();

    silentRefreshInFlightRef.current = promise;
    return promise;
  }, [setAccessTokenFn]);

  const checkSession = useCallback(async (): Promise<boolean> => {
    try {
      const token = getAccessTokenFn();
      if (token) {
        const headers: Record<string, string> = {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        };
        const response = await fetch(`${API_BASE}/session`, {
          method: "GET",
          credentials: "include",
          headers,
        });
        if (response.status === 429 || response.status >= 500)
          return !!cachedUserRef.current;
        const data = await response.json();
        if (data.success && data.data?.user) {
          if (data.data.access_token) setAccessTokenFn(data.data.access_token);
          setUser(mapUser(data.data.user));
          cachedUserRef.current = mapUser(data.data.user);
          hadValidSessionRef.current = true;
          return true;
        }
      }
      // No token or session invalid — try silent refresh via cookie
      const refreshed = await silentRefresh();
      if (refreshed) return true;
      setUser(null);
      cachedUserRef.current = null;
      accessTokenRef.current = null;
      tokenExpiresAtRef.current = null;
      return false;
    } catch {
      return !!cachedUserRef.current;
    } finally {
      setLoading(false);
      sessionFetchedRef.current = true;
    }
  }, [getAccessTokenFn, setAccessTokenFn, silentRefresh]);

  useEffect(() => {
    setTokenGetter(getAccessTokenFn);
    setRefreshFn(silentRefresh);
  }, [getAccessTokenFn, silentRefresh]);

  useEffect(() => {
    checkSession();
    return () => {
      if (refreshTimeoutRef.current) clearTimeout(refreshTimeoutRef.current);
    };
  }, [checkSession]);

  const login = useCallback(
    async (username: string, password: string, remember = false) => {
      setError(null);
      try {
        const response = await fetch(`${API_BASE}/login`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          credentials: "include",
          body: JSON.stringify({ username, password, remember_me: remember }),
        });
        const data = await response.json();
        if (data.success) {
          if (data.data?.totp_required) {
            return {
              success: false,
              requires2FA: true,
              loginToken: data.data.login_token,
              remember,
            };
          }
          // Query keys are user-agnostic (["dashboard"], ["attendance"], …),
          // so anything cached from a previously logged-in user would be
          // served to this one — drop the whole cache on every login.
          queryClient.clear();
          setAccessTokenFn(data.data.access_token, data.data.expires_in);
          setUser(mapUser(data.data.user));
          cachedUserRef.current = mapUser(data.data.user);
          sessionFetchedRef.current = true;
          hadValidSessionRef.current = true;
          return { success: true };
        }
        setError(data.error);
        return { success: false, error: data.error };
      } catch {
        const errorMsg =
          "Chyba připojení. Zkontrolujte prosím připojení k internetu a zkuste to znovu.";
        setError(errorMsg);
        return { success: false, error: errorMsg };
      }
    },
    [setAccessTokenFn],
  );

  const verify2FA = useCallback(
    async (
      loginToken: string,
      code: string,
      remember = false,
      isBackup = false,
    ) => {
      setError(null);
      try {
        // Backup codes have their own endpoint + schema (8-char codes would
        // fail /login/totp's strict 6-digit TotpVerifySchema). Both endpoints
        // complete the same login flow: tokens issued + refresh cookie set.
        const response = await fetch(
          isBackup
            ? `${API_BASE}/totp/backup-verify`
            : `${API_BASE}/login/totp`,
          {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            credentials: "include",
            body: JSON.stringify(
              isBackup
                ? {
                    login_token: loginToken,
                    backup_code: code,
                    remember_me: remember,
                  }
                : {
                    login_token: loginToken,
                    totp_code: code,
                    remember_me: remember,
                  },
            ),
          },
        );
        const data = await response.json();
        if (data.success) {
          // Same reason as in login(): the cache may hold the previous
          // user's data under user-agnostic keys.
          queryClient.clear();
          setAccessTokenFn(data.data.access_token, data.data.expires_in);
          setUser(mapUser(data.data.user));
          cachedUserRef.current = mapUser(data.data.user);
          sessionFetchedRef.current = true;
          hadValidSessionRef.current = true;
          return { success: true };
        }
        setError(data.error);
        return { success: false, error: data.error };
      } catch {
        const errorMsg = "Chyba připojení.";
        setError(errorMsg);
        return { success: false, error: errorMsg };
      }
    },
    [setAccessTokenFn],
  );

  const logout = useCallback(async () => {
    try {
      const token = getAccessTokenFn();
      await fetch(`${API_BASE}/logout`, {
        method: "POST",
        headers: { ...(token && { Authorization: `Bearer ${token}` }) },
        credentials: "include",
      });
    } catch {
      /* ignore */
    } finally {
      accessTokenRef.current = null;
      tokenExpiresAtRef.current = null;
      setUser(null);
      cachedUserRef.current = null;
      sessionFetchedRef.current = false;
      hadValidSessionRef.current = false;
      if (refreshTimeoutRef.current) {
        clearTimeout(refreshTimeoutRef.current);
        refreshTimeoutRef.current = null;
      }
      // The React Query cache outlives the session — without this, the next
      // user to log in on this browser sees the previous user's cached
      // dashboards/lists until each query refetches.
      queryClient.clear();
    }
  }, [getAccessTokenFn]);

  // NOTE: this is a SECOND 401-refresh-and-retry path that parallels
  // `apiFetch` in src/admin/utils/api.ts (which most pages use via the query
  // helpers). The two diverge: `apiRequest` ignores AbortSignals and does NOT
  // set the session-expired flag, whereas `apiFetch` dedupes concurrent
  // refreshes, honors abort, and surfaces the 499 session-expired sentinel.
  // Consolidating onto `apiFetch` is a tracked follow-up but is deferred here
  // because it touches the core auth/refresh flow (high regression risk). If
  // you change refresh/retry semantics in one, mirror it in the other.
  const apiRequest = useCallback(
    async (endpoint: string, options: RequestInit = {}) => {
      let token = getAccessTokenFn();
      if (!token && user) {
        const refreshed = await silentRefresh();
        if (refreshed) token = getAccessTokenFn();
      }
      const headers: Record<string, string> = {
        "Content-Type": "application/json",
        ...(options.headers as Record<string, string>),
      };
      if (token) headers["Authorization"] = `Bearer ${token}`;
      const response = await fetch(`${API_BASE}${endpoint}`, {
        ...options,
        headers,
        credentials: "include",
      });
      if (response.status === 401 && user) {
        const refreshed = await silentRefresh();
        if (refreshed) {
          token = getAccessTokenFn();
          if (token) headers["Authorization"] = `Bearer ${token}`;
          return fetch(`${API_BASE}${endpoint}`, {
            ...options,
            headers,
            credentials: "include",
          });
        }
      }
      return response;
    },
    [getAccessTokenFn, silentRefresh, user],
  );

  const updateUser = useCallback((updates: Partial<User>) => {
    setUser((prev) => (prev ? { ...prev, ...updates } : null));
  }, []);

  const hasPermission = useCallback(
    (permission: string): boolean => {
      if (!user) return false;
      if (user.isAdmin) return true;
      return (user.permissions || []).includes(permission);
    },
    [user],
  );

  const permissions = useMemo(() => user?.permissions || [], [user]);

  const stateValue = useMemo<AuthState>(
    () => ({
      user,
      loading,
      error,
      isAuthenticated: !!user,
      isAdmin: user?.isAdmin || false,
      permissions,
      hasPermission,
    }),
    [user, loading, error, permissions, hasPermission],
  );

  const actionsValue = useMemo<AuthActions>(
    () => ({
      login,
      verify2FA,
      logout,
      checkSession,
      getAccessToken: getAccessTokenFn,
      apiRequest,
      silentRefresh,
      updateUser,
    }),
    [
      login,
      verify2FA,
      logout,
      checkSession,
      getAccessTokenFn,
      apiRequest,
      silentRefresh,
      updateUser,
    ],
  );

  return (
    <AuthActionsContext.Provider value={actionsValue}>
      <AuthStateContext.Provider value={stateValue}>
        {children}
      </AuthStateContext.Provider>
    </AuthActionsContext.Provider>
  );
}

export function useAuth(): AuthState & AuthActions {
  const state = useContext(AuthStateContext);
  const actions = useContext(AuthActionsContext);
  if (!state || !actions)
    throw new Error("useAuth must be used within an AuthProvider");
  return { ...state, ...actions };
}

export function useAuthState(): AuthState {
  const context = useContext(AuthStateContext);
  if (!context)
    throw new Error("useAuthState must be used within an AuthProvider");
  return context;
}

export function useAuthActions(): AuthActions {
  const context = useContext(AuthActionsContext);
  if (!context)
    throw new Error("useAuthActions must be used within an AuthProvider");
  return context;
}

export default AuthStateContext;
