# 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 trytond.i18n import gettext
from trytond.model import Index, ModelSQL, ModelView, Unique, Workflow, fields
from trytond.model.exceptions import AccessError
from trytond.modules.account.exceptions import AccountMissing
from trytond.modules.currency.fields import Monetary
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, If
from trytond.transaction import Transaction, check_access
from trytond.wizard import StateTransition, Wizard


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

    deferred_account_revenue = fields.MultiValue(fields.Many2One(
            'account.account', "Deferred Account Revenue",
            domain=[
                ('type.statement', '=', 'balance'),
                ('company', '=', Eval('context', {}).get('company', -1)),
                ]))
    deferred_account_expense = fields.MultiValue(fields.Many2One(
            'account.account', "Deferred Account Expense",
            domain=[
                ('type.statement', '=', 'balance'),
                ('company', '=', Eval('context', {}).get('company', -1)),
                ]))

    @classmethod
    def multivalue_model(cls, field):
        pool = Pool()
        if field in {'deferred_account_revenue', 'deferred_account_expense'}:
            return pool.get('account.configuration.default_account')
        return super().multivalue_model(field)


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

    deferred_account_revenue = fields.Many2One(
        'account.account', "Deferred Account Revenue",
        domain=[
            ('type.statement', '=', 'balance'),
            ('company', '=', Eval('company', -1)),
            ])
    deferred_account_expense = fields.Many2One(
        'account.account', "Deferred Account Expense",
        domain=[
            ('type.statement', '=', 'balance'),
            ('company', '=', Eval('company', -1)),
            ])


class InvoiceDeferred(Workflow, ModelSQL, ModelView):
    __name__ = 'account.invoice.deferred'

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

    company = fields.Many2One(
        'company.company', "Company", required=True,
        states={
            'readonly': (Eval('state') != 'draft') & Eval('invoice_line'),
            })
    type = fields.Selection([
            ('out', "Customer"),
            ('in', "Supplier"),
            ], "Type", required=True,
        states=_states)
    journal = fields.Many2One(
        'account.journal', "Journal",
        states={
            'readonly': _states['readonly'],
            'required': Eval('state') != 'draft',
            },
        context={
            'company': Eval('company', -1),
            },
        depends={'company'})
    invoice_line = fields.Many2One(
        'account.invoice.line', "Invoice Line", required=True,
        domain=[
            ('product.type', '=', 'service'),
            ('invoice.type', '=', Eval('type')),
            ('invoice.state', 'in', ['posted', 'paid']),
            ('invoice.company', '=', Eval('company', -1)),
            ],
        states=_states)
    amount = Monetary(
        "Amount", currency='currency', digits='currency', required=True,
        states=_states)
    start_date = fields.Date(
        "Start Date", required=True,
        domain=[
            ('start_date', '<', Eval('end_date', None)),
            ],
        states=_states)
    end_date = fields.Date(
        "End Date", required=True,
        domain=[
            ('end_date', '>', Eval('start_date', None)),
            ],
        states=_states)
    moves = fields.One2Many(
        'account.move', 'origin', "Moves", readonly=True,
        order=[
            ('period.start_date', 'ASC'),
            ])
    state = fields.Selection([
            ('draft', "Draft"),
            ('running', "Running"),
            ('closed', "Closed"),
            ], "State", readonly=True, required=True, sort=False)

    currency = fields.Function(fields.Many2One(
            'currency.currency', "Currency"), 'on_change_with_currency')

    del _states

    @classmethod
    def __setup__(cls):
        super().__setup__()
        table = cls.__table__()
        cls._sql_constraints = [
            ('invoice_line_unique', Unique(table, table.invoice_line),
                'account_invoice_defer.msg_defer_invoice_line_unique'),
            ]
        cls._sql_indexes.add(
            Index(
                table, (table.state, Index.Equality(cardinality='low')),
                where=table.state.in_(['draft', 'running'])))
        cls.journal.domain = [
            If(Eval('type') == 'out',
                ('type', 'in', cls._journal_types('out')),
                ('type', 'in', cls._journal_types('in'))),
            ]
        cls._transitions |= set((
                ('draft', 'running'),
                ('running', 'closed'),
                ))
        cls._buttons.update({
                'run': {
                    'invisible': Eval('state') != 'draft',
                    'depends': ['state'],
                    },
                })

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

        # Migration from 6.6: drop not null on journal
        table_h.not_null_action('journal', 'remove')

    @classmethod
    def _journal_types(cls, type):
        if type == 'out':
            return ['revenue']
        else:
            return ['expense']

    @fields.depends(methods=['set_journal'])
    def on_change_type(self):
        self.set_journal()

    @fields.depends('type')
    def set_journal(self, pattern=None):
        pool = Pool()
        Journal = pool.get('account.journal')
        pattern = pattern.copy() if pattern is not None else {}
        pattern.setdefault('type', {
                'out': 'revenue',
                'in': 'expense',
                }.get(self.type))
        self.journal = Journal.find(pattern)

    @fields.depends('invoice_line', 'start_date', 'company')
    def on_change_invoice_line(self):
        pool = Pool()
        Currency = pool.get('currency.currency')
        if self.invoice_line:
            if not self.start_date:
                self.start_date = self.invoice_line.invoice.invoice_date
            invoice = self.invoice_line.invoice
            if self.company and invoice.currency != self.company.currency:
                with Transaction().set_context(date=invoice.currency_date):
                    self.amount = Currency.compute(
                        invoice.currency, self.invoice_line.amount,
                        self.company.currency)
            else:
                self.amount = self.invoice_line.amount

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

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

    @fields.depends('company')
    def on_change_with_currency(self, name=None):
        return self.company.currency if self.company else None

    @classmethod
    @ModelView.button
    @Workflow.transition('running')
    def run(cls, deferrals):
        pool = Pool()
        Period = pool.get('account.period')
        # Ensure it starts at an opened period
        for deferral in deferrals:
            Period.find(deferral.company, deferral.start_date)
        # Set state before create moves and defer amount to pass assert
        cls.write(deferrals, {'state': 'running'})
        deferred_moves = cls.defer_amount(deferrals)
        cls.create_moves(deferrals, _exclude=deferred_moves)
        cls.close_try(deferrals)

    @classmethod
    def close_try(cls, deferrals):
        "Try to close the deferrals if last move has been created"
        to_close = []
        for deferral in deferrals:
            if deferral.moves:
                last_move = deferral.moves[-1]
                if last_move.period.end_date >= deferral.end_date:
                    to_close.append(deferral)
        cls.close(to_close)

    @classmethod
    @Workflow.transition('closed')
    def close(cls, deferrals):
        for deferral in deferrals:
            assert (deferral.moves
                and deferral.moves[-1].period.end_date >= deferral.end_date)

    @classmethod
    def check_modification(cls, mode, deferrals, values=None, external=False):
        super().check_modification(
            mode, deferrals, values=values, external=external)
        if mode == 'delete':
            for deferral in deferrals:
                if deferral.state != 'draft':
                    raise AccessError(gettext(
                            'account_invoice_defer'
                            '.msg_invoice_deferred_delete_draft',
                            deferral=deferral.rec_name))

    @classmethod
    def defer_amount(cls, deferrals):
        pool = Pool()
        Move = pool.get('account.move')
        moves = []
        for deferral in deferrals:
            assert deferral.state == 'running'
            moves.append(deferral.get_move())
        Move.save(moves)
        Move.post(moves)
        return moves

    @classmethod
    def create_moves(cls, deferrals, _exclude=None):
        pool = Pool()
        Period = pool.get('account.period')
        Move = pool.get('account.move')
        exclude = set(_exclude or [])
        moves = []
        for deferral in deferrals:
            assert deferral.state == 'running'
            periods = Period.search([
                    ('company', '=', deferral.company.id),
                    ('type', '=', 'standard'),
                    ('start_date', '<=', deferral.end_date),
                    ('end_date', '>=', deferral.start_date),
                    ])
            for period in sorted(
                    set(periods) - {
                        m.period for m in deferral.moves if m not in exclude},
                    key=lambda p: p.start_date):
                moves.append(deferral.get_move(period))
        Move.save(moves)
        to_save = []
        for deferral in deferrals:
            if deferral.moves:
                last_move = deferral.moves[-1]
                if last_move.period.end_date >= deferral.end_date:
                    remainder = deferral.amount_remainder
                    if remainder:
                        for line in last_move.lines:
                            if line.debit:
                                line.debit -= remainder
                            else:
                                line.credit -= remainder
                        last_move.lines = last_move.lines
                        to_save.append(last_move)
        Move.save(to_save)
        Move.post(moves)

    @property
    def amount_daily(self):
        days = (self.end_date - self.start_date).days + 1
        return self.amount / days

    @property
    def amount_remainder(self):
        balance = 0
        for move in self.moves:
            invoice_account = self.invoice_line.account.current(move.date)
            for line in move.lines:
                if line.account == invoice_account:
                    balance += line.debit - line.credit
        return balance

    def get_move(self, period=None):
        pool = Pool()
        Move = pool.get('account.move')
        Line = pool.get('account.move.line')
        Configuration = pool.get('account.configuration')
        configuration = Configuration(1)
        move = Move(
            company=self.company,
            origin=self,
            journal=self.journal,
            )
        invoice = self.invoice_line.invoice

        income = Line()
        if period is None:
            move.period = invoice.move.period
            move.date = invoice.move.date
            amount = self.amount
            if amount >= 0:
                if invoice.type == 'out':
                    income.debit, income.credit = amount, 0
                else:
                    income.debit, income.credit = 0, amount
            else:
                if invoice.type == 'out':
                    income.debit, income.credit = 0, -amount
                else:
                    income.debit, income.credit = -amount, 0
        else:
            move.period = period
            move.date = period.start_date
            days = (
                min(period.end_date, self.end_date)
                - max(period.start_date, self.start_date)).days + 1
            amount = self.company.currency.round(self.amount_daily * days)
            if amount >= 0:
                if invoice.type == 'out':
                    income.debit, income.credit = 0, amount
                else:
                    income.debit, income.credit = amount, 0
            else:
                if invoice.type == 'out':
                    income.debit, income.credit = -amount, 0
                else:
                    income.debit, income.credit = 0, -amount
        income.account = self.invoice_line.account.current(move.date)
        if not income.account:
            raise AccountMissing(gettext(
                    'account_invoice_defer'
                    '.msg_invoice_deferred_invoice_line_missing_account',
                    deferral=self.rec_name,
                    account=self.invoice_line.account.rec_name))
        if income.account.party_required:
            income.party = invoice.party

        balance = Line()
        if invoice.type == 'out':
            balance.account = configuration.get_multivalue(
                'deferred_account_revenue', company=self.company.id)
            if not balance.account:
                raise AccountMissing(gettext(
                        'account_invoice_defer.'
                        'msg_missing_deferred_account_revenue'))
        else:
            balance.account = configuration.get_multivalue(
                'deferred_account_expense', company=self.company.id)
            if not balance.account:
                raise AccountMissing(gettext(
                        'account_invoice_defer.'
                        'msg_missing_deferred_account_expense'))
        balance.debit, balance.credit = income.credit, income.debit
        if balance.account.party_required:
            balance.party = invoice.party

        move.lines = [balance, income]
        return move


class InvoiceDeferredCreateMoves(Wizard):
    __name__ = 'account.invoice.deferred.create_moves'
    start_state = 'create_moves'
    create_moves = StateTransition()

    def transition_create_moves(self):
        pool = Pool()
        InvoiceDeferred = pool.get('account.invoice.deferred')
        with check_access():
            deferrals = InvoiceDeferred.search([
                    ('state', '=', 'running'),
                    ])
        deferrals = InvoiceDeferred.browse(deferrals)
        InvoiceDeferred.create_moves(deferrals)
        InvoiceDeferred.close_try(deferrals)
        return 'end'


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

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


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

    @classmethod
    def close(cls, periods):
        for period in periods:
            period.check_invoice_deferred_running()
        super().close(periods)

    def check_invoice_deferred_running(self):
        """
        Check if it exists any invoice deferred
        without account move for the period.
        """
        pool = Pool()
        InvoiceDeferred = pool.get('account.invoice.deferred')
        deferrals = InvoiceDeferred.search([
                ('state', '=', 'running'),
                ('company', '=', self.company.id),
                ['OR', [
                        ('start_date', '<=', self.start_date),
                        ('end_date', '>=', self.start_date),
                        ], [
                        ('start_date', '<=', self.end_date),
                        ('end_date', '>=', self.end_date),
                        ], [
                        ('start_date', '>=', self.start_date),
                        ('end_date', '<=', self.end_date),
                        ],
                    ],
                ('moves', 'not where', [
                        ('date', '>=', self.start_date),
                        ('date', '<=', self.end_date),
                        ]),
                ], limit=6)
        if deferrals:
            names = ', '.join(d.rec_name for d in deferrals[:5])
            if len(deferrals) > 5:
                names += '...'
            raise AccessError(
                gettext('account_invoice_defer'
                    '.msg_invoice_deferred_running_close_period',
                    period=self.rec_name,
                    deferrals=names))


class Invoice(metaclass=PoolMeta):
    __name__ = 'account.invoice'

    @classmethod
    def _post(cls, invoices):
        pool = Pool()
        InvoiceDeferred = pool.get('account.invoice.deferred')
        # defer invoices only the first time post is called
        invoices_to_defer = [i for i in invoices if not i.move]
        super()._post(invoices)
        deferrals = []
        for invoice in invoices_to_defer:
            for line in invoice.lines:
                if line.deferrable and line.defer_from and line.defer_to:
                    deferral = InvoiceDeferred(
                        company=invoice.company,
                        type=invoice.type,
                        journal=invoice.journal,
                        invoice_line=line,
                        start_date=line.defer_from,
                        end_date=line.defer_to)
                    deferral.on_change_invoice_line()
                    deferrals.append(deferral)
        InvoiceDeferred.save(deferrals)


class InvoiceLine(metaclass=PoolMeta):
    __name__ = 'account.invoice.line'

    deferrable = fields.Function(
        fields.Boolean("Deferrable"), 'on_change_with_deferrable')
    defer_from = fields.Date(
        "Defer From",
        domain=[
            If(Eval('deferrable', False) & Eval('defer_to', None),
                ('defer_from', '<', Eval('defer_to', None)),
                ()),
            ],
        states={
            'readonly': Eval('invoice_state') != 'draft',
            'invisible': ~Eval('deferrable', False),
            })
    defer_to = fields.Date(
        "Defer To",
        domain=[
            If(Eval('deferrable', False) & Eval('defer_from', None),
                ('defer_to', '>', Eval('defer_from', None)),
                ()),
            ],
        states={
            'readonly': Eval('invoice_state') != 'draft',
            'invisible': ~Eval('deferrable', False),
            })

    @fields.depends('product')
    def on_change_with_deferrable(self, name=None):
        if self.product:
            return self.product.type == 'service'
