from datetime import datetime as dt
import sys
import mailbox
import email
import email.errors
import email.utils
import email.header
import logging
import re
import time
from urllib.request import urlopen

from django.conf import settings
from django.core.management.base import BaseCommand
import django.contrib.auth.models as auth_models
from django.core.files.base import ContentFile
from django.core.exceptions import MultipleObjectsReturned
from django.utils.timezone import utc, make_aware
from django.utils.encoding import force_text
from django.template.defaultfilters import slugify

from docbow_project.docbow import models, utils
from docbow_project.docbow.email_utils import u2u_decode
from django_journal.journal import record
from django.db.transaction import atomic

from docbow_project.docbow import app_settings as docbow_app_settings
from docbow_project.pfwb import app_settings

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

# modes
EXPEDITION = 'expedition'
ATTACHED_FILE = 'attached_file'


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

    def add_arguments(self, parser):
        parser.add_argument('recipient', type=str)
        parser.add_argument("--sender")
        parser.add_argument("--file")

    def handle(self, *args, **options):
        if not options.get('sender'):
            self.error('7.7.1 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 as e:
            self.error('7.7.1 Error parsing message', exit_code=1)
        if settings.SENDMAIL_DEBUG_MBOX:
            try:
                mbox = mailbox.mbox(settings.SENDMAIL_DEBUG_MBOX)
                mbox.add(mail)
            except:
                logger.exception('mbox exception')
        try:
            self.handle_mail(mail, (options['recipient'],), **options)
        except Exception as e:
            logger.exception('Unknown exception')
            self.error('7.7.1 Internal error when handling the mail', exit_code=5)

    def error(self, msg, exit_code=None, **kwargs):
        sys.stderr.write(msg.format(**kwargs) + '\n')
        if hasattr(self, 'message_id'):
            record(
                'warning-smtp-interface',
                'message {message_id} to {all_recipients} containing {filenames} refused: ' + msg,
                message_id=self.message_id,
                all_recipients=self.all_recipients,
                filenames=self.filenames,
                **kwargs,
            )
        else:
            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_text(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 = u''
        from_email = email.utils.parseaddr(options['sender'])[1]
        if not from_email:
            self.error('7.7.1 No sender', exit_code=8)
        if from_email == app_settings.PFWB_SENDMAIL_TABELLIO_EXPEDITION_EMAIL:
            mode = EXPEDITION
        elif from_email == app_settings.PFWB_SENDMAIL_ATTACHED_FILE_EMAIL:
            mode = ATTACHED_FILE
        else:
            self.error('Email unknown, not authorized to post', exit_code=7)
            return
        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:
            self.error('7.7.1 Mail is missing a Message-ID', exit_code=6)

        # determine the filetype
        filetype = None
        if mode == ATTACHED_FILE:
            subject = mail.get('Subject', None)
            if not subject:
                self.error('7.7.1 Mail is missing a subject', exit_code=2)
            try:
                subject = u2u_decode(subject)
            except Exception as e:
                self.error('7.7.1 The subject cannot be decoded', exit_code=3)
            try:
                filetype = models.FileType.objects.get(name=subject)
            except models.FileType.DoesNotExist:
                record('warning', 'unknown filetype ' '{filetype}, using default filetype', filetype=subject)
        else:
            tabellio_doc_type = mail.get('x-tabellio-doc-type')
            if tabellio_doc_type:
                try:
                    filetype = models.FileType.objects.get(
                        tabelliodoctype__tabellio_doc_type=tabellio_doc_type
                    )
                except models.FileType.DoesNotExist:
                    record(
                        'warning',
                        'unknown x-tabellio-doc-type ' '{tabellio_doc_type}, using default filetype',
                        tabellio_doc_type=tabellio_doc_type,
                    )
                except models.FileType.MultipleObjectsReturned:
                    record(
                        'warning',
                        'unknown x-tabellio-doc-type ' '{tabellio_doc_type}, using default filetype',
                        tabellio_doc_type=tabellio_doc_type,
                    )
        if filetype is None:
            try:
                filetype = models.FileType.objects.get(id=app_settings.PFWB_SENDMAIL_DEFAULT_TYPE_ID)
            except models.FileType.DoesNotExist:
                filetype, created = models.FileType.objects.get_or_create(
                    name=app_settings.PFWB_SENDMAIL_DEFAULT_TYPE_NAME
                )

        if mode == ATTACHED_FILE:
            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')
                    description = force_text(part.get_payload(decode=True), charset)

                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)))
        else:
            url = mail.get('x-tabellio-doc-url')
            try:
                stream = urlopen(url)
            except Exception as e:
                self.error('7.7.1 Unable to retrieve %r: %r' % (url, e), exit_code=3)
            content = stream.read()
            subject = mail.get('Subject', None)
            if not subject:
                self.error('7.7.1 Mail is missing a subject', exit_code=2)
            try:
                subject = u2u_decode(subject)
            except Exception as e:
                self.error('7.7.1 The subject cannot be decoded', exit_code=3)
            try:
                name = subject.split(':', 1)[1].strip()
                # be defensive, truncate
                name = name[: docbow_app_settings.TRUNCATE_FILENAME]
            except IndexError:
                self.error('7.7.1 Filename cannot be extracted from the subject', exit_code=3)
            if not name.endswith('.pdf'):
                name += '.pdf'
            attachments.append((name, content))

        for email_address in all_recipients:
            username, domain = email_address.split('@', 1)
            # 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
            try:
                user = auth_models.User.objects.get(username=username)
                recipients.append(user)
            except auth_models.User.DoesNotExist:
                try:
                    users_qs = auth_models.User.objects.filter(email=email_address)
                    if not users_qs.count():
                        raise auth_models.User.DoesNotExist()
                    recipients.extend(users_qs.all())
                except auth_models.User.DoesNotExist:
                    msg = 'Recipient %r is not an user of the platform' % email_address
                    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.')
        try:
            if mode == ATTACHED_FILE:
                user_id = app_settings.PFWB_SENDMAIL_ATTACHED_FILE_USER_ID
            else:
                user_id = app_settings.PFWB_SENDMAIL_TABELLIO_EXPEDITION_USER_ID
            sender = auth_models.User.objects.get(id=user_id)
        except auth_models.User.DoesNotExist:
            content_errors.append('No user match the sender user_id %s in mode ' '%s' % (user_id, mode))
        if content_errors:
            msg = ['7.7.1 The email sent contains many errors:']
            for error in content_errors:
                msg.append(' - %s' % error)
            self.error('\n'.join(msg), exit_code=4)
        else:
            if mode == ATTACHED_FILE:
                record(
                    'smtp-received-document',
                    'mode: {mode} message-id: {message_id} subject: {subject}',
                    mode=mode,
                    message_id=self.message_id,
                    subject=subject,
                )
            else:
                record(
                    'smtp-received-document',
                    'mode: {mode} message-id: '
                    '{message_id} x-tabellio-doc-url: {x_tabellio_doc_url} '
                    'x-tabellio-doc-type: {x_tabellio_doc_type}',
                    mode=mode,
                    message_id=self.message_id,
                    subject=repr(subject),
                    x_tabellio_doc_url=url,
                    x_tabellio_doc_type=tabellio_doc_type,
                )
            document = models.Document(sender=sender, comment=description, filetype=filetype)
            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()
