# 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 sql.aggregate import Sum

from trytond.i18n import gettext
from trytond.model import (
    DeactivableMixin, MatchMixin, ModelSQL, ModelView, Unique, Workflow,
    fields, sequence_ordered)
from trytond.model.exceptions import AccessError
from trytond.modules.company.model import (
    CompanyMultiValueMixin, CompanyValueMixin)
from trytond.modules.currency.fields import Monetary
from trytond.pool import Pool
from trytond.pyson import Bool, Eval, Id
from trytond.tools import (
    grouped_slice, is_full_text, lstrip_wildcard, reduce_ids)
from trytond.transaction import Transaction

STATES = {
    'readonly': Eval('state') == 'closed',
}


class Journal(
        DeactivableMixin, MatchMixin,
        sequence_ordered('matching_sequence', "Matching Sequence"),
        ModelSQL, ModelView, CompanyMultiValueMixin):
    'Journal'
    __name__ = 'account.journal'
    name = fields.Char('Name', size=None, required=True, translate=True)
    code = fields.Char('Code', size=None)
    type = fields.Selection([
            ('general', "General"),
            ('revenue', "Revenue"),
            ('expense', "Expense"),
            ('cash', "Cash"),
            ('situation', "Situation"),
            ('write-off', "Write-Off"),
            ], 'Type', required=True)
    sequence = fields.MultiValue(fields.Many2One(
            'ir.sequence', "Sequence",
            domain=[
                ('sequence_type', '=',
                    Id('account', 'sequence_type_account_journal')),
                ('company', 'in', [
                        Eval('context', {}).get('company', -1), None]),
                ],
            states={
                'required': Bool(Eval('context', {}).get('company', -1)),
                }))
    sequences = fields.One2Many(
        'account.journal.sequence', 'journal', "Sequences")
    debit = fields.Function(Monetary(
            "Debit", currency='currency', digits='currency'),
        'get_debit_credit_balance')
    credit = fields.Function(Monetary(
            "Credit", currency='currency', digits='currency'),
        'get_debit_credit_balance')
    balance = fields.Function(Monetary(
            "Balance", currency='currency', digits='currency'),
        'get_debit_credit_balance')

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

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

    @classmethod
    def default_sequence(cls, **pattern):
        return cls.multivalue_model('sequence').default_sequence()

    @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'
        code_value = operand
        if operator.endswith('like') and is_full_text(operand):
            code_value = lstrip_wildcard(operand)
        return [bool_op,
            ('code', operator, code_value, *extra),
            (cls._rec_name, operator, operand, *extra),
            ]

    @classmethod
    def get_currency(cls, journals, name):
        pool = Pool()
        Company = pool.get('company.company')
        company_id = Transaction().context.get('company')
        if company_id:
            company = Company(company_id)
            currency_id = company.currency.id
        else:
            currency_id = None
        return dict.fromkeys([j.id for j in journals], currency_id)

    @classmethod
    def get_debit_credit_balance(cls, journals, names):
        pool = Pool()
        MoveLine = pool.get('account.move.line')
        Move = pool.get('account.move')
        Account = pool.get('account.account')
        AccountType = pool.get('account.account.type')
        Company = pool.get('company.company')
        context = Transaction().context
        cursor = Transaction().connection.cursor()

        result = {}
        ids = [j.id for j in journals]
        for name in ['debit', 'credit', 'balance']:
            result[name] = dict.fromkeys(ids, 0)

        company_id = Transaction().context.get('company')
        if not company_id:
            return result
        company = Company(company_id)

        line = MoveLine.__table__()
        move = Move.__table__()
        account = Account.__table__()
        account_type = AccountType.__table__()
        where = ((move.date >= context.get('start_date'))
            & (move.date <= context.get('end_date'))
            & ~account_type.receivable
            & ~account_type.payable
            & (move.company == company.id))
        for sub_journals in grouped_slice(journals):
            sub_journals = list(sub_journals)
            red_sql = reduce_ids(move.journal, [j.id for j in sub_journals])
            query = line.join(move, 'LEFT', condition=line.move == move.id
                ).join(account, 'LEFT', condition=line.account == account.id
                ).join(account_type, 'LEFT',
                    condition=account.type == account_type.id
                ).select(move.journal, Sum(line.debit), Sum(line.credit),
                    where=where & red_sql,
                    group_by=move.journal)
            cursor.execute(*query)
            for journal_id, debit, credit in cursor:
                # SQLite uses float for SUM
                if not isinstance(debit, Decimal):
                    debit = Decimal(str(debit))
                if not isinstance(credit, Decimal):
                    credit = Decimal(str(credit))
                result['debit'][journal_id] = company.currency.round(debit)
                result['credit'][journal_id] = company.currency.round(credit)
                result['balance'][journal_id] = company.currency.round(
                    debit - credit)
        return result

    @classmethod
    def find(cls, pattern):
        for journal in cls.search(
                [],
                order=[
                    ('matching_sequence', 'ASC'),
                    ('id', 'ASC'),
                    ]):
            if journal.match(pattern):
                return journal

    @classmethod
    def write(cls, *args):
        pool = Pool()
        Move = pool.get('account.move')
        actions = iter(args)
        for journals, values in zip(actions, actions):
            if 'type' in values:
                for sub_journals in grouped_slice(journals):
                    moves = Move.search([
                            ('journal', 'in', [j.id for j in sub_journals]),
                            ('state', '=', 'posted')
                            ], order=[], limit=1)
                    if moves:
                        move, = moves
                        raise AccessError(gettext(
                                'account.msg_journal_account_moves',
                                journal=move.journal.rec_name))
        super().write(*args)


class JournalSequence(ModelSQL, CompanyValueMixin):
    "Journal Sequence"
    __name__ = 'account.journal.sequence'
    journal = fields.Many2One(
        'account.journal', "Journal", ondelete='CASCADE',
        context={
            'company': Eval('company', -1),
            },
        depends={'company'})
    sequence = fields.Many2One(
        'ir.sequence', "Sequence",
        domain=[
            ('sequence_type', '=',
                Id('account', 'sequence_type_account_journal')),
            ('company', 'in', [Eval('company', -1), None]),
            ],
        depends={'company'})

    @classmethod
    def default_sequence(cls):
        pool = Pool()
        ModelData = pool.get('ir.model.data')
        try:
            return ModelData.get_id('account', 'sequence_account_journal')
        except KeyError:
            return None


class JournalCashContext(ModelView):
    'Journal Cash Context'
    __name__ = 'account.journal.open_cash.context'
    start_date = fields.Date('Start Date', required=True)
    end_date = fields.Date('End Date', required=True)

    @classmethod
    def default_start_date(cls):
        return Pool().get('ir.date').today()
    default_end_date = default_start_date


class JournalPeriod(Workflow, ModelSQL, ModelView):
    'Journal - Period'
    __name__ = 'account.journal.period'
    journal = fields.Many2One(
        'account.journal', 'Journal', required=True, ondelete='CASCADE',
        states=STATES,
        context={
            'company': Eval('company', None),
            },
        depends=['company'])
    period = fields.Many2One('account.period', 'Period', required=True,
            ondelete='CASCADE', states=STATES)
    company = fields.Function(fields.Many2One(
            'company.company', "Company"),
        'on_change_with_company', searcher='search_company')
    icon = fields.Function(fields.Char('Icon'), 'get_icon')
    state = fields.Selection([
        ('open', 'Open'),
        ('closed', 'Closed'),
        ], 'State', readonly=True, required=True, sort=False)

    @classmethod
    def __setup__(cls):
        super(JournalPeriod, cls).__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('journal_period_uniq', Unique(t, t.journal, t.period),
                'account.msg_journal_period_unique'),
            ]
        cls._transitions |= set((
                ('open', 'closed'),
                ('closed', 'open'),
                ))
        cls._buttons.update({
                'close': {
                    'invisible': Eval('state') != 'open',
                    'depends': ['state'],
                    },
                'reopen': {
                    'invisible': Eval('state') != 'closed',
                    'depends': ['state'],
                    },
                })

    @classmethod
    def __register__(cls, module):
        cursor = Transaction().connection.cursor()
        t = cls.__table__()
        super().__register__(module)
        # Migration from 6.8: rename state close to closed
        cursor.execute(
            *t.update([t.state], ['closed'], where=t.state == 'close'))

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

    @classmethod
    def search_company(cls, name, clause):
        return [('period.' + clause[0], *clause[1:])]

    @staticmethod
    def default_state():
        return 'open'

    def get_rec_name(self, name):
        return '%s - %s' % (self.journal.rec_name, self.period.rec_name)

    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
                [('journal.rec_name',) + tuple(clause[1:])],
                [('period.rec_name',) + tuple(clause[1:])],
                ]

    def get_icon(self, name):
        return {
            'open': 'tryton-account-open',
            'closed': 'tryton-account-close',
            }.get(self.state)

    @classmethod
    def _check(cls, periods):
        Move = Pool().get('account.move')
        for period in periods:
            moves = Move.search([
                    ('journal', '=', period.journal.id),
                    ('period', '=', period.period.id),
                    ], limit=1)
            if moves:
                raise AccessError(
                    gettext('account.msg_modify_delete_journal_period_moves',
                        journal_period=period.rec_name))

    @classmethod
    def create(cls, vlist):
        Period = Pool().get('account.period')
        for vals in vlist:
            if vals.get('period'):
                period = Period(vals['period'])
                if period.state != 'open':
                    raise AccessError(
                        gettext('account'
                            '.msg_create_journal_period_closed_period',
                            period=period.rec_name))
        return super(JournalPeriod, cls).create(vlist)

    @classmethod
    def write(cls, *args):
        actions = iter(args)
        for journal_periods, values in zip(actions, actions):
            if (values != {'state': 'closed'}
                    and values != {'state': 'open'}):
                cls._check(journal_periods)
            if values.get('state') == 'open':
                for journal_period in journal_periods:
                    if journal_period.period.state != 'open':
                        raise AccessError(
                            gettext('account'
                                '.msg_open_journal_period_closed_period',
                                journal_period=journal_period.rec_name,
                                period=journal_period.period.rec_name))
        super(JournalPeriod, cls).write(*args)

    @classmethod
    def delete(cls, periods):
        cls._check(periods)
        super(JournalPeriod, cls).delete(periods)

    @classmethod
    @ModelView.button
    @Workflow.transition('closed')
    def close(cls, periods):
        '''
        Close journal - period
        '''
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('open')
    def reopen(cls, periods):
        "Open journal - period"
        pass
