import logging

from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from json import JSONDecodeError

from django.conf import settings
from django.utils import timezone
from django.utils.html import format_html, urlize

import pytz

from ftfy import fix_text
from requests_oauthlib import OAuth1 as OAuth

from newsreader.news.collection.base import (
    PostBuilder,
    PostClient,
    PostCollector,
    PostStream,
    Scheduler,
)
from newsreader.news.collection.choices import RuleTypeChoices, TwitterPostTypeChoices
from newsreader.news.collection.exceptions import (
    BuilderDuplicateException,
    BuilderException,
    BuilderMissingDataException,
    BuilderParseException,
    StreamDeniedException,
    StreamException,
    StreamNotFoundException,
    StreamParseException,
    StreamTimeOutException,
    StreamTooManyException,
)
from newsreader.news.collection.utils import fetch, truncate_text
from newsreader.news.core.models import Post


logger = logging.getLogger(__name__)

TWITTER_URL = "https://twitter.com"
TWITTER_API_URL = "https://api.twitter.com/1.1"
TWITTER_REQUEST_TOKEN_URL = "https://api.twitter.com/oauth/request_token"
TWITTER_AUTH_URL = "https://api.twitter.com/oauth/authorize"
TWITTER_ACCESS_TOKEN_URL = "https://api.twitter.com/oauth/access_token"
TWITTER_REVOKE_URL = f"{TWITTER_API_URL}/oauth/invalidate_token"


class TwitterBuilder(PostBuilder):
    rule_type = RuleTypeChoices.twitter_timeline

    def build(self):
        results = {}

        for post in self.payload:
            try:
                post = self.build_post(post)
            except BuilderDuplicateException:
                logger.warning("Skipping duplicate post")
                continue
            except BuilderException:
                logger.exception("Failed building post")
                continue

            identifier = post.remote_identifier
            results[identifier] = post

        self.instances = results.values()

    def build_post(self, data):
        remote_identifier = data.get("id_str", "")
        rule = self.stream.rule

        if remote_identifier in self.existing_posts:
            raise BuilderDuplicateException(payload=data)

        try:
            body = urlize(data["full_text"], nofollow=True)
            title = truncate_text(
                Post, "title", self.sanitize_fragment(data["full_text"])
            )

            publication_date = pytz.utc.localize(
                datetime.strptime(data["created_at"], "%a %b %d %H:%M:%S +0000 %Y")
            )
        except KeyError as e:
            raise BuilderMissingDataException(payload=data) from e
        except (OverflowError, OSError) as e:
            raise BuilderParseException(payload=data) from e

        url = f"{TWITTER_URL}/{rule.screen_name}/status/{remote_identifier}"

        if "extended_entities" in data:
            try:
                media_entities = self.get_media_entities(data)
                body += media_entities
            except KeyError as e:
                raise BuilderMissingDataException(
                    message="Failed parsing data for media entities", payload=data
                ) from e

        try:
            if "retweeted_status" in data:
                original_post = data["retweeted_status"]
                original_tweet = urlize(original_post["full_text"], nofollow=True)
                body = f"{body} <br><div>Original tweet: {original_tweet}</div>"
            if "quoted_status" in data:
                original_post = data["quoted_status"]
                original_tweet = urlize(original_post["full_text"], nofollow=True)
                body = f"{body} <br><div>Quoted tweet: {original_tweet}</div>"
        except KeyError as e:
            raise BuilderMissingDataException(
                message="Failed parsing data for original tweet", payload=data
            ) from e

        body = self.sanitize_fragment(body)

        return Post(
            **{
                "remote_identifier": remote_identifier,
                "title": fix_text(title),
                "body": fix_text(body),
                "author": rule.screen_name,
                "publication_date": publication_date,
                "url": url,
                "rule": rule,
            }
        )

    def get_media_entities(self, data):
        media_entities = data["extended_entities"]["media"]
        formatted_entities = ""

        for media_entity in media_entities:
            media_type = media_entity["type"]
            media_url = media_entity["media_url_https"]
            title = media_entity["id_str"]

            if media_type == TwitterPostTypeChoices.photo:
                html_fragment = format_html(
                    """<br /><div><img alt="{title}" src="{media_url}" loading="lazy" /></div>""",
                    title=title,
                    media_url=media_url,
                )

                formatted_entities += html_fragment

            elif media_type in (
                TwitterPostTypeChoices.video,
                TwitterPostTypeChoices.animated_gif,
            ):
                meta_data = media_entity["video_info"]

                videos = sorted(
                    [video for video in meta_data["variants"]],
                    reverse=True,
                    key=lambda video: video.get("bitrate", 0),
                )

                if not videos:
                    continue

                video = videos[0]
                content_type = video["content_type"]
                url = video["url"]

                html_fragment = format_html(
                    """<br /><div><video controls muted><source src="{url}" type="{content_type}" /></video></div> """,
                    url=url,
                    content_type=content_type,
                )

                formatted_entities += html_fragment

        return formatted_entities

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


class TwitterStream(PostStream):
    rule_type = RuleTypeChoices.twitter_timeline

    def read(self):
        oauth = OAuth(
            settings.TWITTER_CONSUMER_ID,
            client_secret=settings.TWITTER_CONSUMER_SECRET,
            resource_owner_key=self.rule.user.twitter_oauth_token,
            resource_owner_secret=self.rule.user.twitter_oauth_token_secret,
        )

        response = fetch(self.rule.url, auth=oauth)

        return self.parse(response), self

    def parse(self, response):
        try:
            return response.json()
        except JSONDecodeError as e:
            raise StreamParseException(
                response=response, message="Failed parsing json"
            ) from e

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


class TwitterClient(PostClient):
    stream = TwitterStream

    def __enter__(self):
        streams = [self.stream(timeline) for timeline 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 StreamTooManyException as e:
                    logger.exception("Ratelimit hit, aborting twitter calls")

                    self.set_rule_error(stream.rule, e)

                    break
                except StreamDeniedException as e:
                    logger.warning(
                        f"Access token expired for user {stream.rule.user.pk}"
                    )

                    stream.rule.user.twitter_oauth_token = None
                    stream.rule.user.twitter_oauth_token_secret = None
                    stream.rule.user.save()

                    self.set_rule_error(stream.rule, e)

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

                    self.set_rule_error(stream.rule, e)

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

                    self.set_rule_error(stream.rule, e)

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


class TwitterCollector(PostCollector):
    builder = TwitterBuilder
    client = TwitterClient


# see https://developer.twitter.com/en/docs/twitter-api/v1/rate-limits
class TwitterTimeLineScheduler(Scheduler):
    def __init__(self, user, timelines=[]):
        self.user = user

        if not timelines:
            self.timelines = (
                user.rules.enabled()
                .filter(type=RuleTypeChoices.twitter_timeline)
                .order_by("last_run")[:200]
            )
        else:
            self.timelines = timelines

    def get_scheduled_rules(self):
        max_amount = self.get_current_ratelimit()
        return self.timelines[:max_amount] if max_amount else []

    def get_current_ratelimit(self):
        endpoint = "application/rate_limit_status.json?resources=statuses"

        if (
            not self.user.twitter_oauth_token
            or not self.user.twitter_oauth_token_secret
        ):
            return

        oauth = OAuth(
            settings.TWITTER_CONSUMER_ID,
            client_secret=settings.TWITTER_CONSUMER_SECRET,
            resource_owner_key=self.user.twitter_oauth_token,
            resource_owner_secret=self.user.twitter_oauth_token_secret,
        )

        try:
            response = fetch(f"{TWITTER_API_URL}/{endpoint}", auth=oauth)
        except StreamException:
            logger.exception(f"Unable to retrieve current ratelimit for {self.user.pk}")
            return

        try:
            payload = response.json()
        except JSONDecodeError:
            logger.exception(f"Unable to parse ratelimit request for {self.user.pk}")
            return

        try:
            return payload["resources"]["statuses"]["/statuses/user_timeline"]["limit"]
        except KeyError:
            return
