import logging

from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta
from html import unescape
from json.decoder import JSONDecodeError
from urllib.parse import urlencode
from uuid import uuid4

from django.conf import settings
from django.core.cache import cache
from django.utils import timezone
from django.utils.html import format_html

import pytz
import requests

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


logger = logging.getLogger(__name__)


REDDIT_URL = "https://www.reddit.com"
REDDIT_API_URL = "https://oauth.reddit.com"

RATE_LIMIT = 60
RATE_LIMIT_DURATION = timedelta(seconds=60)

REDDIT_IMAGE_EXTENSIONS = (".jpg", ".png", ".gif")
REDDIT_VIDEO_EXTENSIONS = (".mp4", ".gifv", ".webm")

# see type prefixes on https://www.reddit.com/dev/api/
REDDIT_POST = "t3"


def get_reddit_authorization_url(user):
    state = str(uuid4())
    cache.set(f"{user.email}-reddit-auth", state)

    params = {
        "client_id": settings.REDDIT_CLIENT_ID,
        "redirect_uri": settings.REDDIT_REDIRECT_URL,
        "state": state,
        "response_type": "code",
        "duration": "permanent",
        "scope": "identity,mysubreddits,save,read",
    }

    authorization_url = f"{REDDIT_URL}/api/v1/authorize"
    return f"{authorization_url}?{urlencode(params)}"


def get_reddit_access_token(code, user):
    client_auth = requests.auth.HTTPBasicAuth(
        settings.REDDIT_CLIENT_ID, settings.REDDIT_CLIENT_SECRET
    )

    response = post(
        f"{REDDIT_URL}/api/v1/access_token",
        data={
            "redirect_uri": settings.REDDIT_REDIRECT_URL,
            "grant_type": "authorization_code",
            "code": code,
        },
        auth=client_auth,
    )

    response_data = response.json()

    user.reddit_access_token = response_data["access_token"]
    user.reddit_refresh_token = response_data["refresh_token"]
    user.save()

    cache.delete(f"{user.email}-reddit-auth")

    return response_data["access_token"], response_data["refresh_token"]


# Note that the API always returns 204's with correct basic auth headers
def revoke_reddit_token(user):
    client_auth = requests.auth.HTTPBasicAuth(
        settings.REDDIT_CLIENT_ID, settings.REDDIT_CLIENT_SECRET
    )

    response = post(
        f"{REDDIT_URL}/api/v1/revoke_token",
        data={"token": user.reddit_refresh_token, "token_type_hint": "refresh_token"},
        auth=client_auth,
    )

    return response.status_code == 204


class RedditBuilder(PostBuilder):
    rule_type = RuleTypeChoices.subreddit

    def build(self):
        results = {}

        if not "data" in self.payload or not "children" in self.payload["data"]:
            return

        entries = self.payload["data"]["children"]

        for entry in entries:
            try:
                post = self.build_post(entry)
            except BuilderDuplicateException:
                logger.warning("Skipping duplicate post")
                continue
            except BuilderSkippedException as e:
                logger.warning(e.message)
                continue
            except BuilderException:
                logger.exception("Failed building post")
                continue

            identifier = post.remote_identifier
            results[identifier] = post

        self.instances = results.values()

    def build_post(self, entry):
        rule = self.stream.rule
        entry_data = entry.get("data", {})
        remote_identifier = entry_data.get("id", "")
        kind = entry.get("kind")

        if remote_identifier in self.existing_posts:
            raise BuilderDuplicateException(payload=entry)
        elif kind != REDDIT_POST:
            raise BuilderParseException(
                message=f"Payload is not an reddit post, its of kind {kind}",
                payload=entry,
            )
        elif not entry_data:
            raise BuilderMissingDataException(
                message=f"Post {remote_identifier} did not contain any data",
                payload=entry,
            )

        try:
            title = entry_data["title"]
            author = entry_data["author"]

            post_url_fragment = entry_data["permalink"]
            direct_url = entry_data["url"]

            is_text = entry_data["is_self"]
            is_video = entry_data["is_video"]

            is_nsfw = entry_data["over_18"]
            is_spoiler = entry_data["spoiler"]
            is_viewed = entry_data["clicked"]
            upvotes = entry_data["ups"]
            downvotes = entry_data["downs"]
            comments = entry_data["num_comments"]
        except KeyError as e:
            raise BuilderMissingDataException(payload=entry) from e

        if not rule.reddit_allow_nfsw and is_nsfw:
            raise BuilderSkippedException("Rule does not allow NSFW posts")
        elif not rule.reddit_allow_spoiler and is_spoiler:
            raise BuilderSkippedException("Rule does not allow spoilers")
        elif not rule.reddit_allow_viewed and is_viewed:
            raise BuilderSkippedException("Post was already seen by user")
        elif not upvotes >= rule.reddit_upvotes_min:
            raise BuilderSkippedException(
                "Post does not meet minimum amount of upvotes"
            )
        elif (
            rule.reddit_downvotes_max is not None
            and downvotes > rule.reddit_downvotes_max
        ):
            raise BuilderSkippedException("Post has more downvotes than allowed")
        elif not comments >= rule.reddit_comments_min:
            raise BuilderSkippedException("Post does not have enough comments")

        title = truncate_text(Post, "title", title)
        author = truncate_text(Post, "author", author)

        if is_text:
            body = self.get_text_post(entry_data)
        elif direct_url.endswith(REDDIT_IMAGE_EXTENSIONS):
            body = self.get_image_post(title, direct_url)
        elif is_video:
            body = self.get_native_video_post(entry_data)
        elif direct_url.endswith(REDDIT_VIDEO_EXTENSIONS):
            body = self.get_video_post(direct_url)
        else:
            body = self.get_url_post(title, direct_url)

        try:
            parsed_date = datetime.fromtimestamp(entry_data["created_utc"])
            created_date = pytz.utc.localize(parsed_date)
        except (OverflowError, OSError) as e:
            raise BuilderParseException(payload=entry) from e
        except KeyError as e:
            raise BuilderMissingDataException(payload=entry) from e

        post_entry = {
            "remote_identifier": remote_identifier,
            "title": title,
            "body": body,
            "author": author,
            "url": f"{REDDIT_URL}{post_url_fragment}",
            "publication_date": created_date,
            "rule": rule,
        }

        return Post(**post_entry)

    def get_text_post(self, entry):
        try:
            uncleaned_body = entry["selftext_html"]
        except KeyError as e:
            raise BuilderMissingDataException(payload=entry) from e

        unescaped_body = unescape(uncleaned_body) if uncleaned_body else ""
        return self.sanitize_fragment(unescaped_body) if unescaped_body else ""

    def get_image_post(self, title, url):
        return format_html(
            "<div><img alt='{title}' src='{url}' loading='lazy' /></div>",
            url=url,
            title=title,
        )

    def get_native_video_post(self, entry):
        try:
            video_info = entry["secure_media"]["reddit_video"]
        except KeyError as e:
            raise BuilderMissingDataException(payload=entry) from e

        return format_html(
            "<div><video controls muted><source src='{url}' type='video/mp4' /></video></div>",
            url=video_info["fallback_url"],
        )

    def get_video_post(self, url):
        extension = next(
            extension.replace(".", "")
            for extension in REDDIT_VIDEO_EXTENSIONS
            if url.endswith(extension)
        )

        if extension == "gifv":
            return format_html(
                "<div><video controls muted><source src='{url}' type='video/mp4' /></video></div>",
                url=url.replace(extension, "mp4"),
            )

        return format_html(
            "<div><video controls muted><source src='{url}' type='video/{extension}' /></video></div>",
            url=url,
            extension=extension,
        )

    def get_url_post(self, title, url):
        return format_html(
            "<div><a target='_blank' rel='noopener noreferrer' alt='{title}' href='{url}' class='link'>Direct url</a></div>",
            url=url,
            title=title,
        )

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


class RedditStream(PostStream):
    rule_type = RuleTypeChoices.subreddit
    headers = {}

    def __init__(self, rule):
        super().__init__(rule)

        self.headers = {
            f"Authorization": f"bearer {self.rule.user.reddit_access_token}"
        }

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

        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}: RedditStream"


class RedditClient(PostClient):
    stream = RedditStream

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

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

                if rate_limitted:
                    logger.warning("Aborting requests, ratelimit hit")
                    break

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

                    try:
                        response_data = future.result()

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

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

                        stream.rule.user.reddit_access_token = None
                        stream.rule.user.save()

                        self.set_rule_error(stream.rule, e)

                        RedditTokenTask.delay(stream.rule.user.pk)

                        break
                    except StreamTooManyException as e:
                        logger.exception("Ratelimit hit, aborting batched subreddits")

                        self.set_rule_error(stream.rule, e)

                        rate_limitted = True
                        break
                    except StreamException as e:
                        logger.exception(
                            f"Stream failed reading content from {stream.rule.url}"
                        )

                        self.set_rule_error(stream.rule, e)

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


class RedditCollector(PostCollector):
    builder = RedditBuilder
    client = RedditClient


class RedditScheduler(Scheduler):
    max_amount = RATE_LIMIT
    max_user_amount = RATE_LIMIT / 4

    def __init__(self, subreddits=[]):
        if not subreddits:
            self.subreddits = CollectionRule.objects.filter(
                type=RuleTypeChoices.subreddit,
                user__reddit_access_token__isnull=False,
                user__reddit_refresh_token__isnull=False,
                enabled=True,
            ).order_by("last_run")[:200]
        else:
            self.subreddits = subreddits

    def get_scheduled_rules(self):
        rule_mapping = {}
        current_amount = 0

        for subreddit in self.subreddits:
            user_pk = subreddit.user.pk

            if current_amount == self.max_amount:
                break

            if user_pk in rule_mapping:
                max_amount_reached = len(rule_mapping[user_pk]) == self.max_user_amount

                if max_amount_reached:
                    continue

                rule_mapping[user_pk].append(subreddit)
                current_amount += 1

                continue

            rule_mapping[user_pk] = [subreddit]
            current_amount += 1

        return list(rule_mapping.values())
