from django.db import models
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
import logging
import re
from django.utils import timezone
from django.conf import settings
import os
import uuid
from links.storage import R2Storage
from datetime import timedelta
from urllib.parse import urlparse
from django.utils.text import slugify
import random
from netscan.fields import EncryptedCharField

logger = logging.getLogger(__name__)

class Link(models.Model):
    class LinkType(models.TextChoices):
        LINK = 'LINK', _('Link')
        CUSTOM = 'CUSTOM', _('Custom')
        ACTION = 'ACTION', _('Action')

    alias = models.SlugField(max_length=100, unique=True)
    original_url = models.TextField(blank=True, null=True)
    text = models.TextField(blank=True, null=True)
    link_type = models.CharField(
        max_length=10,
        choices=LinkType.choices,
        default=LinkType.LINK,
    )
    action_config = models.JSONField(
        default=dict,
        blank=True,
        help_text=_("Action configuration (e.g. {'action_type': 'discord_send', 'message_template': '@小黑 {query}'})"),
    )
    click_count = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
    tags = models.ManyToManyField('Tag', blank=True, related_name='links')
    task_states = models.JSONField(default=dict, blank=True, help_text=_("Task checkbox states as {task_hash: bool} mapping"))

    def get_template_parameters(self):
        """Extract template parameters and their default values from original_url"""
        pattern = r'\{([^{}]*)\}'
        matches = re.finditer(pattern, self.original_url)
        params = {}

        for match in matches:
            param_str = match.group(1)
            if ',' in param_str:
                param_name, default_value = param_str.split(',', 1)
                default_value = default_value.strip()
                if default_value.startswith('default='):
                    default_value = default_value[8:].strip('"\'')
                params[param_name.strip()] = default_value
            else:
                params[param_str.strip()] = None

        return params

    def get_processed_url(self, **kwargs):
        """Process template URL with provided parameters"""
        if not self.original_url:
            return None

        processed_url = self.original_url
        pattern = r'\{([^{}]*)\}'
        matches = re.finditer(pattern, self.original_url)

        # 收集所有参数及其默认值
        params = {}
        for match in matches:
            param_str = match.group(1)
            param_full = '{' + param_str + '}'

            # 处理参数字符串
            param_parts = [p.strip() for p in param_str.split(',', 1)]
            param_name = param_parts[0].strip()
            default_value = None

            if len(param_parts) > 1:
                # 处理默认值部分
                default_part = param_parts[1].strip()
                if default_part.startswith('default='):
                    default_value = default_part[8:].strip()  # 移除 'default=' 前缀
                    # 移除引号（如果存在）
                    if (default_value.startswith('"') and default_value.endswith('"')) or \
                       (default_value.startswith("'") and default_value.endswith("'")):
                        default_value = default_value[1:-1]

            params[param_name] = {
                'default': default_value,
                'full_match': param_full
            }

        # 替换所有参数
        for param_name, param_info in params.items():
            value = kwargs.get(param_name, param_info['default'])
            if value is None:
                raise ValueError(f"No value provided for parameter {param_name}")

            # 使用完整的匹配模式进行替换
            processed_url = processed_url.replace(param_info['full_match'], str(value))

        return processed_url

    def __str__(self):
        return self.alias

    def get_absolute_url(self):
        return reverse('link_detail', kwargs={'pk': self.pk})

    def log_url_change(self, old_url):
        try:
            LinkChangeLog.objects.create(
                link=self,
                old_url=old_url,
                new_url=self.original_url
            )
            logger.info(f"Created LinkChangeLog for link {self.pk}")
        except Exception as e:
            logger.error(f"Error creating LinkChangeLog for link {self.pk}: {str(e)}")

class ClickLog(models.Model):
    link = models.ForeignKey(Link, on_delete=models.CASCADE)
    clicked_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-clicked_at']

class LinkChangeLog(models.Model):
    class ChangeType(models.TextChoices):
        URL_CHANGE = 'url_change', _('URL Change')
        TASK_TOGGLE = 'task_toggle', _('Task Toggle')
        CONTENT_EDIT = 'content_edit', _('Content Edit')

    link = models.ForeignKey(Link, on_delete=models.CASCADE, related_name='change_logs')
    change_type = models.CharField(
        _("Change Type"),
        max_length=20,
        choices=ChangeType.choices,
        default=ChangeType.URL_CHANGE
    )
    # For URL changes
    old_url = models.URLField(_("Old URL"), max_length=2000, blank=True, null=True)
    new_url = models.URLField(_("New URL"), max_length=2000, blank=True, null=True)
    # For task toggles and other changes
    metadata = models.JSONField(_("Metadata"), default=dict, blank=True, help_text=_("Additional change data as JSON"))
    changed_at = models.DateTimeField(_("Changed at"), auto_now_add=True)

    class Meta:
        ordering = ['-changed_at']

    def __str__(self):
        if self.change_type == self.ChangeType.URL_CHANGE:
            return f"URL changed from {self.old_url} to {self.new_url}"
        elif self.change_type == self.ChangeType.TASK_TOGGLE:
            task_hash = self.metadata.get('task_hash', 'unknown')
            new_state = self.metadata.get('new_state', 'unknown')
            return f"Task {task_hash[:8]} toggled to {new_state}"
        else:
            return f"{self.get_change_type_display()} at {self.changed_at}"

def generate_random_color():
    colors = [
        'bg-red-100', 'bg-yellow-100', 'bg-green-100', 'bg-blue-100', 'bg-indigo-100', 'bg-purple-100', 'bg-pink-100',
        'bg-orange-100', 'bg-teal-100', 'bg-cyan-100', 'bg-lime-100', 'bg-emerald-100', 'bg-violet-100', 'bg-fuchsia-100'
    ]
    return random.choice(colors)

class Tag(models.Model):
    name = models.CharField(_('Name'), max_length=50, unique=True)
    slug = models.SlugField(_('Slug'), max_length=100, unique=True)
    description = models.TextField(_('Description'), blank=True)
    created_at = models.DateTimeField(_('Created at'), auto_now_add=True)
    updated_at = models.DateTimeField(_('Updated at'), auto_now=True)

    class Meta:
        ordering = ['name']
        verbose_name = _('Tag')
        verbose_name_plural = _('Tags')

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('tag-detail', kwargs={'slug': self.slug})

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name, allow_unicode=True)
        super().save(*args, **kwargs)

class Page(models.Model):
    class ProcessStatus(models.TextChoices):
        PENDING = 'pending', _('Pending')
        PROCESSING = 'processing', _('Processing')
        COMPLETED = 'completed', _('Completed')
        FAILED = 'failed', _('Failed')

    url = models.URLField(_('URL'), max_length=2000)
    title = models.CharField(_('Title'), max_length=200, blank=True)
    summary = models.TextField(_('Summary'), blank=True)
    content = models.TextField(_('Content'), blank=True)
    created_at = models.DateTimeField(_('Created at'), default=timezone.now)
    updated_at = models.DateTimeField(_('Updated at'), auto_now=True)
    tags = models.ManyToManyField('Tag', blank=True, related_name='pages')

    # New fields for processing status
    process_status = models.CharField(
        max_length=20,
        choices=ProcessStatus.choices,
        default=ProcessStatus.PENDING
    )
    retry_count = models.IntegerField(default=0)
    last_retry_at = models.DateTimeField(null=True, blank=True)
    task_states = models.JSONField(default=dict, blank=True, help_text=_("Task checkbox states as {task_hash: bool} mapping"))
    error_message = models.TextField(blank=True)

    # New field for screenshot
    screenshot_path = models.CharField(max_length=255, blank=True)

    # New fields for screenshot errors
    screenshot_error = models.TextField(blank=True, null=True)
    screenshot_last_attempt = models.DateTimeField(null=True)

    class Meta:
        ordering = ['-updated_at']
        verbose_name = _('Page')
        verbose_name_plural = _('Pages')

    def __str__(self):
        return self.title or self.url

    def get_absolute_url(self):
        return reverse('page-detail', kwargs={'pk': self.pk})

    def needs_processing(self):
        return (not self.title or not self.summary or not self.screenshot_path) and \
               self.process_status != self.ProcessStatus.FAILED and \
               self.retry_count < 3

    def get_screenshot_url(self):
        latest_screenshot = self.screenshots.first()
        if latest_screenshot and latest_screenshot.path:
            return os.path.join(settings.MEDIA_URL, latest_screenshot.path)
        return None

    def get_latest_screenshot(self):
        return self.screenshots.first()

class Screenshot(models.Model):
    class Status(models.TextChoices):
        PENDING = 'pending', _('Pending')
        PROCESSING = 'processing', _('Processing')
        COMPLETED = 'completed', _('Completed')
        FAILED = 'failed', _('Failed')

    page = models.ForeignKey('Page', on_delete=models.CASCADE, related_name='screenshots')
    path = models.CharField(max_length=255, blank=True, null=True)
    status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
    error = models.TextField(blank=True, null=True)
    retry_count = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created_at']

    def get_url(self):
        if self.path:
            return os.path.join(settings.MEDIA_URL, self.path)
        return None

class Bookmark(models.Model):
    """
    A bookmarked social-media post (initially x.com / Twitter tweets), imported
    from an external archive (see the x-bookmarks-exporter). Stores the full
    normalised preview payload so the archive page renders offline without
    hitting the source API, plus denormalised index columns for fast list/filter.
    """
    tweet_id = models.CharField(max_length=40, unique=True, db_index=True)
    url = models.URLField(max_length=500, blank=True)

    author_screen_name = models.CharField(max_length=80, blank=True, db_index=True)
    author_name = models.CharField(max_length=120, blank=True)
    author_profile_image_url = models.URLField(max_length=500, blank=True)

    text = models.TextField(blank=True, help_text='Full post text, used for search')
    summary = models.CharField(max_length=300, blank=True,
                               help_text='Optional short human/agent-written summary')

    tweet_created_at = models.DateTimeField(null=True, blank=True, db_index=True)
    bookmark_created_at = models.DateTimeField(null=True, blank=True, db_index=True)

    # engagement counters
    view_count = models.IntegerField(default=0)
    favorite_count = models.IntegerField(default=0)
    retweet_count = models.IntegerField(default=0)
    reply_count = models.IntegerField(default=0)
    bookmark_count = models.IntegerField(default=0)
    quote_count = models.IntegerField(default=0)

    # filter flags (indexed for fast filter pages)
    has_video = models.BooleanField(default=False, db_index=True)
    has_photo = models.BooleanField(default=False, db_index=True)
    has_card = models.BooleanField(default=False, db_index=True)
    is_quote = models.BooleanField(default=False, db_index=True)
    is_retweet = models.BooleanField(default=False, db_index=True)

    # structured payloads for rich rendering (kept separate from full data so
    # the list view can load only what it needs)
    media = models.JSONField(default=list, blank=True,
                             help_text='List of media objects (photo/video w/ poster)')
    card_data = models.JSONField(default=dict, blank=True, null=True)
    quoted_data = models.JSONField(default=dict, blank=True, null=True)

    # the complete normalised export object (for detail view / round-tripping)
    data = models.JSONField(default=dict, blank=True)

    # import dedup tracking
    content_hash = models.CharField(
        max_length=64, blank=True, default='',
        help_text='SHA-256 hash of the import payload; used to skip unchanged re-imports')
    last_imported_at = models.DateTimeField(
        null=True, blank=True,
        help_text='When this bookmark was last imported/refreshed from an external source')

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

    class Meta:
        ordering = ['-tweet_created_at', '-bookmark_created_at', '-created_at']
        indexes = [
            models.Index(fields=['has_video']),
            models.Index(fields=['has_photo']),
            models.Index(fields=['is_quote']),
        ]

    def __str__(self):
        return f'@{self.author_screen_name} {self.tweet_id}'

    def get_absolute_url(self):
        return reverse('bookmark-detail', kwargs={'pk': self.pk})


class Post(models.Model):
    title = models.CharField(_('Title'), max_length=200)
    summary = models.TextField(_('Summary'), blank=True, help_text=_('A brief summary of the post'))
    content = models.TextField(_('Content'))
    is_public = models.BooleanField(_('Is Public'), default=False, db_index=True)
    created_at = models.DateTimeField(_('Created at'), auto_now_add=True)
    updated_at = models.DateTimeField(_('Updated at'), auto_now=True)
    tags = models.ManyToManyField('Tag', blank=True, related_name='posts')
    task_states = models.JSONField(default=dict, blank=True, help_text=_("Task checkbox states as {task_hash: bool} mapping"))

    class Meta:
        ordering = ['-created_at']
        verbose_name = _('Post')
        verbose_name_plural = _('Posts')

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('post-detail', kwargs={'pk': self.pk})

class ImageCollection(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(_('Name'), max_length=200)
    description = models.TextField(_('Description'), blank=True)
    tags = models.ManyToManyField('Tag', verbose_name=_('Tags'), blank=True, related_name='image_collections')
    created_at = models.DateTimeField(_('Created at'), auto_now_add=True)
    updated_at = models.DateTimeField(_('Updated at'), auto_now=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name = _('Image Collection')
        verbose_name_plural = _('Image Collections')

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('collection-detail', kwargs={'pk': self.pk})

class Image(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    collection = models.ForeignKey(ImageCollection, on_delete=models.CASCADE, related_name='images')
    title = models.CharField(_('Title'), max_length=200)
    description = models.TextField(_('Description'), blank=True, null=True)
    # Unified storage: images now live on the same files backend as FileUpload.
    # file_key/content_type/size remain only as legacy R2-era fields until the
    # backfill (manage.py migrate_image_storage) links every image to a FileUpload.
    file = models.OneToOneField('FileUpload', on_delete=models.SET_NULL, null=True, blank=True,
                                related_name='image_record', verbose_name=_('File'))
    file_key = models.CharField(_('File Key'), max_length=255, blank=True)  # legacy R2 storage key
    content_type = models.CharField(_('Content Type'), max_length=100)
    size = models.BigIntegerField(_('Size in bytes'))
    created_at = models.DateTimeField(_('Created at'), auto_now_add=True)
    updated_at = models.DateTimeField(_('Updated at'), auto_now=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name = _('Image')
        verbose_name_plural = _('Images')

    def __str__(self):
        return self.title

    def get_url(self, expires_in=3600):
        """URL of the image.

        Preferred backend: the FileUpload (files) storage, served by Django at
        /ui/files/{pk}-{name}. Falls back to the legacy R2 signed URL for images
        that have not been backfilled yet (pre-migration data).
        """
        if self.file:
            return self.file.download_url
        return R2Storage().get_url(self.file_key, expires_in=expires_in)

    def get_thumbnail_url(self, width=200, height=200, expires_in=3600):
        """URL of the image thumbnail.

        Files backend: server-generated Pillow thumbnail via /ui/files/{pk}/thumb/.
        Legacy fallback: R2 cdn-cgi/image transformation.
        """
        if self.file:
            return self.file.thumbnail_url
        return R2Storage().get_url(
            self.file_key,
            expires_in=expires_in,
            width=width,
            height=height,
            fit='cover'
        )


class FileUpload(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(_('Name'), max_length=255)
    stored_name = models.CharField(_('Stored Name'), max_length=255)
    mime_type = models.CharField(_('MIME Type'), max_length=128, blank=True)
    size = models.PositiveBigIntegerField(_('Size'), default=0)
    is_public = models.BooleanField(_('Is Public'), default=False, db_index=True)
    public_token = models.CharField(_('Public Token'), max_length=64, unique=True, null=True, blank=True)
    expires_at = models.DateTimeField(_('Expires At'), null=True, blank=True)
    source_url = models.URLField(_('Source URL'), max_length=2048, null=True, blank=True, db_index=True)
    download_count = models.PositiveIntegerField(_('Download Count'), default=0)
    created_at = models.DateTimeField(_('Created At'), auto_now_add=True)
    updated_at = models.DateTimeField(_('Updated At'), auto_now=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name = _('File Upload')
        verbose_name_plural = _('File Uploads')

    def __str__(self):
        return self.name

    @property
    def file_path(self):
        return os.path.join(settings.FILE_UPLOADS_FOLDER, self.stored_name)

    @property
    def is_expired(self):
        if self.expires_at:
            return timezone.now() > self.expires_at
        return False

    @property
    def is_image(self):
        return self.mime_type.startswith('image/')

    @property
    def is_publicly_accessible(self):
        return self.is_public and not self.is_expired

    @property
    def download_url(self):
        import re
        safe_name = re.sub(r'[^\w.\-]', '-', self.name)
        return f'/ui/files/{self.pk}-{safe_name}'

    @property
    def thumb_path(self):
        """Disk path for the generated thumbnail (image resize / video first-frame)."""
        folder = os.path.join(settings.FILE_UPLOADS_FOLDER, 'thumbs')
        return os.path.join(folder, f'{self.stored_name}.jpg')

    @property
    def thumbnail_url(self):
        """URL of the server-generated thumbnail; empty for non-media types."""
        if not (self.is_image or self.mime_type.startswith('video/')):
            return ''
        return f'/ui/files/{self.pk}/thumb/'

    @property
    def public_url(self):
        import re as _re
        safe_name = _re.sub(r'[^\w.\-]', '-', self.name)
        path = f'/public/files/{self.pk}-{safe_name}'
        if not self.is_public:
            return path
        try:
            domain = SiteSettings.get().public_sharing_domain.rstrip('/')
        except Exception:
            domain = ''
        return f'{domain}{path}' if domain else path

    def formatted_size(self):
        if self.size < 1024:
            return f"{self.size} B"
        elif self.size < 1024 * 1024:
            return f"{self.size / 1024:.1f} KB"
        elif self.size < 1024 * 1024 * 1024:
            return f"{self.size / (1024 * 1024):.1f} MB"
        return f"{self.size / (1024 * 1024 * 1024):.2f} GB"


class SiteSettings(models.Model):
    """
    Singleton model for app-wide configuration.
    Always use SiteSettings.get() to retrieve the instance.
    """

    telegram_bot_token = EncryptedCharField(
        blank=True,
        help_text=_('Global Telegram bot token (from @BotFather). Used by all apps that send Telegram alerts.'),
    )
    telegram_chat_id = EncryptedCharField(
        blank=True,
        help_text=_('Telegram chat ID to receive alerts. Get yours from @userinfobot.'),
    )
    public_sharing_domain = models.CharField(
        _('Public Sharing Domain'),
        max_length=255,
        blank=True,
        default='',
        help_text=_(
            'Domain (with protocol) used for public sharing links, e.g. https://go.example.com. '
            'Leave blank to use the same domain as the app.'
        ),
    )
    max_concurrent_screenshot_jobs = models.IntegerField(
        _('Max Concurrent Screenshot Jobs'),
        default=2,
        help_text=_(
            'Maximum number of Chromium screenshot processes to run simultaneously. '
            'Lower this value if the container runs out of memory.'
        ),
    )
    schedule_pending_pages_interval = models.IntegerField(
        _('Schedule Pending Pages Interval (seconds)'),
        default=120,
        help_text=_('How often (in seconds) to check for pending pages and queue them for processing.'),
    )
    schedule_pending_screenshots_interval = models.IntegerField(
        _('Schedule Pending Screenshots Interval (seconds)'),
        default=120,
        help_text=_('How often (in seconds) to check for stuck or pending screenshots and retry them.'),
    )
    screenshot_job_timeout_seconds = models.IntegerField(
        _('Screenshot Job Timeout (seconds)'),
        default=300,
        help_text=_(
            'Maximum time in seconds a single screenshot job may run before it is cancelled and '
            'marked as failed. Default is 300 (5 minutes).'
        ),
    )

    class Meta:
        verbose_name = _('Site Settings')

    def __str__(self):
        return 'Site Settings'

    @classmethod
    def get(cls):
        obj, _ = cls.objects.get_or_create(pk=1)
        return obj

    def save(self, *args, **kwargs):
        self.pk = 1
        super().save(*args, **kwargs)
