# 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 defaultdict
from itertools import groupby

from sql import Literal

from trytond.model import (
    ChatMixin, Index, Model, ModelSQL, ModelView, Unique, fields,
    sequence_ordered)
from trytond.modules.currency.fields import Monetary
from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.transaction import Transaction, check_access
from trytond.wizard import (
    Button, StateAction, StateTransition, StateView, Wizard)


class Procedure(ModelSQL, ModelView):
    __name__ = 'account.dunning.procedure'
    name = fields.Char('Name', required=True, translate=True,
        help="The main identifier of the Dunning Procedure.")
    levels = fields.One2Many('account.dunning.level', 'procedure', 'Levels')

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


class Level(sequence_ordered(), ModelSQL, ModelView):
    __name__ = 'account.dunning.level'
    procedure = fields.Many2One(
        'account.dunning.procedure', "Procedure", required=True)
    overdue = fields.TimeDelta('Overdue',
        help="The delay from the maturity date "
        "after which the level should be applied.")

    def get_rec_name(self, name):
        return '%s@%s' % (self.procedure.levels.index(self),
            self.procedure.rec_name)

    def test(self, line, date):
        if self.overdue is not None:
            return (date - line.maturity_date) >= self.overdue


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


class Dunning(ModelSQL, ModelView, ChatMixin):
    __name__ = 'account.dunning'
    company = fields.Many2One(
        'company.company', "Company", required=True, states=_STATES,
        help="Make the dunning belong to the company.")
    line = fields.Many2One('account.move.line', 'Line', required=True,
        help="The receivable line to dun for.",
        domain=[
            ('account.type.receivable', '=', True),
            ('account.company', '=', Eval('company', -1)),
            ['OR',
                ('debit', '>', 0),
                ('credit', '<', 0),
                ],
            ],
        states=_STATES)
    procedure = fields.Many2One('account.dunning.procedure', 'Procedure',
        required=True, states=_STATES)
    level = fields.Many2One('account.dunning.level', 'Level', required=True,
        domain=[
            ('procedure', '=', Eval('procedure', -1)),
            ],
        states=_STATES)
    date = fields.Date(
        "Date", readonly=True,
        states={
            'invisible': Eval('state') == 'draft',
            },
        help="When the dunning reached the level.")
    age = fields.Function(fields.TimeDelta(
            "Age",
            states={
                'invisible': Eval('state') == 'draft',
            },
            help="How long the dunning has been at the level."),
        'get_age')
    blocked = fields.Boolean('Blocked',
        help="Check to block further levels of the procedure.")
    state = fields.Selection([
            ('draft', 'Draft'),
            ('waiting', "Waiting"),
            ('final', "Final"),
            ], 'State', readonly=True, sort=False)
    active = fields.Function(fields.Boolean('Active'), 'get_active',
        searcher='search_active')
    party = fields.Function(fields.Many2One(
            'party.party', 'Party',
            context={
                'company': Eval('company', -1),
                },
            depends={'company'}),
        'get_line_field', searcher='search_line_field')
    amount = fields.Function(Monetary(
            "Amount", currency='currency', digits='currency'),
        'get_amount')
    currency = fields.Function(fields.Many2One(
            'currency.currency', "Currency"), 'get_line_field')
    maturity_date = fields.Function(fields.Date('Maturity Date'),
        'get_line_field', searcher='search_line_field')
    amount_second_currency = fields.Function(Monetary(
            'Amount Second Currency',
            currency='second_currency', digits='second_currency',
            states={
                'invisible': Eval('currency') == Eval('second_currency'),
                }),
        'get_amount_second_currency')
    second_currency = fields.Function(fields.Many2One('currency.currency',
            'Second Currency'), 'get_second_currency')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        table = cls.__table__()
        cls._sql_constraints = [
            ('line_unique', Unique(table, table.line),
                'account_dunning.msg_dunning_line_unique'),
            ]
        cls._sql_indexes.add(
            Index(
                table,
                (table.state, Index.Equality(cardinality='low')),
                where=table.state.in_(['draft', 'waiting'])))
        cls._active_field = 'active'
        cls._buttons.update({
                'reschedule': {
                    'invisible': ~Eval('active', True),
                    'depends': ['active'],
                    },
                })

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

    @staticmethod
    def default_state():
        return 'draft'

    def get_age(self, name):
        pool = Pool()
        Date = pool.get('ir.date')
        if self.date:
            with Transaction().set_context(company=self.company.id):
                return Date.today() - self.date

    @classmethod
    def order_age(cls, tables):
        pool = Pool()
        Date = pool.get('ir.date')
        table, _ = tables[None]
        return [Literal(Date.today()) - table.date]

    def get_active(self, name):
        return not self.line.reconciliation

    def get_line_field(self, name):
        value = getattr(self.line, name)
        if isinstance(value, Model):
            return value.id
        else:
            return value

    @classmethod
    def search_line_field(cls, name, clause):
        return [('line.' + clause[0],) + tuple(clause[1:])]

    def get_amount(self, name):
        return self.line.debit - self.line.credit

    def get_amount_second_currency(self, name):
        amount = self.line.debit - self.line.credit
        if self.line.amount_second_currency:
            return self.line.amount_second_currency.copy_sign(amount)
        else:
            return amount

    def get_second_currency(self, name):
        if self.line.second_currency:
            return self.line.second_currency.id
        else:
            return self.line.account.company.currency.id

    @classmethod
    def search_active(cls, name, clause):
        reverse = {
            '=': '!=',
            '!=': '=',
            }
        if clause[1] in reverse:
            if clause[2]:
                return [('line.reconciliation', clause[1], None)]
            else:
                return [('line.reconciliation', reverse[clause[1]], None)]
        else:
            return []

    def get_rec_name(self, name):
        return f'{self.level.rec_name} [{self.line.rec_name}]'

    @classmethod
    def search_rec_name(cls, name, clause):
        _, operator, operand, *extra = clause
        if operator.startswith('!') or operator.startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
            ('level.rec_name', operator, operand, *extra),
            ('line.rec_name', operator, operand, *extra),
            ]

    @classmethod
    def _overdue_line_domain(cls, date):
        return [
            ('account.type.receivable', '=', True),
            ('dunnings', '=', None),
            ('maturity_date', '<=', date),
            ['OR',
                ('debit', '>', 0),
                ('credit', '<', 0),
                ],
            ('party', '!=', None),
            ('reconciliation', '=', None),
            ]

    @classmethod
    def generate_dunnings(cls, date=None):
        pool = Pool()
        Date = pool.get('ir.date')
        MoveLine = pool.get('account.move.line')

        if date is None:
            date = Date.today()

        set_level = defaultdict(list)
        with check_access():
            dunnings = cls.search([
                    ('state', '=', 'waiting'),
                    ('blocked', '=', False),
                    ])
        dunnings = cls.browse(dunnings)
        for dunning in dunnings:
            procedure = dunning.procedure
            levels = procedure.levels
            levels = levels[levels.index(dunning.level) + 1:]
            if levels:
                for level in levels:
                    if level.test(dunning.line, date):
                        break
                else:
                    level = dunning.level
                if level != dunning.level:
                    set_level[level].append(dunning)
            else:
                set_level[None].append(dunning)
        to_write = []
        for level, dunnings in set_level.items():
            if level:
                to_write.extend((dunnings, {
                            'level': level.id,
                            'state': 'draft',
                            'date': None,
                            }))
            else:
                to_write.extend((dunnings, {
                            'state': 'final',
                            'date': Date.today(),
                            }))
        if to_write:
            cls.write(*to_write)

        with check_access():
            lines = MoveLine.search(cls._overdue_line_domain(date))
        lines = MoveLine.browse(lines)
        dunnings = (cls._get_dunning(line, date) for line in lines)
        cls.save([d for d in dunnings if d])

    @classmethod
    def _get_dunning(cls, line, date):
        procedure = line.dunning_procedure
        if not procedure:
            return
        for level in procedure.levels:
            if level.test(line, date):
                break
        else:
            return
        return cls(
            company=line.account.company,
            line=line,
            procedure=procedure,
            level=level,
            )

    def chat_language(self, audience='internal'):
        language = super().chat_language(audience=audience)
        if audience == 'public':
            language = (
                self.party.lang.code if self.party and self.party.lang
                else None)
        return language

    @classmethod
    def process(cls, dunnings):
        pool = Pool()
        Date = pool.get('ir.date')
        for company, c_dunnings in groupby(dunnings, key=lambda d: d.company):
            with Transaction().set_context(company=company.id):
                today = Date.today()
            cls.write([d for d in c_dunnings
                    if not d.blocked and d.state == 'draft'], {
                    'state': 'waiting',
                    'date': today,
                    })

    @classmethod
    @ModelView.button_action('account_dunning.act_reschedule_dunning_wizard')
    def reschedule(cls, dunnings):
        pass


class CreateDunningStart(ModelView):
    __name__ = 'account.dunning.create.start'
    date = fields.Date('Date', required=True,
        help="Create dunning up to this date.")

    @staticmethod
    def default_date():
        Date = Pool().get('ir.date')
        return Date.today()


class CreateDunning(Wizard):
    __name__ = 'account.dunning.create'
    start = StateView('account.dunning.create.start',
        'account_dunning.dunning_create_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Create', 'create_', 'tryton-ok', default=True),
            ])
    create_ = StateAction('account_dunning.act_dunning_form')

    def do_create_(self, action):
        pool = Pool()
        Dunning = pool.get('account.dunning')
        Dunning.generate_dunnings(date=self.start.date)
        return action, {}


class ProcessDunningStart(ModelView):
    __name__ = 'account.dunning.process.start'


class ProcessDunning(Wizard):
    __name__ = 'account.dunning.process'
    start = StateView('account.dunning.process.start',
        'account_dunning.dunning_process_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Process', 'process', 'tryton-ok', default=True),
        ])
    process = StateTransition()

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

        # _actions is the list that define the order of each state to process
        # after the 'process' state.
        cls._actions = ['process']

    def next_state(self, state):
        "Return the next state for the current state"
        try:
            i = self._actions.index(state)
            return self._actions[i + 1]
        except (ValueError, IndexError):
            return 'end'

    def transition_process(self):
        self.model.process(self.records)
        return self.next_state('process')


class RescheduleDunning(Wizard):
    __name__ = 'account.dunning.reschedule'
    start = StateAction('account.act_reschedule_lines_wizard')

    def do_start(self, action):
        return action, {
            'ids': [self.record.line.id],
            'model': 'account.move.line',
            }
