import type { FetchError, ApiErrorPayload } from "../types/api";
import { isAuthError, isAbortError, isApiError, isNetworkError } from "../types/api";
import { recordWarning, recordError } from "./telemetry";

// ---------------------------------------------------------------------------
// Auth error handler registration
// ---------------------------------------------------------------------------

/**
 * Module-level callback invoked when an auth error (401/403) is encountered
 * in handleFetchError and no other error handler has processed it.
 *
 * Set via setAuthErrorHandler() and called to ensure auth errors are never
 * silently swallowed. If no handler is registered, auth errors are logged as
 * warnings to ensure they remain actionable.
 *
 * @see setAuthErrorHandler
 */
let authErrorHandler: ((error: FetchError) => void) | null = null;

/**
 * Deduplication guard: prevents the auth error handler from firing more than
 * once per 401/403 response. Without this guard, both `setUnauthorizedHandler`
 * (registered by AuthProvider in api/client.ts) and `setAuthErrorHandler` (also
 * registered by AuthProvider) would fire independently on the same 401, causing
 * a double logout that triggers a React state mutation race and potential double
 * navigation to the login page.
 *
 * This flag is set when the first handler fires, and reset when the user reaches
 * the login page (handled in AuthProvider).
 */
let isLoggingOut: boolean = false;

/**
 * Register a callback to be invoked when an auth error (401/403) is detected.
 * Typically called by the AuthProvider on mount.
 *
 * If a handler is registered, it will be called when handleFetchError detects
 * a 401/403 error. If no handler is registered, auth errors will be logged
 * as warnings to the console.
 *
 * @param handler - Callback to invoke on auth error. Pass `null` to clear.
 *
 * @example
 * // In AuthProvider:
 * useEffect(() => {
 *   setAuthErrorHandler((error) => {
 *     handleSessionExpired();
 *   });
 *   return () => setAuthErrorHandler(null);
 * }, [handleSessionExpired]);
 */
export function setAuthErrorHandler(handler: ((error: FetchError) => void) | null): void {
  authErrorHandler = handler;
}

/**
 * Reset the isLoggingOut deduplication guard.
 * Called when the user reaches the login page to allow future 401s to trigger
 * the handler again.
 */
export function resetLogoutState(): void {
  isLoggingOut = false;
}

/**
 * Extract user-friendly message from a FetchError.
 *
 * - abort_error → empty string (typically ignored)
 * - auth_error (401/403) → empty string (handled globally)
 * - api_error → HTTP status and body
 * - network_error → error message
 *
 * @param error - The FetchError to extract message from
 * @returns User-friendly error message string
 */
export function getErrorMessage(error: FetchError): string {
  if (isAbortError(error)) {
    return "";
  }
  if (isAuthError(error)) {
    return "";
  }
  return error.message;
}

/**
 * Normalize typed fetch error handling across hooks.
 *
 * Handles three error cases:
 * 1. Request was aborted — silently ignored (expected cleanup)
 * 2. Auth error (401/403) — invokes registered auth handler with fallback logging
 * 3. Other error — stored in component state or notified via callback
 *
 * **Auth Error Handling Contract:**
 * When a 401/403 auth error is detected, handleFetchError ensures deterministic
 * error handling by invoking the registered `authErrorHandler` (typically
 * AuthProvider's session expiration callback). If no handler is registered,
 * the error is logged as a warning to ensure it remains actionable.
 *
 * This prevents silent error swallowing and ensures auth errors are never lost.
 *
 * Supports both typed FetchError and backward-compatible string error setters.
 *
 * @param err - The caught error (any value caught from Promise.catch)
 * @param setError - State setter (accepts either FetchError | null or string | null)
 * @param fallback - Default error message if err is not an Error instance
 *
 * @see setAuthErrorHandler
 */
export function handleFetchError(
  err: unknown,
  setError: (value: any) => void,
  fallback: string = "Unknown error",
): void {
  // Convert to FetchError
  const fetchError = normalizeFetchError(err, fallback);

  // Abort errors are expected during cleanup — ignore silently
  if (isAbortError(fetchError)) {
    return;
  }

  // Auth errors are handled globally with registered handler or fallback logging.
  // This ensures auth errors are never silently swallowed.
  if (isAuthError(fetchError)) {
    // Extract correlation ID from auth error
    const correlationId = fetchError.correlationId;

    recordWarning(
      "auth_error",
      `Authentication error (${fetchError.status})`,
      {
        status: fetchError.status,
        message: fetchError.message,
      },
      correlationId,
    );

    if (authErrorHandler) {
      if (!isLoggingOut) {
        isLoggingOut = true;
        authErrorHandler(fetchError);
      }
    } else {
      // Fallback: log the auth error to ensure it remains actionable.
      // This indicates that AuthProvider either hasn't mounted yet or
      // failed to register its handler.
      console.warn(
        `Auth error (${fetchError.status}) without registered handler. ` +
          `AuthProvider may not be mounted or handler registration failed.`,
        fetchError,
      );
    }
    return;
  }

  // Log other errors with correlation ID for tracing
  if (isApiError(fetchError)) {
    const apiError = fetchError as ApiErrorPayload;
    recordError(
      "api_error",
      new Error(apiError.message),
      {
        status: apiError.status,
        body_preview: apiError.body?.substring(0, 200),
      },
      apiError.correlationId,
    );
  } else if (isNetworkError(fetchError)) {
    recordError("network_error", new Error(fetchError.message), undefined, undefined);
  }

  // Determine if setError expects FetchError or string by checking current behavior
  // For now, always pass FetchError; consuming code can extract message as needed
  setError(fetchError);
}

/**
 * Create a string-based error setter adapter for handleFetchError.
 *
 * Use this when you have a `string | null` error state but need to call handleFetchError.
 * It automatically converts FetchError to displayable messages.
 *
 * @param setStringError - State setter that expects string | null
 * @returns A setter function compatible with handleFetchError
 *
 * @example
 * const [error, setError] = useState<string | null>(null);
 * handleFetchError(err, createStringErrorAdapter(setError), "Failed to load");
 */
export function createStringErrorAdapter(
  setStringError: (value: string | null) => void,
): (value: FetchError | null) => void {
  return (fetchError: FetchError | null) => {
    if (fetchError === null) {
      setStringError(null);
      return;
    }
    setStringError(getErrorMessage(fetchError));
  };
}

/**
 * Normalize any caught error into a FetchError discriminated union.
 *
 * Handles three cases:
 * 1. DOMException with name "AbortError" → abort_error
 * 2. ApiError with status → api_error
 * 3. Anything else → network_error
 *
 * @param err - The caught error value
 * @param fallback - Default message for unknown errors
 * @returns Typed FetchError
 * @internal
 */
export function normalizeFetchError(err: unknown, fallback: string = "Unknown error"): FetchError {
  // Handle abort errors
  if (err instanceof DOMException && err.name === "AbortError") {
    return {
      type: "abort_error",
      message: "Request aborted",
    };
  }

  // Handle errors that are already typed objects from type guards
  if (typeof err === "object" && err !== null && "type" in err) {
    const fetchError = err as FetchError;
    if (fetchError.type === "api_error" || fetchError.type === "network_error" || fetchError.type === "abort_error") {
      return fetchError;
    }
  }

  // Handle ApiError instances (for backward compatibility)
  if (err instanceof Error && err.name === "ApiError" && "status" in err) {
    const apiError = err as any;
    const errorPayload: ApiErrorPayload = {
      type: "api_error",
      status: apiError.status,
      body: apiError.body,
      message: apiError.message,
      correlationId: apiError.correlationId,
    };

    // Extract parsed error response fields if available
    if (apiError.errorResponse) {
      errorPayload.code = apiError.errorResponse.code;
      errorPayload.detail = apiError.errorResponse.detail;
      errorPayload.metadata = apiError.errorResponse.metadata;
      // Prefer correlation_id from error response if present
      if (apiError.errorResponse.correlation_id) {
        errorPayload.correlationId = apiError.errorResponse.correlation_id;
      }
    }

    return errorPayload;
  }

  // Handle generic Error instances
  if (err instanceof Error) {
    return {
      type: "network_error",
      message: err.message,
    };
  }

  // Fallback for unknown types
  return {
    type: "network_error",
    message: fallback,
  };
}

