// External dependencies
import {useCallback, useEffect, useState, useRef} from 'react';
import {Platform} from 'react-native';
import {RnIapConsole} from '../utils/debug';

// Internal modules
import {
  initConnection,
  purchaseErrorListener,
  purchaseUpdatedListener,
  promotedProductListenerIOS,
  getAvailablePurchases,
  finishTransaction as finishTransactionInternal,
  requestPurchase as requestPurchaseInternal,
  fetchProducts,
  validateReceipt as validateReceiptInternal,
  verifyPurchase as verifyPurchaseTopLevel,
  verifyPurchaseWithProvider as verifyPurchaseWithProviderTopLevel,
  getActiveSubscriptions,
  hasActiveSubscriptions,
  syncIOS,
  getPromotedProductIOS,
  requestPurchaseOnPromotedProductIOS,
  checkAlternativeBillingAvailabilityAndroid,
  showAlternativeBillingDialogAndroid,
  createAlternativeBillingTokenAndroid,
  userChoiceBillingListenerAndroid,
  isStandardIOS,
} from '../';

// Types
import {ErrorCode} from '../types';
import type {
  ProductQueryType,
  RequestPurchaseProps,
  AlternativeBillingModeAndroid,
  BillingProgramAndroid,
  UserChoiceBillingDetails,
  VerifyPurchaseProps,
  VerifyPurchaseResult,
  VerifyPurchaseWithProviderProps,
  VerifyPurchaseWithProviderResult,
  PurchaseOptions,
} from '../types';
import type {
  ActiveSubscription,
  Product,
  Purchase,
  PurchaseError,
  ProductSubscription,
} from '../types';
import type {MutationFinishTransactionArgs} from '../types';

// Types for event subscriptions
interface EventSubscription {
  remove(): void;
}

type UseIap = {
  connected: boolean;
  products: Product[];
  subscriptions: ProductSubscription[];
  availablePurchases: Purchase[];
  promotedProductIOS?: Product;
  activeSubscriptions: ActiveSubscription[];
  finishTransaction: (args: MutationFinishTransactionArgs) => Promise<void>;
  getAvailablePurchases: (options?: PurchaseOptions) => Promise<void>;
  fetchProducts: (params: {
    skus: string[];
    type?: ProductQueryType | null;
  }) => Promise<void>;
  requestPurchase: (params: RequestPurchaseProps) => Promise<void>;
  /**
   * @deprecated Use `verifyPurchase` instead. This function will be removed in a future version.
   */
  validateReceipt: (
    options: VerifyPurchaseProps,
  ) => Promise<VerifyPurchaseResult>;
  /** Verify purchase with the configured providers */
  verifyPurchase: (
    options: VerifyPurchaseProps,
  ) => Promise<VerifyPurchaseResult>;
  /** Verify purchase with a specific provider (e.g., IAPKit) */
  verifyPurchaseWithProvider: (
    options: VerifyPurchaseWithProviderProps,
  ) => Promise<VerifyPurchaseWithProviderResult>;
  restorePurchases: (options?: PurchaseOptions) => Promise<void>;
  getPromotedProductIOS: () => Promise<Product | null>;
  requestPurchaseOnPromotedProductIOS: () => Promise<boolean>;
  getActiveSubscriptions: (
    subscriptionIds?: string[],
  ) => Promise<ActiveSubscription[]>;
  hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
  /**
   * Manually retry the store connection.
   * Useful when the initial auto-connect fails (e.g., Play Store not ready at mount time).
   * Updates the `connected` state on success.
   */
  reconnect: () => Promise<boolean>;
  // Alternative billing (Android)
  checkAlternativeBillingAvailabilityAndroid?: () => Promise<boolean>;
  showAlternativeBillingDialogAndroid?: () => Promise<boolean>;
  createAlternativeBillingTokenAndroid?: (
    sku?: string,
  ) => Promise<string | null>;
};

export interface UseIapOptions {
  onPurchaseSuccess?: (purchase: Purchase) => void;
  onPurchaseError?: (error: PurchaseError) => void;
  /** Callback for non-purchase errors (fetchProducts, getAvailablePurchases, etc.) */
  onError?: (error: Error) => void;
  onPromotedProductIOS?: (product: Product) => void;
  onUserChoiceBillingAndroid?: (details: UserChoiceBillingDetails) => void;
  /**
   * @deprecated Use enableBillingProgramAndroid instead.
   * - 'user-choice' → 'user-choice-billing'
   * - 'alternative-only' → 'external-offer'
   */
  alternativeBillingModeAndroid?: AlternativeBillingModeAndroid;
  /**
   * Enable a specific billing program for Android (8.2.0+)
   * Use 'user-choice-billing' for User Choice Billing (7.0+).
   * Use 'external-offer' for External Offer program.
   * Use 'external-payments' for Developer Provided Billing (Japan only, 8.3.0+).
   */
  enableBillingProgramAndroid?: BillingProgramAndroid;
}

/**
 * React Hook for managing In-App Purchases.
 * See documentation at https://react-native-iap.hyo.dev/docs/hooks/useIAP
 */
export function useIAP(options?: UseIapOptions): UseIap {
  const [connected, setConnected] = useState<boolean>(false);
  const [products, setProducts] = useState<Product[]>([]);
  const [subscriptions, setSubscriptions] = useState<ProductSubscription[]>([]);
  const [availablePurchases, setAvailablePurchases] = useState<Purchase[]>([]);
  const [promotedProductIOS, setPromotedProductIOS] = useState<Product>();
  const [activeSubscriptions, setActiveSubscriptions] = useState<
    ActiveSubscription[]
  >([]);

  const optionsRef = useRef<UseIapOptions | undefined>(options);
  const connectedRef = useRef<boolean>(false);

  // Helper function to merge arrays with duplicate checking
  const mergeWithDuplicateCheck = useCallback(
    <T>(
      existingItems: T[],
      newItems: T[],
      getKey: (item: T) => string,
    ): T[] => {
      const merged = [...existingItems];
      newItems.forEach((newItem) => {
        const isDuplicate = merged.some(
          (existingItem) => getKey(existingItem) === getKey(newItem),
        );
        if (!isDuplicate) {
          merged.push(newItem);
        }
      });
      return merged;
    },
    [],
  );

  useEffect(() => {
    optionsRef.current = options;
  }, [options]);

  useEffect(() => {
    connectedRef.current = connected;
  }, [connected]);

  const subscriptionsRef = useRef<{
    purchaseUpdate?: EventSubscription;
    purchaseError?: EventSubscription;
    promotedProductIOS?: EventSubscription;
    userChoiceBillingAndroid?: EventSubscription;
  }>({});

  // Track if component is mounted to prevent listener leaks on early unmount
  const isMountedRef = useRef<boolean>(true);

  const subscriptionsRefState = useRef<ProductSubscription[]>([]);

  useEffect(() => {
    subscriptionsRefState.current = subscriptions;
  }, [subscriptions]);

  // Helper function to invoke onError callback
  const invokeOnError = useCallback((error: unknown) => {
    if (optionsRef.current?.onError) {
      optionsRef.current.onError(
        error instanceof Error ? error : new Error(String(error)),
      );
    }
  }, []);

  const fetchProductsInternal = useCallback(
    async (params: {
      skus: string[];
      type?: ProductQueryType | null;
    }): Promise<void> => {
      if (!connectedRef.current) {
        RnIapConsole.warn(
          '[useIAP] fetchProducts called before connection; skipping',
        );
        return;
      }
      try {
        const requestType = params.type ?? 'in-app';
        RnIapConsole.debug('[useIAP] Calling fetchProducts with:', {
          skus: params.skus,
          type: requestType,
        });
        const result = await fetchProducts({
          skus: params.skus,
          type: requestType,
        });
        RnIapConsole.debug('[useIAP] fetchProducts result:', result);
        const items = (result ?? []) as (Product | ProductSubscription)[];

        // fetchProducts already returns properly filtered results based on type
        if (requestType === 'subs') {
          // All items are already subscriptions
          setSubscriptions((prevSubscriptions: ProductSubscription[]) =>
            mergeWithDuplicateCheck(
              prevSubscriptions,
              items as ProductSubscription[],
              (subscription: ProductSubscription) => subscription.id,
            ),
          );
          return;
        }

        if (requestType === 'all') {
          // fetchProducts already properly separates products and subscriptions
          const newProducts = items.filter(
            (item): item is Product => item.type === 'in-app',
          );
          const newSubscriptions = items.filter(
            (item): item is ProductSubscription => item.type === 'subs',
          );

          setProducts((prevProducts: Product[]) =>
            mergeWithDuplicateCheck(
              prevProducts,
              newProducts,
              (product: Product) => product.id,
            ),
          );
          setSubscriptions((prevSubscriptions: ProductSubscription[]) =>
            mergeWithDuplicateCheck(
              prevSubscriptions,
              newSubscriptions,
              (subscription: ProductSubscription) => subscription.id,
            ),
          );
          return;
        }

        // For 'in-app' type, all items are already products
        setProducts((prevProducts: Product[]) =>
          mergeWithDuplicateCheck(
            prevProducts,
            items as Product[],
            (product: Product) => product.id,
          ),
        );
      } catch (error) {
        RnIapConsole.error('Error fetching products:', error);
        invokeOnError(error);
      }
    },
    [mergeWithDuplicateCheck, invokeOnError],
  );

  const getAvailablePurchasesInternal = useCallback(
    async (options?: PurchaseOptions): Promise<void> => {
      try {
        const result = await getAvailablePurchases({
          alsoPublishToEventListenerIOS:
            options?.alsoPublishToEventListenerIOS ?? false,
          onlyIncludeActiveItemsIOS: options?.onlyIncludeActiveItemsIOS ?? true,
          includeSuspendedAndroid: options?.includeSuspendedAndroid ?? false,
        });
        setAvailablePurchases(result);
      } catch (error) {
        RnIapConsole.error('Error fetching available purchases:', error);
        invokeOnError(error);
      }
    },
    [invokeOnError],
  );

  const getActiveSubscriptionsInternal = useCallback(
    async (subscriptionIds?: string[]): Promise<ActiveSubscription[]> => {
      try {
        const result = await getActiveSubscriptions(subscriptionIds);
        setActiveSubscriptions(result);
        return result;
      } catch (error) {
        RnIapConsole.error('Error getting active subscriptions:', error);
        invokeOnError(error);
        return [];
      }
    },
    [invokeOnError],
  );

  const hasActiveSubscriptionsInternal = useCallback(
    async (subscriptionIds?: string[]): Promise<boolean> => {
      try {
        return await hasActiveSubscriptions(subscriptionIds);
      } catch (error) {
        RnIapConsole.error('Error checking active subscriptions:', error);
        invokeOnError(error);
        return false;
      }
    },
    [invokeOnError],
  );

  const finishTransaction = useCallback(
    async (args: MutationFinishTransactionArgs): Promise<void> => {
      // Directly delegate to root API finishTransaction without catching errors.
      // This allows the root API's error handling logic to work correctly, including:
      // - iOS: treating "Transaction not found" as success (already-finished transactions)
      // - Proper validation and error messages for required fields
      // Users should handle errors in their onPurchaseSuccess callback if needed.
      await finishTransactionInternal(args);
    },
    [],
  );

  const requestPurchase = useCallback(
    async (requestObj: RequestPurchaseProps): Promise<void> => {
      await requestPurchaseInternal(requestObj);
    },
    [],
  );

  const restorePurchases = useCallback(
    async (options?: PurchaseOptions): Promise<void> => {
      try {
        if (Platform.OS === 'ios') {
          await syncIOS();
        }

        await getAvailablePurchasesInternal(options);
      } catch (error) {
        RnIapConsole.warn('Failed to restore purchases:', error);
        invokeOnError(error);
      }
    },
    [getAvailablePurchasesInternal, invokeOnError],
  );

  const validateReceipt = useCallback(
    async (options: VerifyPurchaseProps): Promise<VerifyPurchaseResult> =>
      validateReceiptInternal(options),
    [],
  );

  const verifyPurchase = useCallback(
    async (options: VerifyPurchaseProps): Promise<VerifyPurchaseResult> => {
      return verifyPurchaseTopLevel(options);
    },
    [],
  );

  const verifyPurchaseWithProvider = useCallback(
    async (
      options: VerifyPurchaseWithProviderProps,
    ): Promise<VerifyPurchaseWithProviderResult> => {
      return verifyPurchaseWithProviderTopLevel(options);
    },
    [],
  );

  // Shared helper: build Android billing config from options
  const buildAndroidConfig = useCallback(() => {
    let config:
      | {
          enableBillingProgramAndroid?: BillingProgramAndroid;
          alternativeBillingModeAndroid?: AlternativeBillingModeAndroid;
        }
      | undefined;

    if (Platform.OS === 'android') {
      if (optionsRef.current?.enableBillingProgramAndroid) {
        config = {
          enableBillingProgramAndroid:
            optionsRef.current.enableBillingProgramAndroid,
        };
      } else if (optionsRef.current?.alternativeBillingModeAndroid) {
        config = {
          alternativeBillingModeAndroid:
            optionsRef.current.alternativeBillingModeAndroid,
        };
      }
    }

    return config;
  }, []);

  // Shared helper: register event listeners if not already active
  const registerListeners = useCallback(() => {
    if (!subscriptionsRef.current.purchaseUpdate) {
      subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(
        async (purchase: Purchase) => {
          try {
            await getActiveSubscriptionsInternal();
            await getAvailablePurchasesInternal();
          } catch (e) {
            RnIapConsole.warn('[useIAP] post-purchase refresh failed:', e);
          }
          if (optionsRef.current?.onPurchaseSuccess) {
            optionsRef.current.onPurchaseSuccess(purchase);
          }
        },
      );
    }

    if (!subscriptionsRef.current.purchaseError) {
      subscriptionsRef.current.purchaseError = purchaseErrorListener(
        (error) => {
          if (
            error.code === ErrorCode.InitConnection &&
            !connectedRef.current
          ) {
            return;
          }
          if (optionsRef.current?.onPurchaseError) {
            optionsRef.current.onPurchaseError(error);
          }
        },
      );
    }

    if (isStandardIOS() && !subscriptionsRef.current.promotedProductIOS) {
      subscriptionsRef.current.promotedProductIOS = promotedProductListenerIOS(
        (product: Product) => {
          setPromotedProductIOS(product);
          if (optionsRef.current?.onPromotedProductIOS) {
            optionsRef.current.onPromotedProductIOS(product);
          }
        },
      );
    }

    if (
      Platform.OS === 'android' &&
      optionsRef.current?.onUserChoiceBillingAndroid &&
      !subscriptionsRef.current.userChoiceBillingAndroid
    ) {
      subscriptionsRef.current.userChoiceBillingAndroid =
        userChoiceBillingListenerAndroid((details) => {
          if (optionsRef.current?.onUserChoiceBillingAndroid) {
            optionsRef.current.onUserChoiceBillingAndroid(details);
          }
        });
    }
  }, [getActiveSubscriptionsInternal, getAvailablePurchasesInternal]);

  // Shared helper: clean up all listeners
  const cleanupListeners = useCallback(() => {
    subscriptionsRef.current.purchaseUpdate?.remove();
    subscriptionsRef.current.purchaseError?.remove();
    subscriptionsRef.current.promotedProductIOS?.remove();
    subscriptionsRef.current.userChoiceBillingAndroid?.remove();
    subscriptionsRef.current.purchaseUpdate = undefined;
    subscriptionsRef.current.purchaseError = undefined;
    subscriptionsRef.current.promotedProductIOS = undefined;
    subscriptionsRef.current.userChoiceBillingAndroid = undefined;
  }, []);

  const initIapWithSubscriptions = useCallback(async (): Promise<void> => {
    const config = buildAndroidConfig();

    try {
      const result = await initConnection(config);

      if (!isMountedRef.current) {
        return;
      }

      if (!result) {
        setConnected(false);
        RnIapConsole.warn('[useIAP] initConnection returned false');
        return;
      }

      registerListeners();
      setConnected(true);
    } catch (error) {
      RnIapConsole.error('initConnection failed:', error);
      cleanupListeners();
      if (isMountedRef.current) {
        setConnected(false);
      }
      invokeOnError(error);
    }
  }, [buildAndroidConfig, registerListeners, cleanupListeners, invokeOnError]);

  const reconnect = useCallback(async (): Promise<boolean> => {
    const config = buildAndroidConfig();

    try {
      const result = await initConnection(config);

      if (!isMountedRef.current) {
        return false;
      }

      if (result) {
        registerListeners();
        setConnected(true);
        return true;
      }

      setConnected(false);
      return false;
    } catch (error) {
      RnIapConsole.error('[useIAP] reconnect failed:', error);
      cleanupListeners();
      if (isMountedRef.current) {
        setConnected(false);
      }
      invokeOnError(error);
      return false;
    }
  }, [buildAndroidConfig, registerListeners, cleanupListeners, invokeOnError]);

  useEffect(() => {
    isMountedRef.current = true;
    initIapWithSubscriptions();

    return () => {
      isMountedRef.current = false;
      cleanupListeners();
      // Keep connection alive across screens to avoid race conditions
      setConnected(false);
    };
  }, [initIapWithSubscriptions, cleanupListeners]);

  return {
    connected,
    products,
    subscriptions,
    finishTransaction,
    availablePurchases,
    promotedProductIOS,
    activeSubscriptions,
    getAvailablePurchases: getAvailablePurchasesInternal,
    fetchProducts: fetchProductsInternal,
    requestPurchase,
    validateReceipt,
    verifyPurchase,
    verifyPurchaseWithProvider,
    restorePurchases,
    getPromotedProductIOS,
    requestPurchaseOnPromotedProductIOS,
    getActiveSubscriptions: getActiveSubscriptionsInternal,
    hasActiveSubscriptions: hasActiveSubscriptionsInternal,
    reconnect,
    // Alternative billing (Android only)
    ...(Platform.OS === 'android'
      ? {
          checkAlternativeBillingAvailabilityAndroid,
          showAlternativeBillingDialogAndroid,
          createAlternativeBillingTokenAndroid,
        }
      : {}),
  };
}
