import React, {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState,
} from "react";
import { Alert, Platform } from "react-native";
import { useIAP, type Purchase } from "expo-iap";
import { authFetch } from "../lib/apiFetch";
import { API_BASE_URL } from "../config";
import { useAuth } from "./AuthContext";

export interface EntitlementConfig {
  paywallEnabled: boolean;
  freeDownloadLimit: number;
  freeDownloadsUsed: number;
  freeDownloadsRemaining: number;
  purchasedCredits: number;
  pricePerDownloadPaise: number;
}

/** A buy-credits product as the server defines it — id + how many credits it's worth. The server is the source of truth for this; the client never hardcodes a product list. Actual displayed price comes from the store (see `storeProducts`). */
export interface CreditProduct {
  productId: string;
  platform: string;
  creditsGranted: number;
}

interface EntitlementContextValue {
  config: EntitlementConfig | null;
  /** Server-defined product catalog (id + credits granted). Empty whenever the paywall is off. */
  products: CreditProduct[];
  refreshEntitlement: () => Promise<void>;
  isBuyModalOpen: boolean;
  /** Opens the buy-credits modal. No-ops if the paywall is off — the payment surface must never appear in that state. */
  openBuyModal: () => void;
  closeBuyModal: () => void;
  purchaseProduct: (productId: string) => Promise<void>;
  /** productId currently mid-purchase, or null. Drives per-row loading state in the modal. */
  purchasingProductId: string | null;
  /** Live store products (real localized price etc.), keyed by id via `.id`. */
  storeProducts: ReturnType<typeof useIAP>["products"];
  storeConnected: boolean;
}

const EntitlementContext = createContext<EntitlementContextValue | null>(null);

export function EntitlementProvider({ children }: { children: React.ReactNode }) {
  const { user } = useAuth();
  const [config, setConfig] = useState<EntitlementConfig | null>(null);
  const [products, setProducts] = useState<CreditProduct[]>([]);
  const [isBuyModalOpen, setBuyModalOpen] = useState(false);
  const [purchasingProductId, setPurchasingProductId] = useState<string | null>(null);

  const refreshEntitlement = useCallback(async () => {
    try {
      const res = await authFetch(`${API_BASE_URL}/config`);
      if (res.ok) setConfig(await res.json());
    } catch {
      // best-effort — this is an informational display, not worth alerting over
    }
  }, []);

  const refreshProducts = useCallback(async () => {
    try {
      const res = await authFetch(`${API_BASE_URL}/purchases/products?platform=${Platform.OS}`);
      if (res.ok) {
        const data = await res.json();
        setProducts(data.products ?? []);
      }
    } catch {
      // best-effort — the modal shows a loading state until this resolves
    }
  }, []);

  useEffect(() => {
    if (user) {
      refreshEntitlement();
      refreshProducts();
    } else {
      // Signed out — don't keep showing a stale entitlement/product list.
      setConfig(null);
      setProducts([]);
    }
  }, [user, refreshEntitlement, refreshProducts]);

  // `finishTransaction` comes from useIAP() below, but the purchase-success
  // handler passed INTO useIAP() needs to call it — a direct reference would
  // either be a temporal-dead-zone error (declared after this point) or, if
  // reordered, force useIAP's onPurchaseSuccess to change identity on every
  // render (finishTransaction isn't guaranteed stable), re-subscribing its
  // native listener each time. Mirror it into a ref instead — same pattern
  // MediaContext uses for its remote-command listeners — so the handler
  // always calls the live function without needing it in its own deps.
  const finishTransactionRef = useRef<
    ((args: { purchase: Purchase; isConsumable?: boolean }) => Promise<void>) | null
  >(null);

  const handlePurchaseCompleted = useCallback(
    async (purchase: Purchase) => {
      try {
        const purchaseToken = purchase.purchaseToken;
        const transactionId = (purchase as { transactionId?: string }).transactionId ?? purchase.id;
        if (!purchaseToken) throw new Error("Purchase completed without a token to verify.");

        const res = await authFetch(`${API_BASE_URL}/purchases/verify`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            platform: Platform.OS,
            productId: purchase.productId,
            transactionId,
            purchaseToken,
          }),
        });
        const data = await res.json().catch(() => ({}));
        if (!res.ok) throw new Error(data.error || `Verification failed (${res.status}).`);

        // Consumable — finish only after the server has actually credited
        // the account, so a verification failure leaves the transaction
        // outstanding and retryable rather than silently discarded.
        await finishTransactionRef.current?.({ purchase, isConsumable: true });
        await refreshEntitlement();
        setBuyModalOpen(false);
        const granted = data.creditsGranted ?? 1;
        Alert.alert("Purchase complete", `${granted} download credit${granted === 1 ? "" : "s"} added to your account.`);
      } catch (err) {
        Alert.alert(
          "Purchase couldn't be verified",
          err instanceof Error ? err.message : "Please try again, or contact support if this keeps happening.",
        );
      } finally {
        setPurchasingProductId(null);
      }
    },
    [refreshEntitlement],
  );

  const { connected, products: storeProducts, fetchProducts, requestPurchase, finishTransaction } = useIAP({
    onPurchaseSuccess: handlePurchaseCompleted,
    onPurchaseError: (error) => {
      setPurchasingProductId(null);
      if (error.code !== "user-cancelled") {
        Alert.alert("Purchase failed", error.message || "Something went wrong.");
      }
    },
  });

  useEffect(() => {
    finishTransactionRef.current = finishTransaction;
  }, [finishTransaction]);

  // expo-iap is iOS/Android only — nothing to connect to on other platforms.
  const iapSupported = Platform.OS === "ios" || Platform.OS === "android";

  useEffect(() => {
    if (iapSupported && connected && products.length > 0) {
      fetchProducts({ skus: products.map((p) => p.productId), type: "in-app" }).catch(() => {});
    }
  }, [iapSupported, connected, products, fetchProducts]);

  const openBuyModal = useCallback(() => {
    // Belt-and-suspenders: the payment surface must never appear when the
    // paywall is off, even if something calls this incorrectly.
    if (!config?.paywallEnabled) return;
    setBuyModalOpen(true);
  }, [config]);

  const closeBuyModal = useCallback(() => setBuyModalOpen(false), []);

  const purchaseProduct = useCallback(
    async (productId: string) => {
      if (!iapSupported || !connected || purchasingProductId) return;
      setPurchasingProductId(productId);
      try {
        const request =
          Platform.OS === "android"
            ? { google: { skus: [productId] } }
            : { apple: { sku: productId } };
        await requestPurchase({ request, type: "in-app" });
      } catch {
        // Deliberately no alert here — a rejected requestPurchase() also
        // fires useIAP's onPurchaseError below with the actual store error
        // (e.g. "SKU not found"), which is more specific. Alerting in both
        // places stacked two alerts for the same failure; this one just
        // resets the loading state as a fallback in case onPurchaseError
        // doesn't fire for some reason.
        setPurchasingProductId(null);
      }
    },
    [iapSupported, connected, purchasingProductId, requestPurchase],
  );

  const value: EntitlementContextValue = {
    config,
    products,
    refreshEntitlement,
    isBuyModalOpen,
    openBuyModal,
    closeBuyModal,
    purchaseProduct,
    purchasingProductId,
    storeProducts,
    storeConnected: connected,
  };

  return <EntitlementContext.Provider value={value}>{children}</EntitlementContext.Provider>;
}

export function useEntitlement() {
  const ctx = useContext(EntitlementContext);
  if (!ctx) throw new Error("useEntitlement must be used within an EntitlementProvider");
  return ctx;
}
