import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { DeliveryMethodEnum } from '../../delivery/interface/delivery';
import { PaymentMethodEnum } from '../../payments/interface/payment';
import { OrderCouponDetail } from '../../orders/interface/order.interface';
import { Cart } from '../interfaces/cart.interface';
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
 import { SetAllCartParmsDto } from '../dto/set-all-cart-params.dto';
import { CouponService } from '../../coupons/providers/coupon.service';
import { CartRepository } from '../repositories/cart.repository';
import { CartValidationService } from './cart-validation.service';
import { CartCalculationService } from './cart-calculation.service';
import { CartItemService } from './cart-item.service';
import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto';
import { CartMessage } from 'src/common/enums/message.enum';

@Injectable()
export class CartService {
  constructor(
    private readonly cartRepository: CartRepository,
    private readonly validationService: CartValidationService,
    private readonly calculationService: CartCalculationService,
    private readonly itemService: CartItemService,
    private readonly couponService: CouponService,
  ) { }

  /**
   * Set all cart parameters at once
   */
  async setAllCartParams(userId: string, restaurantId: string, params: SetAllCartParmsDto): Promise<Cart> {
    const { deliveryMethodId, paymentMethodId, addressId, carAddress, description, tableNumber } = params;

    // get existing cart (or throw)
    const cart = await this.findOneOrFail(userId, restaurantId);

    // Validate and get delivery method
    const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
      restaurantId,
      deliveryMethodId,
    );
    cart.deliveryMethodId = deliveryMethodId;

    // Handle each delivery method with its specific requirements
    switch (deliveryMethod.method) {
      case DeliveryMethodEnum.DineIn:
        // DineIn requires table number and clears incompatible fields
        if (!tableNumber) {
          throw new BadRequestException(CartMessage.TABLE_NUMBER_REQUIRED);
        }
        this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn);
        cart.tableNumber = tableNumber;
        break;

      case DeliveryMethodEnum.CustomerPickup:
        // CustomerPickup just clears incompatible fields
        this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.CustomerPickup);
        break;

      case DeliveryMethodEnum.DeliveryCar:
        // DeliveryCar requires carAddress and clears incompatible fields
        if (!carAddress) {
          throw new BadRequestException(CartMessage.CAR_ADDRESS_REQUIRED);
        }
        this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar);
        const user = await this.validationService.getUserOrFail(userId);
        cart.carAddress = {
          carModel: carAddress.carModel,
          carColor: carAddress.carColor,
          plateNumber: carAddress.plateNumber,
          phone: user.phone,
        };
        break;

      case DeliveryMethodEnum.DeliveryCourier:
        // DeliveryCourier requires addressId and clears incompatible fields
        if (!addressId) {
          throw new BadRequestException(CartMessage.ADDRESS_REQUIRED);
        }
        this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
        const address = await this.validationService.getUserAddressOrFail(addressId);
        this.validationService.validateAddressOwnership(address, userId);

        // ensure address is within restaurant service area
        await this.validationService.assertAddressInsideServiceArea(
          restaurantId,
          address.latitude,
          address.longitude,
        );

        cart.userAddress = {
          address: address.address,
          latitude: address.latitude,
          longitude: address.longitude,
          city: address.city,
          province: address.province || '',
          postalCode: address.postalCode || undefined,
          fullName: `${address.user.firstName || ''} ${address.user.lastName || ''}`.trim(),
          phone: address.user.phone,
        };
        break;
    }

    // Validate and set payment method
    const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
      restaurantId,
      paymentMethodId,
    );

    // Recalculate totals first so wallet check uses up-to-date total (delivery method may have changed above).
    await this.calculationService.recalculateCartTotals(cart);

    if (paymentMethod.method === PaymentMethodEnum.Wallet) {
      await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total);
    }

    cart.paymentMethodId = paymentMethodId;

    // Set description (if provided)
    if (description !== undefined) {
      cart.description = description;
    }

    // Final validation: if effective delivery method is courier, ensure all foods have pickupServe
    await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, deliveryMethod.method);

    // Final recalculation + save and return cart
    return this.recalculateAndSaveCart(cart);
  }

  /**
   * Get or create cart for user and restaurant
   */
  async getOrCreateCart(userId: string, restaurantId: string): Promise<Cart> {
    const existingCart = await this.cartRepository.findByUserAndRestaurant(userId, restaurantId);
    if (existingCart) {
      return existingCart;
    }

    // Find restaurant
    const restaurant = await this.validationService.getRestaurantOrFail(restaurantId);

    // Create new cart
    const now = this.nowIso();
    const cart: Cart = {
      userId,
      restaurantId,
      restaurantName: restaurant.name,
      items: [],
      deliveryFee: 0,
      subTotal: 0,
      itemsDiscount: 0,
      couponDiscount: 0,
      totalDiscount: 0,
      tax: 0,
      total: 0,
      totalItems: 0,
      createdAt: now,
      updatedAt: now,
    };

    await this.cartRepository.save(cart);
    return cart;
  }

  /**
   * Find cart by user and restaurant
   */
  async findOneOrFail(userId: string, restaurantId: string): Promise<Cart> {
    const cart = await this.cartRepository.findByUserAndRestaurant(userId, restaurantId);
    if (!cart) {
      throw new NotFoundException(CartMessage.NOT_FOUND);
    }
    return cart;
  }

  /**
   * Increment item quantity in cart
   */
  async incrementItem(userId: string, restaurantId: string, foodId: string, quantity: number): Promise<Cart> {
    const cart = await this.getOrCreateCart(userId, restaurantId);
    await this.itemService.addOrIncrementItem(cart, foodId, restaurantId, quantity);
    return this.recalculateAndSaveCart(cart);
  }

  /**
   * Decrement item quantity in cart by 1 (removes item if quantity reaches 0)
   */
  async decrementItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> {
    const cart = await this.findOneOrFail(userId, restaurantId);
    await this.itemService.decrementOrRemoveItem(cart, foodId);
    return this.recalculateAndSaveCart(cart);
  }

  /**
   * Bulk add items to cart (increments quantity if items exist)
   */
  async bulkAddItems(userId: string, restaurantId: string, bulkAddItemsDto: BulkAddItemsToCartDto): Promise<Cart> {
    const cart = await this.getOrCreateCart(userId, restaurantId);

    for (const addItemDto of bulkAddItemsDto.items) {
      await this.itemService.addOrIncrementItem(cart, addItemDto.foodId, restaurantId, addItemDto.quantity);
    }

    return this.recalculateAndSaveCart(cart);
  }

  /**
   * Remove item from cart
   */
  async removeItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> {
    const cart = await this.findOneOrFail(userId, restaurantId);
    this.itemService.removeItemOrFail(cart, foodId);
    return this.recalculateAndSaveCart(cart);
  }

  /**
   * Apply coupon to cart
   */
  async applyCoupon(userId: string, restaurantId: string, applyCouponDto: ApplyCouponDto): Promise<Cart> {
    const cart = await this.findOneOrFail(userId, restaurantId);

    const orderAmount = cart.subTotal - cart.itemsDiscount;
    const user = await this.validationService.getUserOrFail(userId);
    // check if coupon is valid and belong to the restaurant
    const { valid, coupon, message } = await this.couponService.validateCoupon(
      applyCouponDto.code,
      restaurantId,
      Number(orderAmount),
      user.phone,
    );

    if (!valid) {
      throw new BadRequestException(message || CartMessage.COUPON_NOT_FOUND);
    }

    if (!coupon) {
      throw new BadRequestException(CartMessage.COUPON_NOT_FOUND);
    }

    // Check maxUsesPerUser limit by counting how many orders this user has with this coupon
    await this.validationService.assertCouponUsageLimit(userId, coupon.id, coupon.maxUsesPerUser);
    await this.validationService.assertCouponMatchesCartIfRestricted(
      cart,
      coupon.foodCategories,
      coupon.foods,
    );

    const couponDetail: OrderCouponDetail = {
      couponId: coupon.id,
      couponName: coupon.name,
      couponCode: coupon.code,
      type: coupon.type,
      value: coupon.value,
      maxDiscount: coupon.maxDiscount,
    };

    cart.coupon = couponDetail;
    return this.recalculateAndSaveCart(cart);
  }

  /**
   * Remove coupon from cart
   */
  async removeCoupon(userId: string, restaurantId: string): Promise<Cart> {
    const cart = await this.findOneOrFail(userId, restaurantId);

    cart.coupon = undefined;
    return this.recalculateAndSaveCart(cart);
  }

  /**
   * Set address for cart
   */
  async setAddress(userId: string, restaurantId: string, addressId: string): Promise<Cart> {
    const cart = await this.findOneOrFail(userId, restaurantId);

    const deliveryMethodId = this.validationService.requireDeliveryMethodId(
      cart,
      'Delivery method must be set before setting address',
    );
    const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
      restaurantId,
      deliveryMethodId,
    );
    this.validationService.assertDeliveryMethod(
      deliveryMethod.method,
      DeliveryMethodEnum.DeliveryCourier,
      'Delivery method must be DeliveryCourier',
    );

    // Find and validate address belongs to user
    const address = await this.validationService.getUserAddressOrFail(addressId);

    // Verify address belongs to the user
    this.validationService.validateAddressOwnership(address, userId);

    // ensure address is within restaurant service area
    await this.validationService.assertAddressInsideServiceArea(
      restaurantId,
      address.latitude,
      address.longitude,
    );

    this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
    cart.userAddress = {
      address: address.address,
      latitude: address.latitude,
      longitude: address.longitude,
      city: address.city,
      province: address.province || '',
      postalCode: address.postalCode || undefined,
      fullName: `${address.user.firstName || ''} ${address.user.lastName || ''}`.trim(),
      phone: address.user.phone,
    };
    return this.saveTouchedCart(cart);
  }

  /**
   * Set payment method for cart
   */
  async setPaymentMethod(
    userId: string,
    restaurantId: string,
    paymentMethodId: string,
  ): Promise<Cart> {
    const cart = await this.findOneOrFail(userId, restaurantId);

    const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
      restaurantId,
      paymentMethodId,
    );
    if (paymentMethod.method === PaymentMethodEnum.Wallet) {
      await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total);
    }
    cart.paymentMethodId = paymentMethodId;
    return this.recalculateAndSaveCart(cart);
  }

  /**
   * Set delivery method for cart
   */
  async setDeliveryMethod(
    userId: string,
    restaurantId: string,
    deliveryMethodId: string,
  ): Promise<Cart> {
    const cart = await this.findOneOrFail(userId, restaurantId);

    const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
      restaurantId,
      deliveryMethodId,
    );

    cart.deliveryMethodId = deliveryMethodId;
    this.clearFieldsIncompatibleWithDeliveryMethod(cart, deliveryMethod.method);

    return this.recalculateAndSaveCart(cart);
  }

  /**
   * Set description for cart
   */
  async setDescription(userId: string, restaurantId: string, description: string): Promise<Cart> {
    const cart = await this.findOneOrFail(userId, restaurantId);

    cart.description = description;
    return this.saveTouchedCart(cart);
  }

  /**
   * Set table number for cart
   */
  async setTableNumber(userId: string, restaurantId: string, tableNumber: string): Promise<Cart> {
    const cart = await this.findOneOrFail(userId, restaurantId);

    const deliveryMethodId = this.validationService.requireDeliveryMethodId(
      cart,
      'Delivery method must be set before setting table number',
    );
    const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
      restaurantId,
      deliveryMethodId,
    );
    this.validationService.assertDeliveryMethod(
      deliveryMethod.method,
      DeliveryMethodEnum.DineIn,
      'Delivery method must be DineIn',
    );

    this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn);
    cart.tableNumber = tableNumber;
    return this.saveTouchedCart(cart);
  }

  /**
   * Set car delivery for cart
   */
  async setCarDelivery(userId: string, restaurantId: string, setCarDeliveryDto: SetCarDeliveryDto): Promise<Cart> {
    const cart = await this.findOneOrFail(userId, restaurantId);

    const deliveryMethodId = this.validationService.requireDeliveryMethodId(
      cart,
      'Delivery method must be set before setting car delivery',
    );
    const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
      restaurantId,
      deliveryMethodId,
    );
    this.validationService.assertDeliveryMethod(
      deliveryMethod.method,
      DeliveryMethodEnum.DeliveryCar,
      'Delivery method must be DeliveryCar',
    );
    const user = await this.validationService.getUserOrFail(userId);

    this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar);
    cart.carAddress = {
      carModel: setCarDeliveryDto.carModel,
      carColor: setCarDeliveryDto.carColor,
      plateNumber: setCarDeliveryDto.plateNumber,
      phone: user.phone,
    };
    return this.saveTouchedCart(cart);
  }

  /**
   * Clear cart from cache
   */
  async clearCart(userId: string, restaurantId: string): Promise<void> {
    return this.cartRepository.delete(userId, restaurantId);
  }

  /**
   * Clears cart fields that are not applicable for a given delivery method.
   */
  private clearFieldsIncompatibleWithDeliveryMethod(cart: Cart, method: DeliveryMethodEnum): void {
    if (method === DeliveryMethodEnum.DineIn) {
      cart.userAddress = null;
      cart.carAddress = null;
      return;
    }

    if (method === DeliveryMethodEnum.CustomerPickup) {
      cart.userAddress = null;
      cart.carAddress = null;
      cart.tableNumber = undefined;
      return;
    }

    if (method === DeliveryMethodEnum.DeliveryCourier) {
      cart.carAddress = null;
      cart.tableNumber = undefined;
      return;
    }

    if (method === DeliveryMethodEnum.DeliveryCar) {
      cart.userAddress = null;
      cart.tableNumber = undefined;
      return;
    }
  }

  /**
   * Save cart with updated timestamp
   */
  private async saveTouchedCart(cart: Cart): Promise<Cart> {
    cart.updatedAt = this.nowIso();
    await this.cartRepository.save(cart);
    return cart;
  }

  /**
   * Recalculate cart totals and save
   */
  private async recalculateAndSaveCart(cart: Cart): Promise<Cart> {
    await this.calculationService.recalculateCartTotals(cart);
    await this.cartRepository.save(cart);
    return cart;
  }

  /**
   * Get current ISO timestamp
   */
  private nowIso(): string {
    return new Date().toISOString();
  }
}
