# -*- coding: utf-8 -*-
"""
    Copyright 2013-2014 Olivier Cortès <oc@1flow.io>

    This file is part of the 1flow project.

    1flow is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of
    the License, or (at your option) any later version.

    1flow is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public
    License along with 1flow.  If not, see http://www.gnu.org/licenses/

"""

# import json
import logging

from statsd import statsd
from celery import chain as tasks_chain
from constance import config

from django.conf import settings
from django.db import models, transaction, IntegrityError
from django.db.models.signals import post_save, pre_save, pre_delete
from django.utils.translation import ugettext_lazy as _
from django.utils.text import slugify

from simple_history.models import HistoricalRecords

from oneflow.base.utils import register_task_method
# from oneflow.base.utils.http import clean_url
# from oneflow.base.utils.dateutils import now, datetime

from oneflow.core.constants import MAIL_MATCH_ACTIONS

from ..common import ORIGINS, CONTENT_TYPES
from ..author import Author

from base import (
    BaseItemQuerySet,
    BaseItemManager,
    BaseItem,
    baseitem_process_task,
    baseitem_create_reads_task,
)

from abstract import ContentItem


LOGGER = logging.getLogger(__name__)


__all__ = [
    'Email',

    # Tasks will be added below by register_task_method().
]


# —————————————————————————————————————————————————————————— Manager / QuerySet


def BaseItemQuerySet_email_method(self):
    """ Patch BaseItemQuerySet to know how to return emails. """

    return self.instance_of(Email)


BaseItemQuerySet.email = BaseItemQuerySet_email_method


# ——————————————————————————————————————————————————————————————————————— Model

# BIG FAT WARNING: inheritance order matters. BaseItem must come first,
# else `create_post_task()` is not found by register_task_method().
class Email(BaseItem, ContentItem):

    """ An e-mail.

    Cf. https://docs.python.org/2/library/email.message.html (but not only).
    """

    class Meta:
        app_label = 'core'
        verbose_name = _(u'E-mail')
        verbose_name_plural = _(u'E-mails')

    objects = BaseItemManager()

    # Django simple history.
    history = HistoricalRecords()

    message_id = models.CharField(
        # http://tools.ietf.org/html/rfc5322#section-3.6.4 → no length specified
        # http://www.imc.org/ietf-usefor/2000/Jun/0020.html → 250 bytes
        max_length=256,
        verbose_name=_(u'Unique message-ID'),
        blank=True, unique=True, db_index=True
    )

    is_hidden = models.BooleanField(
        verbose_name=_(u'hidden from the feed'),
        # Should have a partial index.
        default=False, blank=True,
        help_text=_(u'is_hidden is True when the user  has choosen not '
                    u'to store emails in the feed. 1flow always stores '
                    u'them, but does not display the hidden ones.')
    )

    # is_deleted = models.BooleanField(
    #     verbose_name=_(u'deleted from inbox'),
    #     # Should have a partial index.
    #     default=False, blank=True
    # )

    attachments_fetched = models.BooleanField(
        verbose_name=_(u'Attachments fetched?'),

        # Should have a partial index.
        default=False, blank=True
    )

    attachments = models.ManyToManyField(
        BaseItem, blank=True, null=True,
        verbose_name=_(u'Attachments'),
        related_name='emails'
    )

    # TODO: is_multipart ?
    #
    # subject → name
    # date → date_published
    # lang → BaseItem.language
    # From → authors (but how ?)
    # To → ? (user is already reachable via mailfeed)
    #
    # feeds → the mailfeed.

    # —————————————————————————————————————————————————————————————— Django

    def __unicode__(self):
        return _(u'{0} ({1})').format(
            self.name[:40] + (self.name[40:] and u'…'), self.id)

    # —————————————————————————————————————————————————————————————— Properties

    @property
    def is_good(self):
        """ Return True if the e-mail is not hidden. """

        return not self.is_hidden

    @property
    def is_crawlable(self):
        """ Return True if any of the email feeds says so. """

        return self.feeds.exclude(
            MailFeed___match_action=MAIL_MATCH_ACTIONS.STORE).exists()

    # ————————————————————————————————————————————————————————————————— Methods

    @classmethod
    def create_email(cls, email_data, feeds, **kwargs):
        """ Returns ``True`` if email created, ``False`` if a pure duplicate
            (already exists in the same feed), ``None`` if exists but not in
            the same feed. If more than one feed given, only returns ``True``
            or ``False`` (mutualized state is not checked). """

        email = email_data.get('email')

        message_body = {
            'plain': u'',
            'html': u'',
        }

        name = None
        message_id = None

        for part in email.walk():
            if settings.DEBUG:
                # Print a log of debugging ouput about the email structure.
                part_content_type = part.get_content_type()
                LOGGER.debug(u'   |> part %s (%s keys)',
                             part_content_type, len(part.keys()))

                for key, value in part.items():
                    LOGGER.debug(u'      |> %s (len: %s): %s',
                                 key, len(value),
                                 unicode(value)[:40])

            if 'subject' in part and name is None:

                # Some subjects are long text wrapped at ~80 chars.
                name = part.get('subject').replace(
                    u'\r', u'').replace(u'\n', u'')

            if 'message-id' in part and message_id is None:
                message_id = part.get('message-id').strip()

            if part.is_multipart():
                # Multipart parts are just glue,
                # skip to the interesting parts.
                continue

            if not part.get_content_type().startswith('text'):
                LOGGER.error(u'Skipped e-mail %s part (not implemented yet).',
                             part.get_content_type())
                continue

            part_payload = part.get_payload(decode=True)
            charset = part.get_charset()

            if not bool(charset):
                content_type = part.get('Content-Type',

                                        # Using this as default allows
                                        # to have only one mechanic to
                                        # extract the charset.
                                        'text/plain; charset="utf-8"')
                content_type_parts = [
                    x.strip().lower() for x in content_type.split(';')
                ]

                if len(content_type_parts) != 2:
                    LOGGER.error(u'Could not get email part charset, thus '
                                 u'skipped. E-mail body will probably be '
                                 u'incomplete!')
                    continue

                charset = content_type_parts[1].split(
                    u'=')[1].replace('"', '').replace("'", "")

            # LOGGER.info(u'payload: %s of %s, charset=%s',
            #             len(part_payload), type(part_payload), charset)

            message_body_key = (
                'plain' if content_type_parts[0] == 'text/plain' else 'html'
            )

            # Concatenate every body part to get a full body.
            if isinstance(part_payload, str):
                try:
                    message_body[message_body_key] += part_payload.decode(
                        charset)

                except LookupError:
                    message_body[message_body_key] += part_payload.decode(
                        'utf-8', errors='replace')

            else:
                message_body[message_body_key] += part_payload

        # HTML content has precedence, because data will be richer.
        # In case of newsletters / mailing-lists, HTML content will
        # allow us to follow links, while text-only content will
        # [perhaps] not, or less easily. BTW, text/plain content will
        # not contain markdown links, and we will loose the ability
        # to render them in the GUI, while HTML will be converted to
        # Markdown as usual and the user will see richer content.
        if message_body['html']:
            content_type = CONTENT_TYPES.HTML
            content = message_body['html']

        else:
            content_type = CONTENT_TYPES.MARKDOWN
            content = message_body['plain']

        defaults = {
            'name': name,
            'origin': kwargs.pop('origin', ORIGINS.EMAIL),
            'date_published': email_data.get('date'),
            'content': content,
            'content_type': content_type,
        }

        defaults.update(kwargs)

        if message_id.startswith(u'<'):
            # Remove the <> around the ID.
            message_id = message_id[1:-1]

        email, created = cls.objects.get_or_create(message_id=message_id,
                                                   defaults=defaults)

        if created:
            LOGGER.info(u'Created email #%s in feed(s) %s.', message_id,
                        u', '.join(unicode(f) for f in feeds))

            if feeds:
                try:
                    with transaction.atomic():
                        email.feeds.add(*feeds)

                except IntegrityError:
                    LOGGER.exception(u'Integrity error on created email #%s',
                                     message_id)
                    pass

            od = email.add_original_data(
                'email',
                value=email_data.get('raw_email'),

                # We will commit at next call.
                commit=False,

                # We do not launch the post-processing
                # task, it's not implemented yet anyway.
                launch_task=False
            )

            email.add_original_data(
                'matching_rule',
                value=email_data.get('meta'),
                # Use the OD returned before to commit on.
                original_data=od,
                # No post-processing for matching rules.
                launch_task=False
            )

            return email, True

        # —————————————————————————————————————————————————————— existing email

        # Get a change to catch a duplicate if workers were fast.
        if email.duplicate_of_id:
            LOGGER.info(u'Swaping duplicate email #%s with master #%s on '
                        u'the fly.', email.id, email.duplicate_of_id)

            email = email.duplicate_of

        created_retval = False

        previous_feeds_count = email.feeds.count()

        try:
            with transaction.atomic():
                email.feeds.add(*feeds)

        except IntegrityError:
            # Race condition when backfill_if_needed() is run after
            # reception of first item in a stream, and they both create
            # the same email.
            LOGGER.exception(u'Integrity error when adding feeds %s to '
                             u'email #%s', feeds, message_id)

        else:
            if email.feeds.count() > previous_feeds_count:
                # This email is already there, but has not yet been
                # fetched for this feed. It's mutualized, and as such
                # it is considered at partly new. At least, it's not
                # as bad as being a true duplicate.
                created_retval = None

                LOGGER.info(u'Mutualized email #%s #%s in feed(s) %s.',
                            message_id, email.id,
                            u', '.join(unicode(f) for f in feeds))

                email.create_reads(feeds=feeds)

            else:
                # No statsd, because we didn't create any record in database.
                LOGGER.info(u'Duplicate email “%s” #%s #%s in feed(s) %s.',
                            name, message_id, email.id,
                            u', '.join(unicode(f) for f in feeds))

        return email, created_retval

    def fetch_attachments(self, attachments=None, commit=True):
        """ Fetch Email attachments. """

        raise NotImplementedError('refresh for emails (mentions ←→ To:/Cc:)')

        if self.attachments_fetched:
            LOGGER.info(u'%s: attachments already fetched.', self)
            # return

        if attachments is None:
            attachments = self.original_data.twitter_hydrated['attachments']

        all_went_ok = True

        for attachments_name, fetch_attachments_method in (
            ('urls', self.fetch_attachments_urls, ),
            ('media', self.fetch_attachments_media, ),
            ('user_mentions', self.connect_mentions, ),
        ):
            attachments_values = attachments.get(attachments_name, None)

            if attachments_values:
                if not fetch_attachments_method(attachments_values):
                    all_went_ok = False

        if all_went_ok:
            self.attachments_fetched = True

            if commit:
                self.save()

    def connect_mentions(self, user_mentions):
        """ Connect mentions to the current email. """

        raise NotImplementedError('refresh for emails (mentions ←→ To:/Cc:)')

        all_went_ok = True

        for user_mention in user_mentions:
            try:
                author = Author.get_author_from_twitter_user(user_mention)

                self.mentions.add(author)

            except:
                all_went_ok = False
                LOGGER.exception(u'Could not connect user mention '
                                 u'%s in email %s', user_mention, self)

        return all_went_ok

    def post_create_task(self, apply_now=False):
        """ Method meant to be run from a celery task. """

        if apply_now:
            baseitem_create_reads_task.apply((self.id, ))
            baseitem_process_task.apply((self.id, ))

        else:
            post_create_reads_chain = tasks_chain(
                baseitem_process_task.si(self.id),
            )

            baseitem_create_reads_task.apply_async(
                args=(self.id, ),
                kwargs={'stop_chain_on_false': True},
                link=post_create_reads_chain
            )


# ———————————————————————————————————————————————————————————————— Celery Tasks


register_task_method(Email, Email.post_create_task,
                     globals(), queue=u'create')

# ————————————————————————————————————————————————————————————————————— Signals


def email_pre_save(instance, **kwargs):
    """ Make a slug if none. """

    email = instance

    if not email.slug:
        email.slug = slugify(email.name)


def email_post_save(instance, **kwargs):

    email = instance

    if kwargs.get('created', False):

        with statsd.pipeline() as spipe:
            spipe.gauge('emails.counts.total', 1, delta=True)

        globals()['email_post_create_task'].apply_async(
            args=(email.id, ), countdown=config.POST_CREATE_TASKS_DELAY)


def email_pre_delete(instance, **kwargs):

    with statsd.pipeline() as spipe:
        spipe.gauge('emails.counts.total', -1, delta=True)


pre_delete.connect(email_pre_delete, sender=Email)
pre_save.connect(email_pre_save, sender=Email)
post_save.connect(email_post_save, sender=Email)
