import contextlib

import pghistory
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.text import slugify

from apps.core.choices import RichChoiceField
from apps.core.models import TrackedModel
from apps.core.state_machine import RichFSMField, StateMachineMixin
from config.django import base as settings

from .company import Company


@pghistory.track()
class RideModel(TrackedModel):
    """
    Represents a specific model/type of ride that can be manufactured by different
    companies. This serves as a catalog of ride designs that can be referenced
    by individual ride installations.

    For example: B&M Dive Coaster, Vekoma Boomerang, RMC I-Box, etc.
    """

    name = models.CharField(max_length=255, help_text="Name of the ride model")
    slug = models.SlugField(max_length=255, help_text="URL-friendly identifier (unique within manufacturer)")
    manufacturer = models.ForeignKey(
        Company,
        on_delete=models.SET_NULL,
        related_name="ride_models",
        null=True,
        blank=True,
        limit_choices_to={"roles__contains": ["MANUFACTURER"]},
        help_text="Primary manufacturer of this ride model",
    )
    description = models.TextField(blank=True, help_text="Detailed description of the ride model")
    category = RichChoiceField(
        choice_group="categories",
        domain="rides",
        max_length=2,
        default="",
        blank=True,
        help_text="Primary category classification",
    )
    ride_type = models.CharField(
        max_length=100,
        blank=True,
        help_text="Specific ride type within the category (e.g., 'Flying Coaster', 'Inverted Coaster')",
    )

    # Technical specifications
    typical_height_range_min_ft = models.DecimalField(
        max_digits=6,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Minimum typical height in feet for this model",
    )
    typical_height_range_max_ft = models.DecimalField(
        max_digits=6,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum typical height in feet for this model",
    )
    typical_speed_range_min_mph = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Minimum typical speed in mph for this model",
    )
    typical_speed_range_max_mph = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum typical speed in mph for this model",
    )
    typical_capacity_range_min = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Minimum typical hourly capacity for this model",
    )
    typical_capacity_range_max = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Maximum typical hourly capacity for this model",
    )

    # Design characteristics
    track_type = models.CharField(
        max_length=100,
        blank=True,
        help_text="Type of track system (e.g., tubular steel, I-Box, wooden)",
    )
    support_structure = models.CharField(
        max_length=100,
        blank=True,
        help_text="Type of support structure (e.g., steel, wooden, hybrid)",
    )
    train_configuration = models.CharField(
        max_length=200,
        blank=True,
        help_text="Typical train configuration (e.g., 2 trains, 7 cars per train, 4 seats per car)",
    )
    restraint_system = models.CharField(
        max_length=100,
        blank=True,
        help_text="Type of restraint system (e.g., over-shoulder, lap bar, vest)",
    )

    # Market information
    first_installation_year = models.PositiveIntegerField(
        null=True, blank=True, help_text="Year of first installation of this model"
    )
    last_installation_year = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Year of last installation of this model (if discontinued)",
    )
    is_discontinued = models.BooleanField(default=False, help_text="Whether this model is no longer being manufactured")
    total_installations = models.PositiveIntegerField(
        default=0, help_text="Total number of installations worldwide (auto-calculated)"
    )

    # Design features
    notable_features = models.TextField(
        blank=True,
        help_text="Notable design features or innovations (JSON or comma-separated)",
    )
    target_market = RichChoiceField(
        choice_group="target_markets",
        domain="rides",
        max_length=50,
        blank=True,
        help_text="Primary target market for this ride model",
    )

    # Media
    primary_image = models.ForeignKey(
        "RideModelPhoto",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="ride_models_as_primary",
        help_text="Primary promotional image for this ride model",
    )

    # SEO and metadata
    meta_title = models.CharField(max_length=60, blank=True, help_text="SEO meta title (auto-generated if blank)")
    meta_description = models.CharField(
        max_length=160,
        blank=True,
        help_text="SEO meta description (auto-generated if blank)",
    )

    # Frontend URL
    url = models.URLField(blank=True, help_text="Frontend URL for this ride model")

    # Submission metadata fields (from frontend schema)
    source_url = models.URLField(
        blank=True,
        help_text="Source URL for the data (e.g., manufacturer website)",
    )
    is_test_data = models.BooleanField(
        default=False,
        help_text="Whether this is test/development data",
    )

    class Meta(TrackedModel.Meta):
        verbose_name = "Ride Model"
        verbose_name_plural = "Ride Models"
        ordering = ["manufacturer__name", "name"]
        constraints = [
            # Unique constraints (replacing unique_together for better error messages)
            models.UniqueConstraint(
                fields=["manufacturer", "name"],
                name="ridemodel_manufacturer_name_unique",
                violation_error_message="A ride model with this name already exists for this manufacturer",
            ),
            models.UniqueConstraint(
                fields=["manufacturer", "slug"],
                name="ridemodel_manufacturer_slug_unique",
                violation_error_message="A ride model with this slug already exists for this manufacturer",
            ),
            # Height range validation
            models.CheckConstraint(
                name="ride_model_height_range_logical",
                condition=models.Q(typical_height_range_min_ft__isnull=True)
                | models.Q(typical_height_range_max_ft__isnull=True)
                | models.Q(typical_height_range_min_ft__lte=models.F("typical_height_range_max_ft")),
                violation_error_message="Minimum height cannot exceed maximum height",
            ),
            # Speed range validation
            models.CheckConstraint(
                name="ride_model_speed_range_logical",
                condition=models.Q(typical_speed_range_min_mph__isnull=True)
                | models.Q(typical_speed_range_max_mph__isnull=True)
                | models.Q(typical_speed_range_min_mph__lte=models.F("typical_speed_range_max_mph")),
                violation_error_message="Minimum speed cannot exceed maximum speed",
            ),
            # Capacity range validation
            models.CheckConstraint(
                name="ride_model_capacity_range_logical",
                condition=models.Q(typical_capacity_range_min__isnull=True)
                | models.Q(typical_capacity_range_max__isnull=True)
                | models.Q(typical_capacity_range_min__lte=models.F("typical_capacity_range_max")),
                violation_error_message="Minimum capacity cannot exceed maximum capacity",
            ),
            # Installation years validation
            models.CheckConstraint(
                name="ride_model_installation_years_logical",
                condition=models.Q(first_installation_year__isnull=True)
                | models.Q(last_installation_year__isnull=True)
                | models.Q(first_installation_year__lte=models.F("last_installation_year")),
                violation_error_message="First installation year cannot be after last installation year",
            ),
        ]

    def __str__(self) -> str:
        return self.name if not self.manufacturer else f"{self.manufacturer.name} {self.name}"

    def clean(self) -> None:
        """Validate RideModel business rules."""
        super().clean()
        if self.is_discontinued and not self.last_installation_year:
            raise ValidationError({"last_installation_year": "Discontinued models must have a last installation year"})

    def save(self, *args, **kwargs) -> None:
        if not self.slug:
            from django.utils.text import slugify

            # Only use the ride model name for the slug, not manufacturer
            base_slug = slugify(self.name)
            self.slug = base_slug

            # Ensure uniqueness within the same manufacturer
            counter = 1
            while RideModel.objects.filter(manufacturer=self.manufacturer, slug=self.slug).exclude(pk=self.pk).exists():
                self.slug = f"{base_slug}-{counter}"
                counter += 1

        # Auto-generate meta fields if blank
        if not self.meta_title:
            self.meta_title = str(self)[:60]
        if not self.meta_description:
            desc = f"{self} - {self.description[:100]}" if self.description else str(self)
            self.meta_description = desc[:160]

        # Generate frontend URL
        if self.manufacturer:
            frontend_domain = getattr(settings, "FRONTEND_DOMAIN", "https://thrillwiki.com")
            self.url = f"{frontend_domain}/rides/manufacturers/{self.manufacturer.slug}/{self.slug}/"

        super().save(*args, **kwargs)

    def update_installation_count(self) -> None:
        """Update the total installations count based on actual ride instances."""
        # Import here to avoid circular import
        from django.apps import apps

        Ride = apps.get_model("rides", "Ride")
        self.total_installations = Ride.objects.filter(ride_model=self).count()
        self.save(update_fields=["total_installations"])

    @property
    def installation_years_range(self) -> str:
        """Get a formatted string of installation years range."""
        if self.first_installation_year and self.last_installation_year:
            return f"{self.first_installation_year}-{self.last_installation_year}"
        elif self.first_installation_year:
            return (
                f"{self.first_installation_year}-present"
                if not self.is_discontinued
                else f"{self.first_installation_year}+"
            )
        return "Unknown"

    @property
    def height_range_display(self) -> str:
        """Get a formatted string of height range."""
        if self.typical_height_range_min_ft and self.typical_height_range_max_ft:
            return f"{self.typical_height_range_min_ft}-{self.typical_height_range_max_ft} ft"
        elif self.typical_height_range_min_ft:
            return f"{self.typical_height_range_min_ft}+ ft"
        elif self.typical_height_range_max_ft:
            return f"Up to {self.typical_height_range_max_ft} ft"
        return "Variable"

    @property
    def speed_range_display(self) -> str:
        """Get a formatted string of speed range."""
        if self.typical_speed_range_min_mph and self.typical_speed_range_max_mph:
            return f"{self.typical_speed_range_min_mph}-{self.typical_speed_range_max_mph} mph"
        elif self.typical_speed_range_min_mph:
            return f"{self.typical_speed_range_min_mph}+ mph"
        elif self.typical_speed_range_max_mph:
            return f"Up to {self.typical_speed_range_max_mph} mph"
        return "Variable"


@pghistory.track()
class RideModelVariant(TrackedModel):
    """
    Represents specific variants or configurations of a ride model.
    For example: B&M Hyper Coaster might have variants like "Mega Coaster", "Giga Coaster"
    """

    ride_model = models.ForeignKey(
        RideModel,
        on_delete=models.CASCADE,
        related_name="variants",
        help_text="Base ride model this variant belongs to",
    )
    name = models.CharField(max_length=255, help_text="Name of this variant")
    description = models.TextField(blank=True, help_text="Description of variant differences")

    # Variant-specific specifications
    min_height_ft = models.DecimalField(
        max_digits=6,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Minimum height for this variant",
    )
    max_height_ft = models.DecimalField(
        max_digits=6,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum height for this variant",
    )
    min_speed_mph = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Minimum speed for this variant",
    )
    max_speed_mph = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum speed for this variant",
    )

    # Distinguishing features
    distinguishing_features = models.TextField(
        blank=True, help_text="What makes this variant unique from the base model"
    )

    class Meta(TrackedModel.Meta):
        verbose_name = "Ride Model Variant"
        verbose_name_plural = "Ride Model Variants"
        ordering = ["ride_model", "name"]
        unique_together = ["ride_model", "name"]

    def __str__(self) -> str:
        return f"{self.ride_model} - {self.name}"


@pghistory.track()
class RideModelPhoto(TrackedModel):
    """Photos associated with ride models for catalog/promotional purposes."""

    ride_model = models.ForeignKey(
        RideModel,
        on_delete=models.CASCADE,
        related_name="photos",
        help_text="Ride model this photo belongs to",
    )
    image = models.ForeignKey(
        "django_cloudflareimages_toolkit.CloudflareImage",
        on_delete=models.CASCADE,
        help_text="Photo of the ride model stored on Cloudflare Images",
    )
    caption = models.CharField(max_length=500, blank=True, help_text="Photo caption or description")
    alt_text = models.CharField(max_length=255, blank=True, help_text="Alternative text for accessibility")

    # Photo metadata
    photo_type = RichChoiceField(
        choice_group="photo_types",
        domain="rides",
        max_length=20,
        default="PROMOTIONAL",
        help_text="Type of photo for categorization and display purposes",
    )

    is_primary = models.BooleanField(default=False, help_text="Whether this is the primary photo for the ride model")

    # Attribution
    photographer = models.CharField(max_length=255, blank=True, help_text="Name of the photographer")
    source = models.CharField(max_length=255, blank=True, help_text="Source of the photo")
    copyright_info = models.CharField(max_length=255, blank=True, help_text="Copyright information")

    class Meta(TrackedModel.Meta):
        verbose_name = "Ride Model Photo"
        verbose_name_plural = "Ride Model Photos"
        ordering = ["-is_primary", "-created_at"]

    def __str__(self) -> str:
        return f"Photo of {self.ride_model.name}"

    def save(self, *args, **kwargs) -> None:
        # Ensure only one primary photo per ride model
        if self.is_primary:
            RideModelPhoto.objects.filter(ride_model=self.ride_model, is_primary=True).exclude(pk=self.pk).update(
                is_primary=False
            )
        super().save(*args, **kwargs)


@pghistory.track()
class RideModelTechnicalSpec(TrackedModel):
    """
    Technical specifications for ride models that don't fit in the main model.
    This allows for flexible specification storage.
    """

    ride_model = models.ForeignKey(
        RideModel,
        on_delete=models.CASCADE,
        related_name="technical_specs",
        help_text="Ride model this specification belongs to",
    )

    spec_category = RichChoiceField(
        choice_group="spec_categories",
        domain="rides",
        max_length=50,
        help_text="Category of technical specification",
    )

    spec_name = models.CharField(max_length=100, help_text="Name of the specification")
    spec_value = models.CharField(max_length=255, help_text="Value of the specification")
    spec_unit = models.CharField(max_length=20, blank=True, help_text="Unit of measurement")
    notes = models.TextField(blank=True, help_text="Additional notes about this specification")

    class Meta(TrackedModel.Meta):
        verbose_name = "Ride Model Technical Specification"
        verbose_name_plural = "Ride Model Technical Specifications"
        ordering = ["spec_category", "spec_name"]
        unique_together = ["ride_model", "spec_category", "spec_name"]

    def __str__(self) -> str:
        unit_str = f" {self.spec_unit}" if self.spec_unit else ""
        return f"{self.ride_model.name} - {self.spec_name}: {self.spec_value}{unit_str}"


@pghistory.track()
class Ride(StateMachineMixin, TrackedModel):
    """Model for individual ride installations at parks

    Note: The average_rating field is denormalized and refreshed by background
    jobs. Use selectors or annotations for real-time calculations if needed.
    """

    # Type hint for the reverse relation from RollerCoasterStats
    coaster_stats: "RollerCoasterStats"

    state_field_name = "status"

    name = models.CharField(max_length=255)
    slug = models.SlugField(max_length=255)
    description = models.TextField(blank=True)
    park = models.ForeignKey("parks.Park", on_delete=models.CASCADE, related_name="rides")
    park_area = models.ForeignKey(
        "parks.ParkArea",
        on_delete=models.SET_NULL,
        related_name="rides",
        null=True,
        blank=True,
    )
    category = RichChoiceField(
        choice_group="categories",
        domain="rides",
        max_length=2,
        default="",
        blank=True,
        help_text="Ride category classification",
    )
    manufacturer = models.ForeignKey(
        Company,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="manufactured_rides",
        limit_choices_to={"roles__contains": ["MANUFACTURER"]},
    )
    designer = models.ForeignKey(
        Company,
        on_delete=models.SET_NULL,
        related_name="designed_rides",
        null=True,
        blank=True,
        limit_choices_to={"roles__contains": ["DESIGNER"]},
    )
    ride_model = models.ForeignKey(
        "RideModel",
        on_delete=models.SET_NULL,
        related_name="rides",
        null=True,
        blank=True,
        help_text="The specific model/type of this ride",
    )
    status = RichFSMField(
        choice_group="statuses",
        domain="rides",
        max_length=20,
        default="OPERATING",
        help_text="Current operational status of the ride",
    )
    post_closing_status = RichChoiceField(
        choice_group="post_closing_statuses",
        domain="rides",
        max_length=20,
        null=True,
        blank=True,
        help_text="Status to change to after closing date",
    )
    opening_date = models.DateField(null=True, blank=True)
    opening_date_precision = models.CharField(
        max_length=20,
        choices=[
            ("exact", "Exact Date"),
            ("month", "Month and Year"),
            ("year", "Year Only"),
            ("decade", "Decade"),
            ("century", "Century"),
            ("approximate", "Approximate"),
        ],
        default="exact",
        blank=True,
        help_text="Precision of the opening date",
    )
    closing_date = models.DateField(null=True, blank=True)
    closing_date_precision = models.CharField(
        max_length=20,
        choices=[
            ("exact", "Exact Date"),
            ("month", "Month and Year"),
            ("year", "Year Only"),
            ("decade", "Decade"),
            ("century", "Century"),
            ("approximate", "Approximate"),
        ],
        default="exact",
        blank=True,
        help_text="Precision of the closing date",
    )
    status_since = models.DateField(null=True, blank=True)
    min_height_in = models.PositiveIntegerField(null=True, blank=True)
    max_height_in = models.PositiveIntegerField(null=True, blank=True)
    capacity_per_hour = models.PositiveIntegerField(null=True, blank=True)
    ride_duration_seconds = models.PositiveIntegerField(null=True, blank=True)
    average_rating = models.DecimalField(max_digits=3, decimal_places=2, null=True, blank=True)

    # Additional ride classification
    ride_sub_type = models.CharField(
        max_length=100,
        blank=True,
        help_text="Sub-category of ride (e.g., 'Flying Coaster', 'Inverted Coaster', 'Log Flume')",
    )
    age_requirement = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Minimum age requirement in years (if any)",
    )
    height_requirement_cm = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Minimum height requirement in centimeters",
    )
    duration_seconds = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Ride duration in seconds",
    )
    inversions_count = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Number of inversions (for coasters)",
    )

    # Computed fields for hybrid filtering
    opening_year = models.IntegerField(null=True, blank=True, db_index=True)
    search_text = models.TextField(blank=True, db_index=True)

    # ===== CATEGORY-SPECIFIC FIELDS =====
    # These fields support the frontend validation schemas in entityValidationSchemas.ts
    # Fields are nullable since they only apply to specific ride categories

    # --- Core Stats (6 fields) ---
    max_speed_kmh = models.DecimalField(
        max_digits=6,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum speed in kilometers per hour",
    )
    height_meters = models.DecimalField(
        max_digits=6,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Height of the ride structure in meters",
    )
    length_meters = models.DecimalField(
        max_digits=8,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Total track/ride length in meters",
    )
    drop_meters = models.DecimalField(
        max_digits=6,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum drop height in meters",
    )
    gforce_max = models.DecimalField(
        max_digits=4,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum G-force experienced",
    )
    intensity_level = models.CharField(
        max_length=20,
        blank=True,
        help_text="Intensity classification: family, thrill, or extreme",
    )

    # --- Coaster-Specific (5 fields) ---
    coaster_type = models.CharField(
        max_length=20,
        blank=True,
        help_text="Coaster structure type: steel, wood, or hybrid",
    )
    seating_type = models.CharField(
        max_length=20,
        blank=True,
        help_text="Seating configuration: sit_down, inverted, flying, stand_up, etc.",
    )
    track_material = ArrayField(
        models.CharField(max_length=50),
        blank=True,
        default=list,
        help_text="Track material types (e.g., ['steel', 'wood'])",
    )
    support_material = ArrayField(
        models.CharField(max_length=50),
        blank=True,
        default=list,
        help_text="Support structure material types",
    )
    propulsion_method = ArrayField(
        models.CharField(max_length=50),
        blank=True,
        default=list,
        help_text="Propulsion methods (e.g., ['chain_lift', 'lsm'])",
    )

    # --- Water Ride (5 fields) ---
    water_depth_cm = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Water depth in centimeters",
    )
    splash_height_meters = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum splash height in meters",
    )
    wetness_level = models.CharField(
        max_length=20,
        blank=True,
        help_text="Expected wetness: dry, light, moderate, or soaked",
    )
    flume_type = models.CharField(
        max_length=100,
        blank=True,
        help_text="Type of flume or water channel",
    )
    boat_capacity = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Number of passengers per boat/vehicle",
    )

    # --- Dark Ride (7 fields) ---
    theme_name = models.CharField(
        max_length=200,
        blank=True,
        help_text="Primary theme or IP name",
    )
    story_description = models.TextField(
        blank=True,
        help_text="Narrative or story description for the ride",
    )
    show_duration_seconds = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Duration of show elements in seconds",
    )
    animatronics_count = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Number of animatronic figures",
    )
    projection_type = models.CharField(
        max_length=100,
        blank=True,
        help_text="Type of projection technology used",
    )
    ride_system = models.CharField(
        max_length=100,
        blank=True,
        help_text="Ride system type (e.g., trackless, omnimover)",
    )
    scenes_count = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Number of distinct scenes or show sections",
    )

    # --- Flat Ride (7 fields) ---
    rotation_type = models.CharField(
        max_length=20,
        blank=True,
        help_text="Rotation axis: horizontal, vertical, multi_axis, pendulum, or none",
    )
    motion_pattern = models.CharField(
        max_length=200,
        blank=True,
        help_text="Description of the ride's motion pattern",
    )
    platform_count = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Number of ride platforms or gondolas",
    )
    swing_angle_degrees = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum swing angle in degrees",
    )
    rotation_speed_rpm = models.DecimalField(
        max_digits=6,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Rotation speed in revolutions per minute",
    )
    arm_length_meters = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Length of ride arm in meters",
    )
    max_height_reached_meters = models.DecimalField(
        max_digits=6,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum height reached during ride cycle in meters",
    )

    # --- Kiddie Ride (4 fields) ---
    min_age = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Minimum recommended age in years",
    )
    max_age = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Maximum recommended age in years",
    )
    educational_theme = models.CharField(
        max_length=200,
        blank=True,
        help_text="Educational or learning theme if applicable",
    )
    character_theme = models.CharField(
        max_length=200,
        blank=True,
        help_text="Character or IP theme (e.g., Paw Patrol, Sesame Street)",
    )

    # --- Transportation (6 fields) ---
    transport_type = models.CharField(
        max_length=20,
        blank=True,
        help_text="Transport mode: train, monorail, skylift, ferry, peoplemover, or cable_car",
    )
    route_length_meters = models.DecimalField(
        max_digits=8,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Total route length in meters",
    )
    stations_count = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Number of stations or stops",
    )
    vehicle_capacity = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Passenger capacity per vehicle",
    )
    vehicles_count = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Number of vehicles in operation",
    )
    round_trip_duration_seconds = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Duration of a complete round trip in seconds",
    )

    # Image settings - references to existing photos
    banner_image = models.ForeignKey(
        "RidePhoto",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="rides_using_as_banner",
        help_text="Photo to use as banner image for this ride",
    )
    card_image = models.ForeignKey(
        "RidePhoto",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="rides_using_as_card",
        help_text="Photo to use as card image for this ride",
    )

    # Frontend URL
    url = models.URLField(blank=True, help_text="Frontend URL for this ride")
    park_url = models.URLField(blank=True, help_text="Frontend URL for this ride's park")

    # Submission metadata fields (from frontend schema)
    source_url = models.URLField(
        blank=True,
        help_text="Source URL for the data (e.g., official website, RCDB)",
    )
    is_test_data = models.BooleanField(
        default=False,
        help_text="Whether this is test/development data",
    )

    class Meta(TrackedModel.Meta):
        verbose_name = "Ride"
        verbose_name_plural = "Rides"
        ordering = ["name"]
        unique_together = ["park", "slug"]
        constraints = [
            # Business rule: Closing date must be after opening date
            models.CheckConstraint(
                name="ride_closing_after_opening",
                condition=models.Q(closing_date__isnull=True)
                | models.Q(opening_date__isnull=True)
                | models.Q(closing_date__gte=models.F("opening_date")),
                violation_error_message="Closing date must be after opening date",
            ),
            # Business rule: Height requirements must be logical
            models.CheckConstraint(
                name="ride_height_requirements_logical",
                condition=models.Q(min_height_in__isnull=True)
                | models.Q(max_height_in__isnull=True)
                | models.Q(min_height_in__lte=models.F("max_height_in")),
                violation_error_message="Minimum height cannot exceed maximum height",
            ),
            # Business rule: Height requirements must be reasonable (between 30
            # and 90 inches)
            models.CheckConstraint(
                name="ride_min_height_reasonable",
                condition=models.Q(min_height_in__isnull=True)
                | (models.Q(min_height_in__gte=30) & models.Q(min_height_in__lte=90)),
                violation_error_message=("Minimum height must be between 30 and 90 inches"),
            ),
            models.CheckConstraint(
                name="ride_max_height_reasonable",
                condition=models.Q(max_height_in__isnull=True)
                | (models.Q(max_height_in__gte=30) & models.Q(max_height_in__lte=90)),
                violation_error_message=("Maximum height must be between 30 and 90 inches"),
            ),
            # Business rule: Rating must be between 1 and 10
            models.CheckConstraint(
                name="ride_rating_range",
                condition=models.Q(average_rating__isnull=True)
                | (models.Q(average_rating__gte=1) & models.Q(average_rating__lte=10)),
                violation_error_message="Average rating must be between 1 and 10",
            ),
            # Business rule: Capacity and duration must be positive
            models.CheckConstraint(
                name="ride_capacity_positive",
                condition=models.Q(capacity_per_hour__isnull=True) | models.Q(capacity_per_hour__gt=0),
                violation_error_message="Hourly capacity must be positive",
            ),
            models.CheckConstraint(
                name="ride_duration_positive",
                condition=models.Q(ride_duration_seconds__isnull=True) | models.Q(ride_duration_seconds__gt=0),
                violation_error_message="Ride duration must be positive",
            ),
        ]

    def __str__(self) -> str:
        return f"{self.name} at {self.park.name}"

    # FSM Transition Wrapper Methods
    def open(self, *, user: AbstractBaseUser | None = None) -> None:
        """Transition ride to OPERATING status."""
        self.transition_to_operating(user=user)
        self.save()

    def close_temporarily(self, *, user: AbstractBaseUser | None = None) -> None:
        """Transition ride to CLOSED_TEMP status."""
        self.transition_to_closed_temp(user=user)
        self.save()

    def mark_sbno(self, *, user: AbstractBaseUser | None = None) -> None:
        """Transition ride to SBNO (Standing But Not Operating) status."""
        self.transition_to_sbno(user=user)
        self.save()

    def mark_closing(
        self,
        *,
        closing_date,
        post_closing_status: str,
        user: AbstractBaseUser | None = None,
    ) -> None:
        """Transition ride to CLOSING status with closing date and target status."""
        from django.core.exceptions import ValidationError

        if not post_closing_status:
            raise ValidationError("post_closing_status must be set when entering CLOSING status")
        self.transition_to_closing(user=user)
        self.closing_date = closing_date
        self.post_closing_status = post_closing_status
        self.save()

    def close_permanently(self, *, user: AbstractBaseUser | None = None) -> None:
        """Transition ride to CLOSED_PERM status."""
        self.transition_to_closed_perm(user=user)
        self.save()

    def demolish(self, *, user: AbstractBaseUser | None = None) -> None:
        """Transition ride to DEMOLISHED status."""
        self.transition_to_demolished(user=user)
        self.save()

    def relocate(self, *, user: AbstractBaseUser | None = None) -> None:
        """Transition ride to RELOCATED status."""
        self.transition_to_relocated(user=user)
        self.save()

    def apply_post_closing_status(self, *, user: AbstractBaseUser | None = None) -> None:
        """Apply post_closing_status if closing_date has been reached."""
        from django.core.exceptions import ValidationError
        from django.utils import timezone

        if self.status != "CLOSING":
            raise ValidationError("Ride must be in CLOSING status")

        if not self.closing_date:
            raise ValidationError("closing_date must be set")

        if not self.post_closing_status:
            raise ValidationError("post_closing_status must be set")

        if timezone.now().date() < self.closing_date:
            return  # Not yet time to transition

        # Transition to the target status
        if self.post_closing_status == "SBNO":
            self.transition_to_sbno(user=user)
        elif self.post_closing_status == "CLOSED_PERM":
            self.transition_to_closed_perm(user=user)
        elif self.post_closing_status == "DEMOLISHED":
            self.transition_to_demolished(user=user)
        elif self.post_closing_status == "RELOCATED":
            self.transition_to_relocated(user=user)
        else:
            raise ValidationError(f"Invalid post_closing_status: {self.post_closing_status}")

        self.save()

    @property
    def is_closing(self) -> bool:
        """Returns True if this ride has a closing date in the future (announced closure)."""
        from django.utils import timezone
        if self.closing_date:
            return self.closing_date > timezone.now().date()
        return False

    def save(self, *args, **kwargs) -> None:
        # Handle slug generation and conflicts
        if not self.slug:
            self.slug = slugify(self.name)

        # Check for slug conflicts when park changes or slug is new
        original_ride = None
        if self.pk:
            with contextlib.suppress(Ride.DoesNotExist):
                original_ride = Ride.objects.get(pk=self.pk)

        # If park changed or this is a new ride, ensure slug uniqueness within the park
        park_changed = original_ride and original_ride.park.id != self.park.id
        if not self.pk or park_changed:
            self._ensure_unique_slug_in_park()

        # Handle park area validation when park changes
        if park_changed and self.park_area:  # noqa: SIM102
            # Check if park_area belongs to the new park
            if self.park_area.park.id != self.park.id:
                # Clear park_area if it doesn't belong to the new park
                self.park_area = None

        # Sync manufacturer with ride model's manufacturer
        if self.ride_model and self.ride_model.manufacturer:
            self.manufacturer = self.ride_model.manufacturer
        elif self.ride_model and not self.ride_model.manufacturer:
            # If ride model has no manufacturer, clear the ride's manufacturer
            # to maintain consistency
            self.manufacturer = None

        # Generate frontend URLs
        if self.park:
            frontend_domain = getattr(settings, "FRONTEND_DOMAIN", "https://thrillwiki.com")
            self.url = f"{frontend_domain}/parks/{self.park.slug}/rides/{self.slug}/"
            self.park_url = f"{frontend_domain}/parks/{self.park.slug}/"

        # Populate computed fields
        self._populate_computed_fields()

        super().save(*args, **kwargs)

    def _populate_computed_fields(self) -> None:
        """Populate computed fields for hybrid filtering."""
        # Extract opening year from opening_date
        if self.opening_date:
            self.opening_year = self.opening_date.year
        else:
            self.opening_year = None

        # Build comprehensive search text
        search_parts = []

        # Basic ride info
        if self.name:
            search_parts.append(self.name)
        if self.description:
            search_parts.append(self.description)

        # Park info
        if self.park:
            search_parts.append(self.park.name)
            if hasattr(self.park, "location") and self.park.location:
                if self.park.location.city:
                    search_parts.append(self.park.location.city)
                if self.park.location.state:
                    search_parts.append(self.park.location.state)
                if self.park.location.country:
                    search_parts.append(self.park.location.country)

        # Park area
        if self.park_area:
            search_parts.append(self.park_area.name)

        # Category
        if self.category:
            category_choice = self.get_category_rich_choice()
            if category_choice:
                search_parts.append(category_choice.label)

        # Status
        if self.status:
            status_choice = self.get_status_rich_choice()
            if status_choice:
                search_parts.append(status_choice.label)

        # Companies
        if self.manufacturer:
            search_parts.append(self.manufacturer.name)
        if self.designer:
            search_parts.append(self.designer.name)

        # Ride model
        if self.ride_model:
            search_parts.append(self.ride_model.name)
            if self.ride_model.manufacturer:
                search_parts.append(self.ride_model.manufacturer.name)

        # Roller coaster stats if available
        try:
            if hasattr(self, "coaster_stats") and self.coaster_stats:
                stats = self.coaster_stats
                if stats.track_type:
                    search_parts.append(stats.track_type)
                if stats.track_material:
                    material_choice = stats.get_track_material_rich_choice()
                    if material_choice:
                        search_parts.append(material_choice.label)
                if stats.roller_coaster_type:
                    type_choice = stats.get_roller_coaster_type_rich_choice()
                    if type_choice:
                        search_parts.append(type_choice.label)
                if stats.propulsion_system:
                    propulsion_choice = stats.get_propulsion_system_rich_choice()
                    if propulsion_choice:
                        search_parts.append(propulsion_choice.label)
                if stats.train_style:
                    search_parts.append(stats.train_style)
        except Exception:
            # Ignore if coaster_stats doesn't exist or has issues
            pass

        self.search_text = " ".join(filter(None, search_parts)).lower()

    def _ensure_unique_slug_in_park(self) -> None:
        """Ensure the ride's slug is unique within its park."""
        base_slug = slugify(self.name)
        self.slug = base_slug

        counter = 1
        while Ride.objects.filter(park=self.park, slug=self.slug).exclude(pk=self.pk).exists():
            self.slug = f"{base_slug}-{counter}"
            counter += 1

    def move_to_park(self, new_park, clear_park_area=True):
        """
        Move this ride to a different park with proper handling of related data.

        Args:
            new_park: The new Park instance to move the ride to
            clear_park_area: Whether to clear park_area (default True, since areas are park-specific)

        Returns:
            dict: Summary of changes made
        """

        old_park = self.park
        old_url = self.url
        old_park_area = self.park_area

        # Update park
        self.park = new_park

        # Handle park area
        if clear_park_area:
            self.park_area = None

        # Save will handle slug conflicts and URL updates
        self.save()

        # Return summary of changes
        changes = {
            "old_park": {"id": old_park.id, "name": old_park.name, "slug": old_park.slug},
            "new_park": {"id": new_park.id, "name": new_park.name, "slug": new_park.slug},
            "url_changed": old_url != self.url,
            "old_url": old_url,
            "new_url": self.url,
            "park_area_cleared": clear_park_area and old_park_area is not None,
            "old_park_area": {"id": old_park_area.id, "name": old_park_area.name} if old_park_area else None,
            "slug_changed": self.slug != slugify(self.name),
            "final_slug": self.slug,
        }

        return changes

    @classmethod
    def get_by_slug(cls, slug: str, park=None) -> tuple["Ride", bool]:
        """Get ride by current or historical slug, optionally within a specific park"""
        from django.contrib.contenttypes.models import ContentType

        from apps.core.history import HistoricalSlug

        # Build base query
        base_query = cls.objects
        if park:
            base_query = base_query.filter(park=park)

        try:
            ride = base_query.get(slug=slug)
            return ride, False
        except cls.DoesNotExist:
            # Try historical slugs in HistoricalSlug model
            content_type = ContentType.objects.get_for_model(cls)
            historical_query = HistoricalSlug.objects.filter(content_type=content_type, slug=slug).order_by(
                "-created_at"
            )

            for historical in historical_query:
                try:
                    ride = base_query.get(pk=historical.object_id)
                    return ride, True
                except cls.DoesNotExist:
                    continue

            # Try pghistory events
            event_model = getattr(cls, "event_model", None)
            if event_model:
                historical_events = event_model.objects.filter(slug=slug).order_by("-pgh_created_at")

                for historical_event in historical_events:
                    try:
                        ride = base_query.get(pk=historical_event.pgh_obj_id)
                        return ride, True
                    except cls.DoesNotExist:
                        continue

            raise cls.DoesNotExist("No ride found with this slug") from None


@pghistory.track()
class RollerCoasterStats(models.Model):
    """Model for tracking roller coaster specific statistics"""

    ride = models.OneToOneField(
        Ride,
        on_delete=models.CASCADE,
        related_name="coaster_stats",
        help_text="Ride these statistics belong to",
    )
    height_ft = models.DecimalField(
        max_digits=6,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum height in feet",
    )
    length_ft = models.DecimalField(
        max_digits=7,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Track length in feet",
    )
    speed_mph = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum speed in mph",
    )
    inversions = models.PositiveIntegerField(default=0, help_text="Number of inversions")
    ride_time_seconds = models.PositiveIntegerField(null=True, blank=True, help_text="Duration of the ride in seconds")
    track_type = models.CharField(max_length=255, blank=True, help_text="Type of track (e.g., tubular steel, wooden)")
    track_material = RichChoiceField(
        choice_group="track_materials",
        domain="rides",
        max_length=20,
        default="STEEL",
        blank=True,
        help_text="Track construction material type",
    )
    roller_coaster_type = RichChoiceField(
        choice_group="coaster_types",
        domain="rides",
        max_length=20,
        default="SITDOWN",
        blank=True,
        help_text="Roller coaster type classification",
    )
    max_drop_height_ft = models.DecimalField(
        max_digits=6,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum drop height in feet",
    )
    propulsion_system = RichChoiceField(
        choice_group="propulsion_systems",
        domain="rides",
        max_length=20,
        default="CHAIN",
        help_text="Propulsion or lift system type",
    )
    train_style = models.CharField(max_length=255, blank=True, help_text="Style of train (e.g., floorless, inverted)")
    trains_count = models.PositiveIntegerField(null=True, blank=True, help_text="Number of trains")
    cars_per_train = models.PositiveIntegerField(null=True, blank=True, help_text="Number of cars per train")
    seats_per_car = models.PositiveIntegerField(null=True, blank=True, help_text="Number of seats per car")

    class Meta:
        verbose_name = "Roller Coaster Statistics"
        verbose_name_plural = "Roller Coaster Statistics"
        ordering = ["ride"]

    def __str__(self) -> str:
        return f"Stats for {self.ride.name}"
