import json
import requests
from collections import defaultdict

from django.db import models
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.template.loader import render_to_string
from django.utils import timezone

from .models import INTERNAL, PUBLIC
from .options import MESSAGES
from .tasks import send_mail


User = get_user_model()


def link_to(target, request):
    if target:
        return request.scheme + '://' + request.get_host() + target.get_absolute_url()


def group_reviewers(reviewers):
    groups = defaultdict(list)
    for reviewer in reviewers:
        groups[reviewer.role].append(reviewer.reviewer)
    return groups


def reviewers_message(reviewers):
    messages = []
    for role, reviewers in group_reviewers(reviewers).items():
        message = ', '.join(str(reviewer) for reviewer in reviewers)
        if role:
            message += ' as ' + str(role)
        message += '.'
        messages.append(message)
    return messages


neat_related = {
    MESSAGES.DETERMINATION_OUTCOME: 'determination',
    MESSAGES.BATCH_DETERMINATION_OUTCOME: 'determinations',
    MESSAGES.UPDATE_LEAD: 'old_lead',
    MESSAGES.NEW_REVIEW: 'review',
    MESSAGES.TRANSITION: 'old_phase',
    MESSAGES.BATCH_TRANSITION: 'transitions',
    MESSAGES.APPLICANT_EDIT: 'revision',
    MESSAGES.EDIT: 'revision',
    MESSAGES.COMMENT: 'comment',
    MESSAGES.SCREENING: 'old_status',
    MESSAGES.REVIEW_OPINION: 'opinion',
    MESSAGES.DELETE_REVIEW: 'review',
}


def is_transition(message_type):
    return message_type in [MESSAGES.TRANSITION, MESSAGES.BATCH_TRANSITION]


def is_ready_for_review(message_type):
    return message_type in [MESSAGES.READY_FOR_REVIEW, MESSAGES.BATCH_READY_FOR_REVIEW]


class AdapterBase:
    messages = {}
    always_send = False

    def message(self, message_type, **kwargs):
        try:
            message = self.messages[message_type]
        except KeyError:
            # We don't know how to handle that message type
            return

        try:
            # see if its a method on the adapter
            method = getattr(self, message)
        except AttributeError:
            return self.render_message(message, **kwargs)
        else:
            # Delegate all responsibility to the custom method
            return method(**kwargs)

    def render_message(self, message, **kwargs):
        return message.format(**kwargs)

    def extra_kwargs(self, message_type, **kwargs):
        return {}

    def get_neat_related(self, message_type, related):
        # We translate the related kwarg into something we can understand
        try:
            neat_name = neat_related[message_type]
        except KeyError:
            # Message type doesn't expect a related object
            if related:
                raise ValueError(f"Unexpected 'related' kwarg provided for {message_type}") from None
            return {}
        else:
            if not related:
                raise ValueError(f"{message_type} expects a 'related' kwarg")
            return {neat_name: related}

    def recipients(self, message_type, **kwargs):
        raise NotImplementedError()

    def batch_recipients(self, message_type, submissions, **kwargs):
        # Default batch recipients is to send a message to each of the recipients that would
        # receive a message under normal conditions
        return [
            {
                'recipients': self.recipients(message_type, submission=submission, **kwargs),
                'submissions': [submission]
            }
            for submission in submissions
        ]

    def process_batch(self, message_type, events, request, user, submissions, related=None, **kwargs):
        events_by_submission = {
            event.submission.id: event
            for event in events
        }
        for recipient in self.batch_recipients(message_type, submissions, **kwargs):
            recipients = recipient['recipients']
            submissions = recipient['submissions']
            events = [events_by_submission[submission.id] for submission in submissions]
            self.process_send(message_type, recipients, events, request, user, submissions=submissions, submission=None, related=related, **kwargs)

    def process(self, message_type, event, request, user, submission, related=None, **kwargs):
        recipients = self.recipients(message_type, submission=submission, related=related, **kwargs)
        self.process_send(message_type, recipients, [event], request, user, submission, related=related, **kwargs)

    def process_send(self, message_type, recipients, events, request, user, submission, submissions=list(), related=None, **kwargs):
        kwargs = {
            'request': request,
            'user': user,
            'submission': submission,
            'submissions': submissions,
            'related': related,
            **kwargs,
        }
        kwargs.update(self.get_neat_related(message_type, related))
        kwargs.update(self.extra_kwargs(message_type, **kwargs))

        message = self.message(message_type, **kwargs)
        if not message:
            return

        for recipient in recipients:
            message_logs = self.create_logs(message, recipient, *events)

            if settings.SEND_MESSAGES or self.always_send:
                status = self.send_message(message, recipient=recipient, logs=message_logs, **kwargs)
            else:
                status = 'Message not sent as SEND_MESSAGES==FALSE'

            message_logs.update_status(status)

            if not settings.SEND_MESSAGES:
                if recipient:
                    debug_message = '{} [to: {}]: {}'.format(self.adapter_type, recipient, message)
                else:
                    debug_message = '{}: {}'.format(self.adapter_type, message)
                messages.add_message(request, messages.DEBUG, debug_message)

    def create_logs(self, message, recipient, *events):
        from .models import Message
        messages = Message.objects.bulk_create(
            Message(
                **self.log_kwargs(message, recipient, event)
            )
            for event in events
        )
        return Message.objects.filter(id__in=[message.id for message in messages])

    def log_kwargs(self, message, recipient, event):
        return {
            'type': self.adapter_type,
            'content': message,
            'recipient': recipient or '',
            'event': event,
        }

    def send_message(self, message, **kwargs):
        # Process the message, should return the result of the send
        # Returning None will not record this action
        raise NotImplementedError()


class ActivityAdapter(AdapterBase):
    adapter_type = "Activity Feed"
    always_send = True
    messages = {
        MESSAGES.TRANSITION: 'handle_transition',
        MESSAGES.BATCH_TRANSITION: 'handle_batch_transition',
        MESSAGES.NEW_SUBMISSION: 'Submitted {submission.title} for {submission.page.title}',
        MESSAGES.EDIT: 'Edited',
        MESSAGES.APPLICANT_EDIT: 'Edited',
        MESSAGES.UPDATE_LEAD: 'Lead changed from {old_lead} to {submission.lead}',
        MESSAGES.DETERMINATION_OUTCOME: 'Sent a determination. Outcome: {determination.clean_outcome}',
        MESSAGES.BATCH_DETERMINATION_OUTCOME: 'batch_determination',
        MESSAGES.INVITED_TO_PROPOSAL: 'Invited to submit a proposal',
        MESSAGES.REVIEWERS_UPDATED: 'reviewers_updated',
        MESSAGES.BATCH_REVIEWERS_UPDATED: 'batch_reviewers_updated',
        MESSAGES.PARTNERS_UPDATED: 'partners_updated',
        MESSAGES.NEW_REVIEW: 'Submitted a review',
        MESSAGES.OPENED_SEALED: 'Opened the submission while still sealed',
        MESSAGES.SCREENING: 'Screening status from {old_status} to {submission.screening_status}',
        MESSAGES.REVIEW_OPINION: '{user} {opinion.opinion_display}s with {opinion.review.author}''s review of {submission}'
    }

    def recipients(self, message_type, **kwargs):
        return [None]

    def extra_kwargs(self, message_type, submission, submissions, **kwargs):
        from .models import INTERNAL
        if message_type in [
                MESSAGES.OPENED_SEALED,
                MESSAGES.REVIEWERS_UPDATED,
                MESSAGES.SCREENING,
                MESSAGES.REVIEW_OPINION,
                MESSAGES.BATCH_REVIEWERS_UPDATED,
                MESSAGES.PARTNERS_UPDATED,
        ]:
            return {'visibility': INTERNAL}

        submission = submission or submissions[0]
        if is_transition(message_type) and not submission.phase.permissions.can_view(submission.user):
            # User's shouldn't see status activity changes for stages that aren't visible to the them
            return {'visibility': INTERNAL}
        return {}

    def reviewers_updated(self, added=list(), removed=list(), **kwargs):
        message = ['Reviewers updated.']
        if added:
            message.append('Added:')
            message.extend(reviewers_message(added))

        if removed:
            message.append('Removed:')
            message.extend(reviewers_message(removed))

        return ' '.join(message)

    def batch_reviewers_updated(self, added, **kwargs):
        base = ['Batch Reviewers Updated.']
        base.extend([
            f'{str(user)} as {role.name}.'
            for role, user in added
            if user
        ])
        return ' '.join(base)

    def batch_determination(self, submissions, determinations, **kwargs):
        submission = submissions[0]
        determination = determinations[submission.id]
        return self.messages[MESSAGES.DETERMINATION_OUTCOME].format(
            determination=determination,
            submission=submission,
        )

    def handle_transition(self, old_phase, submission, **kwargs):
        base_message = 'Progressed from {old_display} to {new_display}'

        new_phase = submission.phase

        staff_message = base_message.format(
            old_display=old_phase.display_name,
            new_display=new_phase.display_name,
        )

        if new_phase.permissions.can_view(submission.user):
            # we need to provide a different message to the applicant
            if not old_phase.permissions.can_view(submission.user):
                old_phase = submission.workflow.previous_visible(old_phase, submission.user)

            applicant_message = base_message.format(
                old_display=old_phase.public_name,
                new_display=new_phase.public_name,
            )

            return json.dumps({
                INTERNAL: staff_message,
                PUBLIC: applicant_message,
            })

        return staff_message

    def handle_batch_transition(self, transitions, submissions, **kwargs):
        kwargs.pop('submission')
        for submission in submissions:
            old_phase = transitions[submission.id]
            return self.handle_transition(old_phase=old_phase, submission=submission, **kwargs)

    def partners_updated(self, added, removed, **kwargs):
        message = ['Partners updated.']
        if added:
            message.append('Added:')
            message.append(', '.join([str(user) for user in added]) + '.')

        if removed:
            message.append('Removed:')
            message.append(', '.join([str(user) for user in removed]) + '.')

        return ' '.join(message)

    def send_message(self, message, user, submission, submissions, **kwargs):
        from .models import Activity, PUBLIC
        visibility = kwargs.get('visibility', PUBLIC)

        try:
            # If this was a batch action we want to pull out the submission
            submission = submissions[0]
        except IndexError:
            pass

        related = kwargs['related']
        if isinstance(related, dict):
            try:
                related = related[submission.id]
            except KeyError:
                pass

        has_correct_fields = all(hasattr(related, attr) for attr in ['author', 'submission', 'get_absolute_url'])
        if has_correct_fields and isinstance(related, models.Model):
            related_object = related
        else:
            related_object = None

        Activity.actions.create(
            user=user,
            submission=submission,
            timestamp=timezone.now(),
            message=message,
            visibility=visibility,
            related_object=related_object,
        )


class SlackAdapter(AdapterBase):
    adapter_type = "Slack"
    always_send = True
    messages = {
        MESSAGES.NEW_SUBMISSION: 'A new submission has been submitted for {submission.page.title}: <{link}|{submission.title}>',
        MESSAGES.UPDATE_LEAD: 'The lead of <{link}|{submission.title}> has been updated from {old_lead} to {submission.lead} by {user}',
        MESSAGES.COMMENT: 'A new {comment.visibility} comment has been posted on <{link}|{submission.title}> by {user}',
        MESSAGES.EDIT: '{user} has edited <{link}|{submission.title}>',
        MESSAGES.APPLICANT_EDIT: '{user} has edited <{link}|{submission.title}>',
        MESSAGES.REVIEWERS_UPDATED: 'reviewers_updated',
        MESSAGES.BATCH_REVIEWERS_UPDATED: 'handle_batch_reviewers',
        MESSAGES.PARTNERS_UPDATED: '{user} has updated the partners on <{link}|{submission.title}>',
        MESSAGES.TRANSITION: '{user} has updated the status of <{link}|{submission.title}>: {old_phase.display_name} → {submission.phase}',
        MESSAGES.BATCH_TRANSITION: 'handle_batch_transition',
        MESSAGES.DETERMINATION_OUTCOME: 'A determination for <{link}|{submission.title}> was sent by email. Outcome: {determination.clean_outcome}',
        MESSAGES.BATCH_DETERMINATION_OUTCOME: 'handle_batch_determination',
        MESSAGES.PROPOSAL_SUBMITTED: 'A proposal has been submitted for review: <{link}|{submission.title}>',
        MESSAGES.INVITED_TO_PROPOSAL: '<{link}|{submission.title}> by {submission.user} has been invited to submit a proposal',
        MESSAGES.NEW_REVIEW: '{user} has submitted a review for <{link}|{submission.title}>. Outcome: {review.outcome},  Score: {review.get_score_display}',
        MESSAGES.READY_FOR_REVIEW: 'notify_reviewers',
        MESSAGES.OPENED_SEALED: '{user} has opened the sealed submission: <{link}|{submission.title}>',
        MESSAGES.REVIEW_OPINION: '{user} {opinion.opinion_display}s with {opinion.review.author}''s review of {submission}',
        MESSAGES.BATCH_READY_FOR_REVIEW: 'batch_notify_reviewers',
        MESSAGES.DELETE_SUBMISSION: '{user} has deleted {submission.title}',
        MESSAGES.DELETE_REVIEW: '{user} has deleted {review.author} review for <{link}|{submission.title}>.',
    }

    def __init__(self):
        super().__init__()
        self.destination = settings.SLACK_DESTINATION_URL
        self.target_room = settings.SLACK_DESTINATION_ROOM

    def slack_links(self, links, submissions):
        return ', '.join(
            f'<{links[submission.id]}|{submission.title}>'
            for submission in submissions
        )

    def extra_kwargs(self, message_type, **kwargs):
        submission = kwargs['submission']
        submissions = kwargs['submissions']
        request = kwargs['request']
        link = link_to(submission, request)
        links = {
            submission.id: link_to(submission, request)
            for submission in submissions
        }
        return {
            'link': link,
            'links': links,
        }

    def recipients(self, message_type, submission, related, **kwargs):
        recipients = [self.slack_id(submission.lead)]

        # Notify second reviewer when first reviewer is done.
        if message_type == MESSAGES.NEW_REVIEW and related:
            if submission.assigned.with_roles().count() == 2 and related.author == submission.assigned.with_roles().first().reviewer:
                recipients.append(self.slack_id(submission.assigned.with_roles().last().reviewer))

        return recipients

    def batch_recipients(self, message_type, submissions, **kwargs):
        # We group the messages by lead
        leads = User.objects.filter(id__in=submissions.values('lead'))
        return [
            {
                'recipients': [self.slack_id(lead)],
                'submissions': submissions.filter(lead=lead),
            } for lead in leads
        ]

    def reviewers_updated(self, submission, link, user, added=list(), removed=list(), **kwargs):
        message = [f'{user} has updated the reviewers on <{link}|{submission.title}>.']

        if added:
            message.append('Added:')
            message.extend(reviewers_message(added))

        if removed:
            message.append('Removed:')
            message.extend(reviewers_message(removed))

        return ' '.join(message)

    def handle_batch_reviewers(self, submissions, links, user, added, **kwargs):
        submissions_text = self.slack_links(links, submissions)
        reviewers_text = ' '.join([
            f'{str(user)} as {role.name},'
            for role, user in added
            if user
        ])
        return (
            '{user} has batch added {reviewers_text} as reviewers on: {submissions_text}'.format(
                user=user,
                submissions_text=submissions_text,
                reviewers_text=reviewers_text,
            )
        )

    def handle_batch_transition(self, user, links, submissions, transitions, **kwargs):
        submissions_text = [
            ': '.join([
                self.slack_links(links, [submission]),
                f'{transitions[submission.id].display_name} → {submission.phase}',
            ])
            for submission in submissions
        ]
        submissions_links = ','.join(submissions_text)
        return (
            '{user} has transitioned the following submissions: {submissions_links}'.format(
                user=user,
                submissions_links=submissions_links,
            )
        )

    def handle_batch_determination(self, submissions, links, determinations, **kwargs):
        submissions_links = ','.join([
            self.slack_links(links, [submission])
            for submission in submissions
        ])

        outcome = determinations[submissions[0].id].clean_outcome

        return (
            'Determinations of {outcome} was sent for: {submissions_links}'.format(
                outcome=outcome,
                submissions_links=submissions_links,
            )
        )

    def notify_reviewers(self, submission, link, **kwargs):
        reviewers_to_notify = []
        for reviewer in submission.reviewers.all():
            if submission.phase.permissions.can_review(reviewer):
                reviewers_to_notify.append(reviewer)

        reviewers = ', '.join(
            str(reviewer) for reviewer in reviewers_to_notify
        )

        return (
            '<{link}|{submission.title}> is ready for review. The following are assigned as reviewers: {reviewers}'.format(
                link=link,
                reviewers=reviewers,
                submission=submission,
            )
        )

    def batch_notify_reviewers(self, submissions, links, **kwargs):
        kwargs.pop('submission')
        kwargs.pop('link')
        return '. '.join(
            self.notify_reviewers(submission, link=links[submission.id], **kwargs)
            for submission in submissions
        )

    def slack_id(self, user):
        if user.slack:
            return f'<{user.slack}>'
        return ''

    def slack_channels(self, submission):
        target_rooms = [self.target_room]
        try:
            extra_rooms = submission.get_from_parent('slack_channel').split(',')
        except AttributeError:
            # Not a submission object, no extra rooms.
            pass
        else:
            target_rooms.extend(extra_rooms)

        # Make sure each channel name starts with a "#".
        target_rooms = [
            room if room.startswith('#') else '#' + room
            for room in target_rooms
            if room
        ]

        return target_rooms

    def send_message(self, message, recipient, submission, **kwargs):
        target_rooms = self.slack_channels(submission)

        if not self.destination or not any(target_rooms):
            errors = list()
            if not self.destination:
                errors.append('Destination URL')
            if not target_rooms:
                errors.append('Room ID')
            return 'Missing configuration: {}'.format(', '.join(errors))

        message = ' '.join([recipient, message]).strip()

        data = {
            "room": target_rooms,
            "message": message,
        }
        response = requests.post(self.destination, json=data)

        return str(response.status_code) + ': ' + response.content.decode()


class EmailAdapter(AdapterBase):
    adapter_type = 'Email'
    messages = {
        MESSAGES.NEW_SUBMISSION: 'funds/email/confirmation.html',
        MESSAGES.COMMENT: 'notify_comment',
        MESSAGES.EDIT: 'messages/email/edit.html',
        MESSAGES.TRANSITION: 'messages/email/transition.html',
        MESSAGES.BATCH_TRANSITION: 'handle_batch_transition',
        MESSAGES.DETERMINATION_OUTCOME: 'messages/email/determination.html',
        MESSAGES.BATCH_DETERMINATION_OUTCOME: 'batch_determination',
        MESSAGES.INVITED_TO_PROPOSAL: 'messages/email/invited_to_proposal.html',
        MESSAGES.BATCH_READY_FOR_REVIEW: 'messages/email/batch_ready_to_review.html',
        MESSAGES.READY_FOR_REVIEW: 'messages/email/ready_to_review.html',
    }

    def get_subject(self, message_type, submission):
        if submission:
            if is_ready_for_review(message_type):
                subject = 'Application ready to review: {submission.title}'.format(submission=submission)
            else:
                subject = submission.page.specific.subject or 'Your application to Open Technology Fund: {submission.title}'.format(submission=submission)
            return subject

    def extra_kwargs(self, message_type, submission, submissions, **kwargs):
        return {
            'subject': self.get_subject(message_type, submission),
        }

    def handle_batch_transition(self, transitions, submissions, **kwargs):
        kwargs.pop('submission')
        for submission in submissions:
            old_phase = transitions[submission.id]
            return self.render_message(
                'messages/email/transition.html',
                submission=submission,
                old_phase=old_phase,
                **kwargs
            )

    def batch_determination(self, determinations, submissions, **kwargs):
        kwargs.pop('submission')
        for submission in submissions:
            determination = determinations[submission.id]
            return self.render_message(
                'messages/email/determination.html',
                submission=submission,
                determination=determination,
                **kwargs
            )

    def notify_comment(self, **kwargs):
        comment = kwargs['comment']
        submission = kwargs['submission']
        if not comment.priviledged and not comment.user == submission.user:
            return self.render_message('messages/email/comment.html', **kwargs)

    def recipients(self, message_type, submission, **kwargs):
        if is_ready_for_review(message_type):
            return self.reviewers(submission)

        if is_transition(message_type):
            # Only notify the applicant if the new phase can be seen within the workflow
            if not submission.phase.permissions.can_view(submission.user):
                return []
        return [submission.user.email]

    def batch_recipients(self, message_type, submissions, **kwargs):
        if not is_ready_for_review(message_type):
            return super().batch_recipients(message_type, submissions, **kwargs)

        reviewers_to_message = defaultdict(list)
        for submission in submissions:
            reviewers = self.reviewers(submission)
            for reviewer in reviewers:
                reviewers_to_message[reviewer].append(submission)

        return [
            {
                'recipients': [reviewer],
                'submissions': submissions,
            } for reviewer, submissions in reviewers_to_message.items()
        ]

    def reviewers(self, submission):
        return [
            reviewer.email
            for reviewer in submission.missing_reviewers.all()
            if submission.phase.permissions.can_review(reviewer) and not reviewer.is_apply_staff
        ]

    def render_message(self, template, **kwargs):
        return render_to_string(template, kwargs)

    def send_message(self, message, submission, subject, recipient, logs, **kwargs):
        try:
            send_mail(
                subject,
                message,
                submission.page.specific.from_address,
                [recipient],
                logs=logs
            )
        except Exception as e:
            return 'Error: ' + str(e)


class DjangoMessagesAdapter(AdapterBase):
    adapter_type = 'Django'
    always_send = True

    messages = {
        MESSAGES.BATCH_REVIEWERS_UPDATED: 'batch_reviewers_updated',
        MESSAGES.BATCH_TRANSITION: 'batch_transition',
        MESSAGES.BATCH_DETERMINATION_OUTCOME: 'batch_determinations',
    }

    def batch_reviewers_updated(self, added, submissions, **kwargs):
        reviewers_text = ' '.join([
            f'{str(user)} as {role.name},'
            for role, user in added
            if user
        ])

        return (
            'Batch reviewers added: ' +
            reviewers_text +
            ' to ' +
            ', '.join(['"{}"'.format(submission.title) for submission in submissions])
        )

    def batch_transition(self, submissions, transitions, **kwargs):
        base_message = 'Successfully updated:'
        transition = '{submission} [{old_display} → {new_display}].'
        transition_messages = [
            transition.format(
                submission=submission.title,
                old_display=transitions[submission.id],
                new_display=submission.phase,
            ) for submission in submissions
        ]
        messages = [base_message, *transition_messages]
        return ' '.join(messages)

    def batch_determinations(self, submissions, determinations, **kwargs):
        outcome = determinations[submissions[0].id].clean_outcome

        base_message = f'Successfully determined as {outcome}: '
        submissions_text = [
            str(submission.title) for submission in submissions
        ]
        return base_message + ', '.join(submissions_text)

    def recipients(self, *args, **kwargs):
        return [None]

    def batch_recipients(self, message_type, submissions, *args, **kwargs):
        return [{
            'recipients': [None],
            'submissions': submissions,
        }]

    def send_message(self, message, request, **kwargs):
        messages.add_message(request, messages.INFO, message)


class MessengerBackend:
    def __init__(self, *adpaters):
        self.adapters = adpaters

    def __call__(self, *args, related=None, **kwargs):
        return self.send(*args, related=related, **kwargs)

    def send(self, message_type, request, user, related, submission=None, submissions=list(), **kwargs):
        from .models import Event
        if submission:
            event = Event.objects.create(type=message_type.name, by=user, submission=submission)
            for adapter in self.adapters:
                adapter.process(message_type, event, request=request, user=user, submission=submission, related=related, **kwargs)

        elif submissions:
            events = Event.objects.bulk_create(
                Event(type=message_type.name, by=user, submission=submission)
                for submission in submissions
            )
            for adapter in self.adapters:
                adapter.process_batch(message_type, events, request=request, user=user, submissions=submissions, related=related, **kwargs)


adapters = [
    ActivityAdapter(),
    SlackAdapter(),
    EmailAdapter(),
    DjangoMessagesAdapter(),
]


messenger = MessengerBackend(*adapters)