# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

import mimetypes
from collections import defaultdict
from io import BytesIO

from pypdf import PdfReader, PdfWriter

import trytond.config as config
from trytond.i18n import gettext
from trytond.model import (
    DeactivableMixin, ModelSingleton, ModelSQL, ModelView, Workflow, fields)
from trytond.pool import Pool
from trytond.pyson import Eval, If
from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView, Wizard

from .exceptions import DocumentIncomingSplitError

if config.getboolean('document_incoming', 'filestore', default=True):
    file_id = 'file_id'
    store_prefix = config.get(
        'document_incoming', 'store_prefix', default=None)
else:
    file_id = store_prefix = None


class IncomingConfiguration(ModelSingleton, ModelSQL, ModelView):
    __name__ = 'document.incoming.configuration'


class Incoming(DeactivableMixin, Workflow, ModelSQL, ModelView):
    __name__ = 'document.incoming'

    _states = {
        'readonly': Eval('state') != 'draft',
        }

    name = fields.Char("Name", required=True, states=_states)
    company = fields.Many2One('company.company', "Company", states=_states)
    data = fields.Binary(
        "Data", filename='name',
        file_id=file_id, store_prefix=store_prefix,
        states={
            'required': ~Eval('children'),
            'readonly': _states['readonly'],
            })
    parsed_data = fields.Dict(None, "Parsed Data", readonly=True)
    file_id = fields.Char("File ID", readonly=True)
    mime_type = fields.Function(
        fields.Char("MIME Type"), 'on_change_with_mime_type')
    type = fields.Selection([
            (None, ""),
            ('document_incoming', "Unknown"),
            ], "Type",
        states={
            'required': Eval('active', True) & (Eval('state') == 'done'),
            'readonly': _states['readonly'],
            })
    source = fields.Char("Source", states=_states)
    parent = fields.Many2One(
        'document.incoming', "Parent", readonly=True,
        domain=[
            ('active', '=', False),
            ],
        states={
            'invisible': ~Eval('parent'),
            })
    children = fields.One2Many(
        'document.incoming', 'parent', "Children", readonly=True,
        states={
            'invisible': ~Eval('children'),
            })
    result = fields.Reference(
        "Result", selection='get_results', readonly=True,
        states={
            'required': Eval('active', True) & (Eval('state') == 'done'),
            'invisible': ~Eval('result'),
            })
    state = fields.Selection([
            ('draft', "Draft"),
            ('processing', "Processing"),
            ('done', "Done"),
            ('cancelled', "Cancelled"),
            ], "State", required=True, readonly=True)

    del _states

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._transitions |= {
            ('draft', 'processing'),
            ('draft', 'cancelled'),
            ('processing', 'processing'),
            ('processing', 'done'),
            ('processing', 'draft'),
            ('cancelled', 'draft'),
            }
        cls._buttons.update(
            cancel={
                'invisible': Eval('state') != 'draft',
                'depends': ['state'],
                },
            draft={
                'invisible': ~Eval('state').in_(['processing', 'cancelled']),
                'depends': ['state'],
                },
            split_wizard={
                'invisible': (
                    (Eval('state') != 'draft')
                    | ~Eval('mime_type').in_(cls._split_mime_types())),
                'depends': ['state', 'mime_type'],
                },
            process={
                'pre_validate': [
                    If(~Eval('type'),
                        ('type', '!=', None),
                        ()),
                    ],
                'invisible': Eval('state') != 'draft',
                'depends': ['state'],
                },
            proceed={
                'invisible': Eval('state') != 'processing',
                'depends': ['state'],
                },
            )

    @classmethod
    def __register__(cls, module):
        super().__register__(module)
        table_h = cls.__table_handler__(module)

        # Migration from 7.6: remove not null on data
        table_h.not_null_action('data', action='remove')

    @fields.depends('name')
    def on_change_with_mime_type(self, name=None):
        if self.name:
            type, _ = mimetypes.guess_type(self.name)
            return type

    @classmethod
    def get_results(cls):
        pool = Pool()
        IrModel = pool.get('ir.model')
        get_name = IrModel.get_name
        models = cls._get_results()
        return [(None, '')] + [(m, get_name(m)) for m in models]

    @classmethod
    def _get_results(cls):
        return {'document.incoming'}

    @classmethod
    def default_state(cls):
        return 'draft'

    @classmethod
    def view_attributes(cls):
        process_states = cls._buttons['process'].copy()
        process_states['invisible'] = Eval('state') != 'draft'
        return super().view_attributes() + [
            ('/form//button[@name="process"]', 'states', process_states),
            ]

    @classmethod
    @ModelView.button
    @Workflow.transition('cancelled')
    def cancel(cls, documents):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    def draft(cls, documents):
        pass

    @classmethod
    @ModelView.button_action(
        'document_incoming.wizard_document_incoming_split')
    def split_wizard(cls, documents):
        pass

    @classmethod
    def _split_mime_types(cls):
        return ['application/pdf']

    @classmethod
    def from_inbound_email(cls, email_, rule):
        def sanitize_name(value):
            for forbidden_char in cls.name.forbidden_chars:
                value = value.replace(forbidden_char, ' ')
            return value

        message = email_.as_dict()
        active = not message.get('attachments')
        data = message.get('text', message.get('html'))
        if isinstance(data, str):
            data = data.encode()
        name = sanitize_name(message.get('subject') or 'No Subject')
        document = cls(
            active=active,
            name=name,
            company=rule.document_incoming_company,
            data=data,
            type=rule.document_incoming_type if active else None,
            source='inbound_email',
            )
        children = []
        for attachment in message.get('attachments', []):
            child = cls(
                name=sanitize_name(attachment['filename'] or 'data.bin'),
                company=rule.document_incoming_company,
                data=attachment['data'],
                type=rule.document_incoming_type,
                source='inbound_email')
            children.append(child)
        document.children = children
        document.save()
        return document

    @classmethod
    @ModelView.button
    @Workflow.transition('processing')
    def process(cls, documents, with_children=False):
        transaction = Transaction()
        context = transaction.context
        with transaction.set_context(
                queue_batch=context.get('queue_batch', True)):
            cls.__queue__.proceed(documents, with_children=with_children)

    @classmethod
    @ModelView.button
    @Workflow.transition('done')
    def proceed(cls, documents, with_children=False):
        pool = Pool()
        Attachment = pool.get('ir.attachment')
        results = defaultdict(list)
        attachments = []
        for document in documents:
            if document.result or not document.active:
                continue
            document.result = getattr(document, f'_process_{document.type}')()
            results[document.result.__class__].append(document.result)
            attachment = Attachment(
                name=document.name,
                resource=document.result,
                type='data',
                data=document.data)
            attachments.append(attachment)
        for kls, records in results.items():
            kls.save(records)
        cls.save(documents)
        Attachment.save(attachments)

        if with_children:
            children = list(filter(
                    lambda d: d.type,
                    (c for d in documents for c in d.children)))
            if children:
                cls.process(children, with_children=True)

    def _process_document_incoming(self):
        self.active = False
        self.save()
        document, = self.__class__.copy([self], default={
                'type': None,
                'parent': self.id,
                })
        return document

    @classmethod
    def copy(cls, documents, default=None):
        default = default.copy() if default is not None else {}
        default.setdefault('result')
        default.setdefault('parsed_data')
        default.setdefault('children')
        return super().copy(documents, default=default)


def iter_pages(expression, size):
    ranges = set()
    for pages in expression.split(','):
        pages = pages.split('-')
        if not len(pages):
            continue
        if not pages[0]:
            pages[0] = 1
        if not pages[-1]:
            pages[-1] = size
        pages = list(map(int, filter(None, pages)))
        ranges.add((
                min(max(min(pages) - 1, 0), size),
                min(max(max(pages), 0), size)))
    ranges = sorted(ranges)

    def iter_():
        last = 0
        for start, end in ranges:
            if last != start:
                yield range(last, start)
            yield range(start, end)
            last = end
        if last != size:
            yield range(last, size)
    return iter_()


class IncomingSplit(Wizard):
    __name__ = 'document.incoming.split'

    start = StateView(
        'document.incoming.split.start',
        'document_incoming.document_incoming_split_start_view_form', [
            Button("Cancel", 'end', 'tryton-cancel'),
            Button("Split", 'split', 'tryton-ok', default=True),
            ])
    split = StateTransition()

    def default_start(self, fields):
        if self.record.mime_type == 'application/pdf':
            reader = PdfReader(BytesIO(self.record.data))
            if len(reader.pages) == 1:
                pages = '1'
            else:
                pages = '1-%d' % len(reader.pages)
        else:
            pages = ''
        return {
            'data': self.record.data,
            'pages': pages,
            }

    def transition_split(self):
        pool = Pool()
        Document = pool.get('document.incoming')
        if self.record.active and self.record.mime_type == 'application/pdf':
            self.record.active = False
            self.record.save()
            reader = PdfReader(BytesIO(self.record.data))
            try:
                iter_ = iter_pages(self.start.pages, len(reader.pages))
            except ValueError as exception:
                raise DocumentIncomingSplitError(gettext(
                        'document_incoming.msg_document_split_invalid_pages',
                        expression=self.start.pages,
                        exception=exception)) from exception
            for pages in iter_:
                writer = PdfWriter()
                for i in pages:
                    page = reader.pages[i]
                    writer.add_page(page)
                data = BytesIO()
                writer.write(data)
                Document.copy([self.record], default={
                        'active': True,
                        'data': data.getvalue(),
                        'parent': self.record.id,
                        })
        return 'end'

    def end(self):
        return 'reload'


class IncomingSplitStart(ModelView):
    __name__ = 'document.incoming.split.start'

    data = fields.Binary("Data", readonly=True)
    pages = fields.Char(
        "Pages", required=True,
        help="List pages to split.\n"
        "Ex: 1-3,4,5-6")
