from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.validators import MinValueValidator
from django.utils import timezone
from decimal import Decimal
from django.core.exceptions import ValidationError
import json


class Location(models.Model):
    """
    Business location for multi-location support.

    Invariants:
    - Each tenant must have at least one active location (enforced at API layer)
    - Only one location can be is_primary=True per tenant (DB constraint)
    """
    business = models.ForeignKey(
        "core.Tenant",
        on_delete=models.CASCADE,
        related_name="locations",
    )

    name = models.CharField(max_length=200)
    address_line1 = models.CharField(max_length=255, blank=True)
    address_line2 = models.CharField(max_length=255, blank=True)
    city = models.CharField(max_length=100, blank=True)
    state = models.CharField(max_length=100, blank=True)
    postal_code = models.CharField(max_length=20, blank=True)
    country = models.CharField(max_length=100, default='US')
    phone = models.CharField(max_length=20, blank=True)
    email = models.EmailField(blank=True)
    timezone = models.CharField(max_length=50, blank=True)  # Override business TZ
    is_active = models.BooleanField(default=True)
    is_primary = models.BooleanField(default=False)
    display_order = models.PositiveIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        app_label = 'schedule'
        ordering = ['display_order', 'name']
        indexes = [
            models.Index(fields=['business', 'is_active']),
            models.Index(fields=['business', 'display_order']),
        ]
        constraints = [
            models.UniqueConstraint(
                fields=['business'],
                condition=models.Q(is_primary=True),
                name='unique_primary_location_per_business'
            )
        ]

    def __str__(self):
        return self.name


class Service(models.Model):
    """
    A service offered by the business (e.g., Haircut, Massage, Consultation).
    """
    name = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    duration = models.PositiveIntegerField(
        help_text="Duration in minutes",
        default=60
    )
    price_cents = models.IntegerField(
        default=0,
        help_text="Price in cents (e.g., 1000 = $10.00)"
    )
    display_order = models.PositiveIntegerField(
        default=0,
        help_text="Order in which services appear in menus (lower = first)"
    )
    photos = models.JSONField(
        default=list,
        blank=True,
        help_text="List of photo URLs in display order"
    )
    is_active = models.BooleanField(default=True)

    # Variable pricing - price determined after service completion
    variable_pricing = models.BooleanField(
        default=False,
        help_text="If true, final price is determined after service completion"
    )
    # Deposit configuration
    # - For fixed pricing: can use either deposit_amount OR deposit_percent
    # - For variable pricing: can only use deposit_amount (percent doesn't make sense)
    deposit_amount_cents = models.IntegerField(
        null=True,
        blank=True,
        validators=[MinValueValidator(0)],
        help_text="Fixed deposit amount in cents (e.g., 500 = $5.00)"
    )
    deposit_percent = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        null=True,
        blank=True,
        validators=[MinValueValidator(Decimal('0')), ],
        help_text="Deposit as percentage of price (0-100), only for fixed pricing"
    )

    # Resource assignment
    all_resources = models.BooleanField(
        default=True,
        help_text="If true, service can be performed by any resource. If false, use resource_ids field."
    )
    resource_ids = models.JSONField(
        default=list,
        blank=True,
        help_text="List of specific resource IDs that can perform this service (only used if all_resources=False)"
    )

    # Location assignment
    is_global = models.BooleanField(
        default=True,
        help_text="If True, service available at all locations. If False, use locations M2M."
    )
    locations = models.ManyToManyField(
        'Location',
        blank=True,
        related_name='services',
        help_text="Locations where this service is offered (only used if is_global=False)"
    )

    # Buffer times
    prep_time = models.PositiveIntegerField(
        default=0,
        help_text="Preparation time in minutes before the service"
    )
    takedown_time = models.PositiveIntegerField(
        default=0,
        help_text="Cleanup/takedown time in minutes after the service"
    )

    # Notification settings
    reminder_enabled = models.BooleanField(
        default=False,
        help_text="Whether to send reminder notifications for this service"
    )
    reminder_hours_before = models.PositiveIntegerField(
        default=24,
        help_text="Hours before appointment to send reminder"
    )
    reminder_email = models.BooleanField(
        default=True,
        help_text="Send reminder via email"
    )
    reminder_sms = models.BooleanField(
        default=False,
        help_text="Send reminder via SMS"
    )
    thank_you_email_enabled = models.BooleanField(
        default=False,
        help_text="Send thank you email after service completion"
    )

    # Quota overage archiving
    is_archived_by_quota = models.BooleanField(
        default=False,
        help_text="True if this service was archived due to quota overage"
    )
    archived_by_quota_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text="When this service was archived due to quota overage"
    )

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        app_label = 'schedule'
        ordering = ['display_order', 'name']
        indexes = [models.Index(fields=['is_active', 'display_order'])]

    def __str__(self):
        price_dollars = self.price_cents / 100 if self.price_cents else 0
        return f"{self.name} ({self.duration} min - ${price_dollars:.2f})"

    @property
    def price(self):
        """Return price as Decimal dollars (for backward compatibility and display)"""
        return Decimal(self.price_cents) / 100 if self.price_cents else Decimal('0.00')

    @property
    def deposit_amount(self):
        """Return deposit amount as Decimal dollars (for backward compatibility and display)"""
        if self.deposit_amount_cents is not None:
            return Decimal(self.deposit_amount_cents) / 100
        return None

    @property
    def requires_deposit(self):
        """Check if this service requires a deposit"""
        has_amount = self.deposit_amount_cents and self.deposit_amount_cents > 0
        has_percent = self.deposit_percent and self.deposit_percent > 0
        return has_amount or has_percent

    @property
    def requires_saved_payment_method(self):
        """Customer must have a saved payment method if deposit > 0 or variable pricing"""
        return self.requires_deposit or self.variable_pricing

    def get_deposit_amount(self, price=None):
        """
        Calculate the actual deposit amount.

        Args:
            price: Price to use for percentage calculation (defaults to self.price)

        Returns:
            Decimal: The deposit amount, or None if no deposit required.
        """
        if self.deposit_amount and self.deposit_amount > 0:
            return self.deposit_amount
        if self.deposit_percent and self.deposit_percent > 0:
            base_price = price if price is not None else self.price
            return (base_price * self.deposit_percent / 100).quantize(Decimal('0.01'))
        return None

    def is_available_at_location(self, location):
        """Check if service is available at given location."""
        if self.is_global:
            return True
        return self.locations.filter(pk=location.pk).exists()


class ResourceType(models.Model):
    """
    Custom resource type definitions (e.g., "Hair Stylist", "Massage Room").
    Businesses can create their own types instead of using hardcoded STAFF/ROOM/EQUIPMENT.
    """
    class Category(models.TextChoices):
        STAFF = 'STAFF', 'Staff'  # Requires staff member assignment
        OTHER = 'OTHER', 'Other'  # No staff assignment needed

    name = models.CharField(max_length=100, help_text="User-facing name like 'Stylist' or 'Treatment Room'")
    description = models.TextField(blank=True, help_text="Description of this resource type")
    category = models.CharField(
        max_length=10,
        choices=Category.choices,
        default=Category.OTHER,
        help_text="STAFF types require staff assignment, OTHER types do not"
    )
    is_default = models.BooleanField(
        default=False,
        help_text="Default types cannot be deleted (e.g., 'Staff')"
    )
    icon_name = models.CharField(max_length=50, blank=True, help_text="Optional icon identifier")
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        app_label = 'schedule'
        ordering = ['name']
        indexes = [models.Index(fields=['category', 'name'])]

    def __str__(self):
        return f"{self.name} ({self.get_category_display()})"

    def clean(self):
        """Prevent deletion of default types"""
        if self.is_default and self.pk:
            # Check if being deleted
            if not ResourceType.objects.filter(pk=self.pk).exists():
                raise ValidationError("Cannot delete default resource types.")

    def delete(self, *args, **kwargs):
        """Prevent deletion of default types and types in use"""
        if self.is_default:
            raise ValidationError("Cannot delete default resource types.")
        if self.resources.exists():
            raise ValidationError(f"Cannot delete resource type '{self.name}' because it is in use by {self.resources.count()} resource(s).")
        super().delete(*args, **kwargs)


class Resource(models.Model):
    """
    A bookable resource with configurable concurrency.

    Concurrency Modes:
    - max_concurrent_events = 1: Strict blocking (Dentist Chair, Private Room)
    - max_concurrent_events > 1: Limited overlap (Waiting Room with N seats)
    - max_concurrent_events = 0: Infinite capacity (Virtual Resource/Category)
    """
    class Type(models.TextChoices):
        STAFF = 'STAFF', 'Staff Member'
        ROOM = 'ROOM', 'Room'
        EQUIPMENT = 'EQUIPMENT', 'Equipment'

    name = models.CharField(max_length=200)

    # NEW: Custom resource type (preferred)
    resource_type = models.ForeignKey(
        ResourceType,
        on_delete=models.PROTECT,  # Cannot delete type if resources use it
        related_name='resources',
        null=True,  # For migration compatibility
        blank=True,
        help_text="Custom resource type definition"
    )

    # LEGACY: Hardcoded type (deprecated, kept for backwards compatibility)
    type = models.CharField(
        max_length=20,
        choices=Type.choices,
        default=Type.STAFF,
        help_text="DEPRECATED: Use resource_type instead"
    )

    user = models.ForeignKey(
        'users.User',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='staff_resources',
        help_text="Link to User account for STAFF type resources"
    )

    # Location assignment
    location = models.ForeignKey(
        'Location',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='resources',
        help_text="Fixed location (null if mobile resource)"
    )
    is_mobile = models.BooleanField(
        default=False,
        help_text="If True, resource can work across locations (e.g., plumber, mobile mechanic)"
    )

    description = models.TextField(blank=True)
    max_concurrent_events = models.PositiveIntegerField(
        default=1,
        validators=[MinValueValidator(0)],
        help_text="0=unlimited, 1=exclusive, >1=limited overlap"
    )
    buffer_duration = models.DurationField(
        default=timezone.timedelta(0),
        help_text="Time buffer before/after events. Buffers consume capacity."
    )
    saved_lane_count = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Remembered lane count when multilane is disabled"
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    is_active = models.BooleanField(default=True)

    # Quota overage archiving
    is_archived_by_quota = models.BooleanField(
        default=False,
        help_text="True if this resource was archived due to quota overage"
    )
    archived_by_quota_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text="When this resource was archived due to quota overage"
    )

    # Permission for linked user to edit their own schedule
    user_can_edit_schedule = models.BooleanField(
        default=False,
        help_text="Allow the resource's linked user to edit their own schedule (reschedule/resize) regardless of role"
    )

    class Meta:
        app_label = 'schedule'
        ordering = ['name']
        indexes = [models.Index(fields=['is_active', 'name'])]

    def _is_staff_type(self):
        """Check if this resource is a staff type (requires user assignment)."""
        # Check new resource_type FK first
        if self.resource_type:
            return self.resource_type.category == ResourceType.Category.STAFF
        # Fall back to legacy type field
        return self.type == self.Type.STAFF

    def clean(self):
        """
        Model-level validation ensuring staff resources have assigned users.
        """
        super().clean()
        if self._is_staff_type() and not self.user_id:
            raise ValidationError({
                'user': 'Staff type resources must be assigned to a staff member.'
            })

    def save(self, *args, **kwargs):
        """Run clean() on save to enforce validation."""
        self.clean()
        super().save(*args, **kwargs)

    def __str__(self):
        cap = "Unlimited" if self.max_concurrent_events == 0 else f"{self.max_concurrent_events} concurrent"
        return f"{self.name} ({cap})"


class Event(models.Model):
    """
    A scheduled event. Links to Resources/Staff/Customers via Participant model.
    Can also have attached plugins for automation.
    """
    class Status(models.TextChoices):
        SCHEDULED = 'SCHEDULED', 'Scheduled'
        EN_ROUTE = 'EN_ROUTE', 'En Route'  # Employee is traveling to job site
        IN_PROGRESS = 'IN_PROGRESS', 'In Progress'  # Employee is working on job
        CANCELED = 'CANCELED', 'Canceled'
        COMPLETED = 'COMPLETED', 'Completed'
        AWAITING_PAYMENT = 'AWAITING_PAYMENT', 'Awaiting Payment'  # Service done, waiting for final charge
        PAID = 'PAID', 'Paid'
        NOSHOW = 'NOSHOW', 'No Show'

    title = models.CharField(max_length=200)
    start_time = models.DateTimeField(db_index=True)
    end_time = models.DateTimeField(db_index=True)
    status = models.CharField(max_length=20, choices=Status.choices, default=Status.SCHEDULED, db_index=True)
    notes = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    created_by = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='created_events')

    # Service linkage for pricing
    service = models.ForeignKey(
        Service,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='events',
        help_text="The service being provided in this appointment"
    )

    # Location where event takes place
    location = models.ForeignKey(
        'Location',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='events',
        help_text="Location where event takes place"
    )

    # Pricing fields for variable-price services
    deposit_amount = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Deposit amount collected at booking"
    )
    deposit_transaction_id = models.CharField(
        max_length=255,
        blank=True,
        help_text="Stripe PaymentIntent ID for the deposit"
    )
    final_price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Final price entered after service completion (for variable pricing)"
    )
    final_charge_transaction_id = models.CharField(
        max_length=255,
        blank=True,
        help_text="Stripe PaymentIntent ID for the final charge"
    )

    # Plugin attachments for resource-free scheduling
    plugins = models.ManyToManyField(
        'PluginInstallation',
        through='EventPlugin',
        related_name='events',
        blank=True,
        help_text="Plugins attached to this event for automation"
    )
    
    class Meta:
        app_label = 'schedule'
        ordering = ['start_time']
        indexes = [
            models.Index(fields=['start_time', 'end_time']),
            models.Index(fields=['status', 'start_time']),
        ]
    
    def __str__(self):
        return f"{self.title} ({self.start_time.strftime('%Y-%m-%d %H:%M')})"
    
    @property
    def duration(self):
        """Calculate event duration"""
        return self.end_time - self.start_time

    @property
    def is_variable_pricing(self):
        """Check if this event uses variable pricing"""
        return self.service and self.service.variable_pricing

    @property
    def remaining_balance(self):
        """
        Calculate the remaining balance after deposit.
        Returns None if final_price is not set.
        """
        if self.final_price is None:
            return None
        deposit = self.deposit_amount or Decimal('0.00')
        return max(self.final_price - deposit, Decimal('0.00'))

    @property
    def overpaid_amount(self):
        """
        Calculate overpayment if deposit exceeds final price.
        Returns amount to refund, or None if not applicable.
        """
        if self.final_price is None or self.deposit_amount is None:
            return None
        if self.deposit_amount > self.final_price:
            return self.deposit_amount - self.final_price
        return None

    def execute_plugins(self, trigger='event_created'):
        """
        Execute all attached plugins for this event.

        Args:
            trigger: What triggered the execution ('event_created', 'event_completed', etc.)

        Returns:
            List of execution results
        """
        from .safe_scripting import SafeScriptRunner, SafeScriptAPI
        from .template_parser import TemplateVariableParser

        results = []
        for event_plugin in self.eventplugin_set.filter(trigger=trigger, is_active=True):
            installation = event_plugin.plugin_installation
            template = installation.template

            if not template:
                continue

            try:
                # Compile template with config values
                compiled_code = TemplateVariableParser.compile_template(
                    template.plugin_code,
                    installation.config_values
                )

                # Execute plugin
                runner = SafeScriptRunner()
                api = SafeScriptAPI(business=None)  # TODO: Get business from tenant

                # Add event context to API
                api._event_context = {
                    'event_id': self.id,
                    'event_title': self.title,
                    'start_time': self.start_time.isoformat(),
                    'end_time': self.end_time.isoformat(),
                    'status': self.status,
                }

                result = runner.execute(compiled_code, api)
                results.append({
                    'plugin': template.name,
                    'success': result['success'],
                    'output': result.get('output'),
                    'error': result.get('error'),
                })

            except Exception as e:
                results.append({
                    'plugin': template.name if template else 'Unknown',
                    'success': False,
                    'error': str(e),
                })

        return results


class EventPlugin(models.Model):
    """
    Through model for Event-Plugin relationship.
    Allows configuring when and how a plugin runs for an event.

    Timing works as follows:
    - BEFORE_START with offset_minutes=10 means "10 minutes before event starts"
    - AT_START with offset_minutes=0 means "when event starts"
    - AFTER_START with offset_minutes=15 means "15 minutes after event starts"
    - AFTER_END with offset_minutes=0 means "when event ends"
    - ON_COMPLETE means "when status changes to completed"
    - ON_CANCEL means "when status changes to canceled"
    """
    class Trigger(models.TextChoices):
        BEFORE_START = 'before_start', 'Before Start'
        AT_START = 'at_start', 'At Start'
        AFTER_START = 'after_start', 'After Start'
        AFTER_END = 'after_end', 'After End'
        ON_COMPLETE = 'on_complete', 'When Completed'
        ON_CANCEL = 'on_cancel', 'When Canceled'

    event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='event_plugins')
    plugin_installation = models.ForeignKey('PluginInstallation', on_delete=models.CASCADE)

    trigger = models.CharField(
        max_length=20,
        choices=Trigger.choices,
        default=Trigger.AT_START,
        help_text="When this plugin should execute"
    )

    offset_minutes = models.PositiveIntegerField(
        default=0,
        help_text="Minutes offset from trigger time (e.g., 10 = '10 minutes before/after')"
    )

    is_active = models.BooleanField(
        default=True,
        help_text="Whether this plugin should run for this event"
    )

    execution_order = models.PositiveSmallIntegerField(
        default=0,
        help_text="Order of execution (lower numbers run first)"
    )

    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        app_label = 'schedule'
        ordering = ['execution_order', 'created_at']
        # Allow same plugin with different triggers, but not duplicate trigger+offset
        unique_together = ['event', 'plugin_installation', 'trigger', 'offset_minutes']

    def __str__(self):
        plugin_name = self.plugin_installation.template.name if self.plugin_installation.template else 'Unknown'
        offset_str = f" (+{self.offset_minutes}m)" if self.offset_minutes else ""
        return f"{self.event.title} - {plugin_name} ({self.get_trigger_display()}{offset_str})"

    def get_execution_time(self):
        """Calculate the actual execution time based on trigger and offset"""
        from datetime import timedelta

        if self.trigger == self.Trigger.BEFORE_START:
            return self.event.start_time - timedelta(minutes=self.offset_minutes)
        elif self.trigger == self.Trigger.AT_START:
            return self.event.start_time + timedelta(minutes=self.offset_minutes)
        elif self.trigger == self.Trigger.AFTER_START:
            return self.event.start_time + timedelta(minutes=self.offset_minutes)
        elif self.trigger == self.Trigger.AFTER_END:
            return self.event.end_time + timedelta(minutes=self.offset_minutes)
        else:
            # ON_COMPLETE and ON_CANCEL are event-driven, not time-driven
            return None


class GlobalEventPlugin(models.Model):
    """
    Defines a rule for automatically attaching a plugin to ALL events.

    When created, this rule:
    1. Attaches the plugin to all existing events
    2. Automatically attaches to new events as they are created

    The actual EventPlugin instances are created/managed by signals.
    """
    plugin_installation = models.ForeignKey(
        'PluginInstallation',
        on_delete=models.CASCADE,
        related_name='global_event_rules'
    )

    trigger = models.CharField(
        max_length=20,
        choices=EventPlugin.Trigger.choices,
        default=EventPlugin.Trigger.AT_START,
        help_text="When this plugin should execute for each event"
    )

    offset_minutes = models.PositiveIntegerField(
        default=0,
        help_text="Minutes offset from trigger time"
    )

    is_active = models.BooleanField(
        default=True,
        help_text="Whether this rule is active"
    )

    apply_to_existing = models.BooleanField(
        default=True,
        help_text="Whether to apply this rule to existing events when created"
    )

    execution_order = models.PositiveSmallIntegerField(
        default=0,
        help_text="Order of execution (lower numbers run first)"
    )

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    created_by = models.ForeignKey(
        'users.User',
        on_delete=models.SET_NULL,
        null=True,
        related_name='created_global_event_plugins'
    )

    class Meta:
        app_label = 'schedule'
        ordering = ['execution_order', 'created_at']
        # Prevent duplicate rules
        unique_together = ['plugin_installation', 'trigger', 'offset_minutes']

    def __str__(self):
        plugin_name = self.plugin_installation.template.name if self.plugin_installation.template else 'Unknown'
        offset_str = f" (+{self.offset_minutes}m)" if self.offset_minutes else ""
        return f"Global: {plugin_name} ({self.get_trigger_display()}{offset_str})"

    def apply_to_event(self, event):
        """
        Apply this global rule to a specific event by creating an EventPlugin.
        Returns the created EventPlugin or None if it already exists.
        """
        event_plugin, created = EventPlugin.objects.get_or_create(
            event=event,
            plugin_installation=self.plugin_installation,
            trigger=self.trigger,
            offset_minutes=self.offset_minutes,
            defaults={
                'is_active': self.is_active,
                'execution_order': self.execution_order,
            }
        )
        return event_plugin if created else None

    def apply_to_all_events(self):
        """
        Apply this global rule to all existing events.
        Called when the GlobalEventPlugin is created.
        """
        from django.db.models import Q

        # Get all events that don't already have this plugin with same trigger/offset
        existing_combinations = EventPlugin.objects.filter(
            plugin_installation=self.plugin_installation,
            trigger=self.trigger,
            offset_minutes=self.offset_minutes,
        ).values_list('event_id', flat=True)

        events = Event.objects.exclude(id__in=existing_combinations)

        created_count = 0
        for event in events:
            if self.apply_to_event(event):
                created_count += 1

        return created_count


class Participant(models.Model):
    """
    Links Events to Resources, Staff, and Customers via GenericForeignKey.
    """
    class Role(models.TextChoices):
        RESOURCE = 'RESOURCE', 'Resource'
        STAFF = 'STAFF', 'Staff Member'
        CUSTOMER = 'CUSTOMER', 'Customer'
        OBSERVER = 'OBSERVER', 'Observer'
    
    event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='participants')
    role = models.CharField(max_length=20, choices=Role.choices, default=Role.CUSTOMER)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        app_label = 'schedule'
        unique_together = ['event', 'content_type', 'object_id']
        indexes = [
            models.Index(fields=['event', 'role']),
            models.Index(fields=['content_type', 'object_id']),
        ]
    
    def __str__(self):
        return f"{self.event.title} - {self.role}: {self.content_object}"


class ScheduledTask(models.Model):
    """
    Automated task that runs on a schedule without requiring resource allocation.

    Unlike Events which require resources and are customer-facing, ScheduledTasks
    are internal automated processes (e.g., sending reports, cleanup jobs, webhooks).
    """

    class Status(models.TextChoices):
        ACTIVE = 'ACTIVE', 'Active'
        PAUSED = 'PAUSED', 'Paused'
        DISABLED = 'DISABLED', 'Disabled'

    class ScheduleType(models.TextChoices):
        CRON = 'CRON', 'Cron Expression'
        INTERVAL = 'INTERVAL', 'Fixed Interval'
        ONE_TIME = 'ONE_TIME', 'One-Time'

    name = models.CharField(
        max_length=200,
        help_text="Human-readable name for this scheduled task"
    )
    description = models.TextField(
        blank=True,
        help_text="What this task does"
    )
    plugin_name = models.CharField(
        max_length=100,
        help_text="Name of the plugin to execute",
        db_index=True,
    )
    plugin_code = models.TextField(
        blank=True,
        help_text="Custom plugin code (for custom scripts)"
    )
    plugin_config = models.JSONField(
        default=dict,
        blank=True,
        help_text="Configuration dictionary for the plugin"
    )

    schedule_type = models.CharField(
        max_length=20,
        choices=ScheduleType.choices,
        default=ScheduleType.INTERVAL,
    )

    cron_expression = models.CharField(
        max_length=100,
        blank=True,
        help_text="Cron expression (e.g., '0 0 * * *' for daily at midnight)"
    )

    interval_minutes = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Run every N minutes (for INTERVAL schedule type)"
    )

    run_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text="Specific datetime to run (for ONE_TIME schedule type)"
    )

    status = models.CharField(
        max_length=20,
        choices=Status.choices,
        default=Status.ACTIVE,
        db_index=True,
    )

    last_run_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text="When this task last executed"
    )
    last_run_status = models.CharField(
        max_length=20,
        blank=True,
        help_text="Status of last execution (success/failed)"
    )
    last_run_result = models.JSONField(
        null=True,
        blank=True,
        help_text="Result data from last execution"
    )

    next_run_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text="When this task will next execute",
        db_index=True,
    )

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    created_by = models.ForeignKey(
        'users.User',
        on_delete=models.SET_NULL,
        null=True,
        related_name='created_scheduled_tasks'
    )

    celery_task_id = models.CharField(
        max_length=255,
        blank=True,
        help_text="ID of the associated Celery periodic task"
    )

    class Meta:
        app_label = 'schedule'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['status', 'next_run_at']),
            models.Index(fields=['plugin_name', 'status']),
        ]

    def __str__(self):
        return f"{self.name} ({self.plugin_name})"

    def clean(self):
        """Validate schedule configuration"""
        if self.schedule_type == self.ScheduleType.CRON and not self.cron_expression:
            raise ValidationError("Cron expression is required for CRON schedule type")

        if self.schedule_type == self.ScheduleType.INTERVAL and not self.interval_minutes:
            raise ValidationError("Interval minutes is required for INTERVAL schedule type")

        if self.schedule_type == self.ScheduleType.ONE_TIME and not self.run_at:
            raise ValidationError("Run at datetime is required for ONE_TIME schedule type")

    def get_plugin_instance(self):
        """Get configured plugin instance for this task"""
        from smoothschedule.scheduling.automations.registry import registry
        return registry.get_instance(self.plugin_name, self.plugin_config)

    def update_next_run_time(self):
        """Calculate and update next run time based on schedule"""
        from datetime import timedelta

        if self.schedule_type == self.ScheduleType.ONE_TIME:
            self.next_run_at = self.run_at
        elif self.schedule_type == self.ScheduleType.INTERVAL:
            if self.last_run_at:
                self.next_run_at = self.last_run_at + timedelta(minutes=self.interval_minutes)
            else:
                self.next_run_at = timezone.now() + timedelta(minutes=self.interval_minutes)
        elif self.schedule_type == self.ScheduleType.CRON:
            from django_celery_beat.schedulers import crontab_parser
            try:
                cron = crontab_parser(self.cron_expression)
                now = timezone.now()
                self.next_run_at = cron.next(now)
            except Exception:
                self.next_run_at = None

        self.save(update_fields=['next_run_at'])


class TaskExecutionLog(models.Model):
    """
    Log of scheduled task executions.
    """

    class Status(models.TextChoices):
        SUCCESS = 'SUCCESS', 'Success'
        FAILED = 'FAILED', 'Failed'
        SKIPPED = 'SKIPPED', 'Skipped'

    scheduled_task = models.ForeignKey(
        ScheduledTask,
        on_delete=models.CASCADE,
        related_name='execution_logs'
    )

    started_at = models.DateTimeField(auto_now_add=True, db_index=True)
    completed_at = models.DateTimeField(null=True, blank=True)

    status = models.CharField(
        max_length=20,
        choices=Status.choices,
        db_index=True,
    )

    result = models.JSONField(
        null=True,
        blank=True,
        help_text="Result data returned by the plugin"
    )

    error_message = models.TextField(
        blank=True,
        help_text="Error message if execution failed"
    )

    execution_time_ms = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="How long the execution took in milliseconds"
    )

    class Meta:
        app_label = 'schedule'
        ordering = ['-started_at']
        indexes = [
            models.Index(fields=['scheduled_task', '-started_at']),
            models.Index(fields=['status', '-started_at']),
        ]

    def __str__(self):
        return f"{self.scheduled_task.name} - {self.status} at {self.started_at}"


class WhitelistedURL(models.Model):
    """
    URL whitelist for plugin HTTP access.

    Supports two scopes:
    - Platform-wide: accessible by all plugins (approved_by platform user with can_whitelist_urls permission)
    - Plugin-specific: accessible only by specific plugin
    """

    class Scope(models.TextChoices):
        PLATFORM = 'PLATFORM', 'Platform-wide (all plugins)'
        PLUGIN = 'PLUGIN', 'Plugin-specific'

    # URL Configuration
    url_pattern = models.CharField(
        max_length=500,
        help_text="URL or URL pattern (e.g., https://api.example.com/v1/* or https://hooks.slack.com/*)"
    )

    domain = models.CharField(
        max_length=255,
        db_index=True,
        help_text="Extracted domain for quick lookup (e.g., api.example.com)"
    )

    # Scope & Ownership
    scope = models.CharField(
        max_length=10,
        choices=Scope.choices,
        default=Scope.PLUGIN,
        db_index=True
    )

    scheduled_task = models.ForeignKey(
        'ScheduledTask',
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='whitelisted_urls',
        help_text="Scheduled task (plugin) that owns this whitelist entry (null for platform-wide)"
    )

    # HTTP Methods
    allowed_methods = models.JSONField(
        default=list,
        help_text="List of allowed HTTP methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']"
    )

    # Metadata
    description = models.TextField(
        help_text="Why this URL is whitelisted and what it's used for"
    )

    approved_by = models.ForeignKey(
        'users.User',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='approved_whitelisted_urls',
        help_text="Platform user who approved this whitelist entry"
    )

    approved_at = models.DateTimeField(null=True, blank=True)

    is_active = models.BooleanField(
        default=True,
        db_index=True,
        help_text="Whether this whitelist entry is currently active"
    )

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    # Security metadata
    original_plugin_code = models.TextField(
        blank=True,
        help_text="Original plugin code submitted for approval (for verification)"
    )

    class Meta:
        app_label = 'schedule'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['domain', 'is_active']),
            models.Index(fields=['scope', 'is_active']),
            models.Index(fields=['scheduled_task', 'is_active']),
        ]
        constraints = [
            models.CheckConstraint(
                check=models.Q(scope='PLATFORM', scheduled_task__isnull=True) | models.Q(scope='PLUGIN', scheduled_task__isnull=False),
                name='platform_scope_no_task'
            )
        ]

    def __str__(self):
        scope_label = f"{self.get_scope_display()}"
        methods = ', '.join(self.allowed_methods) if self.allowed_methods else 'No methods'
        return f"{self.url_pattern} ({scope_label}) - {methods}"

    def save(self, *args, **kwargs):
        """Extract domain from URL pattern"""
        from urllib.parse import urlparse

        # Extract domain from URL pattern
        if not self.domain:
            # Remove wildcard for parsing
            url_for_parsing = self.url_pattern.replace('*', '')
            parsed = urlparse(url_for_parsing)
            self.domain = parsed.hostname or ''

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

    def matches_url(self, url: str) -> bool:
        """
        Check if this whitelist entry matches the given URL.

        Supports wildcard patterns:
        - https://api.example.com/* matches all paths under /
        - https://api.example.com/v1/* matches all paths under /v1/
        """
        # Simple implementation: check if URL starts with pattern (minus wildcard)
        pattern = self.url_pattern.replace('*', '')
        return url.startswith(pattern)

    def allows_method(self, method: str) -> bool:
        """Check if this whitelist entry allows the given HTTP method"""
        return method.upper() in [m.upper() for m in self.allowed_methods]

    @classmethod
    def is_url_whitelisted(cls, url: str, method: str, scheduled_task=None) -> bool:
        """
        Check if a URL and HTTP method combination is whitelisted.

        Args:
            url: The URL to check
            method: HTTP method (GET, POST, etc.)
            scheduled_task: Optional ScheduledTask (plugin) to check task-specific whitelist

        Returns:
            True if URL is whitelisted for the given method
        """
        from urllib.parse import urlparse

        parsed = urlparse(url)
        domain = parsed.hostname

        if not domain:
            return False

        # Check platform-wide whitelist
        platform_entries = cls.objects.filter(
            domain=domain,
            scope=cls.Scope.PLATFORM,
            is_active=True
        )

        for entry in platform_entries:
            if entry.matches_url(url) and entry.allows_method(method):
                return True

        # Check task-specific whitelist if scheduled_task provided
        if scheduled_task:
            task_entries = cls.objects.filter(
                domain=domain,
                scope=cls.Scope.PLUGIN,
                scheduled_task=scheduled_task,
                is_active=True
            )

            for entry in task_entries:
                if entry.matches_url(url) and entry.allows_method(method):
                    return True

        return False


class PluginTemplate(models.Model):
    """
    Shareable plugin template in the marketplace.

    Represents a plugin that can be shared across businesses, either:
    - Platform-published: Created by platform team
    - Community-shared: Created by users and approved for sharing
    """

    class Visibility(models.TextChoices):
        PRIVATE = 'PRIVATE', 'Private (only me)'
        PUBLIC = 'PUBLIC', 'Public (marketplace)'
        PLATFORM = 'PLATFORM', 'Platform Official'

    class Category(models.TextChoices):
        EMAIL = 'EMAIL', 'Email & Notifications'
        REPORTS = 'REPORTS', 'Reports & Analytics'
        CUSTOMER = 'CUSTOMER', 'Customer Engagement'
        BOOKING = 'BOOKING', 'Booking & Scheduling'
        INTEGRATION = 'INTEGRATION', 'Third-party Integration'
        AUTOMATION = 'AUTOMATION', 'General Automation'
        OTHER = 'OTHER', 'Other'

    # Basic Info
    name = models.CharField(
        max_length=200,
        help_text="Plugin name (e.g., 'Win Back Inactive Customers')"
    )

    slug = models.SlugField(
        max_length=200,
        unique=True,
        help_text="URL-friendly identifier"
    )

    description = models.TextField(
        help_text="What this plugin does (markdown supported)"
    )

    short_description = models.CharField(
        max_length=200,
        help_text="One-line summary for marketplace listing"
    )

    logo_url = models.URLField(
        max_length=500,
        blank=True,
        help_text="URL to plugin logo/icon image"
    )

    # Code & Configuration
    plugin_code = models.TextField(
        help_text="The Python script code"
    )

    plugin_code_hash = models.CharField(
        max_length=64,
        blank=True,
        help_text="SHA-256 hash of code for verification"
    )

    template_variables = models.JSONField(
        default=dict,
        blank=True,
        help_text="Template variables extracted from code (PROMPT, CONTEXT, DATE)"
    )

    default_config = models.JSONField(
        default=dict,
        blank=True,
        help_text="Default configuration values"
    )

    # Marketplace Info
    visibility = models.CharField(
        max_length=10,
        choices=Visibility.choices,
        default=Visibility.PRIVATE,
        db_index=True
    )

    category = models.CharField(
        max_length=20,
        choices=Category.choices,
        default=Category.OTHER,
        db_index=True
    )

    tags = models.JSONField(
        default=list,
        blank=True,
        help_text="Searchable tags (e.g., ['email', 'customers', 'retention'])"
    )

    # Author & Licensing
    author = models.ForeignKey(
        'users.User',
        on_delete=models.SET_NULL,
        null=True,
        related_name='plugin_templates',
        help_text="Original author"
    )

    author_name = models.CharField(
        max_length=200,
        blank=True,
        help_text="Display name for attribution"
    )

    version = models.CharField(
        max_length=20,
        default='1.0.0',
        help_text="Plugin version (semantic versioning)"
    )

    license_type = models.CharField(
        max_length=10,
        default='SCPL',
        help_text="SCPL for marketplace, or custom for private"
    )

    # Approval & Publishing
    is_approved = models.BooleanField(
        default=False,
        db_index=True,
        help_text="Approved for marketplace by platform staff"
    )

    approved_by = models.ForeignKey(
        'users.User',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='approved_plugins',
        help_text="Platform user who approved this plugin"
    )

    approved_at = models.DateTimeField(
        null=True,
        blank=True
    )

    rejection_reason = models.TextField(
        blank=True,
        help_text="Reason for rejection if not approved"
    )

    # Stats & Engagement
    install_count = models.PositiveIntegerField(
        default=0,
        help_text="Number of times installed"
    )

    rating_average = models.DecimalField(
        max_digits=3,
        decimal_places=2,
        default=0.00,
        help_text="Average rating (0-5)"
    )

    rating_count = models.PositiveIntegerField(
        default=0,
        help_text="Number of ratings"
    )

    # Metadata
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    published_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text="When published to marketplace"
    )

    class Meta:
        app_label = 'schedule'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['visibility', 'is_approved', '-install_count']),
            models.Index(fields=['category', 'visibility']),
            models.Index(fields=['slug']),
        ]

    def __str__(self):
        return f"{self.name} by {self.author_name or 'Platform'}"

    def save(self, *args, **kwargs):
        """Generate slug and code hash on save"""
        import hashlib
        from django.utils.text import slugify
        from .template_parser import TemplateVariableParser

        # Generate slug if not set
        if not self.slug:
            self.slug = slugify(self.name)
            # Ensure uniqueness
            counter = 1
            original_slug = self.slug
            while PluginTemplate.objects.filter(slug=self.slug).exists():
                self.slug = f"{original_slug}-{counter}"
                counter += 1

        # Generate code hash for verification
        if self.plugin_code:
            self.plugin_code_hash = hashlib.sha256(
                self.plugin_code.encode('utf-8')
            ).hexdigest()

            # Parse template variables from plugin code
            variables_list = TemplateVariableParser.extract_variables(self.plugin_code)
            # Convert list to dict keyed by variable name
            self.template_variables = {var['name']: var for var in variables_list}

        # Set author name if not provided
        if self.author and not self.author_name:
            self.author_name = self.author.get_full_name() or self.author.username

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

    def can_be_published(self):
        """Check if plugin meets requirements for marketplace publishing"""
        from .safe_scripting import validate_plugin_whitelist

        # Validate code syntax and whitelist
        validation = validate_plugin_whitelist(self.plugin_code)
        return validation['valid']

    def publish_to_marketplace(self, user):
        """Publish plugin to marketplace"""
        if not self.is_approved:
            raise ValidationError("Plugin must be approved before publishing to marketplace")

        self.visibility = self.Visibility.PUBLIC
        self.published_at = timezone.now()
        self.save()

    def unpublish_from_marketplace(self):
        """Remove plugin from marketplace (existing installations remain)"""
        self.visibility = self.Visibility.PRIVATE
        self.save()


class PluginInstallation(models.Model):
    """
    Tracks installation of a plugin template.

    When a user installs a plugin from the marketplace:
    1. A PluginInstallation record is created (makes it available in "My Plugins")
    2. Optionally, a ScheduledTask can be created later for automatic execution
    3. Plugin can also be used for resource-free scheduling
    """

    template = models.ForeignKey(
        PluginTemplate,
        on_delete=models.SET_NULL,
        null=True,
        related_name='installations',
        help_text="Source template (null if template deleted)"
    )

    scheduled_task = models.OneToOneField(
        ScheduledTask,
        on_delete=models.CASCADE,
        related_name='installation',
        null=True,
        blank=True,
        help_text="Optional scheduled task if plugin is scheduled to run automatically"
    )

    # Installation metadata
    installed_by = models.ForeignKey(
        'users.User',
        on_delete=models.SET_NULL,
        null=True,
        related_name='plugin_installations'
    )

    installed_at = models.DateTimeField(auto_now_add=True)

    # Configuration at install time
    config_values = models.JSONField(
        default=dict,
        help_text="User's configuration values at installation"
    )

    # Template version tracking
    template_version_hash = models.CharField(
        max_length=64,
        blank=True,
        help_text="Hash of template code at install time (for update detection)"
    )

    # User feedback
    rating = models.PositiveSmallIntegerField(
        null=True,
        blank=True,
        help_text="User rating (1-5 stars)"
    )

    review = models.TextField(
        blank=True,
        help_text="User review/feedback"
    )

    reviewed_at = models.DateTimeField(
        null=True,
        blank=True
    )

    class Meta:
        app_label = 'schedule'
        ordering = ['-installed_at']
        indexes = [
            models.Index(fields=['template', '-installed_at']),
            models.Index(fields=['installed_by', '-installed_at']),
        ]

    def __str__(self):
        template_name = self.template.name if self.template else "Deleted Template"
        if self.scheduled_task:
            return f"{template_name} -> {self.scheduled_task.name}"
        return f"{template_name} (installed)"

    def has_update_available(self):
        """Check if template has been updated since installation"""
        if not self.template:
            return False
        return self.template.plugin_code_hash != self.template_version_hash

    def update_to_latest(self):
        """Update scheduled task to latest template version"""
        if not self.template:
            raise ValidationError("Cannot update: template has been deleted")

        # Update scheduled task with latest code
        self.scheduled_task.plugin_code = self.template.plugin_code
        self.scheduled_task.save()

        # Update version hash
        self.template_version_hash = self.template.plugin_code_hash
        self.save()


class Album(models.Model):
    """
    Album for organizing media files.
    """
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    cover_image = models.ForeignKey(
        'MediaFile',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='cover_for_albums',
        help_text="Cover image for this album"
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        app_label = 'schedule'
        ordering = ['-created_at']

    def __str__(self):
        return self.name

    @property
    def file_count(self):
        """Return the number of files in this album."""
        return self.files.count()


class MediaFile(models.Model):
    """
    Media file (image) uploaded by a tenant.

    Storage is handled by django-storages with DigitalOcean Spaces in production.
    File size is tracked for quota enforcement.
    """
    def upload_to(instance, filename):
        """Generate upload path: media/{tenant_schema}/{filename}"""
        from django.db import connection
        schema = connection.schema_name
        return f'media/{schema}/{filename}'

    file = models.ImageField(
        upload_to=upload_to,
        help_text="The uploaded image file"
    )
    filename = models.CharField(
        max_length=255,
        help_text="Original filename"
    )
    alt_text = models.CharField(
        max_length=255,
        blank=True,
        help_text="Alt text for accessibility"
    )
    file_size = models.PositiveIntegerField(
        help_text="File size in bytes"
    )
    width = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Image width in pixels"
    )
    height = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Image height in pixels"
    )
    mime_type = models.CharField(
        max_length=100,
        help_text="MIME type (e.g., image/jpeg)"
    )
    album = models.ForeignKey(
        Album,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='files',
        help_text="Album this file belongs to"
    )
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        app_label = 'schedule'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['album', '-created_at']),
            models.Index(fields=['mime_type']),
        ]

    def __str__(self):
        return self.filename

    @property
    def url(self):
        """Return the URL to access this file."""
        if self.file:
            return self.file.url
        return None

    def delete(self, *args, **kwargs):
        """Delete the file from storage when the model is deleted."""
        # Store file reference before deleting model
        file_to_delete = self.file
        file_size = self.file_size

        # Delete the model first
        super().delete(*args, **kwargs)

        # Delete the actual file from storage
        if file_to_delete:
            file_to_delete.delete(save=False)


class Holiday(models.Model):
    """
    Predefined holiday definitions for the holiday picker.

    Supports three types:
    - FIXED: Same date every year (e.g., Christmas on Dec 25)
    - FLOATING: Nth weekday of a month (e.g., Thanksgiving = 4th Thursday of November)
    - CALCULATED: Algorithm-based (e.g., Easter)
    """

    class Type(models.TextChoices):
        FIXED = 'FIXED', 'Fixed date'
        FLOATING = 'FLOATING', 'Floating (Nth weekday of month)'
        CALCULATED = 'CALCULATED', 'Calculated (algorithm-based)'

    code = models.CharField(
        max_length=50,
        unique=True,
        help_text="Unique identifier (e.g., 'thanksgiving_us', 'christmas')"
    )
    name = models.CharField(max_length=100)
    country = models.CharField(
        max_length=2,
        default='US',
        db_index=True,
        help_text="ISO 3166-1 alpha-2 country code"
    )

    holiday_type = models.CharField(
        max_length=20,
        choices=Type.choices,
        default=Type.FIXED
    )

    # For FIXED holidays (e.g., Dec 25)
    month = models.PositiveSmallIntegerField(
        null=True,
        blank=True,
        help_text="Month (1-12)"
    )
    day = models.PositiveSmallIntegerField(
        null=True,
        blank=True,
        help_text="Day of month (1-31)"
    )

    # For FLOATING holidays (e.g., "4th Thursday of November")
    week_of_month = models.PositiveSmallIntegerField(
        null=True,
        blank=True,
        help_text="Week of month (1-4, or 5 for 'last')"
    )
    day_of_week = models.PositiveSmallIntegerField(
        null=True,
        blank=True,
        help_text="Day of week (0=Monday, 6=Sunday)"
    )

    # For CALCULATED holidays (e.g., Easter)
    calculation_rule = models.CharField(
        max_length=50,
        blank=True,
        help_text="Calculation rule (e.g., 'easter', 'easter-2' for Good Friday)"
    )

    is_active = models.BooleanField(default=True)

    class Meta:
        app_label = 'schedule'
        ordering = ['country', 'name']
        indexes = [
            models.Index(fields=['country', 'is_active']),
        ]

    def __str__(self):
        return f"{self.name} ({self.country})"

    def get_date_for_year(self, year):
        """
        Calculate the actual date for this holiday in a given year.

        Returns:
            date object or None if cannot be calculated
        """
        from datetime import date, timedelta

        if self.holiday_type == self.Type.FIXED:
            if self.month and self.day:
                try:
                    return date(year, self.month, self.day)
                except ValueError:
                    return None

        elif self.holiday_type == self.Type.FLOATING:
            if self.month and self.week_of_month is not None and self.day_of_week is not None:
                # Find Nth weekday of month
                first_day = date(year, self.month, 1)
                first_weekday = first_day.weekday()

                # Days until first occurrence of target weekday
                days_until = (self.day_of_week - first_weekday) % 7
                first_occurrence = first_day + timedelta(days=days_until)

                if self.week_of_month == 5:  # "Last" occurrence
                    # Find last occurrence by going to next month and going back
                    if self.month == 12:
                        next_month = date(year + 1, 1, 1)
                    else:
                        next_month = date(year, self.month + 1, 1)
                    last_day = next_month - timedelta(days=1)
                    days_back = (last_day.weekday() - self.day_of_week) % 7
                    return last_day - timedelta(days=days_back)
                else:
                    return first_occurrence + timedelta(weeks=self.week_of_month - 1)

        elif self.holiday_type == self.Type.CALCULATED:
            if self.calculation_rule.startswith('easter'):
                easter_date = self._calculate_easter(year)
                if '+' in self.calculation_rule:
                    offset = int(self.calculation_rule.split('+')[1])
                    return easter_date + timedelta(days=offset)
                elif '-' in self.calculation_rule:
                    offset = int(self.calculation_rule.split('-')[1])
                    return easter_date - timedelta(days=offset)
                return easter_date

        return None

    @staticmethod
    def _calculate_easter(year):
        """
        Calculate Easter Sunday using the Anonymous Gregorian algorithm.
        """
        from datetime import date

        a = year % 19
        b = year // 100
        c = year % 100
        d = b // 4
        e = b % 4
        f = (b + 8) // 25
        g = (b - f + 1) // 3
        h = (19 * a + b - d - g + 15) % 30
        i = c // 4
        k = c % 4
        l = (32 + 2 * e + 2 * i - h - k) % 7
        m = (a + 11 * h + 22 * l) // 451
        month = (h + l - 7 * m + 114) // 31
        day = ((h + l - 7 * m + 114) % 31) + 1

        return date(year, month, day)


class TimeBlock(models.Model):
    """
    Time blocking model for business closures and resource unavailability.

    Supports three levels with precedence (highest to lowest):
    1. Business-wide (is_business_wide=True): Affects ALL locations and resources
    2. Location-wide (location=FK, resource=None): Affects all resources at location
    3. Resource-specific (resource=FK): Affects specific resource only

    Blocks can be one-time or recurring with various patterns.
    """

    class BlockType(models.TextChoices):
        HARD = 'HARD', 'Hard Block (prevents booking)'
        SOFT = 'SOFT', 'Soft Block (warning only, override allowed)'

    class RecurrenceType(models.TextChoices):
        NONE = 'NONE', 'No recurrence (specific date/range)'
        WEEKLY = 'WEEKLY', 'Weekly (specific days of week)'
        MONTHLY = 'MONTHLY', 'Monthly (specific days of month)'
        YEARLY = 'YEARLY', 'Yearly (specific days of year)'
        HOLIDAY = 'HOLIDAY', 'Holiday (floating dates)'

    class ApprovalStatus(models.TextChoices):
        APPROVED = 'APPROVED', 'Approved'
        PENDING = 'PENDING', 'Pending Review'
        DENIED = 'DENIED', 'Denied'

    class Purpose(models.TextChoices):
        CLOSURE = 'CLOSURE', 'Business Closure (holiday, vacation)'
        UNAVAILABLE = 'UNAVAILABLE', 'Resource Unavailable (time off, maintenance)'
        BUSINESS_HOURS = 'BUSINESS_HOURS', 'Business Operating Hours (outside hours blocked)'
        OTHER = 'OTHER', 'Other (general blocking)'

    # Core identification
    title = models.CharField(
        max_length=200,
        help_text="Display title (e.g., 'Christmas Day', 'Lunch Break')"
    )
    description = models.TextField(
        blank=True,
        help_text="Optional description or reason for block"
    )

    # Level determination (precedence: business-wide > location-wide > resource-specific)
    is_business_wide = models.BooleanField(
        default=False,
        help_text="If True, this block applies to ALL locations and resources"
    )
    location = models.ForeignKey(
        'Location',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='time_blocks',
        help_text="Specific location (null = use resource location or business-wide)"
    )
    resource = models.ForeignKey(
        'Resource',
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='time_blocks',
        help_text="Specific resource (null = location-level or business-wide block)"
    )

    # Block behavior
    block_type = models.CharField(
        max_length=10,
        choices=BlockType.choices,
        default=BlockType.HARD,
        help_text="HARD prevents booking; SOFT shows warning but allows override"
    )

    # Block purpose
    purpose = models.CharField(
        max_length=20,
        choices=Purpose.choices,
        default=Purpose.OTHER,
        db_index=True,
        help_text="Purpose of this time block"
    )

    # Recurrence configuration
    recurrence_type = models.CharField(
        max_length=20,
        choices=RecurrenceType.choices,
        default=RecurrenceType.NONE,
        db_index=True
    )

    # For NONE: specific date range
    start_date = models.DateField(
        null=True,
        blank=True,
        help_text="Start date for one-time blocks"
    )
    end_date = models.DateField(
        null=True,
        blank=True,
        help_text="End date for one-time blocks (same as start for single day)"
    )

    # Time window (applies to all patterns)
    all_day = models.BooleanField(
        default=True,
        help_text="If true, blocks entire day; if false, uses start/end time"
    )
    start_time = models.TimeField(
        null=True,
        blank=True,
        help_text="Start time (if not all-day)"
    )
    end_time = models.TimeField(
        null=True,
        blank=True,
        help_text="End time (if not all-day)"
    )

    # Recurrence patterns (JSON)
    recurrence_pattern = models.JSONField(
        default=dict,
        blank=True,
        help_text="""
        Recurrence configuration:
        - WEEKLY: {"days_of_week": [0,1,2]} (0=Mon, 6=Sun)
        - MONTHLY: {"days_of_month": [1, 15]}
        - YEARLY: {"month": 7, "day": 4} or {"month": 12, "day": 25}
        - HOLIDAY: {"holiday_code": "thanksgiving_us"}
        """
    )

    # Recurrence bounds
    recurrence_start = models.DateField(
        null=True,
        blank=True,
        help_text="When this recurring block becomes active"
    )
    recurrence_end = models.DateField(
        null=True,
        blank=True,
        help_text="When this recurring block ends (null = forever)"
    )

    # Status
    is_active = models.BooleanField(default=True, db_index=True)

    # Approval workflow (for staff time-off requests)
    approval_status = models.CharField(
        max_length=20,
        choices=ApprovalStatus.choices,
        default=ApprovalStatus.APPROVED,
        db_index=True,
        help_text="Approval status for time-off requests"
    )
    reviewed_by = models.ForeignKey(
        'users.User',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='reviewed_time_blocks',
        help_text="Manager/owner who reviewed the request"
    )
    reviewed_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text="When the request was reviewed"
    )
    review_notes = models.TextField(
        blank=True,
        help_text="Optional notes from reviewer"
    )

    # Audit
    created_by = models.ForeignKey(
        'users.User',
        on_delete=models.SET_NULL,
        null=True,
        related_name='created_time_blocks'
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        app_label = 'schedule'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['resource', 'is_active']),
            models.Index(fields=['recurrence_type', 'is_active']),
            models.Index(fields=['start_date', 'end_date']),
            models.Index(fields=['approval_status']),
            models.Index(fields=['purpose', 'is_active']),
        ]

    def __str__(self):
        level = f"Resource: {self.resource.name}" if self.resource else "Business-level"
        return f"{self.title} ({level})"

    @property
    def is_business_level(self):
        """Check if this is a business-level block (affects all resources)."""
        return self.resource is None

    @property
    def is_business_hours(self):
        """Check if this is a business hours definition block."""
        return self.purpose == self.Purpose.BUSINESS_HOURS

    @property
    def is_effective(self):
        """Check if this block is effective (active and approved)."""
        return self.is_active and self.approval_status == self.ApprovalStatus.APPROVED

    @property
    def is_pending_approval(self):
        """Check if this block is pending approval."""
        return self.approval_status == self.ApprovalStatus.PENDING

    @property
    def precedence_level(self):
        """
        Return precedence level for multi-location blocking.

        Precedence levels (1 = highest):
        1. Business-wide (is_business_wide=True) - blocks everything
        2. Location-wide (location set, no resource) - blocks all resources at location
        3. Resource-specific (resource set) - blocks specific resource
        """
        if self.is_business_wide:
            return 1
        elif self.location and not self.resource:
            return 2
        else:
            return 3

    def blocks_resource_at_location(self, resource, location, check_datetime):
        """
        Check if this block applies to a resource+location at a datetime.

        Args:
            resource: Resource to check
            location: Location to check
            check_datetime: datetime to check

        Returns:
            bool: True if this block applies
        """
        if not self.is_effective:
            return False

        if not self.blocks_datetime_range(check_datetime, check_datetime):
            return False

        # Business-wide blocks everything
        if self.is_business_wide:
            return True

        # Location-wide block (location set, no resource)
        if self.location and not self.resource:
            return self.location.pk == location.pk

        # Resource-specific block
        if self.resource:
            if self.resource.pk != resource.pk:
                return False
            # For resource blocks with location constraint
            if self.location:
                return self.location.pk == location.pk
            return True

        return False

    def blocks_date(self, check_date):
        """
        Check if this block applies to a given date.

        Args:
            check_date: date object to check

        Returns:
            bool: True if date is blocked
        """
        from datetime import date

        # Block must be both active and approved to be effective
        if not self.is_effective:
            return False

        # Check recurrence bounds
        if self.recurrence_start and check_date < self.recurrence_start:
            return False
        if self.recurrence_end and check_date > self.recurrence_end:
            return False

        if self.recurrence_type == self.RecurrenceType.NONE:
            # One-time block: check date range
            if self.start_date and self.end_date:
                return self.start_date <= check_date <= self.end_date
            elif self.start_date:
                return check_date == self.start_date
            return False

        elif self.recurrence_type == self.RecurrenceType.WEEKLY:
            # Check if check_date's weekday is in the pattern
            days = self.recurrence_pattern.get('days_of_week', [])
            return check_date.weekday() in days

        elif self.recurrence_type == self.RecurrenceType.MONTHLY:
            # Check if check_date's day of month is in the pattern
            days = self.recurrence_pattern.get('days_of_month', [])
            return check_date.day in days

        elif self.recurrence_type == self.RecurrenceType.YEARLY:
            # Check if month and day match
            month = self.recurrence_pattern.get('month')
            day = self.recurrence_pattern.get('day')
            if month and day:
                return check_date.month == month and check_date.day == day
            return False

        elif self.recurrence_type == self.RecurrenceType.HOLIDAY:
            # Check if date matches the holiday for this year
            holiday_code = self.recurrence_pattern.get('holiday_code')
            if holiday_code:
                try:
                    holiday = Holiday.objects.get(code=holiday_code, is_active=True)
                    holiday_date = holiday.get_date_for_year(check_date.year)
                    return holiday_date == check_date
                except Holiday.DoesNotExist:
                    return False
            return False

        return False

    def blocks_datetime_range(self, start_dt, end_dt):
        """
        Check if this block overlaps with a datetime range.

        Args:
            start_dt: datetime - start of range to check
            end_dt: datetime - end of range to check

        Returns:
            bool: True if any part of the range is blocked
        """
        from datetime import datetime, time, timedelta

        # First check if any date in the range is blocked
        current_date = start_dt.date()
        end_date = end_dt.date()

        while current_date <= end_date:
            if self.blocks_date(current_date):
                # Date is blocked, now check time window
                if self.all_day:
                    return True
                else:
                    # Check time overlap
                    if self.start_time and self.end_time:
                        block_start = datetime.combine(current_date, self.start_time)
                        block_end = datetime.combine(current_date, self.end_time)

                        # Make timezone-aware if start_dt is timezone-aware
                        # Use the SAME timezone as start_dt to ensure correct comparison
                        if start_dt.tzinfo is not None:
                            block_start = block_start.replace(tzinfo=start_dt.tzinfo)
                            block_end = block_end.replace(tzinfo=start_dt.tzinfo)

                        # Check overlap: start_dt < block_end AND end_dt > block_start
                        if start_dt < block_end and end_dt > block_start:
                            return True

            current_date += timedelta(days=1)

        return False

    def get_blocked_dates_in_range(self, range_start, range_end):
        """
        Generate all blocked dates within a date range.
        Used for calendar visualization.

        Args:
            range_start: date - start of range
            range_end: date - end of range

        Returns:
            list of dicts with 'date', 'start_time', 'end_time', 'all_day'
        """
        from datetime import timedelta

        blocked_dates = []
        current_date = range_start

        while current_date <= range_end:
            if self.blocks_date(current_date):
                blocked_dates.append({
                    'date': current_date,
                    'start_time': self.start_time,
                    'end_time': self.end_time,
                    'all_day': self.all_day,
                    'title': self.title,
                    'block_type': self.block_type,
                    'block_id': self.id,
                    'is_business_level': self.is_business_level,
                })
            current_date += timedelta(days=1)

        return blocked_dates