from multiprocessing import Pool
from multiprocessing.dummy import Pool as ThreadPool

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import connection as db_connection
from django.db.models import Q
from django.template import Context, Template
from django.utils.timezone import now

from .connections import connections
from .models import Email, EmailTemplate, Log, PRIORITY, STATUS
from .settings import (get_available_backends, get_batch_size,
                       get_log_level, get_sending_order, get_threads_per_process)
from .utils import (get_email_template, parse_emails, parse_priority,
                    split_emails, create_attachments)
from .logutils import setup_loghandlers
from .signals import email_queued
from .settings import context_field_class, get_log_level, get_template_engine, get_override_recipients
from .connections import connections
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.utils.encoding import smart_str
logger = setup_loghandlers("INFO")


def prority_email_message(from_email, to=None, cc=None, bcc=None, subject='', message='',
           html_message='', context=None,  headers=None,
           template=None, attachments=None,commit=True,log_level=None,
           backend=''):
    """
    Returns a django ``EmailMessage`` or ``EmailMultiAlternatives`` object,
    depending on whether html_message is empty.
    """
    if get_override_recipients():
        self.to = get_override_recipients()
    if template is not None:
        engine = get_template_engine()
        subject = engine.from_string(template.subject).render(context)
        plaintext_message = engine.from_string(template.content).render(context)
        multipart_template = engine.from_string(template.html_content)
        html_message = multipart_template.render(context)

    else:
        subject = smart_str(subject)
        plaintext_message = message
        multipart_template = None
        html_message = html_message

    connection = connections[backend or 'default']

    if html_message:
        if plaintext_message:
            msg = EmailMultiAlternatives(
                subject=subject, body=plaintext_message, from_email=from_email,
                to=to, bcc=bcc, cc=cc,
                headers=headers, connection=connection)
            msg.attach_alternative(html_message, "text/html")
        else:
            msg = EmailMultiAlternatives(
                subject=subject, body=html_message, from_email=from_email,
                to=to, bcc=bcc, cc=cc,
                headers=headers, connection=connection)
            msg.content_subtype = 'html'
        if hasattr(multipart_template, 'attach_related'):
            multipart_template.attach_related(msg)


    else:
        msg = EmailMessage(
            subject=subject, body=plaintext_message, from_email=from_email,
            to=to, bcc=bcc, cc=cc,
            headers=headers, connection=connection)
    if attachments:
        attachments = create_attachments(attachments)
        for attachment in attachments:
            if attachment.headers:
                mime_part = MIMENonMultipart(*attachment.mimetype.split('/'))
                mime_part.set_payload(attachment.file.read())
                for key, val in attachment.headers.items():
                    try:
                        mime_part.replace_header(key, val)
                    except KeyError:
                        mime_part.add_header(key, val)
                msg.attach(mime_part)
            else:
                msg.attach(attachment.name, attachment.file.read(), mimetype=attachment.mimetype or None)
            attachment.file.close()
    msg.send()
    try:
        status = STATUS.sent
        message = ''
        exception_type = ''

    except Exception as e:
        status = STATUS.failed
        message = str(e)
        exception_type = type(e).__name__

        # If run in a bulk sending mode, reraise and let the outer
        # layer handle the exception
        if not commit:
            raise
    return {"status": status, "message": message, "exeception_type": exception_type}


def create(sender, recipients=None, cc=None, bcc=None, subject='', message='',
           html_message='', context=None, scheduled_time=None, headers=None,
           template=None, priority=None, render_on_delivery=False, commit=True,
           backend='',attachments=None,log_level=None):
    """
    Creates an email from supplied keyword arguments. If template is
    specified, email subject and content will be rendered during delivery.
    """
    priority = parse_priority(priority)
    status = None if priority == PRIORITY.now else STATUS.queued
    if recipients is None:
        recipients = []
    if cc is None:
        cc = []
    if bcc is None:
        bcc = []
    if context is None:
        context = ''
    if not status :
        result = prority_email_message(sender, recipients, cc, bcc, subject, message, html_message,
                       context, headers, template,attachments=attachments,log_level=log_level, backend=backend)
        status = result["status"]

    # If email is to be rendered during delivery, save all necessary
    # information
    if render_on_delivery:
        email = Email(
            from_email=sender,
            to=recipients,
            cc=cc,
            bcc=bcc,
            scheduled_time=scheduled_time,
            headers=headers, priority=priority, status=status,
            context=context, template=template, backend_alias=backend
        )

    else:

        if template:
            subject = template.subject
            message = template.content
            html_message = template.html_content

        _context = Context(context or {})
        subject = Template(subject).render(_context)
        message = Template(message).render(_context)
        html_message = Template(html_message).render(_context)

        email = Email(
            from_email=sender,
            to=recipients,
            cc=cc,
            bcc=bcc,
            subject=subject,
            message=message,
            html_message=html_message,
            scheduled_time=scheduled_time,
            headers=headers, priority=priority, status=status,
            backend_alias=backend
        )

    if commit:
        email.save()
    if priority == PRIORITY.now:
        email.save()
        email.prority_mail_log_update(log_level=log_level, log_message=result["message"], exception_type=result["exeception_type"])

    return email


def send(recipients=None, sender=None, template=None, context=None, subject='',
         message='', html_message='', scheduled_time=None, headers=None,
         priority=None, attachments=None, render_on_delivery=False,
         log_level=None, commit=True, cc=None, bcc=None, language='',
         backend=''):

    try:
        recipients = parse_emails(recipients)
    except ValidationError as e:
        raise ValidationError('recipients: %s' % e.message)

    try:
        cc = parse_emails(cc)
    except ValidationError as e:
        raise ValidationError('c: %s' % e.message)

    try:
        bcc = parse_emails(bcc)
    except ValidationError as e:
        raise ValidationError('bcc: %s' % e.message)

    if sender is None:
        sender = settings.DEFAULT_FROM_EMAIL

    priority = parse_priority(priority)

    if log_level is None:
        log_level = get_log_level()

    if not commit:
        if priority == PRIORITY.now:
            raise ValueError("send_many() can't be used with priority = 'now'")
        if attachments:
            raise ValueError("Can't add attachments with send_many()")

    if template:
        if subject:
            raise ValueError('You can\'t specify both "template" and "subject" arguments')
        if message:
            raise ValueError('You can\'t specify both "template" and "message" arguments')
        if html_message:
            raise ValueError('You can\'t specify both "template" and "html_message" arguments')

        # template can be an EmailTemplate instance or name
        if isinstance(template, EmailTemplate):
            template = template
            # If language is specified, ensure template uses the right language
            if language:
                if template.language != language:
                    template = template.translated_templates.get(language=language)
        else:
            template = get_email_template(template, language)

    if backend and backend not in get_available_backends().keys():
        raise ValueError('%s is not a valid backend alias' % backend)
    email = create(sender, recipients, cc, bcc, subject, message, html_message,
                   context, scheduled_time, headers, template, priority,
                   render_on_delivery, attachments=attachments, log_level=log_level,backend=backend,)

    if attachments:
        attachments = create_attachments(attachments)
        email.attachments.add(*attachments)

    # if priority == PRIORITY.now:
    #     email.dispatch(log_level=log_level)
    email_queued.send(sender=Email, emails=[email])

    return email


def send_many(kwargs_list):
    """
    Similar to mail.send(), but this function accepts a list of kwargs.
    Internally, it uses Django's bulk_create command for efficiency reasons.
    Currently send_many() can't be used to send emails with priority = 'now'.
    """
    emails = []
    for kwargs in kwargs_list:
        emails.append(send(commit=False, **kwargs))
    if len(emails) > 0:
        Email.objects.bulk_create(emails)
        email_queued.send(sender=Email, emails=emails)


def get_queued():
    """
    Returns a list of emails that should be sent:
     - Status is queued
     - Has scheduled_time lower than the current time or None
    """
    return Email.objects.filter(status=STATUS.queued) \
        .select_related('template') \
        .filter(Q(scheduled_time__lte=now()) | Q(scheduled_time=None)) \
        .order_by(*get_sending_order()).prefetch_related('attachments')[:get_batch_size()]


def send_queued(processes=1, log_level=None):
    """
    Sends out all queued mails that has scheduled_time less than now or None
    """
    queued_emails = get_queued()
    total_sent, total_failed = 0, 0
    total_email = len(queued_emails)

    logger.info('Started sending %s emails with %s processes.' %
                (total_email, processes))

    if log_level is None:
        log_level = get_log_level()

    if queued_emails:

        # Don't use more processes than number of emails
        if total_email < processes:
            processes = total_email

        if processes == 1:
            total_sent, total_failed = _send_bulk(queued_emails,
                                                  uses_multiprocessing=False,
                                                  log_level=log_level)
        else:
            email_lists = split_emails(queued_emails, processes)

            pool = Pool(processes)
            results = pool.map(_send_bulk, email_lists)
            pool.terminate()

            total_sent = sum([result[0] for result in results])
            total_failed = sum([result[1] for result in results])
    message = '%s emails attempted, %s sent, %s failed' % (
        total_email,
        total_sent,
        total_failed
    )
    logger.info(message)
    return (total_sent, total_failed)


def _send_bulk(emails, uses_multiprocessing=True, log_level=None):
    # Multiprocessing does not play well with database connection
    # Fix: Close connections on forking process
    # https://groups.google.com/forum/#!topic/django-users/eCAIY9DAfG0
    if uses_multiprocessing:
        db_connection.close()

    if log_level is None:
        log_level = get_log_level()

    sent_emails = []
    failed_emails = []  # This is a list of two tuples (email, exception)
    email_count = len(emails)

    logger.info('Process started, sending %s emails' % email_count)

    def send(email):
        try:
            email.dispatch(log_level=log_level, commit=False,
                           disconnect_after_delivery=False)
            sent_emails.append(email)
            logger.debug('Successfully sent email #%d' % email.id)
        except Exception as e:
            logger.debug('Failed to send email #%d' % email.id)
            failed_emails.append((email, e))

    # Prepare emails before we send these to threads for sending
    # So we don't need to access the DB from within threads
    for email in emails:
        # Sometimes this can fail, for example when trying to render
        # email from a faulty Django template
        try:
            email.prepare_email_message()
        except Exception as e:
            failed_emails.append((email, e))

    number_of_threads = min(get_threads_per_process(), email_count)
    pool = ThreadPool(number_of_threads)

    pool.map(send, emails)
    pool.close()
    pool.join()

    connections.close()

    # Update statuses of sent and failed emails
    email_ids = [email.id for email in sent_emails]
    Email.objects.filter(id__in=email_ids).update(status=STATUS.sent)

    email_ids = [email.id for (email, e) in failed_emails]
    Email.objects.filter(id__in=email_ids).update(status=STATUS.failed)

    # If log level is 0, log nothing, 1 logs only sending failures
    # and 2 means log both successes and failures
    if log_level >= 1:

        logs = []
        for (email, exception) in failed_emails:
            logs.append(
                Log(email=email, status=STATUS.failed,
                    message=str(exception),
                    exception_type=type(exception).__name__)
            )

        if logs:
            Log.objects.bulk_create(logs)

    if log_level == 2:

        logs = []
        for email in sent_emails:
            logs.append(Log(email=email, status=STATUS.sent))

        if logs:
            Log.objects.bulk_create(logs)

    logger.info(
        'Process finished, %s attempted, %s sent, %s failed' % (
            email_count, len(sent_emails), len(failed_emails)
        )
    )

    return len(sent_emails), len(failed_emails)
