# 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 decimal import Decimal
from simpleeval import simple_eval
from itertools import chain

from trytond.i18n import gettext
from trytond.pool import Pool, PoolMeta
from trytond.model import (
    ModelSQL, ModelView, Workflow, DeactivableMixin, fields)
from trytond.pyson import Eval, Bool
from trytond.tools import decistmt
from trytond.transaction import Transaction
from trytond.modules.company.model import CompanyMultiValueMixin, CompanyValueMixin

from .exceptions import FormulaError

__all__ = [
    'AdvancePaymentTerm', 'AdvancePaymentTermLine',
    'AdvancePaymentTermLineAccount',
    'AdvancePaymentCondition', 'Sale', 'SaleLine',
    'HandleInvoiceException']


class AdvancePaymentTerm(
        DeactivableMixin, ModelSQL, ModelView):
    "Advance Payment Term"
    __name__ = 'sale.advance_payment_term'

    name = fields.Char("Name", required=True, translate=True)
    lines = fields.One2Many(
        'sale.advance_payment_term.line', 'advance_payment_term', "Lines")

    def get_advance_payment_context(self, sale):
        return {
            'total_amount': sale.total_amount,
            'untaxed_amount': sale.untaxed_amount,
            }

    def get_conditions(self, sale):
        conditions = []
        term_context = self.get_advance_payment_context(sale)
        for line in self.lines:
            condition = line.get_condition(sale.currency, **term_context)
            if condition.amount > 0:
                conditions.append(condition)
        return conditions


class AdvancePaymentTermLine(ModelView, ModelSQL, CompanyMultiValueMixin):
    "Advance Payment Term Line"
    __name__ = 'sale.advance_payment_term.line'
    _rec_name = 'description'

    advance_payment_term = fields.Many2One(
        'sale.advance_payment_term', "Advance Payment Term",
        required=True, ondelete='CASCADE', select=True)
    description = fields.Char(
        "Description", required=True, translate=True,
        help="Used as description for the invoice line.")
    account = fields.MultiValue(
        fields.Many2One('account.account', "Account", required=True,
            domain=[
                ('type.unearned_revenue', '=', True),
                ],
            help="Used for the line of advance payment invoice."))
    accounts = fields.One2Many(
        'sale.advance_payment_term.line.account', 'line', "Accounts")
    block_supply = fields.Boolean(
        "Block Supply",
        help="Check to prevent any supply request before advance payment.")
    block_shipping = fields.Boolean(
        "Block Shipping",
        help="Check to prevent the packing of the shipment "
        "before advance payment.")
    invoice_delay = fields.TimeDelta(
        "Invoice Delay",
        help="Delta to apply on the sale date for the date of "
        "the advance payment invoice.")
    formula = fields.Char('Formula', required=True,
        help="A python expression used to compute the advance payment amount "
            "that will be evaluated with:\n"
            "- total_amount: The total amount of the sale.\n"
            "- untaxed_amount: The total untaxed amount of the sale.")

    @fields.depends('formula')
    def pre_validate(self, **names):
        super(AdvancePaymentTermLine, self).pre_validate()
        names['total_amount'] = names['untaxed_amount'] = 0
        try:
            if not isinstance(self.compute_amount(**names), Decimal):
                raise Exception('The formula does not return a Decimal')
        except Exception as exception:
            raise FormulaError(
                gettext('sale_advance_payment.msg_term_line_invalid_formula',
                    formula=self.formula,
                    term_line=self.rec_name,
                    exception=exception)) from exception

    def get_compute_amount_context(self, **names):
        return {
            'names': names,
            'functions': {
                'Decimal': Decimal,
                },
            }

    def compute_amount(self, **names):
        context = self.get_compute_amount_context(**names)
        return simple_eval(decistmt(self.formula), **context)

    def get_condition(self, currency, **context):
        pool = Pool()
        AdvancePaymentCondition = pool.get('sale.advance_payment.condition')

        return AdvancePaymentCondition(
            block_supply=self.block_supply,
            block_shipping=self.block_shipping,
            amount=currency.round(self.compute_amount(**context)),
            account=self.account,
            invoice_delay=self.invoice_delay,
            description=self.description)


class AdvancePaymentTermLineAccount(ModelSQL, CompanyValueMixin):
    "Advance Payment Term Line Account"
    __name__ = 'sale.advance_payment_term.line.account'

    line = fields.Many2One(
        'sale.advance_payment_term.line', 'Line', required=True, select=True,
        ondelete='CASCADE')
    account = fields.Many2One(
        'account.account', "Account", required=True,
        domain=[
            ('type.unearned_revenue', '=', True),
            ('company', '=', Eval('company', -1)),
            ],
        depends=['company'])


class AdvancePaymentCondition(ModelSQL, ModelView):
    "Advance Payment Condition"
    __name__ = 'sale.advance_payment.condition'
    _rec_name = 'description'

    _states = {
        'readonly': Eval('sale_state') != 'draft',
        }
    _depends = ['sale_state']

    sale = fields.Many2One('sale.sale', 'Sale', required=True,
        ondelete='CASCADE', select=True,
        states={
            'readonly': ((Eval('sale_state') != 'draft')
                & Bool(Eval('sale'))),
            },
        depends=['sale_state'])
    description = fields.Char(
        "Description", required=True, states=_states, depends=_depends)
    amount = fields.Numeric(
        "Amount",
        digits=(16, Eval('_parent_sale', {}).get('currency_digits', 2)),
        states=_states, depends=_depends)
    account = fields.Many2One(
        'account.account', "Account", required=True,
        domain=[
            ('type.unearned_revenue', '=', True),
            ('company', '=', Eval('sale_company')),
            ],
        states=_states,
        depends=_depends + ['sale_company'])
    block_supply = fields.Boolean(
        "Block Supply", states=_states, depends=_depends)
    block_shipping = fields.Boolean(
        "Block Shipping", states=_states, depends=_depends)
    invoice_delay = fields.TimeDelta(
        "Invoice Delay", states=_states, depends=_depends)

    invoice_lines = fields.One2Many(
        'account.invoice.line', 'origin', "Invoice Lines", readonly=True)
    completed = fields.Function(fields.Boolean("Completed"), 'get_completed')

    sale_state = fields.Function(fields.Selection(
            'get_sale_states', "Sale State"), 'on_change_with_sale_state')
    sale_company = fields.Function(fields.Many2One(
            'company.company', "Company"), 'on_change_with_sale_company')

    del _states
    del _depends

    @classmethod
    def __setup__(cls):
        super(AdvancePaymentCondition, cls).__setup__()
        cls._order.insert(0, ('amount', 'ASC'))

    @classmethod
    def get_sale_states(cls):
        pool = Pool()
        Sale = pool.get('sale.sale')
        return Sale.fields_get(['state'])['state']['selection']

    @fields.depends('sale', '_parent_sale.state')
    def on_change_with_sale_state(self, name=None):
        if self.sale:
            return self.sale.state

    @fields.depends('sale', '_parent_sale.company')
    def on_change_with_sale_company(self, name=None):
        if self.sale and self.sale.company:
            return self.sale.company.id

    @classmethod
    def copy(cls, conditions, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('invoice_lines', [])
        return super(AdvancePaymentCondition, cls).copy(conditions, default)

    def create_invoice(self):
        pool = Pool()
        Invoice = pool.get('account.invoice')

        invoice = self.sale._get_invoice_sale()
        invoice.invoice_date = self.sale.sale_date
        if self.invoice_delay:
            invoice.invoice_date += self.invoice_delay
        invoice.payment_term = None

        invoice_lines = self.get_invoice_advance_payment_lines(invoice)
        if not invoice_lines:
            return None
        invoice.lines = invoice_lines
        invoice.save()

        Invoice.update_taxes([invoice])
        return invoice

    def get_invoice_advance_payment_lines(self, invoice):
        pool = Pool()
        InvoiceLine = pool.get('account.invoice.line')

        advance_amount = self._get_advance_amount()
        advance_amount += self._get_ignored_amount()
        if advance_amount >= self.amount:
            return []

        invoice_line = InvoiceLine()
        invoice_line.invoice = invoice
        invoice_line.company = invoice.company
        invoice_line.type = 'line'
        invoice_line.quantity = 1
        invoice_line.account = self.account
        invoice_line.unit_price = self.amount - advance_amount
        invoice_line.description = self.description
        invoice_line.origin = self
        invoice_line.company = self.sale.company
        # Set taxes
        invoice_line.on_change_account()
        return [invoice_line]

    def _get_advance_amount(self):
        return sum(l.amount for l in self.invoice_lines
            if l.invoice.state != 'cancel')

    def _get_ignored_amount(self):
        skips = {l for i in self.sale.invoices_recreated for l in i.lines}
        return sum(l.amount for l in self.invoice_lines
            if l.invoice.state == 'cancel' and l not in skips)

    def get_completed(self, name):
        advance_amount = 0
        lines_ignored = set(l for i in self.sale.invoices_ignored
            for l in i.lines)
        for l in self.invoice_lines:
            if l.invoice.state == 'paid' or l in lines_ignored:
                advance_amount += l.amount
        return advance_amount >= self.amount


class Sale(metaclass=PoolMeta):
    __name__ = 'sale.sale'

    advance_payment_term = fields.Many2One('sale.advance_payment_term',
        'Advance Payment Term',
        ondelete='RESTRICT', states={
            'readonly': Eval('state') != 'draft',
            },
        depends=['state'])
    advance_payment_conditions = fields.One2Many(
        'sale.advance_payment.condition', 'sale',
        'Advance Payment Conditions',
        states={
            'readonly': Eval('state') != 'draft',
            },
        depends=['state'])
    advance_payment_invoices = fields.Function(fields.Many2Many(
            'account.invoice', None, None, "Advance Payment Invoices"),
        'get_advance_payment_invoices',
        searcher='search_advance_payment_invoices')

    @classmethod
    @ModelView.button
    @Workflow.transition('quotation')
    def quote(cls, sales):
        pool = Pool()
        AdvancePaymentCondition = pool.get('sale.advance_payment.condition')

        super(Sale, cls).quote(sales)

        AdvancePaymentCondition.delete(
            list(chain(*(s.advance_payment_conditions for s in sales))))

        for sale in sales:
            sale.set_advance_payment_term()
        cls.save(sales)

    @classmethod
    def copy(cls, sales, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('advance_payment_conditions', None)
        return super(Sale, cls).copy(sales, default=default)

    def set_advance_payment_term(self):
        pool = Pool()
        AdvancePaymentTerm = pool.get('sale.advance_payment_term')
        if self.advance_payment_term:
            if self.party and self.party.lang:
                with Transaction().set_context(language=self.party.lang.code):
                    advance_payment_term = AdvancePaymentTerm(
                        self.advance_payment_term.id)
            else:
                advance_payment_term = self.advance_payment_term
            self.advance_payment_conditions = \
                advance_payment_term.get_conditions(self)

    def get_advance_payment_invoices(self, name):
        invoices = set()
        for condition in self.advance_payment_conditions:
            for invoice_line in condition.invoice_lines:
                if invoice_line.invoice:
                    invoices.add(invoice_line.invoice.id)
        return list(invoices)

    @classmethod
    def search_advance_payment_invoices(cls, name, clause):
        return [('advance_payment_conditions.invoice_lines.invoice'
                + clause[0].lstrip(name),)
            + tuple(clause[1:])]

    def get_invoice_state(self):
        state = super(Sale, self).get_invoice_state()
        skip_ids = set(x.id for x in self.invoices_ignored)
        skip_ids.update(x.id for x in self.invoices_recreated)
        invoices = [i
            for i in self.advance_payment_invoices if i.id not in skip_ids]
        if invoices:
            if any(i.state == 'cancel' for i in invoices):
                return 'exception'
            elif all(i.state == 'paid' for i in invoices):
                return state
            else:
                return 'waiting'
        return state

    def get_recall_lines(self, invoice):
        pool = Pool()
        InvoiceLine = pool.get('account.invoice.line')

        recall_lines = []
        advance_lines = InvoiceLine.search([
                ('origin', 'in', [str(c)
                        for c in self.advance_payment_conditions]),
                ('invoice.state', '=', 'paid'),
                ])
        for advance_line in advance_lines:
            line = InvoiceLine(
                invoice=invoice,
                company=invoice.company,
                type=advance_line.type,
                quantity=advance_line.quantity,
                account=advance_line.account,
                unit_price=-advance_line.amount,
                description=advance_line.description,
                origin=advance_line,
                taxes=advance_line.taxes,
                )
            recall_lines.append(line)

        return recall_lines

    def create_invoice(self):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        InvoiceLine = pool.get('account.invoice.line')

        invoice = super(Sale, self).create_invoice()

        if self.advance_payment_eligible():
            if not self.advance_payment_completed:
                for condition in self.advance_payment_conditions:
                    condition.create_invoice()
            elif invoice is not None:
                recall_lines = self.get_recall_lines(invoice)
                if recall_lines:
                    for line in recall_lines:
                        line.invoice = invoice
                    InvoiceLine.save(recall_lines)
                    Invoice.update_taxes([invoice])

        return invoice

    def advance_payment_eligible(self, shipment_type=None):
        """
        Returns True when the shipment_type is eligible to further processing
        of the sale's advance payment.
        """
        return bool((shipment_type == 'out' or shipment_type is None)
            and self.advance_payment_conditions)

    @property
    def advance_payment_completed(self):
        """
        Returns True when the advance payment process is completed
        """
        return (bool(self.advance_payment_conditions)
            and all(c.completed for c in self.advance_payment_conditions))

    @property
    def supply_blocked(self):
        for condition in self.advance_payment_conditions:
            if not condition.block_supply:
                continue
            if not condition.completed:
                return True
        return False

    @property
    def shipping_blocked(self):
        for condition in self.advance_payment_conditions:
            if not condition.block_shipping:
                continue
            if not condition.completed:
                return True
        return False


class SaleLine(metaclass=PoolMeta):
    __name__ = 'sale.line'

    def get_move(self, shipment_type):
        move = super(SaleLine, self).get_move(shipment_type)
        if (self.sale.advance_payment_eligible(shipment_type)
                and self.sale.supply_blocked):
            return None
        return move

    def get_purchase_request(self):
        request = super(SaleLine, self).get_purchase_request()
        if (self.sale.advance_payment_eligible()
                and self.sale.supply_blocked):
            return None
        return request

    def get_invoice_line(self):
        lines = super(SaleLine, self).get_invoice_line()
        if (self.sale.advance_payment_eligible()
                and not self.sale.advance_payment_completed):
            return []
        return lines


class HandleInvoiceException(metaclass=PoolMeta):
    __name__ = 'sale.handle.invoice.exception'

    def default_ask(self, fields):
        pool = Pool()
        Sale = pool.get('sale.sale')

        default = super(HandleInvoiceException, self).default_ask(fields)
        invoices = default['domain_invoices']

        sale = Sale(Transaction().context['active_id'])
        skips = set(sale.invoices_ignored)
        skips.update(sale.invoices_recreated)
        for invoice in sale.advance_payment_invoices:
            if invoice.state == 'cancel' and invoice not in skips:
                invoices.append(invoice.id)
        return default
