import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/postgresql';
import { Food } from '../../foods/entities/food.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { UserAddress } from '../../users/entities/user-address.entity';
import { User } from '../../users/entities/user.entity';
import { PaymentMethod } from '../../payments/entities/payment-method.entity';
import { Delivery } from '../../delivery/entities/delivery.entity';
import { DeliveryMethodEnum } from '../../delivery/interface/delivery';
import { PointTransaction } from '../../users/entities/point-transaction.entity';
import { Order } from '../../orders/entities/order.entity';
import { OrderStatus } from '../../orders/interface/order.interface';
import { Cart } from '../interfaces/cart.interface';
import { GeographicUtils } from '../utils/geographic.utils';
import { MealType } from '../../foods/interface/food.interface';
import { CartMessage } from 'src/common/enums/message.enum';
import { WalletTransactionRepository } from 'src/modules/users/repositories/wallet-transaction.repository';

@Injectable()
export class CartValidationService {
  constructor(
    private readonly em: EntityManager,
    private readonly walletTransactionRepository: WalletTransactionRepository,
  ) { }

  /**
   * Validate food exists, belongs to restaurant, has inventory, and check stock
   */
  async validateAndGetFood(foodId: string, restaurantId: string, quantity: number): Promise<Food> {
    const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant', 'inventory'] });
    if (!food) {
      throw new NotFoundException(CartMessage.FOOD_NOT_FOUND);
    }

    if (food.restaurant.id !== restaurantId) {
      throw new BadRequestException(CartMessage.FOOD_NOT_BELONGS_TO_RESTAURANT);
    }

    if (!food.inventory) {
      throw new BadRequestException(CartMessage.FOOD_NO_INVENTORY);
    }

    this.validateStock(food, quantity);
    return food;
  }

  /**
   * Validate stock availability for food
   */
  validateStock(food: Food, quantity: number): void {
    const availableStock = food.inventory!.availableStock;
    if (availableStock < quantity) {
      throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK + food.title);
    }
  }

  /**
   * Get user or throw if not found
   */
  async getUserOrFail(userId: string): Promise<User> {
    const user = await this.em.findOne(User, { id: userId });
    if (!user) {
      throw new NotFoundException(CartMessage.USER_NOT_FOUND);
    }
    return user;
  }

  /**
   * Get user address or throw if not found
   */
  async getUserAddressOrFail(addressId: string): Promise<UserAddress> {
    const address = await this.em.findOne(UserAddress, { id: addressId }, { populate: ['user'] });
    if (!address) {
      throw new NotFoundException(CartMessage.ADDRESS_NOT_FOUND);
    }
    return address;
  }

  /**
   * Get food or throw if not found
   */
  async getFoodOrFail(foodId: string): Promise<Food> {
    const food = await this.em.findOne(Food, { id: foodId });
    if (!food) {
      throw new NotFoundException(CartMessage.FOOD_NOT_FOUND);
    }
    return food;
  }

  /**
   * Get restaurant or throw if not found
   */
  async getRestaurantOrFail(restaurantId: string): Promise<Restaurant> {
    const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
    if (!restaurant) {
      throw new NotFoundException(CartMessage.RESTAURANT_NOT_FOUND);
    }
    return restaurant;
  }

  /**
   * Validate address belongs to user
   */
  validateAddressOwnership(address: UserAddress, userId: string): void {
    if (address.user.id !== userId) {
      throw new BadRequestException(CartMessage.ADDRESS_NOT_BELONGS_TO_USER);
    }
  }

  /**
   * Assert address is inside restaurant service area
   */
  async assertAddressInsideServiceArea(
    restaurantId: string,
    latitude?: number | null,
    longitude?: number | null,
  ): Promise<void> {
    if (latitude === undefined || latitude === null || longitude === undefined || longitude === null) {
      throw new BadRequestException(CartMessage.ADDRESS_NO_COORDINATES);
    }

    const restaurant = await this.getRestaurantOrFail(restaurantId);

    const serviceArea = restaurant.serviceArea;
    // If no service area is defined, assume service is available everywhere
    if (!serviceArea || !serviceArea.coordinates || !Array.isArray(serviceArea.coordinates)) return;

    // GeoJSON coordinates: [ [ [lng, lat], ... ] ] -> take first ring
    const rings = serviceArea.coordinates;
    if (!rings || rings.length === 0 || !Array.isArray(rings[0])) return;

    const ring = rings[0];
    const point: [number, number] = [Number(longitude), Number(latitude)];

    // Ensure polygon has the correct tuple typing ([lng, lat] pairs)
    const polygon: [number, number][] = ring.map(
      coord => [Number(coord[0] ?? 0), Number(coord[1] ?? 0)] as [number, number],
    );

    if (!GeographicUtils.isPointInPolygon(point, polygon)) {
      throw new BadRequestException(CartMessage.ADDRESS_OUTSIDE_SERVICE_AREA);
    }
  }

  /**
   * Get delivery method for restaurant or throw if not found
   */
  async getDeliveryMethodForRestaurantOrFail(
    restaurantId: string,
    deliveryMethodId: string,
  ): Promise<Delivery> {
    const deliveryMethod = await this.em.findOne(Delivery, {
      id: deliveryMethodId,
      restaurant: { id: restaurantId },
    });

    if (!deliveryMethod) {
      throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND_FOR_RESTAURANT);
    }

    return deliveryMethod;
  }

  /**
   * Get enabled delivery method or throw if not found or disabled
   */
  async getEnabledDeliveryMethodOrFail(restaurantId: string, deliveryMethodId: string): Promise<Delivery> {
    const deliveryMethod = await this.em.findOne(
      Delivery,
      { id: deliveryMethodId, restaurant: { id: restaurantId } },
      { populate: ['restaurant'] },
    );
    if (!deliveryMethod) {
      throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND);
    }
    if (!deliveryMethod.enabled) {
      throw new BadRequestException(CartMessage.DELIVERY_METHOD_NOT_ENABLED);
    }
    return deliveryMethod;
  }

  /**
   * Assert delivery method matches expected type
   */
  assertDeliveryMethod(actual: DeliveryMethodEnum, expected: DeliveryMethodEnum, message: string): void {
    if (actual !== expected) {
      throw new BadRequestException(message);
    }
  }

  /**
   * Require delivery method ID or throw
   */
  requireDeliveryMethodId(cart: Cart, message: string): string {
    if (!cart.deliveryMethodId) {
      throw new BadRequestException(message);
    }
    return cart.deliveryMethodId;
  }

  /**
   * Get enabled payment method or throw if not found or disabled
   */
  async getEnabledPaymentMethodOrFail(restaurantId: string, paymentMethodId: string): Promise<PaymentMethod> {
    const paymentMethod = await this.em.findOne(
      PaymentMethod,
      { id: paymentMethodId, restaurant: { id: restaurantId } },
      { populate: ['restaurant'] },
    );
    if (!paymentMethod) {
      throw new NotFoundException(CartMessage.PAYMENT_METHOD_NOT_FOUND);
    }
    if (!paymentMethod.enabled) {
      throw new BadRequestException(CartMessage.PAYMENT_METHOD_NOT_ENABLED);
    }
    return paymentMethod;
  }

  /**
   * Assert wallet has enough balance
   */
  async assertWalletHasEnoughBalance(userId: string, restaurantId: string, amount: number): Promise<void> {
    const balance = await this.walletTransactionRepository.getCurrentWalletBalance(userId, restaurantId);

    if (balance < amount) {
      throw new BadRequestException(CartMessage.WALLET_INSUFFICIENT);
    }
  }

  /**
   * Assert coupon usage limit
   */
  async assertCouponUsageLimit(userId: string, couponId: string, maxUsesPerUser?: number): Promise<void> {
    if (!maxUsesPerUser) return;

    const ordersThatUsedTheCouponCount = await this.em.count(Order, {
      user: { id: userId },
      couponDetail: { couponId: couponId },
      status: { $ne: OrderStatus.CANCELED },
      deletedAt: null,
    });

    if (ordersThatUsedTheCouponCount >= maxUsesPerUser) {
      throw new BadRequestException(CartMessage.COUPON_MAX_USES_REACHED);
    }
  }

  /**
   * Assert coupon matches cart if restricted
   */
  async assertCouponMatchesCartIfRestricted(
    cart: Cart,
    foodCategories?: string[] | null,
    foods?: string[] | null,
  ): Promise<void> {
    const categoryRestriction = foodCategories?.filter(Boolean) ?? [];
    const foodRestriction = foods?.filter(Boolean) ?? [];
    const hasCategoryRestriction = categoryRestriction.length > 0;
    const hasFoodRestriction = foodRestriction.length > 0;
    if (!hasCategoryRestriction && !hasFoodRestriction) return;

    const foodMap = await this.getFoodsInCartWithCategories(cart);

    for (const item of cart.items) {
      const food = foodMap.get(item.foodId);
      if (!food) continue;

      const matchesCategory = hasCategoryRestriction && food.category && categoryRestriction.includes(food.category.id);
      const matchesFood = hasFoodRestriction && foodRestriction.includes(food.id);
      if (matchesCategory || matchesFood) return;
    }

    throw new BadRequestException(CartMessage.COUPON_CANNOT_BE_APPLIED);
  }

  /**
   * Get foods in cart with categories
   */
  async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Food>> {
    const foodIds = cart.items.map(item => item.foodId);
    if (foodIds.length === 0) return new Map();

    const foods = await this.em.find(Food, { id: { $in: foodIds } }, { populate: ['category'] });
    return new Map(foods.map(f => [f.id, f]));
  }

  /**
   * Assert all foods in cart have pickupServe true when delivery method is courier
   */
  async assertAllFoodsHavePickupServeForCourier(cart: Cart, deliveryMethod: DeliveryMethodEnum): Promise<void> {
    if (deliveryMethod !== DeliveryMethodEnum.DeliveryCourier) {
      return;
    }

    if (cart.items.length === 0) {
      return;
    }

    const foodIds = cart.items.map(item => item.foodId);
    const foods = await this.em.find(Food, { id: { $in: foodIds } });

    const foodsWithoutPickupServe = foods.filter(food => !food.pickupServe);

    if (foodsWithoutPickupServe.length > 0) {
      const foodTitles = foodsWithoutPickupServe.map(f => f.title || f.id).join(', ');
      throw new BadRequestException(
        CartMessage.FOODS_MUST_HAVE_PICKUP_SERVE_FOR_COURIER.replace('[foodTitles]', foodTitles),
      );
    }
  }

  /**
   * Get current Iran time context (weekday and meal type)
   */
  private getCurrentIranTimeContext(): { iranWeekDay: number; mealType: MealType | null } {
    const nowInIran = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tehran' }));
    const weekDay = nowInIran.getDay(); // 0 = Sunday, 6 = Saturday
    const currentHour = nowInIran.getHours();

    // Convert to Iran weekday: Saturday=0, Sunday=1, ..., Friday=6
    // JavaScript: Sunday=0, Monday=1, ..., Saturday=6
    // Iran week: Saturday=0, Sunday=1, ..., Friday=6
    const iranWeekDay = (weekDay + 1) % 7;

    const mealType = this.getMealTypeByHour(currentHour);

    return { iranWeekDay, mealType };
  }

  /**
   * Determine meal type based on current hour in Iran timezone.
   * @param hour - Current hour (0-23)
   * @returns Meal type or null if outside meal hours
   */
  private getMealTypeByHour(hour: number): MealType | null {
    const MEAL_TIME_RANGES = {
      BREAKFAST: { start: 6, end: 11 },
      LUNCH: { start: 11, end: 15 },
      SNACK: { start: 15, end: 19 },
      DINNER: { start: 19, end: 24 },
    } as const;

    if (hour >= MEAL_TIME_RANGES.BREAKFAST.start && hour < MEAL_TIME_RANGES.BREAKFAST.end) {
      return MealType.BREAKFAST;
    }
    if (hour >= MEAL_TIME_RANGES.LUNCH.start && hour < MEAL_TIME_RANGES.LUNCH.end) {
      return MealType.LUNCH;
    }
    if (hour >= MEAL_TIME_RANGES.SNACK.start && hour < MEAL_TIME_RANGES.SNACK.end) {
      return MealType.SNACK;
    }
    if (hour >= MEAL_TIME_RANGES.DINNER.start && hour < MEAL_TIME_RANGES.DINNER.end) {
      return MealType.DINNER;
    }

    return null;
  }

  /**
   * Assert meal type compatibility - if food has mealTypes, current meal time must be in that array
   */
  assertMealTypeCompatibility(food: Food): void {
    // If food has no meal type restrictions, allow it
    if (!food.mealTypes || food.mealTypes.length === 0) {
      return;
    }

    const { mealType } = this.getCurrentIranTimeContext();

    // If current time is outside meal hours, throw error
    if (mealType === null) {
      const foodTitle = food.title || food.id;
      throw new BadRequestException(
        CartMessage.FOOD_ONLY_AVAILABLE_DURING_MEAL_TIMES.replace('[foodTitle]', foodTitle),
      );
    }

    // Check if current meal type is in food's allowed meal types
    if (!food.mealTypes.includes(mealType)) {
      const foodTitle = food.title || food.id;
      const mealTypeMap: Record<MealType, string> = {
        [MealType.BREAKFAST]: 'صبحانه',
        [MealType.LUNCH]: 'ناهار',
        [MealType.SNACK]: 'عصرانه',
        [MealType.DINNER]: 'شام',
      };
      const allowedMealTypes = food.mealTypes.map(mt => mealTypeMap[mt]).join(', ');
      const currentMealType = mealType ? mealTypeMap[mealType] : mealType;
      throw new BadRequestException(
        CartMessage.FOOD_ONLY_AVAILABLE_FOR_MEAL_TYPES.replace('[foodTitle]', foodTitle)
          .replace('[allowedMealTypes]', allowedMealTypes)
          .replace('[mealType]', currentMealType),
      );
    }
  }

  /**
   * Assert weekday compatibility - current weekday must be in food's weekDays array
   */
  assertWeekdayCompatibility(food: Food): void {
    // If food has no weekday restrictions (empty array or all days), allow it
    if (!food.weekDays || food.weekDays.length === 0 || food.weekDays.length === 7) {
      return;
    }

    const { iranWeekDay } = this.getCurrentIranTimeContext();

    // Check if current weekday is in food's allowed weekdays
    if (!food.weekDays.includes(iranWeekDay)) {
      const dayNames = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه'];
      const currentDayName = dayNames[iranWeekDay];
      const allowedDays = food.weekDays.map(day => dayNames[day]).join(', ');
      const foodTitle = food.title || food.id;
      throw new BadRequestException(
        CartMessage.FOOD_ONLY_AVAILABLE_ON_DAYS.replace('[foodTitle]', foodTitle)
          .replace('[allowedDays]', allowedDays)
          .replace('[currentDay]', currentDayName),
      );
    }
  }
}

