import logging

from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import timedelta

from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.utils import timezone

from feedparser import parse

from newsreader.news.collection.base import (
    PostBuilder,
    PostClient,
    PostCollector,
    PostStream,
)
from newsreader.news.collection.choices import RuleTypeChoices
from newsreader.news.collection.exceptions import (
    StreamException,
    StreamNotFoundException,
    StreamParseException,
    StreamTimeOutException,
)
from newsreader.news.collection.utils import (
    build_publication_date,
    fetch,
    truncate_text,
)
from newsreader.news.core.models import Post


logger = logging.getLogger(__name__)


class FeedBuilder(PostBuilder):
    rule__type = RuleTypeChoices.feed

    def build(self):
        instances = []

        with FeedDuplicateHandler(self.stream.rule) as duplicate_handler:
            entries = self.payload.get("entries", [])

            for entry in entries:
                post = self.build_post(entry)
                instances.append(post)

            self.instances = duplicate_handler.check(instances)

    def build_post(self, entry):
        field_mapping = {
            "id": "remote_identifier",
            "title": "title",
            "summary": "body",
            "link": "url",
            "published_parsed": "publication_date",
            "author": "author",
        }
        data = {"rule_id": self.stream.rule.pk}

        for field, model_field in field_mapping.items():
            if field not in entry:
                continue

            value = truncate_text(Post, model_field, entry[field])

            if field == "published_parsed":
                data[model_field] = build_publication_date(value)
            elif field == "summary":
                data[model_field] = self.sanitize_fragment(value)
            else:
                data[model_field] = value

        content_details = self.get_content_details(entry)

        # use content details key if it contains more information
        if "body" not in data or len(data["body"]) < len(content_details):
            data["body"] = content_details

        return Post(**data)

    def get_content_details(self, entry):
        content_items = entry.get("content")

        if not content_items:
            return ""

        content_details = "\n ".join([item.get("value") for item in content_items])
        return self.sanitize_fragment(content_details)

    def __str__(self):
        return f"{self.stream.rule.pk}: FeedBuilder"


class FeedStream(PostStream):
    rule_type = RuleTypeChoices.feed

    def read(self):
        response = fetch(self.rule.url)

        return self.parse(response), self

    def parse(self, response):
        try:
            return parse(response.content)
        except TypeError as e:
            message = "Could not parse feed"
            raise StreamParseException(response=response, message=message) from e

    def __str__(self):
        return f"{self.rule.pk}: FeedStream"


class FeedClient(PostClient):
    stream = FeedStream

    def __enter__(self):
        streams = [self.stream(rule) for rule in self.rules]

        with ThreadPoolExecutor(max_workers=10) as executor:
            futures = {executor.submit(stream.read): stream for stream in streams}

            for future in as_completed(futures):
                stream = futures[future]

                try:
                    payload = future.result()

                    stream.rule.error = None
                    stream.rule.succeeded = True

                    yield payload
                except (StreamNotFoundException, StreamTimeOutException) as e:
                    logger.warning(f"Request failed for {stream.rule.url}")

                    self.set_rule_error(stream.rule, e)

                    continue
                except StreamException as e:
                    logger.exception(f"Request failed for {stream.rule.url}")

                    self.set_rule_error(stream.rule, e)

                    continue
                finally:
                    stream.rule.last_run = timezone.now()
                    stream.rule.save()


class FeedCollector(PostCollector):
    builder = FeedBuilder
    client = FeedClient


class FeedDuplicateHandler:
    duplicate_fields = ("url", "title", "body", "rule")
    time_slot_minutes = 10

    def __init__(self, rule):
        self.queryset = rule.posts.all()

    def __enter__(self):
        self.existing_identifiers = self.queryset.filter(
            remote_identifier__isnull=False
        ).values_list("remote_identifier", flat=True)
        return self

    def __exit__(self, *args, **kwargs):
        pass

    def check(self, instances):
        deduplicated_instances = self.deduplicate_instances(instances)
        checked_instances = []

        for instance in deduplicated_instances:
            if instance.remote_identifier in self.existing_identifiers:
                existing_post = self.handle_duplicate_identifier(instance)

                checked_instances.append(existing_post)

                continue
            elif self.in_database(instance):
                existing_post = self.get_duplicate_in_database(instance)

                if self.in_time_slot(instance, existing_post):
                    checked_instances.append(
                        self.update_existing_post(instance, existing_post)
                    )
                    continue

            checked_instances.append(instance)

        return checked_instances

    def in_database(self, post):
        values = {field: getattr(post, field, None) for field in self.duplicate_fields}

        for existing_post in self.queryset.filter(**values):
            if self.is_duplicate(existing_post, values):
                return True

    def in_time_slot(self, instance, existing_post):
        time_delta_slot = timedelta(minutes=self.time_slot_minutes)

        time_difference = instance.publication_date - existing_post.publication_date

        if time_difference <= time_delta_slot:
            return True

    def deduplicate_instances(self, instances):
        sorted_instances = sorted(
            instances, key=lambda instance: instance.publication_date, reverse=True
        )
        deduplicated_instances = []

        for instance in sorted_instances:
            instance_identifier = instance.remote_identifier
            duplicate = False

            values = {
                field: getattr(instance, field, None) for field in self.duplicate_fields
            }

            for deduplicated_instance in deduplicated_instances:
                deduplicated_identifier = deduplicated_instance.remote_identifier
                has_identifiers = deduplicated_identifier and instance_identifier

                is_same_identifier = (
                    has_identifiers and deduplicated_identifier == instance_identifier
                )
                is_duplicate = self.is_duplicate(deduplicated_instance, values)

                if is_duplicate or is_same_identifier:
                    duplicate = True
                    break

            if duplicate:
                continue

            deduplicated_instances.append(instance)

        return deduplicated_instances

    def is_duplicate(self, existing_post, values):
        return all(
            getattr(existing_post, field, None) == value
            for field, value in values.items()
        )

    def handle_duplicate_identifier(self, instance):
        try:
            existing_post = self.queryset.get(
                remote_identifier=instance.remote_identifier
            )
        except ObjectDoesNotExist:
            logger.exception(
                f"Duplicate handler tried retrieving post {instance.remote_identifier} but failed doing so."
            )
            return instance
        except MultipleObjectsReturned:
            existing_posts = self.queryset.filter(
                remote_identifier=instance.remote_identifier
            ).order_by("-publication_date")
            existing_post = existing_posts.last()
            existing_posts.exclude(pk=existing_post.pk).delete()

        updated_post = self.update_existing_post(instance, existing_post)

        return updated_post

    def get_duplicate_in_database(self, instance):
        query_values = {
            field: getattr(instance, field, None) for field in self.duplicate_fields
        }

        try:
            existing_post = self.queryset.get(**query_values)
        except ObjectDoesNotExist:
            logger.exception(
                f"Duplicate handler tried retrieving post {instance.remote_identifier} but failed doing so."
            )
            return instance
        except MultipleObjectsReturned:
            existing_posts = self.queryset.filter(**query_values).order_by(
                "-publication_date"
            )
            existing_post = existing_posts.last()
            existing_posts.exclude(pk=existing_post.pk).delete()

        return existing_post

    def update_existing_post(self, instance, existing_post):
        for field in instance._meta.get_fields():
            getattr(existing_post, field.name, object())
            new_value = getattr(instance, field.name, object())

            if new_value and field.name != "id":
                setattr(existing_post, field.name, new_value)

        return existing_post
