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

from sql.conditionals import NullIf
from sql.operators import Equal

import trytond.config as config
from trytond.model import (
    Exclude, MatchMixin, ModelSQL, ModelView, Workflow, dualmethod, fields,
    sequence_ordered)
from trytond.pool import Pool
from trytond.pyson import Eval, If
from trytond.transaction import Transaction

from .exceptions import PeppolServiceError

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


class Peppol(Workflow, ModelSQL, ModelView):
    __name__ = 'edocument.peppol'

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

    direction = fields.Selection([
            ('in', "IN"),
            ('out', "OUT"),
            ], "Direction", required=True, states=_states)
    company = fields.Many2One(
        'company.company', "Company", required=True, states=_states)
    type = fields.Selection([
            (None, ""),
            ('bis-billing-3', "BIS Billing V3"),
            ], "Type", translate=False,
        states={
            'readonly': _states['readonly'],
            'required': Eval('state') != 'draft',
            })
    service = fields.Many2One(
        'edocument.peppol.service', "Service",
        domain=[
            ('company', '=', Eval('company', -1)),
            If(Eval('state') == 'draft',
                ('types', 'in', Eval('type')),
                ()),
            ],
        states={
            'readonly': _states['readonly'],
            'required': Eval('state') != 'draft',
            })
    invoice = fields.Many2One(
        'account.invoice', "Invoice", ondelete='RESTRICT',
        domain=[
            ('company', '=', Eval('company', -1)),
            If(Eval('direction') == 'out', [
                    ('type', '=', 'out'),
                    ('state', 'in', ['posted', 'paid']),
                    ], [
                    ('type', '=', 'in'),
                    ]),
            ],
        states={
            'readonly': _states['readonly'],
            'invisible': (
                ~Eval('type').in_([
                        'bis-billing-3',
                        ])
                | ((Eval('state') == 'draft')
                    & (Eval('direction') == 'in'))),
            'required': (
                (Eval('direction') == 'out')
                & Eval('type').in_([
                        'bis-billing-3',
                        ])),
            })
    data = fields.Binary(
        "Data",
        file_id=file_id, store_prefix=store_prefix,
        states={
            'invisible': (
                (Eval('state') == 'draft')
                & (Eval('direction') == 'out')),
            })
    file_id = fields.Char("File ID", readonly=False)
    transmission_id = fields.Char("Transmission ID", readonly=True)
    document_retried = fields.Many2One(
        'edocument.peppol', "Retry", readonly=True,
        states={
            'invisible': ~Eval('document_retried'),
            'required': Eval('state') == 'retried',
            })
    status = fields.Char(
        "Status", readonly=True,
        states={
            'invisible': ~Eval('status'),
            })
    state = fields.Selection([
            ('draft', "Draft"),
            ('submitted', "Submitted"),
            ('processing', "Processing"),
            ('succeeded', "Succeeded"),
            ('failed', "Failed"),
            ('retried', "Retried"),
            ('cancelled', "Cancelled"),
            ], "State", readonly=True, required=True, sort=False)

    @classmethod
    def __setup__(cls):
        super().__setup__()

        t = cls.__table__()
        cls._sql_constraints += [
            ('service_transmission_id_unique',
                Exclude(t,
                    (t.service, Equal),
                    (NullIf(t.transmission_id, ''), Equal)),
                'edocument_peppol.msg_service_transmission_id_unique'),
            ]

        cls._transitions |= {
            ('draft', 'submitted'),
            ('submitted', 'processing'),
            ('processing', 'processing'),
            ('processing', 'succeeded'),
            ('processing', 'failed'),
            ('failed', 'retried'),
            ('failed', 'cancelled'),
            }
        cls._buttons.update(
            draft={
                'invisible': Eval('state') != 'submitted',
                'depends': ['state'],
                },
            submit={
                'invisible': Eval('state') != 'draft',
                'depends': ['state'],
                },
            process={
                'invisible': ~Eval('state').in_(['submitted', 'processing']),
                'depends': ['state'],
                },
            retry={
                'invisible': Eval('state') != 'failed',
                'depends': ['state'],
                },
            cancel={
                'invisible': Eval('state') != 'failed',
                'depends': ['state'],
                })

    @classmethod
    def default_company(cls):
        return Transaction().context.get('company')

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

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

    @dualmethod
    @ModelView.button
    @Workflow.transition('submitted')
    def submit(cls, documents):
        pool = Pool()
        Service = pool.get('edocument.peppol.service')
        for document in documents:
            if not document.service:
                document.service = Service.get_service(document)
            if document.direction == 'out':
                document.data = document.render()
        cls.save(documents)
        cls.__queue__.process(documents)

    def get_service_pattern(self):
        return {
            'type': self.type,
            }

    def render(self):
        pool = Pool()
        Invoice = pool.get('edocument.ubl.invoice')
        assert self.direction == 'out'
        if self.type == 'bis-billing-3':
            return Invoice(self.invoice).render(
                '2', specification='peppol-bis-3')

    @dualmethod
    @ModelView.button
    @Workflow.transition('processing')
    def process(cls, documents):
        cls.lock(documents)
        # write state before calling _process
        cls.write(documents, {'state': 'processing'})
        for document in documents:
            if document.direction == 'out':
                cls.__queue__._process(document)
            else:
                document._process()

    def _process(self):
        pool = Pool()
        Invoice = pool.get('edocument.ubl.invoice')
        if self.state != 'processing':
            return
        self.lock()
        if self.direction == 'out':
            if self.transmission_id:
                return
            try:
                self.transmission_id = self.service.post(self)
                self.save()
            except PeppolServiceError as e:
                self.fail(status=str(e))
        elif self.direction == 'in':
            if self.type == 'bis-billing-3':
                if self.invoice:
                    return
                self.invoice = Invoice.parse(self.data)
                self.save()
                self.succeed()

    @classmethod
    def update_status(cls, documents=None):
        if documents is None:
            documents = cls.search([
                    ('direction', '=', 'out'),
                    ('state', '=', 'processing'),
                    ])
        for document in documents:
            document._update_status()
        cls.save(documents)

    def _update_status(self):
        assert self.direction == 'out'
        self.service.update_status(self)

    @dualmethod
    @Workflow.transition('succeeded')
    def succeed(cls, documents, status=None):
        cls.write(documents, {'status': status})

    @dualmethod
    @Workflow.transition('failed')
    def fail(cls, documents, status=None):
        cls.write(documents, {'status': status})

    @classmethod
    @Workflow.transition('retried')
    def retry(cls, documents):
        retries = cls.copy(documents)
        for document, retry in zip(documents, retries):
            document.document_retried = retry
        cls.save(documents)
        cls.submit(retries)

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

    @classmethod
    def copy(cls, documents, default=None):
        default = default.copy() if default is not None else {}
        default.setdefault('service')
        default.setdefault('data')
        default.setdefault('transmission_id')
        default.setdefault('document_retried')
        default.setdefault('status')
        return super().copy(documents, default=default)


class PeppolService(sequence_ordered(), MatchMixin, ModelSQL, ModelView):
    __name__ = 'edocument.peppol.service'

    company = fields.Many2One(
        'company.company', "Company", required=True)
    service = fields.Selection([
            ], "Service")
    types = fields.MultiSelection(
        'get_peppol_types', "Types",
        help="The types of document supported by the service provider.")

    @classmethod
    def default_company(cls):
        return Transaction().context.get('company')

    @classmethod
    def get_service(cls, document):
        pattern = document.get_service_pattern()
        for service in cls.search([('company', '=', document.company)]):
            if service.match(pattern):
                return service

    def match(self, pattern, match_none=False):
        if 'type' in pattern:
            pattern = pattern.copy()
            if pattern.pop('type') not in self.types:
                return False
        return super().match(pattern, match_none=match_none)

    @classmethod
    def get_peppol_types(cls):
        pool = Pool()
        Peppol = pool.get('edocument.peppol')
        return [
            (v, l) for v, l in Peppol.fields_get(['type'])['type']['selection']
            if v is not None]

    @classmethod
    def default_types(cls):
        return ['bis-billing-3']

    def post(self, document):
        if meth := getattr(self, f'_post_{self.service}', None):
            return meth(document)

    def update_status(self, document):
        if meth := getattr(self, f'_update_status_{self.service}', None):
            return meth(document)
