import re
from textwrap import dedent
from typing import Iterable
from urllib.parse import urljoin, urlparse
import uuid
from .settings import history4feed_server_settings
from django.db import models
from rest_framework import validators
from uuid import uuid4
from django.utils.text import slugify
import hyperlink
from django.db.models import Min, Max
from django.db.models import OuterRef, Subquery
from django.db.models import F
from django.utils import timezone
from django.db import transaction

POST_DESCRIPTION_MAX_LENGTH = 2 * 1024 * 1024 # 2MiB
FEED_DESCRIPTION_MAX_LENGTH = 10*1024 # 10KiB

class JobState(models.TextChoices):
    PENDING    = "pending"
    RUNNING    = "running"
    SUCCESS    = "success"
    CANCELLED  = "cancelled"
    FAILED     = "failed"

class FeedType(models.TextChoices):
    RSS = "rss"
    ATOM = "atom"
    SKELETON = "skeleton"
    SEARCH_INDEX = "search_index"

# Create your models here.

class SlugField(models.CharField):
    def get_prep_value(self, value):
        return slugify(str(value))

class Category(models.Model):
    name = SlugField(max_length=1000, primary_key=True)


def stix_id(url):
    return uuid.uuid5(uuid.UUID(str(history4feed_server_settings.HISTORY4FEED_NAMESPACE)), url)

def normalize_url(url):
    try:
        url = re.sub(r'(?<!:)/{2,}', '/', url)
        u = hyperlink.parse(url)
        if not u.scheme:
            raise validators.ValidationError('url must be a full path')
        return u.normalize(url).to_text()
    except Exception as e:
        raise validators.ValidationError(f"URL normalization failed")
    
AUTO_TITLE_TRAIL = "%^%*(%"
def title_as_string(value: str):
    if value.endswith(AUTO_TITLE_TRAIL):
        value = value[:-len(AUTO_TITLE_TRAIL)]
    return value

class Feed(models.Model):
    id = models.UUIDField(primary_key=True, help_text="UUID of feed generated by history4feed")
    title = models.CharField(max_length=1000, help_text="found in the <channel> of RSS output. Is always kept up to date with the latest feed import values for this property.")
    description = models.CharField(max_length=FEED_DESCRIPTION_MAX_LENGTH, help_text="found in the <channel> of RSS output. Is always kept up to date with the latest feed import values for this property.", null=True, default=None)
    url = models.URLField(max_length=1000, unique=True, help_text=dedent("""
        The URL of the RSS or ATOM feed

        Note this will be validated to ensure the feed is in the correct format.
    """), validators=[normalize_url])
    earliest_item_pubdate = models.DateTimeField(null=True, help_text="pubdate of earliest post")
    latest_item_pubdate = models.DateTimeField(null=True, help_text="pubdate of latest post")
    datetime_added = models.DateTimeField(auto_now_add=True, editable=False, help_text="date feed entry was added to database")
    datetime_modified = models.DateTimeField(default=None, null=True, help_text="date feed entry was edited in the database")
    feed_type = models.CharField(choices=FeedType.choices, max_length=12, null=False, editable=False, help_text="type of feed")
    pretty_url = models.URLField(max_length=1000, null=True, default=None)
    freshness = models.DateTimeField(null=True, default=None)

    def get_post_count(self):
        return self.posts.filter(deleted_manually=False).count()
    
    def save(self, *args, **kwargs) -> None:
        if not self.id:
            self.id = stix_id(self.url)
        self.earliest_item_pubdate, self.latest_item_pubdate = self.posts.aggregate(min=Min('pubdate'), max=Max('pubdate')).values()
        self.datetime_modified = self.datetime_modified or self.datetime_added
        return super().save(*args, **kwargs)
    
    def get_pretty_url(self):
        return self.pretty_url or self.url
    
    def set_title(self, title):
        if not self.title or self.title.endswith(AUTO_TITLE_TRAIL):
            self.title = title + AUTO_TITLE_TRAIL
    
    def set_description(self, description):
        if not self.description or self.description.endswith(AUTO_TITLE_TRAIL):
            self.description = description + AUTO_TITLE_TRAIL

class Job(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid4, help_text="UUID of job")
    state = models.CharField(choices=JobState.choices, max_length=12, default=JobState.PENDING, null=False, help_text="state of the job")
    run_datetime = models.DateTimeField(auto_now_add=True, editable=False, help_text="time job was executed")
    earliest_item_requested = models.DateTimeField(null=True, help_text="shows the earliest time for posts requested. Useful for when jobs are run to see if the time range it runs across is expected")
    latest_item_requested = models.DateTimeField(null=True, help_text="shows the latest time for posts requested")
    feed = models.ForeignKey(Feed, on_delete=models.CASCADE)
    info = models.CharField(max_length=FEED_DESCRIPTION_MAX_LENGTH, help_text="contains a useful summary of the job (e.g. number of posts retrieved, errors logged)")
    include_remote_blogs = models.BooleanField(default=False)

    def urls(self):
        retval = {}
        ft_job: FulltextJob = None
        for ft_job in self.fulltext_jobs.all():
            retval[ft_job.status] = retval.get(ft_job.status, [])
            retval[ft_job.status].append(dict(url=ft_job.link, id=ft_job.post_id))
        return retval
    
    def should_skip_post(self, post_link: str):
        return (not self.include_remote_blogs) and urlparse(self.feed.url).hostname.split('.')[-2:] != urlparse(post_link).hostname.split('.')[-2:]
    
    def cancel(self):
        self.update_state(JobState.CANCELLED)
        self.save()
        return
    
    def is_cancelled(self):
        return self.state == JobState.CANCELLED
    
    @transaction.atomic
    def update_state(self, state):
        obj = self.__class__.objects.select_for_update().get(pk=self.pk)
        if obj.state not in [JobState.PENDING, JobState.RUNNING]:
            return obj.state
        obj.state = state
        obj.save()
        self.refresh_from_db()
        return obj.state
    


class FullTextState(models.TextChoices):
    RETRIEVED  = "retrieved"
    SKIPPED    = "skipped"
    CANCELLED  = "cancelled"
    FAILED     = "failed"
    RETRIEVING = "retrieving"

class Post(models.Model):
    id = models.UUIDField(primary_key=True, help_text="UUID of items generated by history4feed")
    datetime_added = models.DateTimeField(auto_now_add=True, editable=False)
    datetime_updated = models.DateTimeField(auto_now=True)
    title = models.CharField(max_length=1000, help_text="found in the <item> element of feed output")
    description = models.CharField(max_length=POST_DESCRIPTION_MAX_LENGTH, blank=True, help_text="found in the <item> element of feed output")
    link = models.URLField(max_length=1000, help_text="link to full article. found in the <item> element of feed output", validators=[normalize_url])
    pubdate = models.DateTimeField(help_text="date of publication.")
    author = models.CharField(max_length=1000, help_text="author of the post", null=True, blank=True)
    categories = models.ManyToManyField(Category, related_name="posts", help_text="categories of the post", blank=True)
    feed = models.ForeignKey(Feed, on_delete=models.CASCADE, related_name="posts", help_text="feed id this item belongs too")
    is_full_text = models.BooleanField(default=False, help_text="if full text has been retrieved")
    content_type = models.CharField(default="plain/text", max_length=200, help_text="content type of the description")
    added_manually = models.BooleanField(default=False)
    deleted_manually = models.BooleanField(default=False, help_text="this post is hidden from user")

    class  Meta:
        constraints = [
            models.UniqueConstraint(fields=["link", "feed"], name="unique_link_by_feed"),
        ]

    def add_categories(self, categories):
        categories = categories or []
        categories = [Category.objects.get_or_create(name=name)[0] for name in categories]
        self.categories.set(categories)


    def save(self, *args, **kwargs) -> None:
        if not self.id:
            pubdate = self.pubdate.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
            self.id = stix_id(f"{self.feed.id}+{self.link}+{pubdate}")
        return super().save(*args, **kwargs)
    
    @classmethod
    def visible_posts(cls):
        return cls.objects.filter(deleted_manually=False).annotate(
            last_job_id=Subquery(
                FulltextJob.objects.filter(
                    post_id=OuterRef('pk')
                ).order_by('-job__run_datetime')  # Ordering by publication date to get the latest book
                .values('job__id')[:1]  # We take only the first (most recent) book
            )
        )

class FulltextJob(models.Model):
    post = models.ForeignKey(Post, on_delete=models.SET_NULL, null=True, related_name="fulltext_jobs")
    job = models.ForeignKey(Job, related_name="fulltext_jobs", on_delete=models.CASCADE)
    status = models.CharField(max_length=15, choices=FullTextState.choices, default=FullTextState.RETRIEVING)
    error_str = models.CharField(max_length=1500, null=True, blank=True)
    link = models.CharField(max_length=1500)


        
    def is_cancelled(self):
        return self.job.state == JobState.CANCELLED