# 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 collections import OrderedDict, defaultdict
from itertools import islice

from trytond.i18n import gettext
from trytond.model import (
    MatchMixin, ModelSQL, ModelView, Workflow, fields, sequence_ordered)
from trytond.modules.account.exceptions import ClosePeriodError
from trytond.modules.company.model import CompanyValueMixin
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If
from trytond.transaction import Transaction

from .exceptions import CancelInvoiceMoveWarning


class Configuration(metaclass=PoolMeta):
    __name__ = 'account.configuration'

    default_customer_payment_term = fields.MultiValue(
        fields.Many2One(
            'account.invoice.payment_term', "Default Customer Payment Term"))
    customer_payment_reference_number = fields.MultiValue(
        fields.Selection('get_customer_payment_references',
            "Customer Payment Reference Number",
            help="The number used to generate "
            "the customer payment reference."))

    @classmethod
    def multivalue_model(cls, field):
        pool = Pool()
        if field in 'default_customer_payment_term':
            return pool.get('account.configuration.default_payment_term')
        elif field == 'customer_payment_reference_number':
            return pool.get('account.configuration.payment_reference')
        return super().multivalue_model(field)

    @classmethod
    def get_customer_payment_references(cls):
        pool = Pool()
        PaymentReference = pool.get('account.configuration.payment_reference')
        field = 'customer_payment_reference_number'
        return PaymentReference.fields_get([field])[field]['selection']

    @classmethod
    def default_customer_payment_reference_number(cls, **pattern):
        pool = Pool()
        PaymentReference = pool.get('account.configuration.payment_reference')
        return PaymentReference.default_customer_payment_reference_number()


class ConfigurationDefaultPaymentTerm(ModelSQL, CompanyValueMixin):
    __name__ = 'account.configuration.default_payment_term'

    default_customer_payment_term = fields.Many2One(
        'account.invoice.payment_term', "Default Customer Payment Term")


class ConfigurationPaymentReference(ModelSQL, CompanyValueMixin):
    __name__ = 'account.configuration.payment_reference'

    customer_payment_reference_number = fields.Selection([
            ('invoice', "Invoice"),
            ('party', "Party"),
            ], "Customer Payment Reference Number")

    @classmethod
    def default_customer_payment_reference_number(cls):
        return 'invoice'


class FiscalYear(metaclass=PoolMeta):
    __name__ = 'account.fiscalyear'
    invoice_sequences = fields.One2Many(
        'account.fiscalyear.invoice_sequence', 'fiscalyear',
        "Invoice Sequences",
        domain=[
            ('company', '=', Eval('company', -1)),
            ])

    @staticmethod
    def default_invoice_sequences():
        if Transaction().user == 0:
            return []
        return [{}]


class Period(metaclass=PoolMeta):
    __name__ = 'account.period'

    @classmethod
    @ModelView.button
    @Workflow.transition('closed')
    def close(cls, periods):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        company_ids = list({p.company.id for p in periods})
        invoices = Invoice.search([
                ('company', 'in', company_ids),
                ('state', '=', 'posted'),
                ('move', '=', None),
                ])
        if invoices:
            names = ', '.join(i.rec_name for i in invoices[:5])
            if len(invoices) > 5:
                names += '...'
            raise ClosePeriodError(
                gettext('account_invoice.msg_close_period_non_posted_invoices',
                    invoices=names))
        super().close(periods)


class InvoiceSequence(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
    __name__ = 'account.fiscalyear.invoice_sequence'
    company = fields.Many2One('company.company', "Company", required=True)
    fiscalyear = fields.Many2One(
        'account.fiscalyear', "Fiscal Year", required=True, ondelete='CASCADE',
        domain=[
            ('company', '=', Eval('company', -1)),
            ])
    period = fields.Many2One('account.period', 'Period',
        domain=[
            ('fiscalyear', '=', Eval('fiscalyear', -1)),
            ('type', '=', 'standard'),
            ])
    in_invoice_sequence = fields.Many2One('ir.sequence.strict',
        'Supplier Invoice Sequence', required=True,
        domain=[
            ('sequence_type', '=',
                Id('account_invoice', 'sequence_type_account_invoice')),
            ('company', '=', Eval('company', -1)),
            ])
    in_credit_note_sequence = fields.Many2One('ir.sequence.strict',
        'Supplier Credit Note Sequence', required=True,
        domain=[
            ('sequence_type', '=',
                Id('account_invoice', 'sequence_type_account_invoice')),
            ('company', '=', Eval('company', -1)),
            ])
    out_invoice_sequence = fields.Many2One('ir.sequence.strict',
        'Customer Invoice Sequence', required=True,
        domain=[
            ('sequence_type', '=',
                Id('account_invoice', 'sequence_type_account_invoice')),
            ('company', '=', Eval('company', -1)),
            ])
    out_credit_note_sequence = fields.Many2One('ir.sequence.strict',
        'Customer Credit Note Sequence', required=True,
        domain=[
            ('sequence_type', '=',
                Id('account_invoice', 'sequence_type_account_invoice')),
            ('company', '=', Eval('company', -1)),
            ])

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

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


class Move(metaclass=PoolMeta):
    __name__ = 'account.move'

    @classmethod
    def _get_origin(cls):
        return super()._get_origin() + ['account.invoice']


class MoveLine(metaclass=PoolMeta):
    __name__ = 'account.move.line'

    invoice_payment = fields.Function(fields.Many2One(
            'account.invoice', "Invoice Payment",
            domain=[
                ('account', '=', Eval('account', -1)),
                If(Bool(Eval('party')),
                    ('party', '=', Eval('party', -1)),
                    (),
                    ),
                ],
            states={
                'invisible': Bool(Eval('reconciliation')),
                }),
        'get_invoice_payment',
        setter='set_invoice_payment',
        searcher='search_invoice_payment')
    invoice_payments = fields.Many2Many(
        'account.invoice-account.move.line', 'line', 'invoice',
        "Invoice Payments", readonly=True)

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._check_modify_exclude.add('invoice_payment')

    @classmethod
    def _view_reconciliation_muted(cls):
        pool = Pool()
        ModelData = pool.get('ir.model.data')
        muted = super()._view_reconciliation_muted()
        muted.add(ModelData.get_id(
            'account_invoice', 'move_line_view_list_to_pay'))
        return muted

    @classmethod
    def _get_origin(cls):
        return super()._get_origin() + [
            'account.invoice.line', 'account.invoice.tax']

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

    @classmethod
    def get_invoice_payment(cls, lines, name):
        pool = Pool()
        InvoicePaymentLine = pool.get('account.invoice-account.move.line')

        result = defaultdict(lambda: None)
        payment_lines = InvoicePaymentLine.search([
                ('line', 'in', lines),
                ])
        result.update({p.line.id: p.invoice.id for p in payment_lines})
        return result

    @classmethod
    def set_invoice_payment(cls, lines, name, value):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        Invoice.remove_payment_lines(lines)
        if value:
            Invoice.add_payment_lines({Invoice(value): lines})

    @classmethod
    def search_invoice_payment(cls, name, clause):
        nested = clause[0][len(name):]
        return [('invoice_payments' + nested, *clause[1:])]

    @property
    def product(self):
        pool = Pool()
        InvoiceLine = pool.get('account.invoice.line')
        product = super().product
        if (isinstance(self.origin, InvoiceLine)
                and self.origin.product):
            product = self.origin.product
        return product


def _invoices_to_process(reconciliations):
    pool = Pool()
    Reconciliation = pool.get('account.move.reconciliation')
    Invoice = pool.get('account.invoice')

    move_ids = set()
    others = set()
    for reconciliation in reconciliations:
        for line in reconciliation.lines:
            move_ids.add(line.move.id)
            others.update(line.reconciliations_delegated)

    invoices = set(Invoice.search(['OR',
                ('move', 'in', move_ids),
                ('additional_moves', 'in', move_ids),
                ]))
    if others:
        invoices.update(_invoices_to_process(Reconciliation.browse(others)))

    return invoices


class Reconciliation(metaclass=PoolMeta):
    __name__ = 'account.move.reconciliation'

    @classmethod
    def on_modification(cls, mode, reconciliations, field_names=None):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        transaction = Transaction()
        context = transaction.context

        super().on_modification(mode, reconciliations, field_names=field_names)

        with transaction.set_context(
                queue_batch=context.get('queue_batch', True)):
            Invoice.__queue__.process(
                list(_invoices_to_process(reconciliations)))


class RenewFiscalYear(metaclass=PoolMeta):
    __name__ = 'account.fiscalyear.renew'

    def fiscalyear_defaults(self):
        defaults = super().fiscalyear_defaults()
        defaults['invoice_sequences'] = None
        return defaults

    @property
    def invoice_sequence_fields(self):
        return ['out_invoice_sequence', 'out_credit_note_sequence',
            'in_invoice_sequence', 'in_credit_note_sequence']

    def create_fiscalyear(self):
        pool = Pool()
        Sequence = pool.get('ir.sequence.strict')
        InvoiceSequence = pool.get('account.fiscalyear.invoice_sequence')
        fiscalyear = super().create_fiscalyear()

        def standard_period(period):
            return period.type == 'standard'

        period_mapping = {}
        for previous, new in zip(
                filter(
                    standard_period, self.start.previous_fiscalyear.periods),
                filter(standard_period, fiscalyear.periods)):
            period_mapping[previous] = new.id

        InvoiceSequence.copy(
            self.start.previous_fiscalyear.invoice_sequences,
            default={
                'fiscalyear': fiscalyear.id,
                'period': lambda data: period_mapping.get(data['period']),
                })

        if not self.start.reset_sequences:
            return fiscalyear
        sequences = OrderedDict()
        for invoice_sequence in fiscalyear.invoice_sequences:
            for field in self.invoice_sequence_fields:
                sequence = getattr(invoice_sequence, field, None)
                sequences[sequence.id] = sequence
        copies = Sequence.copy(list(sequences.values()), default={
                'name': lambda data: data['name'].replace(
                    self.start.previous_fiscalyear.name,
                    self.start.name)
                })
        Sequence.write(copies, {
                'number_next': Sequence.default_number_next(),
                })

        mapping = {}
        for previous_id, new_sequence in zip(sequences.keys(), copies):
            mapping[previous_id] = new_sequence.id
        to_write = []
        for new_sequence, old_sequence in zip(
                fiscalyear.invoice_sequences,
                self.start.previous_fiscalyear.invoice_sequences):
            values = {}
            for field in self.invoice_sequence_fields:
                sequence = getattr(old_sequence, field, None)
                values[field] = mapping[sequence.id]
            to_write.extend(([new_sequence], values))
        if to_write:
            InvoiceSequence.write(*to_write)
        return fiscalyear


class RescheduleLines(metaclass=PoolMeta):
    __name__ = 'account.move.line.reschedule'

    @classmethod
    def reschedule_lines(cls, lines, journal, terms):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        move, balance_line = super().reschedule_lines(lines, journal, terms)

        move_ids = list({l.move.id for l in lines})
        invoices = Invoice.search(['OR',
                ('move', 'in', move_ids),
                ('additional_moves', 'in', move_ids),
                ])
        Invoice.write(invoices, {
                'additional_moves': [('add', [move.id])],
                })
        return move, balance_line


class DelegateLines(metaclass=PoolMeta):
    __name__ = 'account.move.line.delegate'

    @classmethod
    def delegate_lines(cls, lines, party, journal, date=None):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        move = super().delegate_lines(lines, party, journal, date=None)

        move_ids = list({l.move.id for l in lines})
        invoices = Invoice.search(['OR',
                ('move', 'in', move_ids),
                ('additional_moves', 'in', move_ids),
                ])
        Invoice.write(invoices, {
                'alternative_payees': [('add', [party.id])],
                'additional_moves': [('add', [move.id])],
                })
        return move


class CancelMoves(metaclass=PoolMeta):
    __name__ = 'account.move.cancel'

    def transition_cancel(self):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        Warning = pool.get('res.user.warning')

        moves_w_invoices = {
            m: m.origin for m in self.records
            if (isinstance(m.origin, Invoice)
                and m.origin.state not in {'paid', 'cancelled'})}
        if moves_w_invoices:
            move_names = ', '.join(m.rec_name
                for m in islice(moves_w_invoices, None, 5))
            invoice_names = ', '.join(i.rec_name
                for i in islice(moves_w_invoices.values(), None, 5))
            if len(moves_w_invoices) > 5:
                move_names += '...'
                invoice_names += '...'
            key = Warning.format('cancel_invoice_move', moves_w_invoices)
            if Warning.check(key):
                raise CancelInvoiceMoveWarning(key,
                    gettext('account_invoice.msg_cancel_invoice_move',
                        moves=move_names, invoices=invoice_names),
                    gettext(
                        'account_invoice.msg_cancel_invoice_move_description'))

        return super().transition_cancel()
