import {
  Checkout,
  Customer,
  Product as SalesforceProduct,
  Search,
} from "commerce-sdk";
import { ShopperBaskets } from "commerce-sdk/dist/checkout/checkout";
import { defaultSort, storeCatalog, TAGS } from "lib/constants";
import { unstable_cache as cache, revalidateTag } from "next/cache";
import { cookies, headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import { getProductRecommendations as getOCProductRecommendations } from "./ocapi";
import {
  Cart,
  CartItem,
  Collection,
  Image,
  Product,
  ProductRecommendations,
} from "./types";

const config = {
  headers: {},
  parameters: {
    clientId: process.env.SFCC_CLIENT_ID,
    organizationId: process.env.SFCC_ORGANIZATIONID,
    shortCode: process.env.SFCC_SHORTCODE,
    siteId: process.env.SFCC_SITEID,
  },
};

type SortedProductResult = {
  productResult: SalesforceProduct.ShopperProducts.Product;
  index: number;
};

export const getCollections = cache(
  async () => {
    return await getSFCCCollections();
  },
  ["get-collections"],
  {
    tags: [TAGS.collections],
  }
);

export function getCollection(handle: string) {
  return getCollections().then((collections) =>
    collections.find((c) => c.handle === handle)
  );
}

export const getProduct = cache(
  async (id: string) => getSFCCProduct(id),
  ["get-product"],
  {
    tags: [TAGS.products],
  }
);

export const getCollectionProducts = cache(
  async ({
    collection,
    reverse,
    sortKey,
  }: {
    collection: string;
    reverse?: boolean;
    sortKey?: string;
  }) => {
    return await searchProducts({ categoryId: collection, sortKey });
  },
  ["get-collection-products"],
  { tags: [TAGS.products, TAGS.collections] }
);

export const getProducts = cache(
  async ({
    query,
    sortKey,
  }: {
    query?: string;
    sortKey?: string;
    reverse?: boolean;
  }) => {
    return await searchProducts({ query, sortKey });
  },
  ["get-products"],
  {
    tags: [TAGS.products],
  }
);

export async function createCart() {
  let guestToken = (await cookies()).get("guest_token")?.value;

  // if there is not a guest token, get one and store it in a cookie
  if (!guestToken) {
    const tokenResponse = await getGuestUserAuthToken();
    guestToken = tokenResponse.access_token;
    (await cookies()).set("guest_token", guestToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "strict",
      maxAge: 60 * 30,
      path: "/",
    });
  }

  // get the guest config
  const config = await getGuestUserConfig(guestToken);

  // initialize the basket config
  const basketClient = new Checkout.ShopperBaskets(config);

  // create an empty ShopperBaskets.Basket
  const createdBasket = await basketClient.createBasket({
    body: {},
  });

  const cartItems = await getCartItems(createdBasket);

  return reshapeBasket(createdBasket, cartItems);
}

export async function getCart(): Promise<Cart | undefined> {
  const cartId = (await cookies()).get("cartId")?.value!;
  // get the guest token to get the correct guest cart
  const guestToken = (await cookies()).get("guest_token")?.value;

  const config = await getGuestUserConfig(guestToken);

  if (!cartId) return;

  try {
    const basketClient = new Checkout.ShopperBaskets(config);

    const basket = await basketClient.getBasket({
      parameters: {
        basketId: cartId,
        organizationId: process.env.SFCC_ORGANIZATIONID,
        siteId: process.env.SFCC_SITEID,
      },
    });

    if (!basket?.basketId) return;

    const cartItems = await getCartItems(basket);
    return reshapeBasket(basket, cartItems);
  } catch (e: any) {
    console.log(await e.response.text());
    return;
  }
}

export async function addToCart(
  lines: { merchandiseId: string; quantity: number }[]
) {
  const cartId = (await cookies()).get("cartId")?.value!;
  // get the guest token to get the correct guest cart
  const guestToken = (await cookies()).get("guest_token")?.value;
  const config = await getGuestUserConfig(guestToken);

  try {
    const basketClient = new Checkout.ShopperBaskets(config);

    const basket = await basketClient.addItemToBasket({
      parameters: {
        basketId: cartId,
        organizationId: process.env.SFCC_ORGANIZATIONID,
        siteId: process.env.SFCC_SITEID,
      },
      body: lines.map((line) => {
        return {
          productId: line.merchandiseId,
          quantity: line.quantity,
        };
      }),
    });

    if (!basket?.basketId) return;

    const cartItems = await getCartItems(basket);
    return reshapeBasket(basket, cartItems);
  } catch (e: any) {
    console.log(await e.response.text());
    return;
  }
}

export async function removeFromCart(lineIds: string[]) {
  const cartId = (await cookies()).get("cartId")?.value!;
  // Next Commerce only sends one lineId at a time
  if (lineIds.length !== 1)
    throw new Error("Invalid number of line items provided");

  // get the guest token to get the correct guest cart
  const guestToken = (await cookies()).get("guest_token")?.value;
  const config = await getGuestUserConfig(guestToken);

  const basketClient = new Checkout.ShopperBaskets(config);

  const basket = await basketClient.removeItemFromBasket({
    parameters: {
      basketId: cartId,
      itemId: lineIds[0]!,
    },
  });

  const cartItems = await getCartItems(basket);
  return reshapeBasket(basket, cartItems);
}

export async function updateCart(
  lines: { id: string; merchandiseId: string; quantity: number }[]
) {
  const cartId = (await cookies()).get("cartId")?.value!;
  // get the guest token to get the correct guest cart
  const guestToken = (await cookies()).get("guest_token")?.value;
  const config = await getGuestUserConfig(guestToken);

  const basketClient = new Checkout.ShopperBaskets(config);

  // ProductItem quantity can not be updated through the API
  // Quantity updates need to remove all items from the cart and add them back with updated quantities
  // See: https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-baskets?meta=updateBasket

  // create removePromises for each line
  const removePromises = lines.map((line) =>
    basketClient.removeItemFromBasket({
      parameters: {
        basketId: cartId,
        itemId: line.id,
      },
    })
  );

  // wait for all removals to resolve
  await Promise.all(removePromises);

  // create addPromises for each line
  const addPromises = lines.map((line) =>
    basketClient.addItemToBasket({
      parameters: {
        basketId: cartId,
      },
      body: [
        {
          productId: line.merchandiseId,
          quantity: line.quantity,
        },
      ],
    })
  );

  // wait for all additions to resolve
  await Promise.all(addPromises);

  // all updates are done, get the updated basket
  const updatedBasket = await basketClient.getBasket({
    parameters: {
      basketId: cartId,
    },
  });

  const cartItems = await getCartItems(updatedBasket);
  return reshapeBasket(updatedBasket, cartItems);
}

export async function getProductRecommendations(productId: string) {
  const ocProductRecommendations =
    await getOCProductRecommendations<ProductRecommendations>(productId);

  if (!ocProductRecommendations?.recommendations?.length) return [];

  const clientConfig = await getGuestUserConfig();
  const productsClient = new SalesforceProduct.ShopperProducts(clientConfig);

  const recommendedProducts: SortedProductResult[] = [];

  await Promise.all(
    ocProductRecommendations.recommendations.map(
      async (recommendation, index) => {
        const productResult = await productsClient.getProduct({
          parameters: {
            organizationId: clientConfig.parameters.organizationId,
            siteId: clientConfig.parameters.siteId,
            id: recommendation.recommended_item_id,
          },
        });
        recommendedProducts.push({ productResult, index });
      }
    )
  );

  const sortedResults = recommendedProducts
    .sort((a: any, b: any) => a.index - b.index)
    .map((item) => item.productResult);

  return reshapeProducts(sortedResults);
}

export async function revalidate(req: NextRequest) {
  const collectionWebhooks = [
    "collections/create",
    "collections/delete",
    "collections/update",
  ];
  const productWebhooks = [
    "products/create",
    "products/delete",
    "products/update",
  ];
  const topic = (await headers()).get("x-sfcc-topic") || "unknown";
  const secret = req.nextUrl.searchParams.get("secret");
  const isCollectionUpdate = collectionWebhooks.includes(topic);
  const isProductUpdate = productWebhooks.includes(topic);

  if (!secret || secret !== process.env.SFCC_REVALIDATION_SECRET) {
    console.error("Invalid revalidation secret.");
    return NextResponse.json({ status: 200 });
  }

  if (!isCollectionUpdate && !isProductUpdate) {
    // We don't need to revalidate anything for any other topics.
    return NextResponse.json({ status: 200 });
  }

  if (isCollectionUpdate) {
    revalidateTag(TAGS.collections);
  }

  if (isProductUpdate) {
    revalidateTag(TAGS.products);
  }

  return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
}

async function getGuestUserAuthToken() {
  const base64data = Buffer.from(
    `${process.env.SFCC_CLIENT_ID}:${process.env.SFCC_SECRET}`
  ).toString("base64");
  const headers = { Authorization: `Basic ${base64data}` };
  const client = new Customer.ShopperLogin(config);

  return await client.getAccessToken({
    headers,
    body: {
      grant_type: "client_credentials",
      channel_id: process.env.SFCC_SITEID,
    },
  });
}

async function getGuestUserConfig(token?: string) {
  const guestToken = token || (await getGuestUserAuthToken()).access_token;

  if (!guestToken) {
    throw new Error("Failed to retrieve access token");
  }

  return {
    ...config,
    headers: {
      authorization: `Bearer ${guestToken}`,
    },
  };
}

async function getSFCCCollections() {
  const config = await getGuestUserConfig();
  const productsClient = new SalesforceProduct.ShopperProducts(config);

  const result = await productsClient.getCategories({
    parameters: {
      ids: storeCatalog.ids,
    },
  });

  return reshapeCategories(result.data || []);
}

async function getSFCCProduct(id: string) {
  const config = await getGuestUserConfig();
  const productsClient = new SalesforceProduct.ShopperProducts(config);

  const product = await productsClient.getProduct({
    parameters: {
      organizationId: config.parameters.organizationId,
      siteId: config.parameters.siteId,
      id,
    },
  });

  return reshapeProduct(product);
}

async function searchProducts(options: {
  query?: string;
  categoryId?: string;
  sortKey?: string;
}) {
  const { query, categoryId, sortKey = defaultSort.sortKey } = options;
  const config = await getGuestUserConfig();

  const searchClient = new Search.ShopperSearch(config);
  const searchResults = await searchClient.productSearch({
    parameters: {
      q: query || "",
      refine: categoryId ? [`cgid=${categoryId}`] : [],
      sort: sortKey,
      limit: 100,
    },
  });

  const results: SortedProductResult[] = [];

  const productsClient = new SalesforceProduct.ShopperProducts(config);
  await Promise.all(
    searchResults.hits.map(
      async (product: { productId: string }, index: number) => {
        const productResult = await productsClient.getProduct({
          parameters: {
            organizationId: config.parameters.organizationId,
            siteId: config.parameters.siteId,
            id: product.productId,
          },
        });
        results.push({ productResult, index });
      }
    )
  );

  const sortedResults = results
    .sort((a: any, b: any) => a.index - b.index)
    .map((item) => item.productResult);

  return reshapeProducts(sortedResults);
}

async function getCartItems(createdBasket: ShopperBaskets.Basket) {
  const cartItems: CartItem[] = [];

  if (createdBasket.productItems) {
    const productsInCart: Product[] = [];

    // Fetch all matching products for items in the cart
    await Promise.all(
      createdBasket.productItems
        .filter((l: ShopperBaskets.ProductItem) => l.productId)
        .map(async (l: ShopperBaskets.ProductItem) => {
          const product = await getProduct(l.productId!);
          productsInCart.push(product);
        })
    );

    // Reshape the sfcc items and push them onto the cartItems
    createdBasket.productItems.map(
      (productItem: ShopperBaskets.ProductItem) => {
        cartItems.push(
          reshapeProductItem(
            productItem,
            createdBasket.currency || "USD",
            productsInCart.find((p) => p.id === productItem.productId)!
          )
        );
      }
    );
  }

  return cartItems;
}

function reshapeCategory(
  category: SalesforceProduct.ShopperProducts.Category
): Collection | undefined {
  if (!category) {
    return undefined;
  }

  return {
    handle: category.id,
    title: category.name || "",
    description: category.description || "",
    seo: {
      title: category.pageTitle || "",
      description: category.description || "",
    },
    updatedAt: "",
    path: `/search/${category.id}`,
  };
}

function reshapeCategories(
  categories: SalesforceProduct.ShopperProducts.Category[]
) {
  const reshapedCategories = [];
  for (const category of categories) {
    if (category) {
      const reshapedCategory = reshapeCategory(category);
      if (reshapedCategory) {
        reshapedCategories.push(reshapedCategory);
      }
    }
  }
  return reshapedCategories;
}

function reshapeProduct(product: SalesforceProduct.ShopperProducts.Product) {
  if (!product.name) {
    throw new Error("Product name is not set");
  }

  const images = reshapeImages(product.imageGroups);

  if (!images[0]) {
    throw new Error("Product image is not set");
  }

  const flattenedPrices =
    product.variants
      ?.filter((variant) => variant.price !== undefined)
      .reduce((acc: number[], variant) => [...acc, variant.price!], [])
      .sort((a, b) => a - b) || [];

  return {
    id: product.id,
    handle: product.id,
    title: product.name,
    description: product.shortDescription || "",
    descriptionHtml: product.longDescription || "",
    tags: product["c_product-tags"] || [],
    featuredImage: images[0],
    // TODO: check dates for whether it is available
    availableForSale: true,
    priceRange: {
      maxVariantPrice: {
        // TODO: verify whether there is another property for this
        amount: flattenedPrices[flattenedPrices.length - 1]?.toString() || "0",
        currencyCode: product.currency || "USD",
      },
      minVariantPrice: {
        amount: flattenedPrices[0]?.toString() || "0",
        currencyCode: product.currency || "USD",
      },
    },
    images: images,
    options:
      product.variationAttributes?.map((attribute) => {
        return {
          id: attribute.id,
          name: attribute.name!,
          // TODO: might be a better way to do this, we are providing the name as the value
          values:
            attribute.values
              ?.filter((v) => v.value !== undefined)
              ?.map((v) => v.name!) || [],
        };
      }) || [],
    seo: {
      title: product.pageTitle || "",
      description: product.pageDescription || "",
    },
    variants: reshapeVariants(product.variants || [], product),
    updatedAt: product["c_updated-date"],
  };
}

function reshapeProducts(
  products: SalesforceProduct.ShopperProducts.Product[]
) {
  const reshapedProducts = [];
  for (const product of products) {
    if (product) {
      const reshapedProduct = reshapeProduct(product);
      if (reshapedProduct) {
        reshapedProducts.push(reshapedProduct);
      }
    }
  }
  return reshapedProducts;
}

function reshapeImages(
  imageGroups: SalesforceProduct.ShopperProducts.ImageGroup[] | undefined
): Image[] {
  if (!imageGroups) return [];

  const largeGroup = imageGroups.filter((g) => g.viewType === "large");

  const images = [...largeGroup].map((group) => group.images).flat();

  return images.map((image) => {
    return {
      altText: image.alt!,
      url: image.disBaseLink || image.link,
      width: image.width || 800,
      height: image.height || 800,
    };
  });
}

function reshapeVariants(
  variants: SalesforceProduct.ShopperProducts.Variant[],
  product: SalesforceProduct.ShopperProducts.Product
) {
  return variants.map((variant) => reshapeVariant(variant, product));
}

function reshapeVariant(
  variant: SalesforceProduct.ShopperProducts.Variant,
  product: SalesforceProduct.ShopperProducts.Product
) {
  return {
    id: variant.productId,
    title: product.name || "",
    availableForSale: variant.orderable || false,
    selectedOptions:
      Object.entries(variant.variationValues || {}).map(([key, value]) => ({
        // TODO: we use the name here instead of the key because the frontend only uses names
        name:
          product.variationAttributes?.find((attr) => attr.id === key)?.name ||
          key,
        // TODO: might be a cleaner way to do this, we need to look up the name on the list of values from the variationAttributes
        value:
          product.variationAttributes
            ?.find((attr) => attr.id === key)
            ?.values?.find((v) => v.value === value)?.name || "",
      })) || [],
    price: {
      amount: variant.price?.toString() || "0",
      currencyCode: product.currency || "USD",
    },
  };
}

function reshapeProductItem(
  item: Checkout.ShopperBaskets.ProductItem,
  currency: string,
  matchingProduct: Product
): CartItem {
  return {
    id: item.itemId || "",
    quantity: item.quantity || 0,
    cost: {
      totalAmount: {
        amount: item.price?.toString() || "0",
        currencyCode: currency,
      },
    },
    merchandise: {
      id: item.productId || "",
      title: item.productName || "",
      selectedOptions:
        item.optionItems?.map((o) => {
          return {
            name: o.optionId!,
            value: o.optionValueId!,
          };
        }) || [],
      product: matchingProduct,
    },
  };
}

function reshapeBasket(
  basket: ShopperBaskets.Basket,
  cartItems: CartItem[]
): Cart {
  return {
    id: basket.basketId!,
    checkoutUrl: "/checkout",
    cost: {
      subtotalAmount: {
        amount: basket.productSubTotal?.toString() || "0",
        currencyCode: basket.currency || "USD",
      },
      totalAmount: {
        amount: `${(basket.productSubTotal ?? 0) + (basket.merchandizeTotalTax ?? 0)}`,
        currencyCode: basket.currency || "USD",
      },
      totalTaxAmount: {
        amount: basket.merchandizeTotalTax?.toString() || "0",
        currencyCode: basket.currency || "USD",
      },
    },
    totalQuantity:
      cartItems?.reduce((acc, item) => acc + (item?.quantity ?? 0), 0) ?? 0,
    lines: cartItems,
  };
}