/**
 * Client-Side Authentication Store
 * Maintains session state using secure httpOnly cookies issued by the BFF.
 */

import { create } from "zustand";
import { apiClient, getNullableData, onUnauthorized } from "@/core/api";
import { parseError } from "@/shared/utils";
import { logger } from "@/core/logger";
import {
  authResponseSchema,
  checkPasswordNeededResponseSchema,
  loginOtpRequiredResponseSchema,
  loginResponseSchema,
  type AuthSession,
  type CheckPasswordNeededResponse,
  type LoginRequest,
  type SignupRequest,
  type LoginOtpRequiredResponse,
  type LoginVerifyOtpRequest,
} from "@customer-portal/domain/auth";
import type { AuthenticatedUser } from "@customer-portal/domain/customer";
import {
  clearLogoutReason,
  LOGOUT_REASON,
  logoutReasonFromErrorCode,
  setLogoutReason,
  type LogoutReason,
} from "@/features/auth/utils/logout-reason";

interface SessionState {
  accessExpiresAt?: string;
  refreshExpiresAt?: string;
}

export interface AuthState {
  user: AuthenticatedUser | null;
  session: SessionState;
  isAuthenticated: boolean;
  loading: boolean;
  error: string | null;
  hasCheckedAuth: boolean;

  // Two-step login with OTP (or direct login in dev mode with SKIP_OTP)
  initiateLogin: (
    credentials: LoginRequest
  ) => Promise<LoginOtpRequiredResponse | { requiresOtp: false }>;
  verifyLoginOtp: (sessionToken: string, code: string) => Promise<void>;
  // Legacy login (kept for backward compatibility during migration)
  login: (credentials: LoginRequest) => Promise<void>;
  signup: (data: SignupRequest) => Promise<void>;
  logout: (options?: { reason?: LogoutReason }) => Promise<void>;
  requestPasswordReset: (email: string) => Promise<void>;
  resetPassword: (token: string, password: string) => Promise<void>;
  changePassword: (currentPassword: string, newPassword: string) => Promise<void>;
  checkPasswordNeeded: (email: string) => Promise<CheckPasswordNeededResponse>;
  setPassword: (email: string, password: string) => Promise<void>;
  refreshUser: () => Promise<void>;
  refreshSession: () => Promise<void>;
  checkAuth: () => Promise<void>;
  clearError: () => void;
  clearLoading: () => void;
  hydrateUserProfile: (profile: Partial<AuthenticatedUser>) => void;
}

type AuthResponseData = {
  user: AuthenticatedUser;
  session: AuthSession;
};

let unauthorizedSubscriptionInitialized = false;

// Rate limit backoff state (module-level to persist across store recreations)
let rateLimitBackoffUntil = 0;

/**
 * Check if we should actually refresh the token.
 * Only refresh if:
 * 1. We're not in a rate limit backoff period
 * 2. The token is expiring within the threshold (default: 5 minutes)
 */
function shouldRefreshToken(session: SessionState, thresholdMs = 5 * 60 * 1000): boolean {
  // Don't refresh if we're in a rate limit backoff
  if (Date.now() < rateLimitBackoffUntil) {
    logger.debug("Skipping refresh: rate limit backoff active");
    return false;
  }

  // If we don't have expiry info, refresh to get it
  if (!session.accessExpiresAt) {
    return true;
  }

  const expiresAt = new Date(session.accessExpiresAt).getTime();
  const now = Date.now();
  const timeUntilExpiry = expiresAt - now;

  // Only refresh if token expires within threshold
  return timeUntilExpiry < thresholdMs;
}

export const useAuthStore = create<AuthState>()((set, get) => {
  const applyAuthResponse = (data: AuthResponseData, keepLoading = false) => {
    set({
      user: data.user,
      session: {
        accessExpiresAt: data.session.expiresAt,
        refreshExpiresAt: data.session.refreshExpiresAt,
      },
      isAuthenticated: true,
      hasCheckedAuth: true, // Auth verified via successful login/signup
      loading: keepLoading,
      error: null,
    });
  };

  let refreshPromise: Promise<void> | null = null;

  const runTokenRefresh = async (): Promise<void> => {
    try {
      const response = await apiClient.POST("/api/auth/refresh", { body: {} });
      const parsed = authResponseSchema.safeParse(response.data);
      if (!parsed.success) {
        throw new Error(parsed.error.issues?.[0]?.message ?? "Session refresh failed");
      }
      // Clear any rate limit backoff on successful refresh
      rateLimitBackoffUntil = 0;
      applyAuthResponse(parsed.data);
    } catch (error) {
      logger.error("Failed to refresh session", error);
      const parsed = parseError(error);

      // Handle rate limiting: set backoff period instead of logging out
      // The token is still valid, user just needs to wait
      if (parsed.code === "NET_003") {
        // Back off for 60 seconds on rate limit (will be extended if hit again)
        rateLimitBackoffUntil = Date.now() + 60 * 1000;
        logger.warn("Token refresh rate limited, backing off for 60s");
        throw error;
      }

      // Only logout for errors that require it (e.g., token revoked, session expired)
      if (parsed.shouldLogout) {
        const reason = logoutReasonFromErrorCode(parsed.code) ?? LOGOUT_REASON.SESSION_EXPIRED;
        await get().logout({ reason });
      }

      throw error;
    }
  };

  /**
   * Ensures only one refresh runs at a time, and only if the token actually needs refreshing.
   * @param force - If true, skip the expiry check (used when we know the token is invalid)
   */
  const ensureSingleRefresh = async (force = false): Promise<void> => {
    // Check if we should refresh (unless forced)
    if (!force && !shouldRefreshToken(get().session)) {
      logger.debug("Skipping refresh: token not expiring soon");
      return;
    }

    if (!refreshPromise) {
      refreshPromise = (async () => {
        try {
          await runTokenRefresh();
        } finally {
          refreshPromise = null;
        }
      })();
    }

    await refreshPromise;
  };

  // Set up global listener for 401 errors from API client.
  // This is intentionally NOT a DOM event to keep it testable and SSR-safe.
  // Guard to prevent duplicate subscriptions during dev/HMR.
  if (!unauthorizedSubscriptionInitialized) {
    unauthorizedSubscriptionInitialized = true;
    onUnauthorized(detail => {
      logger.warn("401 Unauthorized detected - triggering logout", {
        url: detail?.url,
        status: detail?.status,
      });
      void get().logout({ reason: LOGOUT_REASON.SESSION_EXPIRED });
    });
  }

  return {
    user: null,
    session: {},
    isAuthenticated: false,
    loading: false,
    error: null,
    hasCheckedAuth: false,

    /**
     * Step 1 of two-step login: Validate credentials and initiate OTP
     * Returns OTP session info for the frontend to display OTP input
     * In dev mode with SKIP_OTP=true, returns direct login result
     */
    initiateLogin: async credentials => {
      set({ loading: true, error: null });
      try {
        const response = await apiClient.POST("/api/auth/login", {
          body: credentials,
          disableCsrf: true, // Public auth endpoint, exempt from CSRF
        });
        const parsed = loginResponseSchema.safeParse(response.data);
        if (!parsed.success) {
          throw new Error(parsed.error.issues?.[0]?.message ?? "Login failed");
        }

        // Check if OTP is required or if we got a direct login (dev mode)
        if ("requiresOtp" in parsed.data && parsed.data.requiresOtp === true) {
          set({ loading: false });
          return parsed.data;
        }

        // Direct login - apply auth response and signal completion
        applyAuthResponse(parsed.data as AuthResponseData);
        return { requiresOtp: false as const };
      } catch (error) {
        const parsed = parseError(error);
        set({ loading: false, error: parsed.message, isAuthenticated: false });
        throw error;
      }
    },

    /**
     * Step 2 of two-step login: Verify OTP and complete login
     */
    verifyLoginOtp: async (sessionToken, code) => {
      set({ loading: true, error: null });
      try {
        const body: LoginVerifyOtpRequest = { sessionToken, code };
        const response = await apiClient.POST("/api/auth/login/verify-otp", {
          body,
          disableCsrf: true, // Public auth endpoint, exempt from CSRF
        });
        const parsed = authResponseSchema.safeParse(response.data);
        if (!parsed.success) {
          throw new Error(parsed.error.issues?.[0]?.message ?? "Verification failed");
        }
        applyAuthResponse(parsed.data);
      } catch (error) {
        const parsed = parseError(error);
        set({ loading: false, error: parsed.message, isAuthenticated: false });
        throw error;
      }
    },

    /**
     * Legacy login method - now redirects through two-step flow
     * Kept for backward compatibility but will throw since OTP is always required
     * @deprecated Use initiateLogin + verifyLoginOtp instead
     */
    login: async credentials => {
      set({ loading: true, error: null });
      try {
        const response = await apiClient.POST("/api/auth/login", {
          body: credentials,
          disableCsrf: true, // Public auth endpoint, exempt from CSRF
        });
        // With OTP required, this will always return OTP response, not auth response
        // This legacy method cannot complete login - use the two-step flow
        const otpParsed = loginOtpRequiredResponseSchema.safeParse(response.data);
        if (otpParsed.success) {
          set({ loading: false });
          throw new Error("OTP verification required. Please use the updated login flow.");
        }
        // Fallback for any edge case where direct login works
        const parsed = authResponseSchema.safeParse(response.data);
        if (!parsed.success) {
          throw new Error(parsed.error.issues?.[0]?.message ?? "Login failed");
        }
        applyAuthResponse(parsed.data);
      } catch (error) {
        const parsed = parseError(error);
        set({ loading: false, error: parsed.message, isAuthenticated: false });
        throw error;
      }
    },

    signup: async data => {
      set({ loading: true, error: null });
      try {
        const response = await apiClient.POST("/api/auth/signup", {
          body: data,
          disableCsrf: true, // Public auth endpoint, exempt from CSRF
        });
        const parsed = authResponseSchema.safeParse(response.data);
        if (!parsed.success) {
          throw new Error(parsed.error.issues?.[0]?.message ?? "Signup failed");
        }
        applyAuthResponse(parsed.data);
      } catch (error) {
        set({
          loading: false,
          error: error instanceof Error ? error.message : "Signup failed",
        });
        throw error;
      }
    },

    logout: async (options?: { reason?: LogoutReason }) => {
      if (options?.reason) {
        setLogoutReason(options.reason);
      } else {
        clearLogoutReason();
      }
      try {
        await apiClient.POST("/api/auth/logout", {});
      } catch (error) {
        logger.warn("Logout API call failed", { error });
      } finally {
        set({
          user: null,
          session: {},
          isAuthenticated: false,
          error: null,
          loading: false,
        });
      }
    },

    requestPasswordReset: async (email: string) => {
      set({ loading: true, error: null });
      try {
        await apiClient.POST("/api/auth/request-password-reset", {
          body: { email },
          disableCsrf: true, // Public auth endpoint, exempt from CSRF
        });
        set({ loading: false });
      } catch (error) {
        set({
          loading: false,
          error: error instanceof Error ? error.message : "Password reset request failed",
        });
        throw error;
      }
    },

    resetPassword: async (token: string, password: string) => {
      set({ loading: true, error: null });
      try {
        await apiClient.POST("/api/auth/reset-password", {
          body: { token, password },
          disableCsrf: true, // Public auth endpoint, exempt from CSRF
        });
        // Password reset does NOT create a session; BFF clears auth cookies to force re-login.
        set({
          user: null,
          session: {},
          isAuthenticated: false,
          loading: false,
          error: null,
        });
      } catch (error) {
        set({
          loading: false,
          error: error instanceof Error ? error.message : "Password reset failed",
        });
        throw error;
      }
    },

    changePassword: async (currentPassword: string, newPassword: string) => {
      set({ loading: true, error: null });
      try {
        const response = await apiClient.POST("/api/auth/change-password", {
          body: { currentPassword, newPassword },
        });
        const parsed = authResponseSchema.safeParse(response.data);
        if (!parsed.success) {
          throw new Error(parsed.error.issues?.[0]?.message ?? "Password change failed");
        }
        applyAuthResponse(parsed.data);
      } catch (error) {
        set({
          loading: false,
          error: error instanceof Error ? error.message : "Password change failed",
        });
        throw error;
      }
    },

    checkPasswordNeeded: async (email: string) => {
      set({ loading: true, error: null });
      try {
        const response = await apiClient.POST("/api/auth/check-password-needed", {
          body: { email },
          disableCsrf: true, // Public auth endpoint, exempt from CSRF
        });

        const parsed = checkPasswordNeededResponseSchema.safeParse(response.data);
        if (!parsed.success) {
          throw new Error(parsed.error.issues?.[0]?.message ?? "Check failed");
        }

        set({ loading: false });
        return parsed.data;
      } catch (error) {
        set({
          loading: false,
          error: error instanceof Error ? error.message : "Check failed",
        });
        throw error;
      }
    },

    setPassword: async (email: string, password: string) => {
      set({ loading: true, error: null });
      try {
        const response = await apiClient.POST("/api/auth/set-password", {
          body: { email, password },
          disableCsrf: true, // Public auth endpoint, exempt from CSRF
        });
        const parsed = authResponseSchema.safeParse(response.data);
        if (!parsed.success) {
          throw new Error(parsed.error.issues?.[0]?.message ?? "Set password failed");
        }
        applyAuthResponse(parsed.data);
      } catch (error) {
        set({
          loading: false,
          error: error instanceof Error ? error.message : "Set password failed",
        });
        throw error;
      }
    },

    refreshUser: async () => {
      const fetchProfile = async (): Promise<void> => {
        try {
          const profileResponse = await apiClient.GET<AuthenticatedUser>("/api/me");
          const profile = getNullableData(profileResponse);
          if (profile) {
            set({
              user: profile,
              isAuthenticated: true,
              error: null,
            });
            return;
          }
        } catch {
          // Fall back to lightweight auth check.
        }

        try {
          const authResponse = await apiClient.GET<{
            isAuthenticated?: boolean;
            user?: AuthenticatedUser;
          }>("/api/auth/me");
          const authData = getNullableData(authResponse);
          if (authData?.isAuthenticated && authData.user) {
            set({
              user: authData.user,
              isAuthenticated: true,
              error: null,
            });
            return;
          }
        } catch {
          // Both auth checks failed - user is not authenticated
        }

        set({ user: null, isAuthenticated: false, session: {} });
      };

      try {
        await fetchProfile();
      } catch (error) {
        const parsed = parseError(error);
        if (parsed.shouldLogout) {
          const reason = logoutReasonFromErrorCode(parsed.code) ?? LOGOUT_REASON.SESSION_EXPIRED;
          await get().logout({ reason });
          return;
        }

        try {
          // Force refresh since we got an auth error - the token is invalid
          await ensureSingleRefresh(true);
          await fetchProfile();
        } catch (refreshError) {
          logger.error("Failed to refresh session after auth error", refreshError);
          // Ensure we mark user as not authenticated when refresh fails
          set({ user: null, isAuthenticated: false, session: {} });
        }
      }
    },

    refreshSession: async () => {
      await ensureSingleRefresh();
    },

    checkAuth: async () => {
      set({ loading: true });
      try {
        await get().refreshUser();
      } finally {
        set({ hasCheckedAuth: true, loading: false });
      }
    },

    clearError: () => set({ error: null }),

    clearLoading: () => set({ loading: false }),

    hydrateUserProfile: profile => {
      set(state => {
        if (!state.user) {
          return state;
        }

        const definedEntries = Object.entries(profile).filter(([, value]) => value !== undefined);
        if (definedEntries.length === 0) {
          return state;
        }

        return {
          ...state,
          user: {
            ...state.user,
            ...(Object.fromEntries(definedEntries) as Partial<AuthenticatedUser>),
          },
        };
      });
    },
  };
});

export const selectIsAuthenticated = (state: AuthState) => state.isAuthenticated;
export const selectAuthUser = (state: AuthState) => state.user;
export const selectSession = (state: AuthState) => state.session;

export const useAuthSession = () => {
  const user = useAuthStore(selectAuthUser);
  const isAuthenticated = useAuthStore(selectIsAuthenticated);
  const session = useAuthStore(selectSession);

  return {
    user,
    isAuthenticated,
    session,
  };
};
