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 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 = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        default=Decimal('0.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)

    # 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:
        ordering = ['display_order', 'name']
        indexes = [models.Index(fields=['is_active', 'display_order'])]

    def __str__(self):
        return f"{self.name} ({self.duration} min - ${self.price})"


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:
        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"
    )
    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"
    )

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

    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'
        CANCELED = 'CANCELED', 'Canceled'
        COMPLETED = 'COMPLETED', 'Completed'
        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')

    # 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:
        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

    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:
        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:
        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:
        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:
        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 .plugins 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:
        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:
        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:
        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:
        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 EmailTemplate(models.Model):
    """
    Reusable email template for plugins and automations.

    Supports both text and HTML content with template variable substitution.
    Business templates are tenant-specific, Platform templates are shared/system-wide.
    """

    class Scope(models.TextChoices):
        BUSINESS = 'BUSINESS', 'Business'  # Tenant-specific
        PLATFORM = 'PLATFORM', 'Platform'  # Platform-wide (shared)

    class Category(models.TextChoices):
        APPOINTMENT = 'APPOINTMENT', 'Appointment'
        REMINDER = 'REMINDER', 'Reminder'
        CONFIRMATION = 'CONFIRMATION', 'Confirmation'
        MARKETING = 'MARKETING', 'Marketing'
        NOTIFICATION = 'NOTIFICATION', 'Notification'
        REPORT = 'REPORT', 'Report'
        OTHER = 'OTHER', 'Other'

    name = models.CharField(max_length=200)
    description = models.TextField(blank=True)

    # Email structure
    subject = models.CharField(
        max_length=500,
        help_text="Email subject line - supports template variables like {{CUSTOMER_NAME}}"
    )
    html_content = models.TextField(
        blank=True,
        help_text="HTML email body"
    )
    text_content = models.TextField(
        blank=True,
        help_text="Plain text email body (fallback for non-HTML clients)"
    )

    # Scope
    scope = models.CharField(
        max_length=20,
        choices=Scope.choices,
        default=Scope.BUSINESS,
    )

    # Only for PLATFORM scope templates
    is_default = models.BooleanField(
        default=False,
        help_text="Default template for certain system triggers"
    )

    # Category for organization
    category = models.CharField(
        max_length=50,
        choices=Category.choices,
        default=Category.OTHER,
    )

    # Preview data for visual preview
    preview_context = models.JSONField(
        default=dict,
        blank=True,
        help_text="Sample data for rendering preview"
    )

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

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

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

    def render(self, context: dict, force_footer: bool = False):
        """
        Render the template with given context.

        Args:
            context: Dictionary of template variables
            force_footer: If True, append "Powered by Smooth Schedule" footer

        Returns:
            Tuple of (subject, html_content, text_content)
        """
        from .template_parser import TemplateVariableParser

        subject = TemplateVariableParser.replace_insertion_codes(
            self.subject, context
        )
        html = TemplateVariableParser.replace_insertion_codes(
            self.html_content, context
        ) if self.html_content else ''
        text = TemplateVariableParser.replace_insertion_codes(
            self.text_content, context
        ) if self.text_content else ''

        # Append footer for free tier if applicable
        if force_footer:
            html = self._append_html_footer(html)
            text = self._append_text_footer(text)

        return subject, html, text

    def _append_html_footer(self, html: str) -> str:
        """Append Powered by Smooth Schedule footer to HTML"""
        import re
        footer = '''
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #e5e7eb; text-align: center; color: #9ca3af; font-size: 12px;">
    <p>
        Powered by
        <a href="https://smoothschedule.com" style="color: #6366f1; text-decoration: none; font-weight: 500;">
            SmoothSchedule
        </a>
    </p>
</div>
'''
        # Insert before </body> if present, otherwise append
        if '</body>' in html.lower():
            return re.sub(r'(</body>)', footer + r'\1', html, flags=re.IGNORECASE)
        return html + footer

    def _append_text_footer(self, text: str) -> str:
        """Append Powered by Smooth Schedule footer to plain text"""
        footer = "\n\n---\nPowered by SmoothSchedule - https://smoothschedule.com"
        return text + footer