# core/models.py

from shlex import split as shlex_split

from django.conf import settings
from django.db import models
from django.utils.text import slugify
from django.core.exceptions import ValidationError


class UserAgent(models.Model):
    name = models.CharField(
        max_length=512, unique=True, help_text="The User-Agent name."
    )
    user_agent = models.CharField(
        max_length=512,
        unique=True,
        help_text="The complete User-Agent string sent by the client.",
    )
    description = models.CharField(
        max_length=255,
        blank=True,
        help_text="An optional description of the client or device type.",
    )
    is_active = models.BooleanField(
        default=True,
        help_text="Whether this user agent is currently allowed/recognized.",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name


PROXY_PROFILE_NAME = "Proxy"
REDIRECT_PROFILE_NAME = "Redirect"


class StreamProfile(models.Model):
    name = models.CharField(max_length=255, help_text="Name of the stream profile")
    command = models.CharField(
        max_length=255,
        help_text="Command to execute (e.g., 'yt.sh', 'streamlink', or 'vlc')",
        blank=True,
    )
    parameters = models.TextField(
        help_text="Command-line parameters. Use {userAgent} and {streamUrl} as placeholders.",
        blank=True,
    )
    locked = models.BooleanField(
        default=False, help_text="Protected - can't be deleted or modified"
    )
    is_active = models.BooleanField(
        default=True, help_text="Whether this profile is active"
    )
    user_agent = models.ForeignKey(
        "UserAgent",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        help_text="Optional user agent to use. If not set, you can fall back to a default.",
    )

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if self.pk:  # Only check existing records
            orig = StreamProfile.objects.get(pk=self.pk)
            if orig.locked:
                allowed_fields = {"user_agent_id"}  # Only allow this field to change
                for field in self._meta.fields:
                    field_name = field.name

                    # Convert user_agent to user_agent_id for comparison
                    orig_value = getattr(orig, field_name)
                    new_value = getattr(self, field_name)

                    # Ensure that ForeignKey fields compare their ID values
                    if isinstance(orig_value, models.Model):
                        orig_value = orig_value.pk
                    if isinstance(new_value, models.Model):
                        new_value = new_value.pk

                    if field_name not in allowed_fields and orig_value != new_value:
                        raise ValidationError(
                            f"Cannot modify {field_name} on a protected profile."
                        )

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

    @classmethod
    def update(cls, pk, **kwargs):
        instance = cls.objects.get(pk=pk)

        if instance.locked:
            allowed_fields = {"user_agent_id"}  # Only allow updating this field

            for field_name, new_value in kwargs.items():
                if field_name not in allowed_fields:
                    raise ValidationError(
                        f"Cannot modify {field_name} on a protected profile."
                    )

                # Ensure user_agent ForeignKey updates correctly
                if field_name == "user_agent" and isinstance(
                    new_value, cls._meta.get_field("user_agent").related_model
                ):
                    new_value = new_value.pk  # Convert object to ID if needed

                setattr(instance, field_name, new_value)

        instance.save()
        return instance

    def is_proxy(self):
        if self.locked and self.name == PROXY_PROFILE_NAME:
            return True
        return False

    def is_redirect(self):
        if self.locked and self.name == REDIRECT_PROFILE_NAME:
            return True
        return False

    def build_command(self, stream_url, user_agent):

        if self.is_proxy():
            return []

        replacements = {
            "{streamUrl}": stream_url,
            "{userAgent}": user_agent,
        }

        # Split the command and iterate through each part to apply replacements
        cmd = [self.command] + [
            self._replace_in_part(part, replacements)
            for part in shlex_split(self.parameters) # use shlex to handle quoted strings
        ]

        return cmd

    def _replace_in_part(self, part, replacements):
        # Iterate through the replacements and replace each part of the string
        for key, value in replacements.items():
            part = part.replace(key, value)
        return part


class OutputProfile(models.Model):
    """
    Defines a pre-delivery transcode step applied to a channel's TS stream.

    The command and parameters must accept raw MPEG-TS via pipe:0 (stdin) and
    write the transcoded output to pipe:1 (stdout). One transcode process runs
    per active (channel, OutputProfile) pair regardless of how many clients
    request it; all clients share the same Redis-backed output buffer.

    Example parameters for a 720p transcode:
        -i pipe:0 -c:v libx264 -b:v 2000k -vf scale=-2:720 -c:a copy -f mpegts pipe:1
    """

    name = models.CharField(max_length=255, unique=True, help_text="Display name for this output profile")
    command = models.CharField(
        max_length=255,
        help_text="Executable to run (e.g. 'ffmpeg')",
    )
    parameters = models.TextField(
        help_text="Command-line parameters. Must read from pipe:0 (stdin) and write to pipe:1 (stdout).",
    )
    locked = models.BooleanField(
        default=False, help_text="Protected - can't be deleted or modified"
    )
    is_active = models.BooleanField(
        default=True, help_text="Whether this profile is available for use"
    )

    def __str__(self):
        return self.name

    def build_command(self):
        """Return the full command as a list suitable for subprocess.Popen."""
        from shlex import split as shlex_split
        return [self.command] + shlex_split(self.parameters)


# Setting group keys
STREAM_SETTINGS_KEY = "stream_settings"
DVR_SETTINGS_KEY = "dvr_settings"
BACKUP_SETTINGS_KEY = "backup_settings"
PROXY_SETTINGS_KEY = "proxy_settings"
NETWORK_ACCESS_KEY = "network_access"
SYSTEM_SETTINGS_KEY = "system_settings"
EPG_SETTINGS_KEY = "epg_settings"
USER_LIMITS_SETTINGS_KEY = "user_limit_settings"


class CoreSettings(models.Model):
    key = models.CharField(
        max_length=255,
        unique=True,
    )
    name = models.CharField(
        max_length=255,
    )
    value = models.JSONField(
        default=dict,
        blank=True,
    )

    def __str__(self):
        return "Core Settings"

    # Helper methods to get/set grouped settings
    @classmethod
    def _get_group(cls, key, defaults=None):
        """Get a settings group, returning defaults if not found."""
        try:
            return cls.objects.get(key=key).value or (defaults or {})
        except cls.DoesNotExist:
            return defaults or {}

    @classmethod
    def _update_group(cls, key, name, updates):
        """Update specific fields in a settings group."""
        obj, created = cls.objects.get_or_create(
            key=key,
            defaults={"name": name, "value": {}}
        )
        current = obj.value if isinstance(obj.value, dict) else {}
        current.update(updates)
        obj.value = current
        obj.save()
        return current

    # Stream Settings
    @classmethod
    def get_stream_settings(cls):
        """Get all stream-related settings."""
        return cls._get_group(STREAM_SETTINGS_KEY, {
            "default_user_agent": None,
            "default_stream_profile": None,
            "m3u_hash_key": "",
            "preferred_region": None,
            "auto_import_mapped_files": None,
            "default_output_format": "mpegts",
        })

    @classmethod
    def get_default_user_agent_id(cls):
        return cls.get_stream_settings().get("default_user_agent")

    @classmethod
    def get_default_stream_profile_id(cls):
        return cls.get_stream_settings().get("default_stream_profile")

    @classmethod
    def get_default_output_format(cls):
        return cls.get_stream_settings().get("default_output_format", "mpegts")

    @classmethod
    def get_m3u_hash_key(cls):
        return cls.get_stream_settings().get("m3u_hash_key", "")

    @classmethod
    def get_preferred_region(cls):
        return cls.get_stream_settings().get("preferred_region")

    @classmethod
    def get_auto_import_mapped_files(cls):
        return cls.get_stream_settings().get("auto_import_mapped_files")

    # EPG Settings
    @classmethod
    def get_epg_settings(cls):
        """Get all EPG-related settings."""
        return cls._get_group(EPG_SETTINGS_KEY, {
            "epg_match_mode": "default",
            "epg_match_ignore_prefixes": [],
            "epg_match_ignore_suffixes": [],
            "epg_match_ignore_custom": [],
        })

    @classmethod
    def _safe_string_list(cls, value):
        """Return a list of strings, filtering out non-list or non-string values."""
        if not isinstance(value, list):
            return []
        return [v for v in value if isinstance(v, str)]

    @classmethod
    def get_epg_match_ignore_prefixes(cls):
        return cls._safe_string_list(cls.get_epg_settings().get("epg_match_ignore_prefixes", []))

    @classmethod
    def get_epg_match_ignore_suffixes(cls):
        return cls._safe_string_list(cls.get_epg_settings().get("epg_match_ignore_suffixes", []))

    @classmethod
    def get_epg_match_ignore_custom(cls):
        return cls._safe_string_list(cls.get_epg_settings().get("epg_match_ignore_custom", []))

    # DVR Settings
    @classmethod
    def get_dvr_settings(cls):
        """Get all DVR-related settings."""
        return cls._get_group(DVR_SETTINGS_KEY, {
            "tv_template": "TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv",
            "movie_template": "Movies/{title} ({year}).mkv",
            "tv_fallback_dir": "TV_Shows",
            "tv_fallback_template": "TV_Shows/{show}/{start}.mkv",
            "movie_fallback_template": "Movies/{start}.mkv",
            "comskip_enabled": False,
            "comskip_custom_path": "",
            "pre_offset_minutes": 0,
            "post_offset_minutes": 0,
            "series_rules": [],
        })

    @classmethod
    def get_dvr_tv_template(cls):
        return cls.get_dvr_settings().get("tv_template", "TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv")

    @classmethod
    def get_dvr_movie_template(cls):
        return cls.get_dvr_settings().get("movie_template", "Movies/{title} ({year}).mkv")

    @classmethod
    def get_dvr_tv_fallback_dir(cls):
        return cls.get_dvr_settings().get("tv_fallback_dir", "TV_Shows")

    @classmethod
    def get_dvr_tv_fallback_template(cls):
        return cls.get_dvr_settings().get("tv_fallback_template", "TV_Shows/{show}/{start}.mkv")

    @classmethod
    def get_dvr_movie_fallback_template(cls):
        return cls.get_dvr_settings().get("movie_fallback_template", "Movies/{start}.mkv")

    @classmethod
    def get_dvr_comskip_enabled(cls):
        return bool(cls.get_dvr_settings().get("comskip_enabled", False))

    @classmethod
    def get_dvr_comskip_custom_path(cls):
        return cls.get_dvr_settings().get("comskip_custom_path", "")

    @classmethod
    def set_dvr_comskip_custom_path(cls, path: str | None):
        value = (path or "").strip()
        cls._update_group(DVR_SETTINGS_KEY, "DVR Settings", {"comskip_custom_path": value})
        return value

    @classmethod
    def get_dvr_pre_offset_minutes(cls):
        return int(cls.get_dvr_settings().get("pre_offset_minutes", 0) or 0)

    @classmethod
    def get_dvr_post_offset_minutes(cls):
        return int(cls.get_dvr_settings().get("post_offset_minutes", 0) or 0)

    @classmethod
    def get_dvr_series_rules(cls):
        rules = cls.get_dvr_settings().get("series_rules", [])
        if not isinstance(rules, list):
            return []
        return [r for r in rules if isinstance(r, dict)]

    @classmethod
    def set_dvr_series_rules(cls, rules):
        clean = [r for r in rules if isinstance(r, dict)] if isinstance(rules, list) else []
        cls._update_group(DVR_SETTINGS_KEY, "DVR Settings", {"series_rules": clean})
        return clean

    # Proxy Settings
    @classmethod
    def get_proxy_settings(cls):
        """Get proxy settings."""
        return cls._get_group(PROXY_SETTINGS_KEY, {
            "buffering_timeout": 15,
            "buffering_speed": 1.0,
            "redis_chunk_ttl": 60,
            "channel_shutdown_delay": 0,
            "channel_init_grace_period": 5,
            "new_client_behind_seconds": 5,
        })

    # System Settings
    @classmethod
    def get_system_settings(cls):
        """Get all system-related settings."""
        return cls._get_group(SYSTEM_SETTINGS_KEY, {
            "time_zone": getattr(settings, "TIME_ZONE", "UTC") or "UTC",
            "max_system_events": 100,
        })

    @classmethod
    def get_system_time_zone(cls):
        return cls.get_system_settings().get("time_zone") or getattr(settings, "TIME_ZONE", "UTC") or "UTC"

    @classmethod
    def set_system_time_zone(cls, tz_name: str | None):
        value = (tz_name or "").strip() or getattr(settings, "TIME_ZONE", "UTC") or "UTC"
        cls._update_group(SYSTEM_SETTINGS_KEY, "System Settings", {"time_zone": value})
        return value

    @classmethod
    def get_user_limits_settings(cls):
        return cls._get_group(USER_LIMITS_SETTINGS_KEY, {
            "terminate_on_limit_exceeded": True,
            "prioritize_single_client_channels": True,
            "ignore_same_channel_connections": False,
            "terminate_oldest": True,
        })


class SystemEvent(models.Model):
    """
    Tracks system events like channel start/stop, buffering, failover, client connections.
    Maintains a rolling history based on max_system_events setting.
    """
    EVENT_TYPES = [
        ('channel_start', 'Channel Started'),
        ('channel_stop', 'Channel Stopped'),
        ('channel_buffering', 'Channel Buffering'),
        ('channel_failover', 'Channel Failover'),
        ('channel_reconnect', 'Channel Reconnected'),
        ('channel_error', 'Channel Error'),
        ('client_connect', 'Client Connected'),
        ('client_disconnect', 'Client Disconnected'),
        ('recording_start', 'Recording Started'),
        ('recording_end', 'Recording Ended'),
        ('stream_switch', 'Stream Switched'),
        ('m3u_refresh', 'M3U Refreshed'),
        ('m3u_download', 'M3U Downloaded'),
        ('epg_refresh', 'EPG Refreshed'),
        ('epg_download', 'EPG Downloaded'),
        ('login_success', 'Login Successful'),
        ('login_failed', 'Login Failed'),
        ('logout', 'User Logged Out'),
        ('m3u_blocked', 'M3U Download Blocked'),
        ('epg_blocked', 'EPG Download Blocked'),
        ('vod_start', 'VOD Started'),
        ('vod_stop', 'VOD Stopped'),
    ]

    event_type = models.CharField(max_length=50, choices=EVENT_TYPES, db_index=True)
    timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
    channel_id = models.UUIDField(null=True, blank=True, db_index=True)
    channel_name = models.CharField(max_length=255, null=True, blank=True)
    details = models.JSONField(default=dict, blank=True)

    class Meta:
        ordering = ['-timestamp']
        indexes = [
            models.Index(fields=['-timestamp']),
            models.Index(fields=['event_type', '-timestamp']),
        ]

    def __str__(self):
        return f"{self.event_type} - {self.channel_name or 'N/A'} @ {self.timestamp}"


class SystemNotification(models.Model):
    """
    Stores system notifications that users can view and dismiss.
    Used for version updates, recommended settings, announcements, etc.
    """
    class NotificationType(models.TextChoices):
        VERSION_UPDATE = 'version_update', 'Version Update Available'
        SETTING_RECOMMENDATION = 'setting_recommendation', 'Recommended Setting Change'
        ANNOUNCEMENT = 'announcement', 'System Announcement'
        WARNING = 'warning', 'Warning'
        INFO = 'info', 'Information'

    class Priority(models.TextChoices):
        LOW = 'low', 'Low'
        NORMAL = 'normal', 'Normal'
        HIGH = 'high', 'High'
        CRITICAL = 'critical', 'Critical'

    class Source(models.TextChoices):
        SYSTEM = 'system', 'System Generated'
        DEVELOPER = 'developer', 'Developer Notification'

    # Unique identifier for the notification (e.g., 'version-0.19.0', 'setting-proxy-buffer')
    # This allows deduplication and targeted dismissals
    notification_key = models.CharField(max_length=255, unique=True, db_index=True)

    notification_type = models.CharField(
        max_length=50,
        choices=NotificationType.choices,
        default=NotificationType.INFO,
        db_index=True
    )
    priority = models.CharField(
        max_length=20,
        choices=Priority.choices,
        default=Priority.NORMAL
    )

    # Source of the notification (system-generated vs developer-defined)
    source = models.CharField(
        max_length=20,
        choices=Source.choices,
        default=Source.SYSTEM,
        db_index=True
    )

    title = models.CharField(max_length=255)
    message = models.TextField()

    # Optional action data (e.g., setting key/value for recommendations, release URL for versions)
    action_data = models.JSONField(default=dict, blank=True)

    # Whether this notification is currently active
    is_active = models.BooleanField(default=True, db_index=True)

    # Admin-only notifications require admin privileges to view
    admin_only = models.BooleanField(default=False)

    # Auto-expire after this date (null = never expires)
    expires_at = models.DateTimeField(null=True, blank=True, db_index=True)

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

    class Meta:
        ordering = ['-priority', '-created_at']
        indexes = [
            models.Index(fields=['is_active', '-created_at']),
            models.Index(fields=['notification_type', 'is_active']),
            models.Index(fields=['source', 'is_active']),
        ]

    def __str__(self):
        return f"[{self.notification_type}] {self.title}"

    @classmethod
    def create_version_notification(cls, version, release_url=None, release_notes=None):
        """Create or update a version update notification. Returns (notification, created) tuple."""
        key = f"version-{version}"
        notification, created = cls.objects.update_or_create(
            notification_key=key,
            defaults={
                'notification_type': cls.NotificationType.VERSION_UPDATE,
                'priority': cls.Priority.HIGH,
                'title': f'Version {version} Available',
                'message': f'A new version of Dispatcharr ({version}) is available.',
                'action_data': {
                    'version': version,
                    'release_url': release_url,
                    'release_notes': release_notes,
                },
                'is_active': True,
                'admin_only': True,
            }
        )
        return notification, created

    @classmethod
    def create_setting_recommendation(cls, setting_key, recommended_value, reason, current_value=None):
        """Create a setting recommendation notification. Returns (notification, created) tuple."""
        key = f"setting-{setting_key}"
        notification, created = cls.objects.update_or_create(
            notification_key=key,
            defaults={
                'notification_type': cls.NotificationType.SETTING_RECOMMENDATION,
                'priority': cls.Priority.NORMAL,
                'title': f'Recommended Setting: {setting_key}',
                'message': reason,
                'action_data': {
                    'setting_key': setting_key,
                    'recommended_value': recommended_value,
                    'current_value': current_value,
                },
                'is_active': True,
                'admin_only': True,
            }
        )
        return notification, created


class NotificationDismissal(models.Model):
    """
    Tracks which users have dismissed which notifications.
    Allows users to dismiss notifications once without seeing them again.
    """
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='dismissed_notifications'
    )
    notification = models.ForeignKey(
        SystemNotification,
        on_delete=models.CASCADE,
        related_name='dismissals'
    )
    dismissed_at = models.DateTimeField(auto_now_add=True)

    # Optional: track if user accepted/applied the recommendation
    action_taken = models.CharField(max_length=50, blank=True, null=True)

    class Meta:
        unique_together = ['user', 'notification']
        indexes = [
            models.Index(fields=['user', 'notification']),
        ]

    def __str__(self):
        return f"{self.user.username} dismissed {self.notification.notification_key}"
