import csv
import math
import random

from django.contrib.auth.decorators import login_required
from django.core.mail import send_mass_mail
from django.db import transaction
from django.db.models import Q
from django.http import HttpResponse
from django.http import HttpResponseBadRequest, HttpResponseNotAllowed
from django.shortcuts import render, redirect, get_object_or_404
from django.template import Context, Template
from django.views.decorators.http import require_POST

# @@@ switch to pinax-teams
from symposion.teams.models import Team

from symposion.conf import settings
from symposion.proposals.models import ProposalBase, ProposalSection
from symposion.utils.mail import send_email

from symposion.reviews.forms import ReviewForm, SpeakerCommentForm
from symposion.reviews.forms import BulkPresentationForm
from symposion.reviews.models import (
    ReviewAssignment, Review, LatestVote, ProposalResult, NotificationTemplate,
    ResultNotification, promote_proposal
)


def access_not_permitted(request):
    return render(request, "symposion/reviews/access_not_permitted.html")


def proposals_generator(request, queryset, user_pk=None, check_speaker=True):

    for obj in queryset:
        # @@@ this sucks; we can do better
        if check_speaker:
            if request.user in [s.user for s in obj.speakers()]:
                continue

        try:
            obj.result
        except ProposalResult.DoesNotExist:
            ProposalResult.objects.get_or_create(proposal=obj)

        obj.comment_count = obj.result.comment_count
        obj.score = obj.result.score
        obj.total_votes = obj.result.vote_count
        obj.plus_two = obj.result.plus_two
        obj.plus_one = obj.result.plus_one
        obj.minus_one = obj.result.minus_one
        obj.minus_two = obj.result.minus_two
        lookup_params = dict(proposal=obj)

        if user_pk:
            lookup_params["user__pk"] = user_pk
        else:
            lookup_params["user"] = request.user

        try:
            obj.user_vote = LatestVote.objects.get(**lookup_params).vote
            obj.user_vote_css = LatestVote.objects.get(**lookup_params).css_class()
        except LatestVote.DoesNotExist:
            obj.user_vote = None
            obj.user_vote_css = "no-vote"

        yield obj


VOTE_THRESHOLD = settings.SYMPOSION_VOTE_THRESHOLD

POSITIVE = "positive"
NEGATIVE = "negative"
INDIFFERENT = "indifferent"
CONTROVERSIAL = "controversial"
TOO_FEW = "too_few"

REVIEW_STATUS_FILTERS = {
    # proposals with at least VOTE_THRESHOLD reviews and at least one +2 and no -2s, sorted by
    # the 'score'
    POSITIVE: lambda qs: qs.filter(result__vote_count__gte=VOTE_THRESHOLD, result__plus_two__gt=0,
                                   result__minus_two=0).order_by("-result__score"),
    # proposals with at least VOTE_THRESHOLD reviews and at least one -2 and no +2s, reverse
    # sorted by the 'score'
    NEGATIVE: lambda qs: qs.filter(result__vote_count__gte=VOTE_THRESHOLD, result__minus_two__gt=0,
                                   result__plus_two=0).order_by("result__score"),
    # proposals with at least VOTE_THRESHOLD reviews and neither a +2 or a -2, sorted by total
    # votes (lowest first)
    INDIFFERENT: lambda qs: qs.filter(result__vote_count__gte=VOTE_THRESHOLD, result__minus_two=0,
                                      result__plus_two=0).order_by("result__vote_count"),
    # proposals with at least VOTE_THRESHOLD reviews and both a +2 and -2, sorted by total
    # votes (highest first)
    CONTROVERSIAL: lambda qs: qs.filter(
        result__vote_count__gte=VOTE_THRESHOLD, result__plus_two__gt=0,
        result__minus_two__gt=0).order_by("-result__vote_count"),
    # proposals with fewer than VOTE_THRESHOLD reviews
    TOO_FEW: lambda qs: qs.filter(
        result__vote_count__lt=VOTE_THRESHOLD).order_by("result__vote_count"),
}


# Returns a list of all proposals, proposals reviewed by the user, or the proposals the user has
# yet to review depending on the link user clicks in dashboard
@login_required
def review_section(request, section_slug, assigned=False, reviewed="all"):

    if not request.user.has_perm("reviews.can_review_%s" % section_slug):
        return access_not_permitted(request)

    section = get_object_or_404(ProposalSection, section__slug=section_slug)
    queryset = ProposalBase.objects.filter(kind__section=section.section).order_by('id')

    if assigned:
        assignments = ReviewAssignment.objects.filter(user=request.user)\
            .values_list("proposal__id")
        queryset = queryset.filter(id__in=assignments)

    # passing reviewed in from reviews.urls and out to review_list for
    # appropriate template header rendering
    if reviewed == "all":
        queryset = queryset.select_related("result").select_subclasses()
        reviewed = "all_reviews"
    elif reviewed == "reviewed":
        queryset = queryset.filter(reviews__user=request.user)
        reviewed = "user_reviewed"
    else:
        queryset = queryset.exclude(reviews__user=request.user).exclude(
            speaker__user=request.user)
        reviewed = "user_not_reviewed"

    # lca2017 #21 -- chairs want to be able to see their own proposals in the list
    check_speaker = not request.user.has_perm("reviews.can_manage_%s" % section_slug)
    proposals = proposals_generator(request, queryset, check_speaker=check_speaker)

    ctx = {
        "proposals": proposals,
        "section": section,
        "reviewed": reviewed,
    }

    return render(request, "symposion/reviews/review_list.html", ctx)


@login_required
def review_all_proposals_csv(request):
    ''' Returns a CSV representation of all of the proposals this user has
    permisison to review. '''

    response = HttpResponse("text/csv")
    response['Content-Disposition'] = 'attachment; filename="proposals.csv"'
    writer = csv.writer(response, quoting=csv.QUOTE_NONNUMERIC)

    queryset = ProposalBase.objects.filter()

    # The fields from each proposal object to report in the csv
    fields = [
        "id", "proposal_type", "speaker_name", "speaker_email", "title",
        "submitted", "other_speakers", "speaker_travel",
        "speaker_accommodation", "cancelled", "status", "score", "total_votes",
        "minus_two", "minus_one", "plus_one", "plus_two",
    ]

    # Fields are the heading
    writer.writerow(fields)

    for proposal in proposals_generator(request, queryset, check_speaker=False):

        proposal.speaker_name = proposal.speaker.name
        section_slug = proposal.kind.section.slug
        kind_slug = proposal.kind.slug
        proposal.proposal_type = kind_slug

        proposal.other_speakers = ", ".join(
            speaker.name
            for speaker in proposal.additional_speakers.all()
        )

        proposal.speaker_travel = ", ".join(
            str(bool(speaker.travel_assistance))
            for speaker in proposal.speakers()
        )

        proposal.speaker_accommodation = ", ".join(
            str(bool(speaker.accommodation_assistance))
            for speaker in proposal.speakers()
        )

        if not request.user.has_perm("reviews.can_review_%s" % section_slug):
            continue

        csv_line = [getattr(proposal, field) for field in fields]

        writer.writerow(csv_line)

    return response


@login_required
def review_random_proposal(request, section_slug):
    # lca2017 #16 view for random proposal

    if not request.user.has_perm("reviews.can_review_%s" % section_slug):
        return access_not_permitted(request)

    section = get_object_or_404(ProposalSection, section__slug=section_slug)
    queryset = ProposalBase.objects.filter(kind__section=section.section)
    # Remove ones already reviewed
    queryset = queryset.exclude(reviews__user=request.user)
    # Remove talks the reviewer can't vote on -- their own.
    queryset = queryset.exclude(speaker__user=request.user)
    queryset = queryset.exclude(additional_speakers__user=request.user)

    if len(queryset) == 0:
        return redirect("review_section", section_slug=section_slug, reviewed="all")

    # Direct reviewers to underreviewed proposals
    too_few_set = REVIEW_STATUS_FILTERS[TOO_FEW](queryset)
    controversial_set = REVIEW_STATUS_FILTERS[CONTROVERSIAL](queryset)

    if len(too_few_set) > 0:
        proposals = too_few_set.all()
    elif len(controversial_set) > 0 and random.random() < 0.2:
        proposals = controversial_set.all()
    else:
        # Select a proposal with less than the median number of total votes
        proposals = proposals_generator(request, queryset, check_speaker=False)
        proposals = list(proposals)
        proposals.sort(key=lambda proposal: proposal.total_votes)
        # The first half is the median or less.
        proposals = proposals[:math.ceil(len(proposals) / 2)]

    # Realistically, there shouldn't be all that many proposals to choose
    # from, so this should be cheap.
    chosen = random.choice(proposals)
    return redirect("review_detail", pk=chosen.pk)


@login_required
def review_list(request, section_slug, user_pk):

    # if they're not a reviewer admin and they aren't the person whose
    # review list is being asked for, don't let them in
    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
        if not request.user.pk == user_pk:
            return access_not_permitted(request)

    queryset = ProposalBase.objects.select_related("speaker__user", "result")
    reviewed = LatestVote.objects.filter(user__pk=user_pk).values_list("proposal", flat=True)
    queryset = queryset.filter(kind__section__slug=section_slug)
    queryset = queryset.filter(pk__in=reviewed)
    proposals = queryset.order_by("submitted")

    admin = request.user.has_perm("reviews.can_manage_%s" % section_slug)

    proposals = proposals_generator(request, proposals, user_pk=user_pk, check_speaker=not admin)

    ctx = {
        "proposals": proposals,
    }
    return render(request, "symposion/reviews/review_list.html", ctx)


@login_required
def review_admin(request, section_slug):

    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
        return access_not_permitted(request)

    def reviewers():
        already_seen = set()

        for team in Team.objects.filter(permissions__codename="can_review_%s" % section_slug):
            for membership in team.memberships.filter(Q(state="member") | Q(state="manager")):
                user = membership.user
                if user.pk in already_seen:
                    continue
                already_seen.add(user.pk)

                user.comment_count = Review.objects.filter(
                    user=user,
                    proposal__kind__section__slug=section_slug,
                ).count()

                user_votes = LatestVote.objects.filter(
                    user=user,
                    proposal__kind__section__slug=section_slug,
                )
                user.total_votes = user_votes.exclude(
                    vote=LatestVote.VOTES.ABSTAIN,
                ).count()
                user.plus_two = user_votes.filter(
                    vote=LatestVote.VOTES.PLUS_TWO,
                ).count()
                user.plus_one = user_votes.filter(
                    vote=LatestVote.VOTES.PLUS_ONE,
                ).count()
                user.minus_one = user_votes.filter(
                    vote=LatestVote.VOTES.MINUS_ONE,
                ).count()
                user.minus_two = user_votes.filter(
                    vote=LatestVote.VOTES.MINUS_TWO,
                ).count()
                user.abstain = user_votes.filter(
                    vote=LatestVote.VOTES.ABSTAIN,
                ).count()
                if user.total_votes == 0:
                    user.average = "-"
                else:
                    user.average = (
                        ((user.plus_two * 2) + user.plus_one) -
                        ((user.minus_two * 2) + user.minus_one)
                    ) / (user.total_votes * 1.0)

                yield user

    reviewers_sorted = list(reviewers())
    reviewers_sorted.sort(key=lambda reviewer: 0 - reviewer.total_votes)

    ctx = {
        "section_slug": section_slug,
        "reviewers": reviewers_sorted,
    }
    return render(request, "symposion/reviews/review_admin.html", ctx)


# FIXME: This view is too complex according to flake8
@login_required
@transaction.atomic
def review_detail(request, pk):

    proposals = ProposalBase.objects.select_related("result").select_subclasses()
    proposal = get_object_or_404(proposals, pk=pk)

    if not request.user.has_perm("reviews.can_review_%s" % proposal.kind.section.slug):
        return access_not_permitted(request)

    speakers = [s.user for s in proposal.speakers()]

    if not request.user.is_superuser and request.user in speakers:
        return access_not_permitted(request)

    admin = request.user.has_perm("reviews.can_manage_%s" % proposal.kind.section.slug)

    try:
        latest_vote = LatestVote.objects.get(proposal=proposal, user=request.user)
    except LatestVote.DoesNotExist:
        latest_vote = None

    if request.method == "POST":
        if request.user in speakers:
            return access_not_permitted(request)

        if "vote_submit" in request.POST or "vote_submit_and_random" in request.POST:
            review_form = ReviewForm(request.POST)
            if review_form.is_valid():

                review = review_form.save(commit=False)
                review.user = request.user
                review.proposal = proposal
                review.save()

                if "vote_submit_and_random" in request.POST:
                    next_page = redirect("user_random", proposal.kind.section.slug)
                    next_page["Location"] += "#invalid_fragment"  # Hack.
                else:
                    next_page = redirect(request.path)

                return next_page
            else:
                message_form = SpeakerCommentForm()
        elif "message_submit" in request.POST:
            message_form = SpeakerCommentForm(request.POST)
            if message_form.is_valid():

                message = message_form.save(commit=False)
                message.user = request.user
                message.proposal = proposal
                message.save()

                for speaker in speakers:
                    if speaker and speaker.email:
                        ctx = {
                            "proposal": proposal,
                            "message": message,
                            "reviewer": False,
                        }
                        send_email(
                            [speaker.email], "proposal_new_message",
                            context=ctx
                        )

                return redirect(request.path)
            else:
                initial = {}
                if latest_vote:
                    initial["vote"] = latest_vote.vote
                if request.user in speakers:
                    review_form = None
                else:
                    review_form = ReviewForm(initial=initial)
        elif "result_submit" in request.POST:
            if admin:
                result = request.POST["result_submit"]

                if result == "accept":
                    proposal.result.status = "accepted"
                    proposal.result.save()
                elif result == "reject":
                    proposal.result.status = "rejected"
                    proposal.result.save()
                elif result == "undecide":
                    proposal.result.status = "undecided"
                    proposal.result.save()
                elif result == "standby":
                    proposal.result.status = "standby"
                    proposal.result.save()
                return redirect(request.path)
        elif "publish_changes" in request.POST:
            if admin and proposal.result.status == "accepted":
                promote_proposal(proposal)
                return redirect(request.path)
    else:
        initial = {}
        if latest_vote:
            initial["vote"] = latest_vote.vote
        if request.user in speakers:
            review_form = None
        else:
            review_form = ReviewForm(initial=initial)
        message_form = SpeakerCommentForm()

    proposal.comment_count = proposal.result.comment_count
    proposal.total_votes = proposal.result.vote_count
    proposal.plus_two = proposal.result.plus_two
    proposal.plus_one = proposal.result.plus_one
    proposal.minus_one = proposal.result.minus_one
    proposal.minus_two = proposal.result.minus_two

    reviews = Review.objects.filter(proposal=proposal).order_by("-submitted_at")
    messages = proposal.messages.order_by("submitted_at")

    return render(request, "symposion/reviews/review_detail.html", {
        "proposal": proposal,
        "latest_vote": latest_vote,
        "reviews": reviews,
        "review_messages": messages,
        "review_form": review_form,
        "message_form": message_form,
        "is_manager": admin
    })


@login_required
@require_POST
@transaction.atomic
def review_delete(request, pk):
    review = get_object_or_404(Review, pk=pk)
    section_slug = review.section

    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
        return access_not_permitted(request)

    review = get_object_or_404(Review, pk=pk)
    review.delete()

    return redirect("review_detail", pk=review.proposal.pk)


@login_required
def review_status(request, section_slug=None, key=None):

    if not request.user.has_perm("reviews.can_review_%s" % section_slug):
        return access_not_permitted(request)

    ctx = {
        "section_slug": section_slug,
        "vote_threshold": VOTE_THRESHOLD,
    }

    queryset = ProposalBase.objects.select_related("speaker__user", "result").select_subclasses()
    if section_slug:
        queryset = queryset.filter(kind__section__slug=section_slug)

    proposals = dict((key, filt(queryset)) for key, filt in REVIEW_STATUS_FILTERS.items())

    admin = request.user.has_perm("reviews.can_manage_%s" % section_slug)

    for status in proposals:
        if key and key != status:
            continue
        proposals[status] = list(proposals_generator(request, proposals[status], check_speaker=not admin))

    if key:
        ctx.update({
            "key": key,
            "proposals": proposals[key],
        })
    else:
        ctx["proposals"] = proposals

    return render(request, "symposion/reviews/review_stats.html", ctx)


@login_required
def review_assignments(request):
    if not request.user.groups.filter(name="reviewers").exists():
        return access_not_permitted(request)
    assignments = ReviewAssignment.objects.filter(
        user=request.user,
        opted_out=False
    )
    return render(request, "symposion/reviews/review_assignment.html", {
        "assignments": assignments,
    })


@login_required
@require_POST
def review_assignment_opt_out(request, pk):
    review_assignment = get_object_or_404(
        ReviewAssignment, pk=pk, user=request.user)
    if not review_assignment.opted_out:
        review_assignment.opted_out = True
        review_assignment.save()
        ReviewAssignment.create_assignments(
            review_assignment.proposal, origin=ReviewAssignment.AUTO_ASSIGNED_LATER)
    return redirect("review_assignments")


@login_required
@transaction.atomic
def review_bulk_update(request, section_slug):
    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
        return access_not_permitted(request)
    if request.method == "POST":
        form = BulkPresentationForm(request.POST)
        if form.is_valid():
            talk_ids = form.cleaned_data["talk_ids"].split(",")
            status = form.cleaned_data["status"] or "accepted"
            talks = ProposalBase.objects.filter(id__in=talk_ids).select_related("result")
            for talk in talks:
                talk.result.status = status
                talk.result.save()
            return redirect("review_section", section_slug=section_slug)
    else:
        form = BulkPresentationForm()

    return render(request, "symposion/reviews/review_bulk_update.html", {
        "form": form,
    })


@login_required
def result_notification(request, section_slug, status):
    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
        return access_not_permitted(request)

    proposals = ProposalBase.objects.filter(kind__section__slug=section_slug, result__status=status).select_related("speaker__user", "result").select_subclasses()
    notification_templates = NotificationTemplate.objects.all()

    ctx = {
        "section_slug": section_slug,
        "status": status,
        "proposals": proposals,
        "notification_templates": notification_templates,
    }
    return render(request, "symposion/reviews/result_notification.html", ctx)


@login_required
def result_notification_prepare(request, section_slug, status):
    if request.method != "POST":
        return HttpResponseNotAllowed(["POST"])

    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
        return access_not_permitted(request)

    proposal_pks = []
    try:
        for pk in request.POST.getlist("_selected_action"):
            proposal_pks.append(int(pk))
    except ValueError:
        return HttpResponseBadRequest()
    proposals = ProposalBase.objects.filter(
        kind__section__slug=section_slug,
        result__status=status,
    )
    proposals = proposals.filter(pk__in=proposal_pks)
    proposals = proposals.select_related("speaker__user", "result")
    proposals = proposals.select_subclasses()

    notification_template_pk = request.POST.get("notification_template", "")
    if notification_template_pk:
        notification_template = NotificationTemplate.objects.get(pk=notification_template_pk)
    else:
        notification_template = None

    ctx = {
        "section_slug": section_slug,
        "status": status,
        "notification_template": notification_template,
        "proposals": proposals,
        "proposal_pks": ",".join([str(pk) for pk in proposal_pks]),
    }
    return render(request, "symposion/reviews/result_notification_prepare.html", ctx)


@login_required
def result_notification_send(request, section_slug, status):
    if request.method != "POST":
        return HttpResponseNotAllowed(["POST"])

    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
        return access_not_permitted(request)

    if not all([k in request.POST for k in ["proposal_pks", "from_address", "subject", "body"]]):
        return HttpResponseBadRequest()

    try:
        proposal_pks = [int(pk) for pk in request.POST["proposal_pks"].split(",")]
    except ValueError:
        return HttpResponseBadRequest()

    proposals = ProposalBase.objects.filter(
        kind__section__slug=section_slug,
        result__status=status,
    )
    proposals = proposals.filter(pk__in=proposal_pks)
    proposals = proposals.select_related("speaker__user", "result")
    proposals = proposals.select_subclasses()

    notification_template_pk = request.POST.get("notification_template", "")
    if notification_template_pk:
        notification_template = NotificationTemplate.objects.get(pk=notification_template_pk)
    else:
        notification_template = None

    emails = []

    for proposal in proposals:
        rn = ResultNotification()
        rn.proposal = proposal
        rn.template = notification_template
        rn.to_address = proposal.speaker_email
        rn.from_address = request.POST["from_address"]
        proposal_context = proposal.notification_email_context()
        rn.subject = Template(request.POST["subject"]).render(
            Context({
                "proposal": proposal_context
            })
        )
        rn.body = Template(request.POST["body"]).render(
            Context({
                "proposal": proposal_context
            })
        )
        rn.save()
        emails.append(rn.email_args)

    send_mass_mail(emails)

    return redirect("result_notification", section_slug=section_slug, status=status)