import "@shopify/ui-extensions/preact";
import { render } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import {
  useAttributes,
  useBuyerJourneyActiveStep,
  useBuyerJourneyIntercept,
  useCheckoutToken,
  useCurrency,
  useDiscountCodes,
  useLanguage,
  useLocalizationCountry,
  useNote,
  useShippingAddress,
} from "@shopify/ui-extensions/checkout/preact";
import { BACKEND_URL_FROM_ENV } from "./backend-url.generated";

const EXTENSION_SOURCE = "free-gift-checkout-sync";
const EXTENSION_TARGET = "purchase.checkout.block.render";
const AUTO_FREE_GIFT_ATTRIBUTE = { key: "_auto_free_gift", value: "true" };

function getCustomerContext() {
  const customer = shopify.buyerIdentity?.customer?.value;
  const email = shopify.buyerIdentity?.email?.value || customer?.email;

  return {
    customerEmail: email || null,
    customerName:
      customer?.fullName ||
      [customer?.firstName, customer?.lastName].filter(Boolean).join(" ") ||
      null,
    customerId: customer?.id || null,
  };
}

function toSerializable(value, seen = new WeakSet(), depth = 0) {
  if (value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
    return value ?? null;
  }

  if (typeof value === "function" || typeof value === "symbol") {
    return undefined;
  }

  if (depth > 6) {
    return "[Max depth reached]";
  }

  if (typeof value === "object") {
    if (seen.has(value)) {
      return "[Circular]";
    }

    seen.add(value);

    if (Array.isArray(value)) {
      return value.map((item) => toSerializable(item, seen, depth + 1));
    }

    return Object.entries(value).reduce((result, [key, entry]) => {
      const serialized = toSerializable(entry, seen, depth + 1);
      if (serialized !== undefined) {
        result[key] = serialized;
      }

      return result;
    }, {});
  }

  return String(value);
}

function serializeCartLine(line) {
  const merchandise = line.merchandise;

  return {
    id: line.id,
    quantity: line.quantity,
    cost: toSerializable(line.cost),
    attributes: toSerializable(line.attributes ?? []),
    discountAllocations: toSerializable(line.discountAllocations ?? []),
    lineComponents: toSerializable(line.lineComponents ?? []),
    parentRelationship: toSerializable(line.parentRelationship),
    merchandise: merchandise
      ? {
          id: merchandise.id || null,
          type: merchandise.type || null,
          title: merchandise.title || null,
          subtitle: merchandise.subtitle || null,
          sku: merchandise.sku || null,
          requiresShipping: merchandise.requiresShipping ?? null,
          image: toSerializable(merchandise.image),
          selectedOptions: toSerializable(merchandise.selectedOptions ?? []),
          sellingPlan: toSerializable(merchandise.sellingPlan),
          product: merchandise.product
            ? {
                id: merchandise.product.id || null,
                vendor: merchandise.product.vendor || null,
                productType: merchandise.product.productType || null,
              }
            : null,
        }
      : null,
    raw: toSerializable(line),
  };
}

function getCheckoutData({
  lines,
  discountCodes,
  shippingAddress,
  attributes,
  note,
  language,
  currency,
  country,
  activeStep,
}) {
  const cartLines = lines?.value ?? [];

  return {
    activeStep: activeStep?.handle || activeStep?.id || null,
    note: note || null,
    attributes: Array.isArray(attributes)
      ? attributes.map((attribute) => ({
          key: attribute.key,
          value: attribute.value,
        }))
      : [],
    discountCodes: Array.isArray(discountCodes)
      ? discountCodes.map((entry) => entry?.code).filter(Boolean)
      : [],
    language: language?.isoCode || language?.name || null,
    currency: currency?.isoCode || null,
    country: country?.isoCode || country?.name || null,
    shippingAddress: shippingAddress
      ? {
          firstName: shippingAddress.firstName || null,
          lastName: shippingAddress.lastName || null,
          company: shippingAddress.company || null,
          address1: shippingAddress.address1 || null,
          address2: shippingAddress.address2 || null,
          city: shippingAddress.city || null,
          provinceCode: shippingAddress.provinceCode || null,
          zip: shippingAddress.zip || null,
          countryCode: shippingAddress.countryCode || null,
          phone: shippingAddress.phone || null,
        }
      : null,
    lineCount: cartLines.length,
    lines: cartLines.map(serializeCartLine),
  };
}

function isAutoFreeGiftLine(line) {
  const attributes = Array.isArray(line?.attributes) ? line.attributes : [];

  return attributes.some(
    (attribute) =>
      attribute?.key === "_auto_free_gift" &&
      String(attribute?.value || "").toLowerCase() === "true",
  );
}

function withAutoFreeGiftAttribute(attributes) {
  const nextAttributes = Array.isArray(attributes)
    ? attributes.filter((attribute) => attribute?.key !== AUTO_FREE_GIFT_ATTRIBUTE.key)
    : [];

  nextAttributes.push(AUTO_FREE_GIFT_ATTRIBUTE);

  return nextAttributes;
}

export default function () {
  render(<Extension />, document.body);
}

async function logEvent(shop, level, eventType, message, payload = {}) {
  const backendUrl = String(BACKEND_URL_FROM_ENV || "")
    .trim()
    .replace(/\/$/, "");
  if (!backendUrl || !shop) return;

  try {
    await fetch(`${backendUrl}/api/app-event-log`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        shop,
        source: EXTENSION_SOURCE,
        extensionTarget: EXTENSION_TARGET,
        level,
        eventType,
        message,
        checkoutToken: shopify.checkoutToken?.value || null,
        ...getCustomerContext(),
        payload: {
          ...payload,
          checkoutData:
            payload.checkoutData ??
            getCheckoutData({
              lines: shopify.lines,
              discountCodes: shopify.discountCodes?.value,
              shippingAddress: shopify.shippingAddress?.value,
              attributes: shopify.attributes?.value,
              note: shopify.note?.value,
              language: shopify.localization?.language?.value,
              currency: shopify.currency?.value,
              country: shopify.localization?.country?.value,
              activeStep: shopify.buyerJourney?.activeStep?.value,
            }),
        },
      }),
    });
  } catch (error) {
    console.error("Free gift log failed", error);
  }
}

function Extension() {
  const { applyCartLinesChange, lines } = shopify;
  useShippingAddress();
  useAttributes();
  useNote();
  useLanguage();
  useCurrency();
  useLocalizationCountry();
  useDiscountCodes();
  useBuyerJourneyActiveStep();
  useCheckoutToken();
  const [freeGiftMapping, setFreeGiftMapping] = useState({});
  const [configLoaded, setConfigLoaded] = useState(false);
  const [isSyncing, setIsSyncing] = useState(false);
  const [cartNeedsSync, setCartNeedsSync] = useState(false);
  const [syncErrorMessage, setSyncErrorMessage] = useState("");
  const [retryTick, setRetryTick] = useState(0);
  const syncingRef = useRef(false);

  const cartLineIds =
    (lines?.value ?? [])
      .map((line) => `${line.id}:${line.quantity}`)
      .join(",") || "";

  useEffect(() => {
    fetchFreeGiftConfig();
  }, [shopify.shop.myshopifyDomain]);

  useEffect(() => {
    syncFreeGiftLines();
  }, [cartLineIds, JSON.stringify(freeGiftMapping), retryTick]);

  useBuyerJourneyIntercept(({ canBlockProgress }) => {
    if (!canBlockProgress) {
      return { behavior: "allow" };
    }

    if (syncErrorMessage) {
      return {
        behavior: "block",
        reason: "free_gift_sync_error",
        errors: [
          {
            message: syncErrorMessage,
            target: "$.cart",
          },
        ],
      };
    }

    if (!configLoaded || isSyncing || cartNeedsSync) {
      return {
        behavior: "block",
        reason: "free_gift_sync_in_progress",
      };
    }

    return { behavior: "allow" };
  });

  async function fetchFreeGiftConfig() {
    try {
      const backendUrl = BACKEND_URL_FROM_ENV;
      const shop = shopify.shop.myshopifyDomain;

      if (!backendUrl || !shop) {
        setFreeGiftMapping({});
        setConfigLoaded(true);
        return;
      }

      const response = await fetch(
        `${backendUrl}/api/free-gift-config?shop=${encodeURIComponent(shop)}`,
      );

      if (!response.ok) {
        console.error("Failed to load free gift config", await response.text());
        setFreeGiftMapping({});
        setConfigLoaded(true);
        return;
      }

      const json = await response.json();
      const productMapping =
        json?.productMapping && typeof json.productMapping === "object"
          ? json.productMapping
          : {};

      setFreeGiftMapping(productMapping);
    } catch (error) {
      console.error("Failed to fetch free gift config", error);
      setFreeGiftMapping({});
    } finally {
      setConfigLoaded(true);
    }
  }

  async function syncFreeGiftLines() {
    if (syncingRef.current) return;

    const mappingEntries = Object.entries(freeGiftMapping);
    if (!mappingEntries.length) {
      setCartNeedsSync(false);
      setSyncErrorMessage("");
      return;
    }

    const cartLines = lines?.value ?? [];
    if (!cartLines.length) {
      setCartNeedsSync(false);
      setSyncErrorMessage("");
      return;
    }

    const normalizedMappings = mappingEntries.reduce(
      (result, [source, gifts]) => {
        const normalizedGifts = Array.isArray(gifts)
          ? gifts.flatMap((value) => {
              if (typeof value === "string") {
                return value
                  ? [{ variantId: value, quantityMultiplier: 1 }]
                  : [];
              }

              if (!value || typeof value !== "object" || Array.isArray(value)) {
                return [];
              }

              const variantId =
                typeof value.variantId === "string" ? value.variantId : "";
              const quantityMultiplier = Number.isFinite(
                value.quantityMultiplier,
              )
                ? Math.max(1, Math.trunc(value.quantityMultiplier))
                : 1;

              return variantId ? [{ variantId, quantityMultiplier }] : [];
            })
          : typeof gifts === "string"
            ? [{ variantId: gifts, quantityMultiplier: 1 }]
            : [];

        if (!normalizedGifts.length) return result;

        result[source] = normalizedGifts;

        return result;
      },
      {},
    );

    const sourceQuantities = {};
    const managedGiftLinesByVariant = {};
    const unmanagedGiftLinesByVariant = {};
    const managedGiftVariantIds = new Set(
      Object.values(normalizedMappings).flatMap((gifts) =>
        gifts.map((gift) => gift.variantId),
      ),
    );

    for (const line of cartLines) {
      const variantId = line.merchandise?.id;
      if (!variantId || typeof variantId !== "string") continue;
      const autoFreeGiftLine = isAutoFreeGiftLine(line);

      if (normalizedMappings[variantId] && !autoFreeGiftLine) {
        sourceQuantities[variantId] =
          (sourceQuantities[variantId] ?? 0) + line.quantity;
      }

      if (managedGiftVariantIds.has(variantId)) {
        const targetCollection = autoFreeGiftLine
          ? managedGiftLinesByVariant
          : unmanagedGiftLinesByVariant;

        if (!targetCollection[variantId]) {
          targetCollection[variantId] = [];
        }

        targetCollection[variantId].push({
          id: line.id,
          quantity: line.quantity,
          attributes: line.attributes ?? [],
        });
      }
    }

    const changes = [];
    const desiredQuantitiesByGift = Object.entries(normalizedMappings).reduce(
      (result, [sourceVariantId, gifts]) => {
        const sourceQuantity = sourceQuantities[sourceVariantId] ?? 0;
        if (sourceQuantity <= 0) return result;

        for (const gift of gifts) {
          result[gift.variantId] =
            (result[gift.variantId] ?? 0) +
            sourceQuantity * gift.quantityMultiplier;
        }

        return result;
      },
      {},
    );

    for (const giftVariantId of managedGiftVariantIds) {
      const desiredQuantity = desiredQuantitiesByGift[giftVariantId] ?? 0;
      const managedGiftLines = managedGiftLinesByVariant[giftVariantId] ?? [];
      const unmanagedGiftLines =
        unmanagedGiftLinesByVariant[giftVariantId] ?? [];
      const totalManagedGiftQuantity = managedGiftLines.reduce(
        (sum, line) => sum + line.quantity,
        0,
      );

      if (desiredQuantity <= 0) {
        for (const giftLine of unmanagedGiftLines) {
          changes.push({
            type: "removeCartLine",
            id: giftLine.id,
            quantity: giftLine.quantity,
          });
        }

        for (const giftLine of managedGiftLines) {
          changes.push({
            type: "removeCartLine",
            id: giftLine.id,
            quantity: giftLine.quantity,
          });
        }
        continue;
      }

      if (managedGiftLines.length === 0) {
        const [primaryUnmanagedGiftLine, ...extraUnmanagedGiftLines] =
          unmanagedGiftLines;

        if (primaryUnmanagedGiftLine) {
          changes.push({
            type: "updateCartLine",
            id: primaryUnmanagedGiftLine.id,
            quantity: desiredQuantity,
            attributes: withAutoFreeGiftAttribute(primaryUnmanagedGiftLine.attributes),
          });

          for (const extraGiftLine of extraUnmanagedGiftLines) {
            changes.push({
              type: "removeCartLine",
              id: extraGiftLine.id,
              quantity: extraGiftLine.quantity,
            });
          }

          continue;
        }

        changes.push({
          type: "addCartLine",
          merchandiseId: giftVariantId,
          quantity: desiredQuantity,
          attributes: [AUTO_FREE_GIFT_ATTRIBUTE],
        });
        continue;
      }

      for (const giftLine of unmanagedGiftLines) {
        changes.push({
          type: "removeCartLine",
          id: giftLine.id,
          quantity: giftLine.quantity,
        });
      }

      const [primaryGiftLine, ...extraGiftLines] = managedGiftLines;

      if (totalManagedGiftQuantity !== desiredQuantity) {
        changes.push({
          type: "updateCartLine",
          id: primaryGiftLine.id,
          quantity: desiredQuantity,
        });
      }

      for (const extraGiftLine of extraGiftLines) {
        changes.push({
          type: "removeCartLine",
          id: extraGiftLine.id,
          quantity: extraGiftLine.quantity,
        });
      }
    }

    if (!changes.length) {
      setCartNeedsSync(false);
      setSyncErrorMessage("");
      return;
    }

    setCartNeedsSync(true);
    setSyncErrorMessage("");
    syncingRef.current = true;
    setIsSyncing(true);
    let failed = false;

    try {
      for (const change of changes) {
        const result = await applyCartLinesChange(change);

        if (result.type === "error") {
          failed = true;
          setSyncErrorMessage(
            "Free gift items could not be updated. Please refresh checkout or try again.",
          );
          console.error("Free gift checkout sync failed", result.message);
          void logEvent(
            shopify.shop.myshopifyDomain,
            "error",
            "free_gift_sync_error",
            result.message,
            { change, extensionTarget: EXTENSION_TARGET },
          );
          break;
        }

        const eventDetails =
          change.type === "addCartLine"
            ? {
                eventType: "free_gift_auto_added",
                message: "Free gift was auto-added to checkout.",
                variantId: change.merchandiseId,
                quantity: change.quantity,
              }
            : change.type === "removeCartLine"
              ? {
                  eventType: "free_gift_auto_removed",
                  message: "Free gift was auto-removed from checkout.",
                  lineId: change.id,
                  quantity: change.quantity,
                }
              : change.type === "updateCartLine"
                ? {
                    eventType: "free_gift_qty_updated",
                    message: "Free gift quantity was updated automatically.",
                    lineId: change.id,
                    quantity: change.quantity,
                  }
                : null;

        if (eventDetails) {
          void logEvent(
            shopify.shop.myshopifyDomain,
            "info",
            eventDetails.eventType,
            eventDetails.message,
            {
              ...eventDetails,
              extensionTarget: EXTENSION_TARGET,
            },
          );
        }
      }
    } finally {
      syncingRef.current = false;
      setIsSyncing(false);
      if (!failed) {
        setSyncErrorMessage("");
      }
      setTimeout(() => setRetryTick((value) => value + 1), 350);
    }
  }

  return null;
}
