import datetime
import time
from email.utils import formatdate, parsedate
from xml.sax.saxutils import escape, quoteattr

from django.conf import settings
from django.http import Http404, HttpResponse, HttpResponseNotAllowed, HttpResponseNotModified, StreamingHttpResponse
from django.shortcuts import redirect
from django.views.decorators.cache import cache_control, never_cache
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.gzip import gzip_page
from django.views.decorators.http import require_POST

import accounts.payment_plans as plans
import analytics.log as analytics_log
from .models import Podcast, PodcastEpisode, PodcastSlugMigration
from accounts.models import UserSettings
from analytics.analyze import get_request_ip
from payments.models import RecurringTip
from pinecast.helpers import get_object_or_404, json_response, render, reverse
from pinecast.signatures import signer


DEFAULT_EPISODE_PREFIX = 'S{season}E{episode} - '
VALID_SOURCES = ['direct', 'rss', 'jsonfeed', 'embed']

@never_cache
def listen(req, episode_id):
    ep = get_object_or_404(PodcastEpisode, id=episode_id)

    source = req.GET.get('source', 'direct')
    if source in VALID_SOURCES:
        listen = analytics_log.get_listen_obj(
            ep=ep,
            source=source,
            req=req,
            ip=get_request_ip(req),
            ua=req.META.get('HTTP_USER_AGENT', 'Unknown'),
            timestamp=datetime.datetime.now(),
        )
        analytics_log.commit_listens([listen])

    return redirect(ep.get_raw_url())

def feed(req, podcast_slug):
    pod, redirect = _get_pod_or_redirect(podcast_slug)
    if redirect:
        return redirect

    episodes = pod.get_episodes(select_related=('audio', 'artwork', 'episodefeedbackprompt'))

    # Write the log of this to the analytics back-end(s)
    analytics_log.write_subscription(req, pod, is_private=False)

    caching_response = _handle_caching(req, pod, episodes)
    if caching_response:
        return caching_response

    return _gen_feed(req, pod, episodes)

def feed_private(req, podcast_slug, subscriber):
    try:
        sm = PodcastSlugMigration.objects.select_related('podcast').get(migrate_from=podcast_slug)
        return redirect(reverse('feed_private', podcast_slug=sm.podcast.slug, subscriber=subscriber))
    except PodcastSlugMigration.DoesNotExist:
        pass
    pod = get_object_or_404(Podcast, slug=podcast_slug)

    recurring_tip = get_object_or_404(RecurringTip, podcast=pod, tipper__uuid=subscriber, deactivated=False)
    setattr(recurring_tip, 'podcast', pod)  # ✨magic optimization ✨
    if not recurring_tip.eligible_to_access_private():
        raise Http404()
    episodes = pod.get_episodes(include_private=True)

    # Write the log of this to the analytics back-end(s)
    analytics_log.write_subscription(req, pod, is_private=True)

    caching_response = _handle_caching(req, pod, episodes)
    if caching_response:
        return caching_response

    return _gen_feed(req, pod, episodes, is_private=True)


if settings.FEED_GZIP:
    feed = gzip_page(feed)
    feed_private = gzip_page(feed_private)


def _get_pod_or_redirect(slug):
    try:
        sm = PodcastSlugMigration.objects.select_related('podcast').get(migrate_from=slug)
        return None, redirect(reverse('feed', podcast_slug=sm.podcast.slug))
    except PodcastSlugMigration.DoesNotExist:
        pass
    return get_object_or_404(Podcast.objects.select_related('cover_art', 'owner', 'site'), slug=slug), None

def _handle_caching(req, pod, episodes):
    if req.method not in ('GET', 'HEAD'):
        return HttpResponseNotAllowed(permitted_methods=['GET', 'HEAD'])

    last_update = max(pod.last_feed_update, pod.last_feed_update, *[e.publish for e in episodes])
    last_update = last_update - datetime.timedelta(microseconds=last_update.microsecond)

    expected_etag = last_update.isoformat()

    match_etag = req.META.get('HTTP_IF_NONE_MATCH')
    if match_etag:
        if match_etag.strip('"') == expected_etag:
            return HttpResponseNotModified()
        elif settings.DEBUG:
            print('Expected "{}" to match "{}"'.format(match_etag, expected_etag))

    now = datetime.datetime.now()
    ims = req.META.get('HTTP_IF_MODIFIED_SINCE')
    if ims:
        try:
            ims_parsed = datetime.datetime(*parsedate(ims)[:6])
            if ims_parsed >= last_update:
                return HttpResponseNotModified()
            elif settings.DEBUG:
                print(ims_parsed, last_update)
        except Exception as e:
            if settings.DEBUG:
                print(e)

    return None

def _gen_feed(req, pod, episodes, is_private=False):
    start_time = datetime.datetime.now()

    items = []
    is_demo = UserSettings.get_from_user(pod.owner).plan == plans.PLAN_DEMO

    channel_explicit_tag = '<itunes:explicit>%s</itunes:explicit>' % ('yes' if pod.is_explicit else 'no')

    if not isinstance(episodes, list):
        episodes = list(episodes)

    pod_is_serial = pod.episode_release_type == 'serial'
    # `1, 1,` because if there are no episodes, you'll get an `int object is not iterable` error
    newest_season = max(1, 1, *[x.season for x in episodes if x.season])

    episodes_by_season = {}
    for ep in episodes:
        if not ep.season:
            continue
        season_set = episodes_by_season.setdefault(ep.season, set())
        if not ep.season_episode:
            continue
        season_set.add(ep.season_episode)
    episodes_by_season.setdefault(newest_season, set())  # For empty podcasts
    episodes_without_nums_by_season = {
        season: sum(1 for e in episodes if (e.season == season or not e.season) and not e.season_episode) for
        season in episodes_by_season
    }

    episode_prefix = pod.serial_ep_prefix_format or DEFAULT_EPISODE_PREFIX

    # TODO: Make the database do this with `values()`?
    categories = sorted([c.category for c in pod.podcastcategory_set.all()], key=lambda c: len(c))
    category_map = {}
    for cat in categories:
        spl = cat.split('/')
        cursor = category_map
        for i in spl:
            cursor.setdefault(i, {})
            cursor = cursor[i]

    def render_cat(c):
        for k, v in c.items():
            if not v:
                yield '<itunes:category text=%s />' % quoteattr(k)
            else:
                yield (
                    '<itunes:category text={cat}>{inner}</itunes:category>'
                        .format(cat=quoteattr(k), inner='\n'.join(render_cat(v)))
                )

    def generate_item(ep):
        ep_url = ep.get_url('rss')

        md_desc = ep.get_html_description(is_demo=is_demo)

        title = ep.title
        if pod_is_serial and ep.episode_type == 'full' and ep.season and ep.season_episode:
            title = episode_prefix.format(season=ep.season, episode=ep.season_episode)[:256] + title

        season = ep.season if ep.season else newest_season
        if ep.season_episode:
            season_episode = str(ep.season_episode)
        elif ep.episode_type != 'full':
            season_episode = 1
        else:
            season_episode = episodes_without_nums_by_season[season] + len(episodes_by_season[season])
            while season_episode in episodes_by_season[season]:
                season_episode -= 1
            episodes_by_season[season].add(season_episode)
            episodes_without_nums_by_season[season] -= 1

        season = str(season)
        season_episode = str(season_episode)

        yield (
            '<item>'
            '<title>{title}</title>'
            '<description><![CDATA[{desc}]]></description>'
            '<link>{url}</link>'
            '<guid isPermaLink="false">{guid}</guid>'
            '<pubDate>{publish}</pubDate>'
            '<itunes:author>{author}</itunes:author>'
            '<itunes:subtitle>{subtitle}</itunes:subtitle>'
            '<itunes:image href={artwork} />'
            '<itunes:duration>{duration}</itunes:duration>'.format(
                title=escape(title),
                desc=md_desc,
                url=escape(ep.get_site_url() or ep_url),
                guid=escape(ep.get_guid()),
                publish=formatdate(time.mktime(ep.publish.timetuple())),
                author=escape(pod.author_name),
                subtitle=escape(ep.subtitle),
                artwork=quoteattr(ep.get_image_url()),
                duration=escape(ep.formatted_duration()),
            )
        )

        if title != ep.title:
            yield '<itunes:title>%s</itunes:title>' % escape(ep.title)
        if ep.explicit_override != PodcastEpisode.EXPLICIT_OVERRIDE_CHOICE_NONE:
            yield '<itunes:explicit>%s</itunes:explicit>' % (
                'yes' if ep.explicit_override == PodcastEpisode.EXPLICIT_OVERRIDE_CHOICE_EXPLICIT else 'clean')
        else:
            yield channel_explicit_tag
        if ep.audio:
            yield '<enclosure url=%s length=%s type=%s />' % (
                quoteattr(ep_url),
                quoteattr(str(ep.audio.content_size)),
                quoteattr(ep.audio.content_type)
            )
        if ep.episode_type != 'full':
            yield '<itunes:episodeType>%s</itunes:episodeType>' % escape(ep.episode_type)
        if pod_is_serial:
            yield '<itunes:season>%s</itunes:season>' % escape(season)
        if pod_is_serial and ep.episode_type == 'full':
            yield '<itunes:episode>%s</itunes:episode>' % escape(season_episode)
        if ep.copyright:
            yield '<dc:copyright>%s</dc:copyright>' % escape(ep.copyright)
        if ep.license:
            yield '<dc:rights>%s</dc:rights>' % escape(ep.license)
        yield '</item>'

    def generate_content():
        yield (
            '<?xml version="1.0" encoding="UTF-8"?>\n'
            '<?xml-stylesheet type="text/xsl" media="screen" href="/static/rss.xsl"?>\n'
            '<rss xmlns:atom="http://www.w3.org/2005/Atom"\n'
            '     xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"\n'
            '     xmlns:dc="http://purl.org/dc/elements/1.1/"\n'
            '     xmlns:pinecast="https://pinecast.com/rss-dtd/1.0/"\n'
            '     version="2.0">\n'
            '<channel>\n'
            '<title>{title}</title>\n'
            '<link>{homepage}</link>\n'
            '<atom:link href="{canonical}" rel="self" type="application/rss+xml" />\n'
            '<generator>Pinecast (https://pinecast.com)</generator>\n'
            '{pinecast_site}'
            '<language>{language}</language>'.format(
                title=escape(pod.name),
                homepage=escape(pod.homepage),
                canonical=escape(pod.canonical_feed_url()),
                language=escape(pod.language),
                pinecast_site=(
                    '<pinecast:site>{}</pinecast:site>\n'.format(pod.get_site().get_domain()) if
                    pod.get_site() else
                    ''
                ),
            )
        )

        if pod.copyright:
            yield '<copyright>%s</copyright>' % escape(pod.copyright)
        if pod.episode_release_type != 'episodic':
            yield '<itunes:type>%s</itunes:type>' % escape(pod.episode_release_type)
        if pod.rss_redirect:
            yield '<itunes:new-feed-url>%s</itunes:new-feed-url>' % escape(pod.canonical_feed_url())
        if pod.subtitle:
            yield '<itunes:subtitle>%s</itunes:subtitle>' % escape(pod.subtitle)

        yield (
            '<itunes:author>{author}</itunes:author>\n'
            '<description><![CDATA[{description}]]></description>\n'
            '<itunes:owner>\n'
            '<itunes:name>{author_name}</itunes:name>\n'
            '<itunes:email>{owner_email}</itunes:email>\n'
            '</itunes:owner>\n'
            '{channel_explicit_tag}\n'
            '<itunes:image href={cover_art_attr} />\n'
            '<image>\n'
            '<title>{title}</title>\n'
            '<link>{homepage}</link>\n'
            '<url>{cover_art}</url>\n'
            '</image>'.format(
                author=escape(pod.author_name),
                description=pod.description,
                author_name=escape(pod.author_name),
                owner_email=escape(pod.owner.email),
                channel_explicit_tag=channel_explicit_tag,
                cover_art_attr=quoteattr(pod.get_cover_image_url()),
                cover_art=escape(pod.get_cover_image_url()),
                title=escape(pod.name),
                homepage=escape(pod.homepage),
            )
        )

        yield '\n'.join(render_cat(category_map))

        for ep in episodes:
            if settings.FEED_STREAMING:
                yield '\n'.join(generate_item(ep))
            else:
                yield from generate_item(ep)

        yield '</channel>\n</rss>'

        if UserSettings.get_from_user(pod.owner).plan == plans.PLAN_DEMO:
            if len(episodes) > 10:
                yield '<!-- This feed is truncated because the owner is not a paid customer. -->'
            else:
                yield '<!-- This feed will be truncated at 10 items because the owner is not a paid customer. -->'

        end_time = datetime.datetime.now()
        delta = end_time - start_time
        yield '<!-- generated in {}s {}us -->'.format(delta.seconds, delta.microseconds)

        if settings.DEBUG_TOOLBAR:
            yield '</body>'

    if pod.rss_redirect:
        resp = HttpResponse(status=301)
        resp.setdefault('Location', pod.rss_redirect)
        return resp

    user_agent = req.META.get('HTTP_USER_AGENT', '')
    if settings.DEBUG_TOOLBAR:
        content_type = 'text/html'
    else:
        content_type = 'text/xml' if user_agent.startswith('Mozilla') else 'application/rss+xml'
    content_type_with_encoding = content_type + '; charset=utf-8'

    if settings.FEED_STREAMING:
        resp = StreamingHttpResponse(
            (c + '\n' for c in generate_content()),
            content_type=content_type_with_encoding,
            status=200,
        )
    else:
        resp = HttpResponse(
            '\n'.join(generate_content()),
            content_type=content_type_with_encoding,
            status=200,
        )

    # Get the estimated last update timestamp
    last_update = max(pod.last_feed_update, pod.last_feed_update, *[e.publish for e in episodes])
    # Shave off the microsecond component
    last_update = last_update - datetime.timedelta(microseconds=last_update.microsecond)

    resp.setdefault('ETag', 'W/"{}"'.format(last_update.isoformat()))
    resp.setdefault('Last-Modified', formatdate(time.mktime(last_update.timetuple())))

    resp.setdefault('Access-Control-Allow-Origin', '*')
    resp.setdefault('Access-Control-Request-Method', 'GET')

    return resp

@gzip_page
@json_response(cors=True)
def json_feed(req, podcast_slug):
    pod, redirect = _get_pod_or_redirect(podcast_slug)
    if redirect:
        return redirect
    is_demo = UserSettings.get_from_user(pod.owner).plan == plans.PLAN_DEMO
    pod_is_serial = pod.episode_release_type == 'serial'
    episode_prefix = pod.serial_ep_prefix_format or DEFAULT_EPISODE_PREFIX
    episodes = pod.get_episodes()
    out = {
        'version': 'https://jsonfeed.org/version/1',
        'title': pod.name,
        'description': pod.description,
        'icon': pod.get_cover_image_url(),
        'author': {'name': pod.author_name},
        'feed_url': pod.canonical_feed_url(),
        'items': [
            {
                'id': str(ep.id),
                'url': ep.get_site_url() or ep.get_url('jsonfeed'),
                'title': (
                    episode_prefix.format(season=ep.season, episode=ep.season_episode) + ep.title if
                    pod_is_serial and ep.episode_type == 'full' and ep.season and ep.season_episode else
                    ep.title
                ),
                'content_html': ep.get_html_description(is_demo=is_demo),
                'image': ep.get_image_url(),
                'date_published': ep.publish.strftime('%Y-%m-%dT%H:%M:%SZ'),
                'attachments': [
                    {
                        'url': ep.get_url('jsonfeed'),
                        'mime_type': ep.audio.content_type,
                        'size_in_bytes': ep.audio.content_size,
                        'duration_in_seconds': ep.duration,
                    },
                ],
            } for
            ep in
            episodes
        ]
    }
    if pod.homepage:
        out['home_page_url'] = pod.homepage

    return out


PLAYER_THEMES = set(['minimal', 'thick', 'slim'])
@gzip_page
@cache_control(public=True, max_age=3600)
def player(req, episode_id):
    ep = get_object_or_404(PodcastEpisode.objects.select_related('audio', 'artwork'), id=episode_id)
    pod = get_object_or_404(Podcast.objects.select_related('owner'), id=ep.podcast_id)
    setattr(ep, 'podcast', pod)

    if ep.check_is_private() and (not req.user or req.user.id != pod.owner):
        raise Http404()

    theme = 'minimal'
    if req.GET.get('theme') in PLAYER_THEMES:
        theme = req.GET.get('theme')

    ctx = {'episode': ep}
    if req.GET.get('card'):
        ctx['card'] = True
    resp = render(req, 'player/%s.html' % theme, ctx)

    # If the user is not a demo user, allow the player to be used outside the app.
    if UserSettings.user_meets_plan(ep.podcast.owner, plans.FEATURE_MIN_PLAYER):
        resp.xframe_options_exempt = True
    return resp

@gzip_page
@cache_control(public=True, max_age=3600)
def player_latest(req, podcast_slug):
    pod = get_object_or_404(Podcast, slug__iexact=podcast_slug)
    eps = pod.get_episodes()
    if not eps:
        raise Http404()

    url = reverse('player', episode_id=str(eps[0].id))
    theme = req.GET.get('theme', 'minimal')
    return redirect(url + '?theme={}'.format(theme))

@csrf_exempt
@require_POST
def update_duration(req):
    try:
        ep_id = signer.unsign(req.POST.get('ep_id', ''), max_age=3600).decode('utf-8')
    except Exception as e:
        return HttpResponse(status=400)

    ep = get_object_or_404(PodcastEpisode, id=ep_id)
    try:
        ep.duration = int(float(req.POST.get('duration', '0')))
    except Exception as e:
        return HttpResponse(status=400)
    ep.save()
    return HttpResponse(status=204)
