import { NextRequest } from "next/server";
import { connectDB } from "@/lib/db";
import { Cart, Product } from "@/models";
import {
  successResponse,
  createdResponse,
  notFoundResponse,
} from "@/lib/api/response";
import { ValidationError, handleApiError } from "@/lib/api/errors";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { rateLimitByIP, rateLimitByUser } from "@/lib/api/rate-limit-middleware";
import { validateBody } from "@/lib/api/validate";
import { CartAddItemSchema, CartUpdateItemSchema } from "@/lib/validations";
import { resolveItemShipping } from "@/lib/product-shipping";
import { PRODUCT_STATUS } from "@/config/app.config";
import {
  isStorefrontMultiVendorEnabled,
  isStorefrontProductSourceAllowed,
} from "@/lib/product-visibility";
import { getPurchasableQuantity } from "@/lib/products/stock-policy";

// Cart item type for type annotations
interface CartItem {
  productId: { toString: () => string };
  variantId?: { toString: () => string };
  quantity: number;
  price: number;
  name: string;
  image: string;
}

type StoredCartItem = CartItem & Record<string, unknown>;

type LeanVariant = {
  _id: { toString: () => string };
  name: string;
  price: number;
  stock: number;
  image?: string;
  mediaId?: string;
};

type LeanProduct = {
  name: string;
  price: number;
  stock: number;
  status?: string;
  productSource?: unknown;
  images?: string[];
  media?: { _id: string; url: string }[];
  variants?: LeanVariant[];
  shipping?: { isPhysicalProduct?: boolean };
  inventory?: { tracked?: boolean; continueSellingWhenOutOfStock?: boolean };
};

async function filterAvailableCartItems(
  items: StoredCartItem[],
): Promise<StoredCartItem[]> {
  if (!items.length) return items;

  const isMultiVendorEnabled = await isStorefrontMultiVendorEnabled();
  const productIds = Array.from(
    new Set(
      items
        .map((item) => item.productId?.toString())
        .filter((id): id is string => Boolean(id)),
    ),
  );
  const products = await Product.find({ _id: { $in: productIds } })
    .select("status productSource")
    .lean<Array<{ _id: { toString: () => string }; status?: string; productSource?: unknown }>>();
  const visibleProductIds = new Set(
    products
      .filter(
        (product) =>
          product.status === PRODUCT_STATUS.ACTIVE &&
          isStorefrontProductSourceAllowed(
            product.productSource,
            isMultiVendorEnabled,
          ),
      )
      .map((product) => product._id.toString()),
  );

  return items.filter((item) => visibleProductIds.has(item.productId.toString()));
}

/**
 * True when at least one cart line still needs physical shipping — the same
 * product/variant rule (`variant.requiresShipping ?? product.isPhysicalProduct`)
 * the order and payment routes use. An empty cart counts as shippable so the
 * checkout never renders its digital-only mode transiently while loading.
 */
async function cartHasShippableItems(
  items: StoredCartItem[],
): Promise<boolean> {
  if (!items.length) return true;

  const productIds = Array.from(
    new Set(items.map((item) => item.productId?.toString()).filter(Boolean)),
  );
  const products = await Product.find({ _id: { $in: productIds } })
    .select("shipping.isPhysicalProduct variants._id variants.requiresShipping")
    .lean<
      Array<{
        _id: { toString: () => string };
        shipping?: { isPhysicalProduct?: boolean };
        variants?: { _id: { toString: () => string }; requiresShipping?: boolean }[];
      }>
    >();
  const byId = new Map(products.map((p) => [p._id.toString(), p]));

  return items.some((item) => {
    const product = byId.get(item.productId?.toString() || "");
    // Unknown product — assume shippable rather than hiding the address step.
    if (!product) return true;
    const variant = item.variantId
      ? product.variants?.find(
          (v) => v._id.toString() === item.variantId?.toString(),
        )
      : undefined;
    return resolveItemShipping({
      productShipping: product.shipping,
      variantShipping: { requiresShipping: variant?.requiresShipping },
    }).requiresShipping;
  });
}

/**
 * Merge a lingering guest cart (keyed by the cart_session cookie) into the
 * logged-in user's cart. Without this, items added before login silently
 * disappear from view — and on a shared browser the stale cookie can surface
 * a previous guest's cart to the next visitor. Item identity is
 * (productId, variantId): quantities are combined for matching lines; guest
 * lines whose product already exists with a different purchaseType are
 * dropped (standard vs preorder for one product is mutually exclusive).
 */
async function mergeGuestCartIntoUserCart(
  userId: string,
  sessionId: string,
): Promise<void> {
  // Delete-first claim: right after login the header and the page often BOTH
  // fetch the cart, so two requests can reach here concurrently. Only the one
  // that wins the delete performs the merge — otherwise each would add the
  // guest quantities on top of the other's merge (doubled lines).
  const guestCart = await Cart.findOneAndDelete({
    sessionId,
    userId: { $exists: false },
  }).lean();
  if (!guestCart) return;

  const guestItems = (guestCart.items || []) as StoredCartItem[];
  if (guestItems.length === 0) return;

  const userCart = await Cart.findOne({ userId });
  if (!userCart) {
    // No user cart yet — recreate the claimed guest cart as the user's cart
    // (create() runs the sliding-TTL pre-save hook).
    const {
      _id: _guestId,
      sessionId: _guestSessionId,
      __v: _v,
      createdAt: _createdAt,
      updatedAt: _updatedAt,
      ...guestFields
    } = guestCart as Record<string, unknown>;
    await Cart.create({ ...guestFields, userId });
    return;
  }

  const keyOf = (item: StoredCartItem) =>
    `${item.productId?.toString()}::${item.variantId?.toString() || ""}`;
  const merged = [...(userCart.items as StoredCartItem[])];
  const byKey = new Map(merged.map((item) => [keyOf(item), item]));
  const productPurchaseTypes = new Map(
    merged.map((item) => [
      item.productId?.toString(),
      (item as { purchaseType?: string }).purchaseType || "standard",
    ]),
  );

  for (const guestItem of guestItems) {
    const existing = byKey.get(keyOf(guestItem));
    if (existing) {
      existing.quantity =
        Number(existing.quantity || 0) + Number(guestItem.quantity || 0);
      continue;
    }
    const productKey = guestItem.productId?.toString();
    const existingType = productPurchaseTypes.get(productKey);
    const guestType =
      (guestItem as { purchaseType?: string }).purchaseType || "standard";
    if (existingType && existingType !== guestType) continue;
    merged.push(guestItem);
    byKey.set(keyOf(guestItem), guestItem);
    productPurchaseTypes.set(productKey, guestType);
  }

  // save() (not updateOne) so the sliding-TTL pre-save hook also pushes
  // expiresAt out — a near-expiry cart that just received merged items must
  // not get TTL-deleted moments later.
  userCart.items = merged as typeof userCart.items;
  await userCart.save();
}

/**
 * GET /api/cart
 * Get current user's cart
 */
export async function GET(request: NextRequest) {
  try {
    await connectDB();

    // Get session
    const session = await auth.api.getSession({
      headers: await headers(),
    });

    const userId = session?.user?.id;
    const sessionId = request.cookies.get("cart_session")?.value;

    if (userId) {
      await rateLimitByUser(
        request,
        userId,
        "cart:get",
        "lenient",
        session?.user?.role
      );
    } else {
      await rateLimitByIP(request, "lenient");
    }

    if (!userId && !sessionId) {
      return successResponse({ items: [], totalItems: 0, subtotal: 0 });
    }

    let clearGuestCookie = false;
    if (userId && sessionId) {
      await mergeGuestCartIntoUserCart(userId, sessionId).catch((err) =>
        console.error("Failed to merge guest cart on login:", err),
      );
      clearGuestCookie = true;
    }

    const query = userId ? { userId } : { sessionId };
    const cart = await Cart.findOne(query).lean();

    // The guest cart (if any) has been merged into the user cart above, so
    // the stale cookie must not resurface it — especially on a shared browser
    // where it may belong to a previous visitor.
    const withCookieCleanup = (response: ReturnType<typeof successResponse>) => {
      if (clearGuestCookie) response.cookies.delete("cart_session");
      return response;
    };

    if (!cart) {
      return withCookieCleanup(
        successResponse({ items: [], totalItems: 0, subtotal: 0 }),
      );
    }

    const visibleItems = await filterAvailableCartItems(
      cart.items as StoredCartItem[],
    );
    if (visibleItems.length !== cart.items.length) {
      // Optimistic guard: this read-endpoint write must not clobber an item a
      // concurrent add just pushed — only apply if the cart is unchanged since
      // we read it. On interference the next GET re-filters anyway.
      await Cart.updateOne(
        { ...query, updatedAt: (cart as { updatedAt?: Date }).updatedAt },
        { $set: { items: visibleItems } },
      );
    }

    // Calculate totals
    const totalItems = visibleItems.reduce(
      (sum: number, item: { quantity: number }) => sum + item.quantity,
      0
    );
    const subtotal = visibleItems.reduce(
      (sum: number, item: { price: number; quantity: number }) =>
        sum + item.price * item.quantity,
      0
    );

    return withCookieCleanup(
      successResponse({
        ...cart,
        items: visibleItems,
        totalItems,
        subtotal,
        // Digital-only carts (ebooks, downloads) skip the shipping address
        // and shipping method steps at checkout — same rule the order/payment
        // routes apply server-side via resolveItemShipping.
        hasShippableItems: await cartHasShippableItems(visibleItems),
      }),
    );
  } catch (error) {
    return handleApiError(error);
  }
}

/**
 * POST /api/cart
 * Add item to cart
 */
export async function POST(request: NextRequest) {
  try {
    await connectDB();

    // Get session
    const session = await auth.api.getSession({
      headers: await headers(),
    });

    const userId = session?.user?.id;
    let sessionId = request.cookies.get("cart_session")?.value;

    if (userId) {
      await rateLimitByUser(
        request,
        userId,
        "cart:add",
        "moderate",
        session?.user?.role
      );
    } else {
      await rateLimitByIP(request, "moderate");
    }

    const { productId, quantity, variantId } =
      await validateBody(request, CartAddItemSchema);

    // Generate session ID for guest users
    if (!userId && !sessionId) {
      sessionId = crypto.randomUUID();
    }

    const isMultiVendorEnabled = await isStorefrontMultiVendorEnabled();
    const product = await Product.findById(productId).lean<LeanProduct>();
    if (!product) {
      return notFoundResponse("Product");
    }
    if (
      product.status !== PRODUCT_STATUS.ACTIVE ||
      !isStorefrontProductSourceAllowed(
        product.productSource,
        isMultiVendorEnabled,
      )
    ) {
      throw new ValidationError("Product is not available");
    }

    const selectedVariant = variantId
      ? product.variants?.find((v) => v._id.toString() === variantId)
      : undefined;

    if (variantId && !selectedVariant) {
      return notFoundResponse("Variant");
    }

    // A product with variants must be added WITH a specific variant, otherwise
    // it is priced at the cheapest-variant mirror and can't be fulfilled.
    if (!variantId && (product.variants?.length ?? 0) > 0) {
      throw new ValidationError("Please select a variant for this product");
    }

    // Digital products and products with stock tracking off have no stock to
    // run out of; "continue selling when out of stock" opts a tracked product
    // out of the limit too. getPurchasableQuantity() owns that rule so the buy
    // box on the product page and this guard can't disagree.
    const availableStock = getPurchasableQuantity(
      product,
      selectedVariant ? selectedVariant.stock : product.stock,
    );
    if (availableStock <= 0 || quantity > availableStock) {
      throw new ValidationError("Insufficient stock");
    }

    const price = selectedVariant ? selectedVariant.price : product.price;
    const name = product.name;
    const variantName = selectedVariant?.name;
    const image =
      selectedVariant?.image ||
      (selectedVariant?.mediaId
        ? product.media?.find((m) => m._id === selectedVariant.mediaId)?.url
        : undefined) ||
      product.images?.[0] ||
      product.media?.[0]?.url ||
      "";

    const query = userId ? { userId } : { sessionId };

    // Find or create cart
    let cart = await Cart.findOne(query);

    if (!cart) {
      cart = new Cart({
        userId: userId || undefined,
        sessionId: userId ? undefined : sessionId,
        items: [],
      });
    }

    // Check if product already in cart
    const existingItemIndex = cart.items.findIndex(
      (item: CartItem) =>
        item.productId.toString() === productId &&
        (variantId ? item.variantId?.toString() === variantId : !item.variantId)
    );

    // Cap distinct lines (oversized-document / abuse guard).
    if (existingItemIndex === -1 && cart.items.length >= 100) {
      throw new ValidationError("Cart is full. Remove some items first.");
    }

    if (existingItemIndex > -1) {
      // Update quantity
      const nextQuantity = cart.items[existingItemIndex].quantity + quantity;
      if (nextQuantity > availableStock) {
        throw new ValidationError("Insufficient stock");
      }
      cart.items[existingItemIndex].quantity = nextQuantity;
      cart.items[existingItemIndex].price = price;
      cart.items[existingItemIndex].name = name;
      cart.items[existingItemIndex].variantName = variantName;
      cart.items[existingItemIndex].image = image;
    } else {
      // Add new item
      cart.items.push({
        productId,
        variantId,
        quantity,
        price,
        name,
        variantName,
        image,
      });
    }

    cart.lastActionAt = new Date();
    cart.status = "active";
    await cart.save();

    // Return response with cookie for guest users
    const response = createdResponse(cart);

    if (!userId && sessionId) {
      response.headers.set(
        "Set-Cookie",
        `cart_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${
          60 * 60 * 24 * 30
        }`
      );
    }

    return response;
  } catch (error) {
    return handleApiError(error);
  }
}

/**
 * PUT /api/cart
 * Update cart item quantity
 */
export async function PUT(request: NextRequest) {
  try {
    await connectDB();

    // Get session
    const session = await auth.api.getSession({
      headers: await headers(),
    });

    const userId = session?.user?.id;
    const sessionId = request.cookies.get("cart_session")?.value;

    if (userId) {
      await rateLimitByUser(
        request,
        userId,
        "cart:update",
        "moderate",
        session?.user?.role
      );
    } else {
      await rateLimitByIP(request, "moderate");
    }

    const { productId, quantity, variantId } = await validateBody(
      request,
      CartUpdateItemSchema,
    );

    if (!userId && !sessionId) {
      return notFoundResponse("Cart");
    }

    const query = userId ? { userId } : { sessionId };
    const cart = await Cart.findOne(query);

    if (!cart) {
      return notFoundResponse("Cart");
    }

    // Find item
    const itemIndex = cart.items.findIndex(
      (item: CartItem) =>
        item.productId.toString() === productId &&
        (!variantId || item.variantId?.toString() === variantId)
    );

    if (itemIndex === -1) {
      return notFoundResponse("Item not found in cart");
    }

    if (quantity <= 0) {
      // Remove item
      cart.items.splice(itemIndex, 1);
    } else {
      // Update quantity
      cart.items[itemIndex].quantity = quantity;
    }

    cart.lastActionAt = new Date();
    cart.status = "active";
    await cart.save();

    return successResponse(cart);
  } catch (error) {
    return handleApiError(error);
  }
}

/**
 * DELETE /api/cart
 * Clear cart or remove specific item
 */
export async function DELETE(request: NextRequest) {
  try {
    await connectDB();

    const searchParams = request.nextUrl.searchParams;
    const productId = searchParams.get("productId");
    const variantId = searchParams.get("variantId");
    const clearAll = searchParams.get("clearAll") === "true";
    const shouldClearAll = clearAll || !productId;

    // Get session
    const session = await auth.api.getSession({
      headers: await headers(),
    });

    const userId = session?.user?.id;
    const sessionId = request.cookies.get("cart_session")?.value;

    if (userId) {
      await rateLimitByUser(
        request,
        userId,
        "cart:delete",
        "moderate",
        session?.user?.role
      );
    } else {
      await rateLimitByIP(request, "moderate");
    }

    if (!userId && !sessionId) {
      return notFoundResponse("Cart");
    }

    const query = userId ? { userId } : { sessionId };

    if (shouldClearAll) {
      await Cart.deleteOne(query);
      return successResponse({ message: "Cart cleared" });
    }

    const cart = await Cart.findOne(query);

    if (!cart) {
      return notFoundResponse("Cart");
    }

    // Remove specific item
    cart.items = cart.items.filter(
      (item: CartItem) =>
        !(
          item.productId.toString() === productId &&
          (!variantId || item.variantId?.toString() === variantId)
        )
    );

    await cart.save();

    return successResponse(cart);
  } catch (error) {
    return handleApiError(error);
  }
}
