/**
 * 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, rememberDevice?: boolean) => 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;
  applyAuthResponse: (data: { user: AuthenticatedUser; session: AuthSession }) => 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;

/**
 * Helper: run an async auth action with standard loading/error handling.
 * Sets loading=true + error=null before, handles errors uniformly.
 */
async function runAuthAction<T>(
  set: (partial: Partial<AuthState>) => void,
  action: () => Promise<T>,
  options?: { markUnauthenticated?: boolean }
): Promise<T> {
  set({ loading: true, error: null });
  try {
    return await action();
  } catch (error) {
    const parsed = parseError(error);
    const errorState: Partial<AuthState> = { loading: false, error: parsed.message };
    if (options?.markUnauthenticated) {
      errorState.isAuthenticated = false;
    }
    set(errorState);
    throw error;
  }
}

/**
 * Parse an API response with a Zod schema, throwing on failure.
 */
function parseResponse<T>(
  schema: {
    safeParse: (
      data: unknown
    ) =>
      | { success: true; data: T }
      | { success: false; error: { issues?: Array<{ message: string }> } };
  },
  data: unknown,
  fallbackMessage: string
): T {
  const parsed = schema.safeParse(data);
  if (!parsed.success) {
    throw new Error(parsed.error.issues?.[0]?.message ?? fallbackMessage);
  }
  return parsed.data;
}

/**
 * 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;
}

function hydrateProfile(state: AuthState, profile: Partial<AuthenticatedUser>): Partial<AuthState> {
  if (!state.user) return {};
  const definedEntries = Object.entries(profile).filter(([, value]) => value !== undefined);
  if (definedEntries.length === 0) return {};
  return {
    user: { ...state.user, ...(Object.fromEntries(definedEntries) as Partial<AuthenticatedUser>) },
  };
}

async function fetchUserProfile(set: (partial: Partial<AuthState>) => void): 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 */
  }

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

async function refreshUserProfile(
  set: (partial: Partial<AuthState>) => void,
  get: () => AuthState,
  ensureSingleRefresh: (force?: boolean) => Promise<void>
): Promise<void> {
  try {
    await fetchUserProfile(set);
  } catch (error) {
    const parsed = parseError(error);
    if (parsed.shouldLogout) {
      const reason = logoutReasonFromErrorCode(parsed.code) ?? LOGOUT_REASON.SESSION_EXPIRED;
      await get().logout({ reason });
      return;
    }
    try {
      await ensureSingleRefresh(true);
      await fetchUserProfile(set);
    } catch (refreshError) {
      logger.error("Failed to refresh session after auth error", refreshError);
      set({ user: null, isAuthenticated: false, session: {} });
    }
  }
}

type SetState = (partial: Partial<AuthState> | ((state: AuthState) => Partial<AuthState>)) => void;
type GetState = () => AuthState;
type ApplyAuth = (data: AuthResponseData, keepLoading?: boolean) => void;
type EnsureRefresh = (force?: boolean) => Promise<void>;

function createAuthActions(
  set: SetState,
  get: GetState,
  applyAuthResponse: ApplyAuth,
  ensureSingleRefresh: EnsureRefresh
) {
  return {
    signup: async (data: SignupRequest) => {
      await runAuthAction(set, async () => {
        const response = await apiClient.POST("/api/auth/signup", {
          body: data,
          disableCsrf: true,
        });
        applyAuthResponse(parseResponse(authResponseSchema, response.data, "Signup failed"));
      });
    },
    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) => {
      await runAuthAction(set, async () => {
        await apiClient.POST("/api/auth/request-password-reset", {
          body: { email },
          disableCsrf: true,
        });
        set({ loading: false });
      });
    },
    resetPassword: async (token: string, password: string) => {
      await runAuthAction(set, async () => {
        await apiClient.POST("/api/auth/reset-password", {
          body: { token, password },
          disableCsrf: true,
        });
        set({ user: null, session: {}, isAuthenticated: false, loading: false, error: null });
      });
    },
    changePassword: async (currentPassword: string, newPassword: string) => {
      await runAuthAction(set, async () => {
        const response = await apiClient.POST("/api/auth/change-password", {
          body: { currentPassword, newPassword },
        });
        applyAuthResponse(
          parseResponse(authResponseSchema, response.data, "Password change failed")
        );
      });
    },
    checkPasswordNeeded: async (email: string) => {
      return runAuthAction(set, async () => {
        const response = await apiClient.POST("/api/auth/check-password-needed", {
          body: { email },
          disableCsrf: true,
        });
        const data = parseResponse(
          checkPasswordNeededResponseSchema,
          response.data,
          "Check failed"
        );
        set({ loading: false });
        return data;
      });
    },
    setPassword: async (email: string, password: string) => {
      await runAuthAction(set, async () => {
        const response = await apiClient.POST("/api/auth/set-password", {
          body: { email, password },
          disableCsrf: true,
        });
        applyAuthResponse(parseResponse(authResponseSchema, response.data, "Set password failed"));
      });
    },
    refreshUser: async () => {
      await refreshUserProfile(set, get, ensureSingleRefresh);
    },
    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 }),
    applyAuthResponse,
    hydrateUserProfile: (profile: Partial<AuthenticatedUser>) => {
      set(state => hydrateProfile(state, profile));
    },
  };
}

async function runTokenRefresh(applyAuthResponse: ApplyAuth, get: GetState): Promise<void> {
  try {
    const response = await apiClient.POST("/api/auth/refresh", { body: {} });
    rateLimitBackoffUntil = 0;
    applyAuthResponse(parseResponse(authResponseSchema, response.data, "Session refresh failed"));
  } catch (error) {
    logger.error("Failed to refresh session", error);
    const parsed = parseError(error);
    if (parsed.code === "NET_003") {
      rateLimitBackoffUntil = Date.now() + 60 * 1000;
      logger.warn("Token refresh rate limited, backing off for 60s");
      throw error;
    }
    if (parsed.shouldLogout) {
      const reason = logoutReasonFromErrorCode(parsed.code) ?? LOGOUT_REASON.SESSION_EXPIRED;
      await get().logout({ reason });
    }
    throw error;
  }
}

function createRefreshManager(applyAuthResponse: ApplyAuth, get: GetState) {
  let refreshPromise: Promise<void> | null = null;
  return async (force = false): Promise<void> => {
    if (!force && !shouldRefreshToken(get().session)) {
      logger.debug("Skipping refresh: token not expiring soon");
      return;
    }
    if (!refreshPromise) {
      refreshPromise = runTokenRefresh(applyAuthResponse, get).finally(() => {
        refreshPromise = null;
      });
    }
    await refreshPromise;
  };
}

function createLoginActions(set: SetState, applyAuthResponse: ApplyAuth) {
  return {
    initiateLogin: async (credentials: LoginRequest) => {
      return runAuthAction(
        set,
        async () => {
          const response = await apiClient.POST("/api/auth/login", {
            body: credentials,
            disableCsrf: true,
          });
          const data = parseResponse(loginResponseSchema, response.data, "Login failed");
          if ("requiresOtp" in data && data.requiresOtp === true) {
            set({ loading: false });
            return data;
          }
          applyAuthResponse(data as AuthResponseData);
          return { requiresOtp: false as const };
        },
        { markUnauthenticated: true }
      );
    },
    verifyLoginOtp: async (sessionToken: string, code: string, rememberDevice?: boolean) => {
      await runAuthAction(
        set,
        async () => {
          const body: LoginVerifyOtpRequest = {
            sessionToken,
            code,
            ...(rememberDevice !== undefined && { rememberDevice }),
          };
          const response = await apiClient.POST("/api/auth/login/verify-otp", {
            body,
            disableCsrf: true,
          });
          applyAuthResponse(
            parseResponse(authResponseSchema, response.data, "Verification failed")
          );
        },
        { markUnauthenticated: true }
      );
    },
    /** @deprecated Use initiateLogin + verifyLoginOtp instead */
    login: async (credentials: LoginRequest) => {
      await runAuthAction(
        set,
        async () => {
          const response = await apiClient.POST("/api/auth/login", {
            body: credentials,
            disableCsrf: true,
          });
          const otpParsed = loginOtpRequiredResponseSchema.safeParse(response.data);
          if (otpParsed.success) {
            set({ loading: false });
            throw new Error("OTP verification required. Please use the updated login flow.");
          }
          applyAuthResponse(parseResponse(authResponseSchema, response.data, "Login failed"));
        },
        { markUnauthenticated: true }
      );
    },
  };
}

function applyAuthResponseFactory(set: SetState): ApplyAuth {
  return (data: AuthResponseData, keepLoading = false) => {
    set({
      user: data.user,
      session: {
        accessExpiresAt: data.session.expiresAt,
        refreshExpiresAt: data.session.refreshExpiresAt,
      },
      isAuthenticated: true,
      hasCheckedAuth: true,
      loading: keepLoading,
      error: null,
    });
  };
}

export const useAuthStore = create<AuthState>()((set, get) => {
  const applyAuthResponse = applyAuthResponseFactory(set);
  const ensureSingleRefresh = createRefreshManager(applyAuthResponse, get);

  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,
    ...createLoginActions(set, applyAuthResponse),
    ...createAuthActions(set, get, applyAuthResponse, ensureSingleRefresh),
  };
});

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,
  };
};
