import email
import email.errors
import email.header
import email.utils
import logging
import mailbox
import os
import re
import sys
import time
from datetime import datetime as dt

import django.contrib.auth.models as auth_models
from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned
from django.core.files.base import ContentFile
from django.core.management.base import BaseCommand
from django.db.models.query import Q
from django.db.transaction import atomic
from django.template.defaultfilters import slugify
from django.utils.encoding import force_str
from django.utils.timezone import utc
from django_journal.journal import error_record, record

from docbow_project.docbow import models, utils
from docbow_project.docbow.email_utils import u2u_decode

logger = logging.getLogger('docbow.mail_interface')

# modes
EXPEDITION = 'expedition'
ATTACHED_FILE = 'attached_file'
PRIVATE_PREFIX = 'Private - '
PRIVATE_SUFFIX = '-private'


class Command(BaseCommand):
    args = ''
    help = '''Convert a mail to a document send.
    
In case of failure the following return value is returned:
 - 1 failure to parse the message on stdin
 - 2 the mail is missing a subject
 - 3 the subject could not be decoded
 - 4 obligatory attributes or contents were missing
 - 5 notifications mails could not be sent
 - 6 missing Message-ID
 - 7 sender not authorized
 - 8 missing sender
'''

    all_recipients = []
    filenames = []
    message_id = None
    subject = ''

    def add_arguments(self, parser):
        parser.add_argument('--sender')
        parser.add_argument('--file')

    def handle(self, *args, **options):
        if not options.get('sender'):
            self.error('5.6.0 No sender', exit_code=8)
        self.setup_mailing_list_dict()
        try:
            if options.get('file'):
                mail = email.message_from_binary_file(open(options['file'], 'rb'))
            else:
                mail = email.message_from_binary_file(sys.stdin.buffer)
        except email.errors.MessageParseError:
            self.error('5.6.0 Error parsing message', exit_code=1)
        if settings.SENDMAIL_DEBUG_MBOX:
            try:
                mbox = mailbox.mbox(settings.SENDMAIL_DEBUG_MBOX)
                mbox.add(mail)
            except Exception:
                logger.exception('mbox exception')
        try:
            self.handle_mail(mail, (os.environ['RECIPIENT'],), **options)
        except Exception:
            logger.exception('Unknown exception')
            self.error('5.6.0 Internal error when handling the mail', exit_code=5)

    def error(self, msg, exit_code=None, **kwargs):
        sys.stderr.write(msg.format(**kwargs))
        if hasattr(self, 'message_id'):
            error_record(
                'warning-smtp-interface',
                'message {message_id} to {all_recipients} containing {filenames} with subject {subject} refused: '
                + msg,
                message_id=repr(self.message_id),
                all_recipients=', '.join(map(repr, self.all_recipients)),
                filenames=', '.join(map(repr, self.filenames)),
                subject=repr(self.subject),
                **kwargs,
            )
        else:
            error_record('warning-smtp-interface', 'message refused: ' + msg, **kwargs)
        if exit_code:
            sys.exit(exit_code)

    def decode_filename(self, filename):
        '''See if the filename contains encoded-word work around bugs in FileMakerPro'''
        m = re.match(r'=\?(.*)\?(.*)\?(.*)\?=', filename)
        if m:
            result = []
            for content, encoding in email.header.decode_header(filename):
                result.append(force_str(content, encoding or 'ascii'))
            return ''.join(result)
        else:
            return filename

    def setup_mailing_list_dict(self):
        self.mailing_lists = {}
        for mailing_list in models.MailingList.objects.all():
            self.mailing_lists[slugify(mailing_list.name)] = mailing_list

    def resolve_username_for_list(self, username):
        if not username.startswith('liste-'):
            return None
        return self.mailing_lists.get(username[len('liste-') :])

    @atomic
    def handle_mail(self, mail, mail_recipients, **options):
        content_errors = []
        attachments = []
        recipients = []
        mailing_list_recipients = []
        description = ''
        from_email = email.utils.parseaddr(options['sender'])[1]
        if options.get('sender'):
            try:
                sender = auth_models.User.objects.filter(
                    Q(docbowprofile__is_guest=False) | Q(docbowprofile__isnull=True)
                ).get(username=options['sender'])
            except auth_models.User.DoesNotExist:
                self.error('5.6.0 Unknown sender %r' % options['sender'], exit_code=8)
        else:
            try:
                sender = auth_models.User.objects.filter(
                    Q(docbowprofile__is_guest=False) | Q(docbowprofile__isnull=True)
                ).get(email=from_email)
            except auth_models.User.DoesNotExist:
                content_errors.append('No sender user have mail %r' % from_email)
            except MultipleObjectsReturned:
                content_errors.append('Too many sender users have mail %r' % from_email)
        tos = mail.get_all('to', [])
        ccs = mail.get_all('cc', [])
        resent_tos = mail.get_all('resent-to', [])
        resent_ccs = mail.get_all('resent-cc', [])
        self.all_recipients = all_recipients = mail_recipients or [
            b for a, b in email.utils.getaddresses(tos + ccs + resent_tos + resent_ccs)
        ]
        self.message_id = mail.get('Message-ID', None)
        if not self.message_id:
            content_errors.append('Mail is missing a Message-ID')

        try:
            self.private = bool(int(mail.get('Private', 0)))
        except ValueError:
            content_errors.append('Private header is invalid: %r' % mail.get('Private'))

        # determine the filetype
        filetype = None
        self.subject = subject = mail.get('Subject', '')
        if subject.startswith(PRIVATE_PREFIX):
            self.private = True
            subject = subject[len(PRIVATE_PREFIX) :]
        if not subject:
            content_errors.append('Mail is missing a filetype subject')
        else:
            try:
                subject = u2u_decode(subject)
            except Exception:
                content_errors.append('The subject cannot be decoded')
            else:
                try:
                    filetype = models.FileType.objects.get(name=subject)
                except models.FileType.DoesNotExist:
                    content_errors.append('Unkown filetype %r in the subject' % subject)

        for part in mail.walk():
            filename = part.get_filename(None)
            if part.get_content_type() == 'text/plain' and (
                'Content-Disposition' not in part or 'inline' in part['Content-Disposition']
            ):
                charset = part.get_content_charset('us-ascii')
                for cset in (charset, 'iso-8859-15', 'utf-8'):
                    try:
                        description = force_str(part.get_payload(decode=True), cset)
                    except UnicodeDecodeError:
                        continue
                    break
                else:
                    content_errors.append('Error decoding description')
                    continue

            if filename:
                filename = self.decode_filename(filename)
                # be defensive, truncate at 230 characters !
                filename = utils.truncate_filename(filename)
                attachments.append((filename, part.get_payload(decode=True)))

        for email_address in all_recipients:
            username, domain = email_address.split('@', 1)
            if username.endswith(PRIVATE_SUFFIX):
                self.private = True
                username = username[: -len(PRIVATE_SUFFIX)]
                email_address = '%s@%s' % (username, domain)
            # mailing list case
            mailing_list = self.resolve_username_for_list(username)
            if mailing_list is not None:
                mailing_list_recipients.append(mailing_list)
                continue
            # classic user case
            users = auth_models.User.objects.filter(
                Q(docbowprofile__is_guest=False) | Q(docbowprofile__isnull=True)
            ).filter(email=email_address)
            if users:
                for user in users:
                    recipients.append(user)
            else:
                try:
                    user = auth_models.User.objects.get(username=username)
                    recipients.append(user)
                except auth_models.User.DoesNotExist:
                    msg = 'Recipient %r is not an user of the platform' % username
                    content_errors.append(msg)
        self.filenames = [a for a, b in attachments]
        if not len(attachments):
            content_errors.append('You must send at least one attached file')
        if not len(all_recipients):
            content_errors.append('You must have at least one recipient in your message.')

        if content_errors:
            msg = ['5.6.0 The email sent contains many errors:']
            for error in content_errors:
                msg.append(' - %s' % error)
            self.error('\n'.join(msg), exit_code=4)
        else:
            record(
                'smtp-received-document',
                'message-id: {message_id} subject: {subject} to: {all_recipients} filenames: {filenames} private: {private}',
                message_id=self.message_id,
                subject=subject,
                all_recipients=', '.join(map(repr, self.all_recipients)),
                filenames=', '.join(map(repr, self.filenames)),
                private=self.private,
            )
            document = models.Document(
                sender=sender, comment=description, filetype=filetype, private=self.private
            )
            document.save()
            document.to_user.set(recipients)
            document.to_list.set(mailing_list_recipients)
            for filename, payload in attachments:
                content = ContentFile(payload)
                attached_file = models.AttachedFile(document=document, name=filename)
                attached_file.content.save(filename, content, save=False)
                attached_file.save()
            document._timestamp = time.time()
            document.date = dt.fromtimestamp(document._timestamp, utc)
            document.save()
            document.post()
