"""Marketplace PART 3: Fulfillment, Inventory, Reviews, and Shipping.

OWNERSHIP MAP:
    OWNS: ProductReview, ReviewMedia, ContentHelpfulness
    OWNS: FulfillmentZone, FulfillmentZonePincode, FulfillmentRoute, FulfillmentTask
    OWNS: InventoryWarehouse, WarehouseStock, StockMovement, ProductStockSummary
    OWNS: LowStockAlert, ShippingRule, ShippingRulePincode,
                PincodeServiceability, DeliveryEstimation

DOES NOT OWN (references only):
    products.Product, orders.Order, marketplace.OrderItem, marketplace.Vendor,
    geo.FranchiseStore

REMOVED IN CONSOLIDATION:
    FulfillmentMode DB table, ShipmentTracking, DeliveryStatus,
    ProductRating, RatingHelpfulness, ReviewHelpfulness,
    ShippingRule ArrayField pincode lists.
"""

from decimal import Decimal
from django.db import models
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.validators import MinValueValidator, MaxValueValidator
import uuid

User = get_user_model()


# =============================================================================
# FULFILLMENT MODE CONSTANT
# REMOVED: FulfillmentMode DB table (was a 4-row config table that added a join
# to every fulfillment query and could be changed by admin, silently breaking
# routing logic). Replaced with a Python constant used in CharField choices.
# =============================================================================

class FulfillmentMode(models.TextChoices):
    """
    Fulfillment mode choices. Used in OrderItem.fulfillment_mode,
    FulfillmentRoute.fulfillment_mode, and FulfillmentTask.fulfillment_mode.
    This is a constant, not a DB table.
    """
    VENDOR = 'VENDOR_FULFILLED', 'Vendor Fulfilled'
    COMPANY = 'COMPANY_FULFILLED', 'Company Fulfilled'
    FRANCHISE = 'FRANCHISE_FULFILLED', 'Franchise Fulfilled'
    PICKUP = 'PICKUP_POINT', 'Customer Pickup Point'


# =============================================================================
# COMMUNITY LAYER: RATINGS & REVIEWS (MERGED)
# ProductRating + ProductReview → one unified ProductReview model.
# RatingHelpfulness + ReviewHelpfulness → one generic ContentHelpfulness model.
# =============================================================================

class ProductReview(models.Model):
    """
    Unified product rating + review model.
    Replaces the previous split of ProductRating (star score) + ProductReview (text).
    One record per (product, user) — enforced by unique_together.
    A review without text content is still valid (rating-only). Title is optional.

    helpful_votes / unhelpful_votes: cached counts, incremented via ContentHelpfulness signal.
    NEVER increment directly — always update via ContentHelpfulness creation.

    products.Product.average_rating and total_ratings are updated via signal after save.
    """
    MODERATION_CHOICES = [
        ('PENDING', 'Pending Review'),
        ('APPROVED', 'Approved'),
        ('REJECTED', 'Rejected'),
    ]

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # CORRECTED: FK to existing products.Product
    product = models.ForeignKey(
        'products.Product', on_delete=models.CASCADE, related_name='reviews',
    )
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='product_reviews')
    # CORRECTED: FK to existing orders.Order
    order = models.ForeignKey(
        'orders.Order', on_delete=models.SET_NULL, null=True, blank=True,
        help_text="Set when review is from a verified purchase",
    )

    rating = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)],
        help_text="Overall star rating 1–5",
    )
    title = models.CharField(max_length=255, blank=True)
    content = models.TextField(blank=True)

    # Optional sub-dimension ratings
    quality_rating = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)], null=True, blank=True,
    )
    value_rating = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)], null=True, blank=True,
    )
    delivery_rating = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)], null=True, blank=True,
    )

    is_verified_purchase = models.BooleanField(default=False)
    is_published = models.BooleanField(default=False, help_text="Set to True after moderation approval")

    # Cached vote counts — updated via ContentHelpfulness post_save signal, never directly
    helpful_votes = models.PositiveIntegerField(default=0)
    unhelpful_votes = models.PositiveIntegerField(default=0)

    moderation_status = models.CharField(
        max_length=20, choices=MODERATION_CHOICES, default='PENDING', db_index=True,
    )
    moderation_reason = models.TextField(blank=True, null=True)
    moderated_by = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=True, blank=True,
        related_name='moderated_reviews',
        limit_choices_to={'is_staff': True},
    )
    moderated_at = models.DateTimeField(null=True, blank=True)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        # One review per (product, user) — prevent duplicate reviews
        unique_together = ('product', 'user')
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['product', 'is_published', '-created_at']),
            models.Index(fields=['product', 'moderation_status']),
            models.Index(fields=['user']),
        ]

    def __str__(self):
        return f'{self.product_id} — {self.rating}★ by {self.user_id}'

    @property
    def helpfulness_score(self):
        total = self.helpful_votes + self.unhelpful_votes
        return round((self.helpful_votes / total) * 100, 1) if total > 0 else 0.0


class ReviewMedia(models.Model):
    """Images or videos attached to a product review."""
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    review = models.ForeignKey(ProductReview, on_delete=models.CASCADE, related_name='media')
    media_type = models.CharField(
        max_length=20, choices=[('IMAGE', 'Image'), ('VIDEO', 'Video')],
    )
    media_url = models.URLField()
    caption = models.CharField(max_length=255, blank=True)
    uploaded_at = models.DateTimeField(auto_now_add=True)


class ContentHelpfulness(models.Model):
    """
    Generic helpful/unhelpful vote for any reviewable content.
    Replaces both RatingHelpfulness and ReviewHelpfulness with a single model.
    Uses Django's contenttypes framework for the generic FK.

    After save/delete, a signal updates ProductReview.helpful_votes /
    unhelpful_votes via aggregation (not incremental update to avoid drift).

    Supported content types: marketplace.ProductReview (extendable to others).
    """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.UUIDField(db_index=True)
    content_object = GenericForeignKey('content_type', 'object_id')

    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='helpfulness_votes')
    is_helpful = models.BooleanField()
    voted_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('content_type', 'object_id', 'user')
        indexes = [
            models.Index(fields=['content_type', 'object_id']),
            models.Index(fields=['user']),
        ]


# =============================================================================
# FULFILLMENT ROUTING LAYER
# FulfillmentRoute uses zone-based routing (not pincode-per-row).
# This replaces the old architecture where product × pincode = 1B rows at scale.
# =============================================================================

class FulfillmentZone(models.Model):
    """
    Named delivery zone (e.g. 'Mumbai-West', 'Telangana-Rural').
    Routes are defined per zone, not per pincode.
    Pincodes are associated via FulfillmentZonePincode junction table.
    """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=255, unique=True)
    description = models.TextField(blank=True)
    state = models.CharField(max_length=100, blank=True, db_index=True)
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name


class FulfillmentZonePincode(models.Model):
    """
    Junction table: maps individual pincodes to FulfillmentZones.
    O(1) zone lookup: SELECT zone FROM fulfillment_zone_pincode WHERE pincode = X
    """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    zone = models.ForeignKey(FulfillmentZone, on_delete=models.CASCADE, related_name='pincodes')
    pincode = models.CharField(max_length=20, db_index=True)

    class Meta:
        unique_together = ('zone', 'pincode')
        indexes = [models.Index(fields=['pincode'])]

    def __str__(self):
        return f'{self.zone.name} → {self.pincode}'


class FulfillmentRoute(models.Model):
    """
    Routing rule: for a given (product, zone), which fulfillment mode and partner applies.
    FIXED: zone-based routing replaces the old pincode-per-row design.
    At scale: 10K products × 500 zones = 5M rows (manageable vs 1B with pincode-per-row).
    Priority: lower number = higher priority (1 = first evaluated).

    Fulfillment precedence example:
      priority=1: FRANCHISE_FULFILLED for a specific franchise in the zone
      priority=2: VENDOR_FULFILLED    for a specific vendor
      priority=3: COMPANY_FULFILLED   (global fallback)
    """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # CORRECTED: FK to existing products.Product
    product = models.ForeignKey(
        'products.Product', on_delete=models.CASCADE, related_name='fulfillment_routes',
    )
    zone = models.ForeignKey(
        FulfillmentZone, on_delete=models.SET_NULL, null=True, blank=True,
        help_text="null = global fallback route (applies to all zones)",
    )
    priority = models.PositiveIntegerField(
        default=0,
        help_text="Lower = higher priority. Evaluated in ascending order.",
    )
    fulfillment_mode = models.CharField(
        max_length=25, choices=FulfillmentMode.choices,
    )
    # CORRECTED: FK to marketplace.Vendor (not FranchiseHead)
    vendor = models.ForeignKey(
        'marketplace.Vendor', on_delete=models.SET_NULL, null=True, blank=True,
        related_name='fulfillment_routes',
    )
    # CORRECTED: FK to geo.FranchiseStore (canonical franchise entity)
    franchise_store = models.ForeignKey(
        'geo.FranchiseStore', on_delete=models.SET_NULL, null=True, blank=True,
        related_name='fulfillment_routes',
    )
    sla_days = models.PositiveIntegerField(help_text="Expected days to fulfill from this route")
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['priority']
        unique_together = ('product', 'zone', 'priority')
        indexes = [
            models.Index(fields=['product', 'is_active']),
            models.Index(fields=['zone', 'fulfillment_mode']),
        ]


class FulfillmentTask(models.Model):
    """
    Per-item fulfillment assignment created after order confirmation.
    Assigned to either a Vendor or a FranchiseStore (not both).
    Status transitions: PENDING → ACCEPTED → PACKED → PICKED_UP → DELIVERED (or FAILED).
    """
    STATUS_CHOICES = [
        ('PENDING', 'Pending'),
        ('ACCEPTED', 'Accepted'),
        ('PACKED', 'Packed'),
        ('PICKED_UP', 'Picked Up'),
        ('IN_TRANSIT', 'In Transit'),
        ('DELIVERED', 'Delivered'),
        ('FAILED', 'Failed'),
    ]

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    order_item = models.OneToOneField(
        'marketplace.OrderItem', on_delete=models.CASCADE, related_name='fulfillment_task',
    )
    # CORRECTED: FK to marketplace.Vendor (not FranchiseHead)
    assigned_to_vendor = models.ForeignKey(
        'marketplace.Vendor', on_delete=models.SET_NULL, null=True, blank=True,
        related_name='fulfillment_tasks',
    )
    # CORRECTED: FK to geo.FranchiseStore (canonical franchise entity)
    assigned_to_store = models.ForeignKey(
        'geo.FranchiseStore', on_delete=models.SET_NULL, null=True, blank=True,
        related_name='fulfillment_tasks',
    )
    fulfillment_mode = models.CharField(max_length=25, choices=FulfillmentMode.choices)
    status = models.CharField(
        max_length=20, choices=STATUS_CHOICES, default='PENDING', db_index=True,
    )
    deadline = models.DateTimeField()
    assigned_at = models.DateTimeField(auto_now_add=True)
    accepted_at = models.DateTimeField(null=True, blank=True)
    completed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        indexes = [
            models.Index(fields=['assigned_to_vendor', 'status']),
            models.Index(fields=['assigned_to_store', 'status']),
            models.Index(fields=['status', 'deadline']),
        ]


# =============================================================================
# INVENTORY LAYER (COMPANY WAREHOUSE)
# SINGLE SOURCE OF TRUTH for company-warehouse stock.
# Vendor stock → marketplace.VendorInventory
# Franchise stock → geo.FranchiseStoreInventory
# Company warehouse stock → WarehouseStock (here)
# Aggregate → ProductStockSummary (cached)
# =============================================================================

class InventoryWarehouse(models.Model):
    """Company-owned inventory warehouse."""
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=255)
    address = models.TextField()
    city = models.CharField(max_length=100, db_index=True)
    state = models.CharField(max_length=100, db_index=True)
    pincode = models.CharField(max_length=10)
    country = models.CharField(max_length=100, default='India')
    manager_name = models.CharField(max_length=255)
    manager_email = models.EmailField()
    manager_phone = models.CharField(max_length=20)
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f'{self.name} ({self.city})'


class WarehouseStock(models.Model):
    """
    Stock level for a product at a specific warehouse.
    SINGLE SOURCE OF TRUTH for company-warehouse inventory.
    Use SELECT FOR UPDATE when reading stock for order reservation.
    """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    warehouse = models.ForeignKey(
        InventoryWarehouse, on_delete=models.CASCADE, related_name='stock',
    )
    # CORRECTED: FK to existing products.Product
    product = models.ForeignKey(
        'products.Product', on_delete=models.CASCADE, related_name='warehouse_stock',
    )
    stock = models.PositiveIntegerField(default=0)
    reserved_stock = models.PositiveIntegerField(default=0)
    reorder_point = models.PositiveIntegerField(default=50)
    last_restocked = models.DateTimeField(null=True, blank=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ('warehouse', 'product')
        indexes = [
            models.Index(fields=['product', 'warehouse']),
            models.Index(fields=['warehouse', 'product']),
        ]

    @property
    def available_stock(self):
        return max(0, self.stock - self.reserved_stock)

    @property
    def is_low_stock(self):
        return self.available_stock <= self.reorder_point


class StockMovement(models.Model):
    """
    Append-only audit log of every inventory movement at a company warehouse.
    Created by InventoryService — never mutated after creation.
    """
    MOVEMENT_TYPE_CHOICES = [
        ('IN', 'Stock In'),
        ('OUT', 'Stock Out'),
        ('ADJUSTMENT', 'Manual Adjustment'),
        ('DAMAGE', 'Damage Write-off'),
        ('RETURN', 'Customer Return'),
        ('RESERVE', 'Order Reservation'),
        ('RELEASE', 'Reservation Release'),
    ]

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    warehouse = models.ForeignKey(
        InventoryWarehouse, on_delete=models.CASCADE, related_name='stock_movements',
    )
    # CORRECTED: FK to existing products.Product
    product = models.ForeignKey('products.Product', on_delete=models.PROTECT)
    movement_type = models.CharField(max_length=20, choices=MOVEMENT_TYPE_CHOICES, db_index=True)
    quantity = models.IntegerField(help_text="Positive=in, Negative=out")
    # CORRECTED: FK to existing orders.Order
    reference_order = models.ForeignKey(
        'orders.Order', on_delete=models.SET_NULL, null=True, blank=True,
    )
    reference_po = models.CharField(max_length=100, blank=True)
    notes = models.TextField(blank=True)
    recorded_by = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=True, related_name='stock_movements',
    )
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['product', '-created_at']),
            models.Index(fields=['warehouse', '-created_at']),
            models.Index(fields=['movement_type', '-created_at']),
        ]


class ProductStockSummary(models.Model):
    """
    Cached aggregate of total available stock across all sources for a product.
    Replaces Product.total_stock (denormalized field that caused inventory drift).
    Updated asynchronously via Celery task triggered by StockMovement post_save signal.
    NEVER read for order placement — use SELECT FOR UPDATE on the source table.
    Use only for: product listings, search, admin dashboards.
    """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # CORRECTED: FK to existing products.Product
    product = models.OneToOneField(
        'products.Product', on_delete=models.CASCADE, related_name='stock_summary',
    )
    company_available = models.PositiveIntegerField(
        default=0, help_text="Sum of WarehouseStock.available_stock across all warehouses",
    )
    vendor_available = models.PositiveIntegerField(
        default=0, help_text="Sum of VendorInventory.available_stock across all vendors",
    )
    franchise_available = models.PositiveIntegerField(
        default=0, help_text="Sum of FranchiseStoreInventory.available_stock across all stores",
    )
    total_available = models.PositiveIntegerField(
        default=0, help_text="company + vendor + franchise available. For display only.",
    )
    last_updated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f'StockSummary({self.product_id}) total={self.total_available}'


class LowStockAlert(models.Model):
    """
    Alert record when any inventory source drops below threshold.
    Created by Celery task triggered by stock-level signals.
    Resolved manually by admin or automatically on replenishment.
    """
    ALERT_TYPE_CHOICES = [
        ('COMPANY_LOW', 'Company Warehouse Low'),
        ('VENDOR_LOW', 'Vendor Inventory Low'),
        ('FRANCHISE_LOW', 'Franchise Store Low'),
    ]

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # CORRECTED: FK to existing products.Product
    product = models.ForeignKey(
        'products.Product', on_delete=models.CASCADE, related_name='low_stock_alerts',
    )
    alert_type = models.CharField(max_length=20, choices=ALERT_TYPE_CHOICES, db_index=True)
    current_stock = models.PositiveIntegerField()
    threshold = models.PositiveIntegerField()
    is_resolved = models.BooleanField(default=False, db_index=True)
    resolved_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            models.Index(fields=['product', 'is_resolved']),
            models.Index(fields=['alert_type', 'is_resolved']),
        ]


# =============================================================================
# SHIPPING LAYER
# FIXED: ShippingRule.serviceable_pincodes ArrayField → ShippingRulePincode junction table
# FIXED: DeliveryEstimation.from_city (free text) → from_warehouse FK
# FIXED: PincodeServiceability.assigned_franchise → FK to geo.FranchiseStore
# =============================================================================

class ShippingRule(models.Model):
    """
    Shipping cost and policy configuration.
    FIXED: serviceable_pincodes is no longer an ArrayField.
    Pincodes are managed via ShippingRulePincode junction table for O(1) lookup.
    """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    min_order_value = models.DecimalField(
        max_digits=12, decimal_places=2, default=Decimal('0.00'),
    )
    max_order_value = models.DecimalField(
        max_digits=12, decimal_places=2, null=True, blank=True,
    )
    base_shipping_cost = models.DecimalField(max_digits=12, decimal_places=2)
    free_shipping_threshold = models.DecimalField(
        max_digits=12, decimal_places=2, null=True, blank=True,
    )
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name


class ShippingRulePincode(models.Model):
    """
    Junction table: maps pincodes to ShippingRules.
    Replaces the ArrayField on ShippingRule (which couldn't be indexed).
    Lookup: SELECT rule_id FROM shipping_rule_pincode WHERE pincode = X AND is_serviceable = True
    Index on (pincode, is_serviceable) enables O(1) lookup.
    """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    rule = models.ForeignKey(ShippingRule, on_delete=models.CASCADE, related_name='pincode_rules')
    pincode = models.CharField(max_length=20)
    is_serviceable = models.BooleanField(default=True)

    class Meta:
        unique_together = ('rule', 'pincode')
        indexes = [
            models.Index(fields=['pincode', 'is_serviceable']),
            models.Index(fields=['rule', 'is_serviceable']),
        ]

    def __str__(self):
        return f'{self.rule.name} → {self.pincode} ({"✓" if self.is_serviceable else "✗"})'


class PincodeServiceability(models.Model):
    """
    Per-pincode delivery capability and franchise assignment.
    FIXED: assigned_franchise now points to geo.FranchiseStore (not removed FranchiseHead).
    """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    pincode = models.CharField(max_length=10, unique=True, db_index=True)
    city = models.CharField(max_length=100, blank=True)
    state = models.CharField(max_length=100, blank=True)
    is_serviceable = models.BooleanField(default=True, db_index=True)
    standard_delivery_days = models.PositiveIntegerField(default=5)
    express_delivery_days = models.PositiveIntegerField(default=2)
    # CORRECTED: FK to geo.FranchiseStore (single franchise identity)
    assigned_franchise = models.ForeignKey(
        'geo.FranchiseStore',
        on_delete=models.SET_NULL, null=True, blank=True,
        related_name='serviceable_pincodes',
    )
    franchise_can_deliver = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        indexes = [
            models.Index(fields=['is_serviceable']),
            models.Index(fields=['assigned_franchise', 'is_serviceable']),
        ]

    def __str__(self):
        return f'{self.pincode} — {"serviceable" if self.is_serviceable else "not serviceable"}'


class DeliveryEstimation(models.Model):
    """
    SLA estimation for a warehouse→pincode shipping route.
    FIXED: from_warehouse is now an FK to InventoryWarehouse (not a free-text city string).
    This enforces referential integrity and enables proper JOIN queries.
    """
    SHIPPING_METHOD_CHOICES = [
        ('STANDARD', 'Standard'),
        ('FAST', 'Fast Delivery'),
        ('EXPRESS', 'Express'),
    ]

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    from_warehouse = models.ForeignKey(
        InventoryWarehouse, on_delete=models.CASCADE,
        related_name='delivery_estimations',
        help_text="Source warehouse for this SLA estimate (replaces from_city free text)",
    )
    to_pincode = models.CharField(max_length=10, db_index=True)
    shipping_method = models.CharField(max_length=20, choices=SHIPPING_METHOD_CHOICES)
    estimated_days = models.PositiveIntegerField()
    cost = models.DecimalField(max_digits=12, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('from_warehouse', 'to_pincode', 'shipping_method')
        indexes = [
            models.Index(fields=['to_pincode', 'shipping_method']),
            models.Index(fields=['from_warehouse', 'to_pincode']),
        ]

