from decimal import Decimal, ROUND_HALF_UP, InvalidOperation
import uuid

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import DecimalField, ExpressionWrapper, F, Sum
from django.utils import timezone
from django.utils.functional import cached_property

from product_module.models import Product
from django.core.validators import MinValueValidator, MaxValueValidator

# ============================================
# استان / شهر / آدرس / زون
# ============================================

class Zone(models.Model):
    name = models.CharField(
        max_length=50,
        unique=True,
        verbose_name="نام زون",
    )
    description = models.TextField(
        blank=True,
        verbose_name="توضیحات",
    )
    is_active = models.BooleanField(
        default=True,
        verbose_name="فعال",
    )

    class Meta:
        verbose_name = "زون منطقه‌ای"
        verbose_name_plural = "زون‌های منطقه‌ای"
        ordering = ("name",)

    def __str__(self):
        return self.name

class Province(models.Model):
    name = models.CharField(
        max_length=100,
        unique=True,
        verbose_name="نام استان",
    )
    zone = models.ForeignKey(
        "Zone",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="provinces",
        verbose_name="زون پیش‌فرض استان",
        help_text="اگر شهر زون اختصاصی نداشته باشد، این زون استفاده می‌شود.",
    )

    class Meta:
        verbose_name = "استان"
        verbose_name_plural = "استان‌ها"
        ordering = ("name",)

    def __str__(self):
        return self.name

class City(models.Model):
    province = models.ForeignKey(
        Province,
        on_delete=models.CASCADE,
        related_name="cities",
        verbose_name="استان",
    )
    name = models.CharField(
        max_length=100,
        verbose_name="نام شهر",
    )
    zone = models.ForeignKey(
        Zone,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="cities",
        verbose_name="زون اختصاصی شهر",
        help_text="فقط زمانی انتخاب شود که زون شهر با زون پیش‌فرض استان متفاوت باشد.",
    )

    class Meta:
        verbose_name = "شهر"
        verbose_name_plural = "شهرها"
        ordering = ("province__name", "name")
        constraints = [
            models.UniqueConstraint(
                fields=("province", "name"),
                name="unique_city_name_per_province",
            ),
        ]
        indexes = [
            models.Index(
                fields=("province", "name"),
                name="city_province_name_idx",
            ),
        ]

    def __str__(self):
        return f"{self.province.name} - {self.name}"

    @property
    def effective_zone(self):
        return self.zone or self.province.zone

class Address(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="addresses",
        verbose_name="کاربر",
    )
    province = models.ForeignKey(
        Province,
        on_delete=models.PROTECT,
        related_name="addresses",
        verbose_name="استان",
    )
    city = models.ForeignKey(
        City,
        on_delete=models.PROTECT,
        related_name="addresses",
        verbose_name="شهر",
    )
    address_detail = models.TextField(verbose_name="جزئیات آدرس")
    postal_code = models.CharField(
        max_length=10,
        verbose_name="کد پستی",
    )
    receiver_name = models.CharField(
        max_length=100,
        verbose_name="نام گیرنده",
    )
    receiver_phone = models.CharField(
        max_length=11,
        verbose_name="شماره تماس گیرنده",
    )

    class Meta:
        verbose_name = "آدرس کاربر"
        verbose_name_plural = "آدرس‌های کاربران"
        indexes = [
            models.Index(
                fields=("user", "province", "city"),
                name="address_user_location_idx",
            ),
        ]

    def clean(self):
        super().clean()
        if self.city_id and self.province_id and self.city.province_id != self.province_id:
            raise ValidationError({
                "city": "شهر انتخاب‌شده متعلق به استان انتخاب‌شده نیست."
            })

    @property
    def full_address(self):
        return f"{self.province.name}، {self.city.name}، {self.address_detail}"

    @property
    def shipping_zone(self):
        if not self.city_id:
            return None
        return self.city.effective_zone

    def __str__(self):
        return f"{self.receiver_name} - {self.full_address[:50]}"

    def save(self, *args, **kwargs):
        self.full_clean()
        super().save(*args, **kwargs)

# ============================================
# روش ارسال
# ============================================

class ShippingMethod(models.Model):
    code = models.SlugField(
        max_length=50,
        unique=True,
        help_text='مثال: tipax، post، courier',
        verbose_name="کد سیستمی",
    )
    is_cod = models.BooleanField(
        default=False,
        verbose_name="پس‌کرایه است؟",
        help_text="هزینه ارسال این روش در مقصد از گیرنده دریافت می‌شود؛ پس‌کرایه بودن به معنی صفر بودن هزینه نیست."
    )
    name = models.CharField(
        max_length=100,
        unique=True,
        verbose_name="نام",
    )
    active = models.BooleanField(
        default=True,
        verbose_name="فعال/غیرفعال",
    )

    class Meta:
        verbose_name = "روش ارسال"
        verbose_name_plural = "روش‌های ارسال"
        ordering = ("name",)

    def __str__(self):
        return self.name

class ShippingOption(models.Model):
    method = models.ForeignKey(
        ShippingMethod,
        on_delete=models.CASCADE,
        related_name="options",
        verbose_name="روش اصلی",
    )
    title = models.CharField(max_length=100, verbose_name="عنوان")
    is_cod = models.BooleanField(default=False, verbose_name="پس‌کرایه است؟")
    active = models.BooleanField(default=True, verbose_name="فعال")

    class Meta:
        verbose_name = "گزینه ارسال"
        verbose_name_plural = "گزینه‌های ارسال"
        constraints = [
            models.UniqueConstraint(
                fields=("method", "title"),
                name="unique_shipping_option_per_method",
            ),
        ]

    def __str__(self):
        return f"{self.method.name} - {self.title}"

# ============================================
# نرخ‌های ارسال
# ============================================

class ShippingRate(models.Model):
    method = models.ForeignKey(
        ShippingMethod,
        on_delete=models.CASCADE,
        related_name="rates",
        verbose_name="روش ارسال",
    )
    zone = models.ForeignKey(
        Zone,
        on_delete=models.CASCADE,
        related_name="shipping_rates",
        verbose_name="زون",
    )
    min_weight = models.PositiveIntegerField(
        default=0,
        verbose_name="حداقل وزن (گرم)",
        help_text="حداقل وزن مشمول این نرخ، بر حسب گرم",
    )
    max_weight = models.PositiveIntegerField(
        verbose_name="حداکثر وزن (گرم)",
        help_text="حداکثر وزن مشمول این نرخ، بر حسب گرم",
    )
    price = models.DecimalField(
        max_digits=12,
        decimal_places=0,
        verbose_name="قیمت",
        help_text="هزینه ارسال به تومان",
    )
    is_active = models.BooleanField(
        default=True,
        verbose_name="فعال",
    )

    class Meta:
        verbose_name = "نرخ ارسال"
        verbose_name_plural = "نرخ‌های ارسال"
        ordering = ("method", "zone", "min_weight", "max_weight")

    def __str__(self):
        return (
            f"{self.method.name} - {self.zone.name} - "
            f"{self.min_weight} تا {self.max_weight} گرم - "
            f"{self.price:,.0f} تومان"
        )

class TipaxRate(models.Model):
    destination_zone = models.ForeignKey(
        Zone,
        on_delete=models.CASCADE,
        verbose_name="زون مقصد",
    )
    max_weight = models.PositiveIntegerField(verbose_name="حداکثر وزن (گرم)")
    price = models.DecimalField(max_digits=12, decimal_places=0, verbose_name="قیمت")

    class Meta:
        verbose_name = "نرخ تیپاکس"
        verbose_name_plural = "نرخ‌های تیپاکس"

    def __str__(self):
        return f"{self.destination_zone.name} - تا {self.max_weight} گرم"

# ============================================
# تنظیمات سفارش
# ============================================

class OrderSettings(models.Model):
    tax_percent = models.IntegerField(default=9, verbose_name="درصد مالیات")

    class Meta:
        verbose_name = "تنظیمات سبد خرید"
        verbose_name_plural = "تنظیمات سبد خرید"

    def __str__(self):
        return "تنظیمات بخش سفارشات"

# ============================================
# کوپن تخفیف
# ============================================

class Coupon(models.Model):
    code = models.CharField(max_length=50, unique=True, verbose_name="کد تخفیف")
    valid_from = models.DateTimeField(verbose_name="معتبر از")
    valid_to = models.DateTimeField(verbose_name="معتبر تا")
    discount = models.IntegerField(
        validators=[MinValueValidator(0), MaxValueValidator(100)],
        verbose_name="درصد تخفیف"
    )
    min_order_value = models.DecimalField(
        max_digits=12,
        decimal_places=0,
        null=True,
        blank=True,
        verbose_name="حداقل مبلغ خرید (تومان)",
    )
    max_discount = models.DecimalField(
        max_digits=12,
        decimal_places=0,
        null=True,
        blank=True,
        verbose_name="سقف تخفیف (تومان)",
    )
    active = models.BooleanField(default=True, verbose_name="فعال")

    class Meta:
        verbose_name = "کوپن تخفیف"
        verbose_name_plural = "کوپن‌های تخفیف"

    def __str__(self):
        return f"{self.code} ({self.discount}%)"


def _is_tipax_method(method):
    """
    بررسی می‌کند روش ارسال، تیپاکس باشد یا خیر.
    هم بر اساس کد سیستمی و هم بر اساس نام بررسی می‌شود.
    """
    if method is None:
        return False

    method_code = str(getattr(method, "code", "") or "").strip().lower()
    method_name = str(getattr(method, "name", "") or "").strip().lower()

    return (
        "tipax" in method_code
        or "tipax" in method_name
        or "تیپاکس" in method_name
    )


# ============================================
# سفارش
# ============================================

class Order(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    address = models.ForeignKey(
        Address,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name="آدرس ارسال",
    )
    coupon = models.ForeignKey(
        Coupon, 
        on_delete=models.SET_NULL, 
        null=True, 
        blank=True,
        verbose_name='کوپن'
    )
    is_paid = models.BooleanField(default=False, verbose_name='پرداخت شده/نشده')
    payment_date = models.DateTimeField(null=True, blank=True, verbose_name='تاریخ پرداخت')
    invoice_number = models.CharField(max_length=20, unique=True, blank=True, null=True, verbose_name='شماره فاکتور')
    created_at = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')
    payment_authority = models.CharField(max_length=100, blank=True, null=True, verbose_name='کد تراکنش پرداخت')
    shipping_method = models.ForeignKey(
        ShippingMethod,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name="روش ارسال",
    )
    shipping_option = models.ForeignKey(
        ShippingOption,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name="گزینه ارسال",
    )

    shipping_cost = models.DecimalField(
        max_digits=12,
        decimal_places=0,
        default=0,
        verbose_name="هزینه ارسال ثبت‌شده",
    )
    shipping_calculated_at = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name="زمان محاسبه هزینه ارسال",
    )

    package_length = models.PositiveIntegerField(default=0, verbose_name="طول بسته (cm)")
    package_width = models.PositiveIntegerField(default=0, verbose_name="عرض بسته (cm)")
    package_height = models.PositiveIntegerField(default=0, verbose_name="ارتفاع بسته (cm)")

    class Meta:
        verbose_name = "سفارش"
        verbose_name_plural = "سفارش‌ها"

    def __str__(self):
        return f"سفارش شماره {self.id} - {self.user}"

    def clean(self):
        super().clean()
        errors = {}

        if self.shipping_option_id and not self.shipping_method_id:
            errors["shipping_method"] = (
                "برای گزینه ارسال، ابتدا باید روش ارسال انتخاب شود."
            )

        if self.shipping_option_id and self.shipping_method_id:
            try:
                option = self.shipping_option
            except ShippingOption.DoesNotExist:
                option = None

            if option is not None and option.method_id != self.shipping_method_id:
                errors["shipping_option"] = (
                    "این گزینه ارسال متعلق به روش ارسال انتخاب‌شده نیست."
                )

        if errors:
            raise ValidationError(errors)

    def save(self, *args, **kwargs):
        # فقط در ذخیره‌سازی کامل، اعتبارسنجی انجام شود؛
        # ذخیره‌سازی‌های جزئی با update_fields نباید خطای ۵۰۰ بدهند.
        if not kwargs.get("update_fields"):
            self.full_clean()

        if self.is_paid and not self.invoice_number:
            self.invoice_number = self.generate_invoice_number()

        super().save(*args, **kwargs)

    def generate_invoice_number(self):
        return f"INV-{uuid.uuid4().hex[:10].upper()}"
    
    def get_coupon_discount(self):
        """محاسبه مبلغ تخفیف کوپن"""
        if not self.coupon:
            return Decimal("0")
        
        products_price = self.get_products_price()
        if self.coupon.discount:
            # اگر درصد تخفیف است
            discount = (products_price * Decimal(str(self.coupon.discount))) / Decimal("100")
            if self.coupon.max_discount and discount > self.coupon.max_discount:
                discount = self.coupon.max_discount
        else:
            discount = Decimal("0")
            
        return min(discount, products_price)

    def get_final_price(self):
        """مجموع نهایی قابل پرداخت: (قیمت محصولات + مالیات + هزینه ارسال) - تخفیف"""
        products_price = self.get_products_price()
        tax = self.get_tax()
        shipping = self.get_shipping_cost()
        discount = self.get_coupon_discount()

        # اگر روش ارسال پس‌کرایه باشد، هزینه ارسال آنلاین صفر در نظر گرفته می‌شود
        if self.is_shipping_cod():
            shipping = Decimal("0")

        total = products_price + tax + shipping - discount
        return max(total, Decimal("0"))

    
    
    @cached_property
    def products_price(self):
        total = self.orderdetail_set.aggregate(
            total_price=Sum(
                ExpressionWrapper(
                    F("final_price") * F("count"),
                    output_field=DecimalField(max_digits=18, decimal_places=0),
                )
            )
        )["total_price"]
        return total or Decimal("0")

    def get_products_price(self):
        return self.products_price

    def calculate_total_price(self):
        total = Decimal("0")
        for item in self.orderdetail_set.all():
            total += item.get_total_price()
        return total

    def get_total_weight(self):
        total_weight = Decimal("0")
        for item in self.orderdetail_set.select_related("product"):
            total_weight += item.item_total_weight
        return total_weight

    def get_tax(self):
        total_tax = Decimal("0")
        for item in self.orderdetail_set.select_related("product"):
            total_tax += item.item_total_tax
        return total_tax

    def get_tipax_billable_weight(self):
        actual_weight = self.get_total_weight()
        if not all([self.package_length, self.package_width, self.package_height]):
            return actual_weight
        volumetric_weight = (
            Decimal(self.package_length)
            * Decimal(self.package_width)
            * Decimal(self.package_height)
            / Decimal("6")
        )
        return max(actual_weight, volumetric_weight)

    def get_destination_zone(self):
        """
        زون مقصد سفارش را برمی‌گرداند.
        """
        if not self.address_id:
            return None

        try:
            return self.address.shipping_zone
        except (Address.DoesNotExist, AttributeError):
            return None

    def get_billable_weight_for_method(self, method):
        """
        وزن قابل محاسبه برای هر روش ارسال، بر حسب گرم.
        """
        if method is None:
            return Decimal("0")

        if _is_tipax_method(method):
            return self.get_tipax_billable_weight()

        return self.get_total_weight()

    def find_shipping_rate(self, method):
        """
        پیدا کردن نرخ فعال روش ارسال از جدول ShippingRate.
        """
        if method is None or not method.active:
            return None

        zone = self.get_destination_zone()

        if zone is None or not zone.is_active:
            return None

        billable_weight = self.get_billable_weight_for_method(method)

        return (
            ShippingRate.objects
            .filter(
                method_id=method.pk,
                zone_id=zone.pk,
                is_active=True,
                min_weight__lte=billable_weight,
                max_weight__gte=billable_weight,
            )
            .order_by("min_weight", "max_weight")
            .first()
        )

    def find_tipax_rate(self):
        """
        پیدا کردن نرخ تیپاکس از جدول TipaxRate.
        این متد زمانی استفاده می‌شود که نرخ تیپاکس در TipaxRate
        ثبت شده باشد، نه ShippingRate.
        """
        zone = self.get_destination_zone()

        if zone is None or not zone.is_active:
            return None

        billable_weight = self.get_tipax_billable_weight()

        return (
            TipaxRate.objects
            .filter(
                destination_zone_id=zone.pk,
                max_weight__gte=billable_weight,
            )
            .order_by("max_weight")
            .first()
        )

    def is_option_valid_for_method(self, method, option=None):
        """
        بررسی می‌کند گزینه ارسال متعلق به روش انتخاب‌شده باشد
        و خودش نیز فعال باشد.
        """
        if method is None:
            return False

        if option is None:
            return True

        return (
            option.active
            and option.method_id == method.pk
        )

    def is_shipping_cod(self, method=None, option=None):
        """
        مشخص می‌کند هزینه ارسال پس‌کرایه است یا خیر.
        """
        method = method or self.shipping_method
        option = option if option is not None else self.shipping_option

        if method is not None and method.is_cod:
            return True

        if option is not None and option.is_cod:
            return True

        return False

    def calculate_shipping_cost_for_option(self, method, option=None):
        """
        محاسبه هزینه ارسال:

        - COD: صفر برای پرداخت آنلاین
        - تیپاکس: ابتدا ShippingRate و سپس TipaxRate
        - سایر روش‌ها: فقط ShippingRate
        - نبود نرخ: None، نه صفر
        """
        if method is None or not method.active:
            return None

        if not self.is_option_valid_for_method(method, option):
            return None

        # پس‌کرایه؛ هزینه واقعی در مقصد دریافت می‌شود.
        if self.is_shipping_cod(method, option):
            return Decimal("0")

        # برای تیپاکس ابتدا جدول عمومی ShippingRate بررسی می‌شود.
        rate = self.find_shipping_rate(method)

        if rate is None and _is_tipax_method(method):
            tipax_rate = self.find_tipax_rate()

            if tipax_rate is not None:
                rate = tipax_rate

        if rate is None:
            return None

        try:
            return Decimal(str(rate.price))
        except (InvalidOperation, TypeError, ValueError):
            return None

    def update_shipping_cost(self, save=True):
        """
        بروزرسانی هزینه ارسال سفارش.

        نبود نرخ، رایگان محسوب نمی‌شود؛ در این حالت None برگردانده می‌شود.
        """
        cost = self.calculate_shipping_cost_for_option(
            method=self.shipping_method,
            option=self.shipping_option,
        )

        if cost is None:
            self.shipping_cost = Decimal("0")
            self.shipping_calculated_at = None

            if save and self.pk:
                super(Order, self).save(
                    update_fields=[
                        "shipping_cost",
                        "shipping_calculated_at",
                    ]
                )

            return None

        self.shipping_cost = cost
        self.shipping_calculated_at = timezone.now()

        if save and self.pk:
            super(Order, self).save(
                update_fields=[
                    "shipping_cost",
                    "shipping_calculated_at",
                ]
            )

        return cost

    def get_shipping_cost(self):
        """
        فقط هزینه معتبر ذخیره‌شده را برمی‌گرداند.
        """
        return Decimal(str(self.shipping_cost or "0"))

    def get_suitable_shipping_methods(self):
        """
        فقط روش‌های ارسال معتبر را برمی‌گرداند.

        قوانین:
        - روش باید فعال باشد.
        - روش COD حتی بدون نرخ ShippingRate قابل استفاده است.
        - گزینه COD فعال، روش را قابل استفاده می‌کند.
        - روش عادی فقط در صورت وجود نرخ معتبر نمایش داده می‌شود.
        - برای تیپاکس، نرخ TipaxRate نیز بررسی می‌شود.
        """

        methods = (
            ShippingMethod.objects
            .filter(active=True)
            .order_by("name")
        )

        valid_method_ids = []

        for method in methods:
            # روش‌های COD در پرداخت آنلاین هزینه ارسال ندارند
            # و هزینه در مقصد دریافت می‌شود.
            if getattr(method, "is_cod", False):
                valid_method_ids.append(method.pk)
                continue

            # بررسی گزینه‌های فعال متعلق به همین روش
            has_active_cod_option = ShippingOption.objects.filter(
                method_id=method.pk,
                active=True,
                is_cod=True,
            ).exists()

            if has_active_cod_option:
                valid_method_ids.append(method.pk)
                continue

            # ابتدا نرخ عمومی ShippingRate بررسی می‌شود
            shipping_rate = self.find_shipping_rate(method)

            if shipping_rate is not None:
                valid_method_ids.append(method.pk)
                continue

            # اگر روش تیپاکس است، جدول TipaxRate نیز بررسی شود
            if _is_tipax_method(method):
                tipax_rate = self.find_tipax_rate()

                if tipax_rate is not None:
                    valid_method_ids.append(method.pk)
                    continue

        return (
            ShippingMethod.objects
            .filter(
                pk__in=valid_method_ids,
                active=True,
            )
            .prefetch_related("options")
            .order_by("name")
        )
    def total_items(self):
        result = self.orderdetail_set.aggregate(
            total=Sum("count")
        )

        return int(result["total"] or 0)    

# ============================================
# جزئیات سفارش
# ============================================

class OrderDetail(models.Model):
    order = models.ForeignKey(
        Order,
        on_delete=models.CASCADE,
        verbose_name="سفارش",
    )
    product = models.ForeignKey(
        Product,
        on_delete=models.PROTECT,
        verbose_name="محصول",
    )
    count = models.PositiveIntegerField(default=1, verbose_name="تعداد")
    final_price = models.DecimalField(
        max_digits=15,
        decimal_places=0,
        null=True,
        blank=True,
        verbose_name="قیمت واحد",
    )

    class Meta:
        verbose_name = "جزئیات سفارش"
        verbose_name_plural = "جزئیات سفارش‌ها"

    def __str__(self):
        return f"{self.product.title} ({self.count})"

    def save(self, *args, **kwargs):
        if self.final_price is None:
            self.final_price = self.product.price
        super().save(*args, **kwargs)

    def get_total_price(self):
        return Decimal(self.final_price or 0) * Decimal(self.count)

    @property
    def item_total_weight(self):
        product_weight = getattr(self.product, "weight", 0) or 0
        return Decimal(str(product_weight)) * Decimal(self.count)

    @property
    def item_total_tax(self):
        if not getattr(self.product, "is_taxable", False):
            return Decimal("0")

        settings_obj = OrderSettings.objects.first()
        if not settings_obj or settings_obj.tax_percent is None:
            return Decimal("0")

        tax_percent = Decimal(settings_obj.tax_percent) / Decimal("100")
        return (
            Decimal(self.final_price or 0) * Decimal(self.count) * tax_percent
        ).quantize(Decimal("1"), rounding=ROUND_HALF_UP)

# ============================================
# پرداخت
# ============================================

class Payment(models.Model):
    order = models.ForeignKey(
        Order,
        on_delete=models.CASCADE,
        related_name="payments",
        verbose_name="سفارش",
    )
    authority = models.CharField(max_length=255, verbose_name="Authority")
    ref_id = models.CharField(max_length=255, null=True, blank=True, verbose_name="Ref ID")
    amount = models.DecimalField(max_digits=12, decimal_places=0, verbose_name="مبلغ")
    status = models.CharField(max_length=50, default="pending", verbose_name="وضعیت")
    created_at = models.DateTimeField(default=timezone.now, verbose_name="تاریخ ایجاد")

    class Meta:
        verbose_name = "پرداخت"
        verbose_name_plural = "پرداخت‌ها"

    def __str__(self):
        return f"Payment {self.id} - Order {self.order.id}"
