import { FetcherWithComponents, useFetcher } from "react-router";
import { CartInfoToEnable } from "../types";
import { CartForm, CartReturn, OptimisticCart } from "@shopify/hydrogen";
import React, { useCallback, useEffect, useRef } from "react";
import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
import { CartLine, ComponentizableCartLine } from "@shopify/hydrogen-react/storefront-api-types";

const DEFAULT_REDO_ENABLED_CART_ATTRIBUTE = "redo_opted_in_from_cart";
const CONCIERGE_ATTRIBUTION_CART_ATTRIBUTE_KEY = "redo.conciergeAssisted";
const CONCIERGE_CONVERSATION_IDS_STORAGE_KEY = "redoConciergeConversationIds";

const isCartWithActionsDocs = (
  cart: CartReturn | CartWithActionsDocs | OptimisticCart,
): cart is CartWithActionsDocs => {
  return Array.isArray(cart.lines) && "linesAdd" in cart && typeof cart.linesAdd === "function";
};

const getCartLines = (
  cart: CartReturn | CartWithActionsDocs | OptimisticCart,
): Array<CartLine | ComponentizableCartLine> => {
  if (isOptimisticCart(cart)) {
    return cart.lines.nodes;
  } else if (isCartWithActionsDocs(cart)) {
    return cart.lines;
  } else {
    return cart.lines.nodes ?? cart.lines.edges.map((edge) => edge.node);
  }
};

// https://shopify.dev/docs/api/hydrogen/2025-01/hooks/useoptimisticcart
const isOptimisticCart = (cart: CartReturn | CartWithActionsDocs | OptimisticCart): cart is OptimisticCart => {
  return "isOptimistic" in cart && (cart.isOptimistic ?? false);
};

const addProductToCartIfNeeded = async ({
  cart,
  fetcher,
  waitCartIdle,
  cartInfoToEnable,
}: {
  cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
  fetcher: FetcherWithComponents<unknown>;
  waitCartIdle: WaitCartIdleCallback;
  cartInfoToEnable: CartInfoToEnable;
}) => {
  if (!cart) {
    return await addProductToCart({ cart, fetcher, waitCartIdle, cartInfoToEnable });
  }

  const redoProductsInCart = getCartLines(cart).filter((cartLine) => {
    return cartLine.merchandise.product.vendor === "re:do";
  });
  const correctRedoProductInCart = redoProductsInCart?.filter((cartLine) => {
    return cartLine.merchandise.id === `gid://shopify/ProductVariant/${cartInfoToEnable.variantId}`;
  });
  if (redoProductsInCart.length === 0) {
    return await addProductToCart({ cart, fetcher, waitCartIdle, cartInfoToEnable });
  } else if (
    redoProductsInCart.length === 1 &&
    correctRedoProductInCart.length === 1 &&
    correctRedoProductInCart[0].quantity === 1
  ) {
    // No action needed
    return;
  } else {
    await removeLinesFromCart({
      cart,
      fetcher,
      waitCartIdle,
      lineIds: redoProductsInCart.map((cartLine) => cartLine.id),
    });
    await addProductToCart({ cart, fetcher, waitCartIdle, cartInfoToEnable });
  }
};

const removeLinesFromCart = async ({
  cart,
  fetcher,
  waitCartIdle,
  lineIds,
}: {
  cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
  fetcher: FetcherWithComponents<unknown>;
  waitCartIdle: WaitCartIdleCallback;
  lineIds: string[];
}) => {
  const formInput = {
    action: CartForm.ACTIONS.LinesRemove,
    inputs: {
      lineIds,
    },
  };

  if (cart && isCartWithActionsDocs(cart)) {
    cart.linesRemove(lineIds);
    await waitCartIdle();
  } else {
    await fetcher.submit(
      {
        [CartForm.INPUT_NAME]: JSON.stringify(formInput),
      },
      { method: "POST", action: "/cart" },
    );
  }
};

const removeProductFromCartIfNeeded = async ({
  cart,
  fetcher,
  waitCartIdle,
}: {
  cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
  fetcher: FetcherWithComponents<unknown>;
  waitCartIdle: WaitCartIdleCallback;
  cartInfoToEnable: CartInfoToEnable;
}) => {
  if (!cart) {
    console.error("No cart");
    return;
  }

  const redoProductsInCart = getCartLines(cart).filter((cartLine) => {
    return cartLine.merchandise.product.vendor === "re:do";
  });

  if (redoProductsInCart.length !== 0) {
    await removeLinesFromCart({
      cart,
      fetcher,
      waitCartIdle,
      lineIds: redoProductsInCart.map((cartLine) => cartLine.id),
    });
  }
};

const addProductToCart = async ({
  waitCartIdle,
  cart,
  fetcher,
  cartInfoToEnable,
}: {
  waitCartIdle: WaitCartIdleCallback;
  cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
  fetcher: FetcherWithComponents<unknown>;
  cartInfoToEnable: CartInfoToEnable;
}) => {
  const redoProductLine = {
    merchandiseId: `gid://shopify/ProductVariant/${cartInfoToEnable.variantId}`,
    quantity: 1,
  };

  const formInput = {
    action: CartForm.ACTIONS.LinesAdd,
    inputs: {
      lines: [redoProductLine],
    },
  };

  if (cart && isCartWithActionsDocs(cart)) {
    cart.linesAdd([redoProductLine]);
    await waitCartIdle();
  } else {
    await fetcher.submit(
      {
        [CartForm.INPUT_NAME]: JSON.stringify(formInput),
      },
      { method: "POST", action: "/cart" },
    );
  }
};

interface ConversationIdWithExpiry {
  conversationId: string;
  expiresAt: number;
}

function getConciergeConversationIdsFromStorage(): string[] | null {
  try {
    const stored = localStorage.getItem(CONCIERGE_CONVERSATION_IDS_STORAGE_KEY);
    if (!stored) {
      return null;
    }

    const conversationIdsWithExpiry: ConversationIdWithExpiry[] = JSON.parse(stored);
    const now = Date.now();

    const validConversationIds = conversationIdsWithExpiry
      .filter((item) => item.expiresAt > now)
      .map((item) => item.conversationId);

    return validConversationIds.length > 0 ? validConversationIds : null;
  } catch (_error) {
    return null;
  }
}

const setCartRedoEnabledAttribute = async ({
  cart,
  fetcher,
  waitCartIdle,
  cartInfoToEnable,
  enabled,
}: {
  cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
  fetcher: FetcherWithComponents<unknown>;
  waitCartIdle: WaitCartIdleCallback;
  cartInfoToEnable: CartInfoToEnable | null;
  enabled: boolean;
}) => {
  const redoCartAttribute = {
    key: cartInfoToEnable?.cartAttribute || DEFAULT_REDO_ENABLED_CART_ATTRIBUTE,
    value: enabled.toString(),
  };

  const existingAttributes = cart?.attributes || [];

  const existingAttributesMap = new Map(existingAttributes.map((attr) => [attr.key, attr.value]));

  existingAttributesMap.set(redoCartAttribute.key, redoCartAttribute.value);
  const conciergeConversationIds = getConciergeConversationIdsFromStorage();
  if (conciergeConversationIds && conciergeConversationIds.length > 0) {
    existingAttributesMap.set(
      CONCIERGE_ATTRIBUTION_CART_ATTRIBUTE_KEY,
      JSON.stringify({
        conciergeConversationIds: conciergeConversationIds,
      }),
    );
  }

  const updatedAttributes = Array.from(existingAttributesMap.entries()).map(([key, value]) => ({
    key,
    value: value ?? "",
  }));

  const formInput = {
    action: CartForm.ACTIONS.AttributesUpdateInput,
    inputs: {
      attributes: updatedAttributes,
    },
  };

  if (cart && isCartWithActionsDocs(cart)) {
    cart.cartAttributesUpdate(updatedAttributes);
    await waitCartIdle();
  } else {
    await fetcher.submit(
      {
        [CartForm.INPUT_NAME]: JSON.stringify(formInput),
      },
      { method: "POST", action: "/cart" },
    );
  }
};

type FetcherData<T> = NonNullable<T | unknown>; // FIXME: used to use SerializeFrom which is deprecated. Can this be better typed?
type ResolveFunction<T> = (value: FetcherData<T>) => void;

function useFetcherWithPromise<TData = unknown>(opts?: Parameters<typeof useFetcher>[0]) {
  const fetcher = useFetcher<TData>(opts);
  const resolveRef = React.useRef<ResolveFunction<TData> | null>(null);
  const promiseRef = React.useRef<Promise<FetcherData<TData>> | null>(null);

  if (!promiseRef.current) {
    promiseRef.current = new Promise<FetcherData<TData>>((resolve) => {
      resolveRef.current = resolve;
    });
  }

  const resetResolver = React.useCallback(() => {
    promiseRef.current = new Promise((resolve) => {
      resolveRef.current = resolve;
    });
  }, [promiseRef, resolveRef]);

  const submit = React.useCallback(
    async (...args: Parameters<typeof fetcher.submit>): Promise<void> => {
      fetcher.submit(...args);
      await promiseRef.current;
    },
    [fetcher, promiseRef],
  );

  React.useEffect(() => {
    if (fetcher.state === "idle") {
      if (fetcher.data) {
        resolveRef.current?.(fetcher.data);
      }
      resetResolver();
    }
  }, [fetcher, resetResolver]);

  return { ...fetcher, submit };
}

type WaitCartIdleCallback = () => Promise<CartReturn | CartWithActionsDocs | OptimisticCart>;

// This function allows us to await a cart idle state without breaking React rules.
// It returns a function, which returns a promise, which will resolve once the cart value passed in reaches an idle state.
// Not intended for use with CartReturn, but will accept that value if passed in to avoid breaking rules of hooks
type CartUnion = CartReturn | CartWithActionsDocs | OptimisticCart;

const useWaitCartIdle = (cart: CartUnion | undefined) => {
  const resolveRef = useRef<((value: CartUnion) => void) | null>(null);
  const promiseRef = useRef<Promise<CartUnion>>(null!);

  if (!promiseRef.current) {
    promiseRef.current = new Promise<CartReturn | CartWithActionsDocs | OptimisticCart>((resolve) => {
      resolveRef.current = resolve;
    });
  }

  const resetResolver = useCallback(() => {
    promiseRef.current = new Promise((resolve) => {
      resolveRef.current = resolve;
    });
  }, [promiseRef, resolveRef]);

  const waitCartIdle = useCallback(async () => {
    return promiseRef.current;
  }, [cart, promiseRef]);

  useEffect(() => {
    if (!cart) {
      return;
    }
    if (!isCartWithActionsDocs(cart)) {
      // Wrong type of cart. Just resolve.
      resolveRef.current?.(cart);
      resetResolver();
    } else if (cart.status === "idle") {
      resolveRef.current?.(cart);
      resetResolver();
    }
  }, [cart, resetResolver]);

  return waitCartIdle;
};

export type { WaitCartIdleCallback };

export {
  DEFAULT_REDO_ENABLED_CART_ATTRIBUTE,
  addProductToCartIfNeeded,
  removeProductFromCartIfNeeded,
  setCartRedoEnabledAttribute,
  useFetcherWithPromise,
  useWaitCartIdle,
  isCartWithActionsDocs,
  getCartLines,
  isOptimisticCart,
};
