# 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 xml.sax.saxutils import quoteattr

from simpleeval import InvalidExpression, simple_eval

from trytond.i18n import gettext
from trytond.model import (
    DeactivableMixin, ModelSQL, ModelView, fields, sequence_ordered)
from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.tools import decistmt
from trytond.transaction import Transaction
from trytond.wizard import (
    Button, StateAction, StateTransition, StateView, Wizard)

from .exceptions import (
    MoveTemplateExpressionError, MoveTemplateKeywordValidationError,
    PeriodNotFoundError)


class MoveTemplate(DeactivableMixin, ModelSQL, ModelView):
    __name__ = 'account.move.template'
    name = fields.Char('Name', required=True, translate=True)
    keywords = fields.One2Many('account.move.template.keyword', 'move',
        'Keywords')
    company = fields.Many2One('company.company', 'Company', required=True)
    journal = fields.Many2One(
        'account.journal', 'Journal', required=True,
        context={
            'company': Eval('company', -1),
            },
        depends={'company'})
    description = fields.Char('Description',
        help="Keyword value substitutions are identified "
        "by braces ('{' and '}').")
    lines = fields.One2Many('account.move.line.template', 'move', 'Lines',
        domain=[
            ('account.company', '=', Eval('company', -1)),
            ])

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

    @classmethod
    def validate_fields(cls, templates, field_names):
        super().validate_fields(templates, field_names)
        cls.check_description(templates, field_names)

    @classmethod
    def check_description(cls, templates, field_names=None):
        pool = Pool()
        Keyword = pool.get('account.move.template.keyword')
        if field_names and not (field_names & {'description', 'keywords'}):
            return
        for template in templates:
            if template.description:
                values = {k.name: '' for k in template.keywords}
                try:
                    template.description.format(
                        **dict(Keyword.format_values(template, values)))
                except (KeyError, ValueError) as e:
                    raise MoveTemplateKeywordValidationError(
                        gettext(
                            'account.msg_move_template_invalid_description',
                            description=template.description,
                            template=template.rec_name,
                            error=e)) from e

    def get_move(self, values):
        'Return the move for the keyword values'
        pool = Pool()
        Move = pool.get('account.move')
        Keyword = pool.get('account.move.template.keyword')

        move = Move()
        move.company = self.company
        move.journal = self.journal
        if self.description:
            try:
                move.description = self.description.format(
                    **dict(Keyword.format_values(self, values)))
            except (KeyError, ValueError) as e:
                raise MoveTemplateExpressionError(
                    gettext(
                        'account.msg_move_template_invalid_description',
                        description=self.description,
                        template=self.rec_name,
                        error=e)) from e
        move.lines = [l.get_line(values) for l in self.lines]

        return move


class MoveTemplateKeyword(sequence_ordered(), ModelSQL, ModelView):
    __name__ = 'account.move.template.keyword'
    name = fields.Char('Name', required=True)
    string = fields.Char('String', required=True, translate=True)
    move = fields.Many2One(
        'account.move.template', "Move", required=True, ondelete='CASCADE')
    type_ = fields.Selection([
            ('char', 'Char'),
            ('numeric', 'Numeric'),
            ('date', 'Date'),
            ('party', 'Party'),
            ], 'Type')
    required = fields.Boolean('Required')
    digits = fields.Integer('Digits', states={
            'invisible': Eval('type_') != 'numeric',
            'required': Eval('type_') == 'numeric',
            })

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__access__.add('move')

    @classmethod
    def validate(cls, keywords):
        for keyword in keywords:
            keyword.check_name()

    def check_name(self):
        if self.name and not self.name.isidentifier():
            raise MoveTemplateKeywordValidationError(
                gettext('account.msg_name_not_valid',
                    name=self.name))

    @staticmethod
    def default_required():
        return False

    def get_field(self):
        field = getattr(self, '_get_field_%s' % self.type_)()
        field.update({
                'name': self.name,
                'string': self.string,
                'required': self.required,
                'help': '',
                })
        return field

    def _get_field_char(self):
        return {'type': 'char'}

    def _get_field_numeric(self):
        return {'type': 'numeric', 'digits': (16, self.digits)}

    def _format_numeric(self, lang, value):
        if value:
            return lang.format('%.*f', (self.digits, value), True)
        else:
            return ''

    def _get_field_date(self):
        return {'type': 'date'}

    def _format_date(self, lang, value):
        if value:
            return lang.strftime(value)
        else:
            return ''

    def _get_field_party(self):
        return {
            'type': 'many2one',
            'relation': 'party.party',
            }

    def _format_party(self, lang, value):
        pool = Pool()
        Party = pool.get('party.party')
        if value:
            return Party(value).rec_name
        else:
            return ''

    @staticmethod
    def format_values(template, values):
        "Yield key and formatted value"
        pool = Pool()
        Lang = pool.get('ir.lang')

        lang, = Lang.search([
                ('code', '=', Transaction().language),
                ])
        keywords = {k.name: k for k in template.keywords}

        for k, v in values.items():
            keyword = keywords[k]
            func = getattr(keyword, '_format_%s' % keyword.type_, None)
            if func:
                yield k, func(lang, v)
            else:
                yield k, v


class MoveLineTemplate(ModelSQL, ModelView):
    __name__ = 'account.move.line.template'
    move = fields.Many2One(
        'account.move.template', "Move", required=True, ondelete='CASCADE')
    operation = fields.Selection([
            ('debit', 'Debit'),
            ('credit', 'Credit'),
            ], 'Operation', required=True)
    amount = fields.Char('Amount', required=True,
        help="A python expression that will be evaluated with the keywords.")
    account = fields.Many2One('account.account', 'Account', required=True,
        domain=[
            ('type', '!=', None),
            ('closed', '!=', True),
            ('company', '=', Eval('_parent_move', {}).get('company', -1)),
            ])
    party = fields.Char('Party',
        states={
            'required': Eval('party_required', False),
            'invisible': ~Eval('party_required', False),
            },
        help="The name of the 'Party' keyword.")
    party_required = fields.Function(fields.Boolean('Party Required'),
        'on_change_with_party_required')
    description = fields.Char('Description',
        help="Keyword value substitutions are identified "
        "by braces ('{' and '}').")
    taxes = fields.One2Many('account.tax.line.template', 'line', 'Taxes')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__access__.add('move')

    @fields.depends('account')
    def on_change_with_party_required(self, name=None):
        if self.account:
            return self.account.party_required
        return False

    def get_line(self, values):
        'Return the move line for the keyword values'
        pool = Pool()
        Line = pool.get('account.move.line')
        Keyword = pool.get('account.move.template.keyword')

        line = Line()
        try:
            amount = simple_eval(decistmt(self.amount),
                functions={'Decimal': Decimal}, names=values)
        except (InvalidExpression, SyntaxError) as e:
            raise MoveTemplateExpressionError(
                gettext('account.msg_move_template_invalid_expression',
                    expression=values,
                    template=self.move.rec_name,
                    error=e)) from e

        if not isinstance(amount, Decimal):
            raise MoveTemplateExpressionError(
                gettext('account.msg_move_template_expression_not_number',
                    value=amount,
                    expression=self.move.name,
                    template=self.move.rec_name))

        amount = self.move.company.currency.round(amount)
        if self.operation == 'debit':
            line.debit = amount
        else:
            line.credit = amount
        line.account = self.account
        if self.party:
            line.party = values.get(self.party)
        if self.description:
            try:
                line.description = self.description.format(
                    **dict(Keyword.format_values(self.move, values)))
            except KeyError as e:
                raise MoveTemplateExpressionError(
                    gettext('account.msg_move_template_invalid_expression',
                        expression=values,
                        template=self.move.name,
                        error=e)) from e
        line.tax_lines = [t.get_line(values) for t in self.taxes]

        return line


class TaxLineTemplate(ModelSQL, ModelView):
    __name__ = 'account.tax.line.template'
    line = fields.Many2One(
        'account.move.line.template', "Line",
        required=True, ondelete='CASCADE')
    amount = fields.Char('Amount', required=True,
        help="A python expression that will be evaluated with the keywords.")
    type = fields.Selection([
            ('tax', "Tax"),
            ('base', "Base"),
            ], "Type", required=True)

    tax = fields.Many2One('account.tax', 'Tax',
        domain=[
            ('company', '=', Eval('_parent_line', {}
                    ).get('_parent_move', {}).get('company', -1)),
            ],
        depends={'line'})

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__access__.add('line')

    def get_line(self, values):
        'Return the tax line for the keyword values'
        pool = Pool()
        TaxLine = pool.get('account.tax.line')

        line = TaxLine()
        try:
            amount = simple_eval(decistmt(self.amount),
                functions={'Decimal': Decimal}, names=values)
        except (InvalidExpression, SyntaxError) as e:
            raise MoveTemplateExpressionError(
                gettext('account.msg_template_invalid_expression',
                    expression=values,
                    template=self.line.rec_name,
                    error=e)) from e

        if not isinstance(amount, Decimal):
            raise MoveTemplateExpressionError(
                gettext('account.msg_not_number',
                    result=amount,
                    expression=self.move.rec_name))
        amount = self.line.move.company.currency.round(amount)
        line.amount = amount
        line.type = self.type
        line.tax = self.tax
        return line


class KeywordStateView(StateView):

    def get_view(self, wizard, state_name):
        fields = {}
        view = {
            'model': 'account.move.template.create.keywords',
            'view_id': 0,
            'type': 'form',
            'fields': fields,
            }
        if not wizard.template.template:
            return view
        template = wizard.template.template
        field_template = ('<label name=%(name)s/>'
            '<field name=%(name)s/>')
        view['arch'] = ('<?xml version="1.0"?>'
            '<form col="2" string=%s>%s</form>' % (
                quoteattr(template.name),
                ''.join(field_template % {'name': quoteattr(keyword.name)}
                    for keyword in template.keywords)
                ))
        for keyword in template.keywords:
            fields[keyword.name] = keyword.get_field()
        return view

    def get_defaults(self, wizard, state_name, fields):
        return {}


class CreateMove(Wizard):
    __name__ = 'account.move.template.create'
    start = StateTransition()
    template = StateView('account.move.template.create.template',
        'account.move_template_create_template_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Next', 'keywords', 'tryton-forward', default=True),
            ])
    keywords = KeywordStateView('account.move.template.create.keywords',
        None, [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Create', 'create_', 'tryton-ok', default=True),
            ])
    create_ = StateTransition()
    open_ = StateAction('account.act_move_from_template')

    def create_move(self):
        template = self.template.template
        values = {}
        for keyword in template.keywords:
            values[keyword.name] = getattr(self.keywords, keyword.name, None)
        move = template.get_move(values)
        move.date = self.template.date
        move.period = self.template.period
        move.save()
        return move

    def transition_start(self):
        context = Transaction().context
        action_id = context.get('action_id')
        period = context.get('period')
        if self.model and self.model.__name__ == 'account.move.line':
            # Template id is used as action
            self.template.template = action_id
            self.template.period = period
            return 'keywords'
        else:
            return 'template'

    def transition_create_(self):
        if self.model and self.model.__name__ == 'account.move.line':
            self.create_move()
            return 'end'
        else:
            return 'open_'

    def do_open_(self, action):
        move = self.create_move()
        return action, {'res_id': move.id}

    def end(self):
        if self.model and self.model.__name__ == 'account.move.line':
            return 'reload'


class CreateMoveTemplate(ModelView):
    __name__ = 'account.move.template.create.template'
    template = fields.Many2One('account.move.template', 'Template',
        required=True,
        domain=[
            ('company', '=', Eval('context', {}).get('company', -1)),
            ])
    date = fields.Date('Effective Date', required=True)
    period = fields.Many2One('account.period', 'Period', required=True,
        domain=[
            ('state', '!=', 'closed'),
            ('fiscalyear.company.id', '=',
                Eval('context', {}).get('company', 0)),
            ])

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

    @classmethod
    def default_period(cls):
        pool = Pool()
        Period = pool.get('account.period')
        company = Transaction().context.get('company')
        try:
            period = Period.find(company)
        except PeriodNotFoundError:
            return None
        return period.id

    @fields.depends('date', 'period')
    def on_change_date(self):
        pool = Pool()
        Period = pool.get('account.period')
        company = Transaction().context.get('company')
        if self.date:
            if (not self.period
                or not (
                    self.period.start_date <= self.date
                    <= self.period.end_date)):
                try:
                    self.period = Period.find(company, date=self.date)
                except PeriodNotFoundError:
                    pass

    @fields.depends('period', 'date')
    def on_change_period(self):
        pool = Pool()
        Date = pool.get('ir.date')
        today = Date.today()
        if self.period:
            start_date = self.period.start_date
            end_date = self.period.end_date
            if (not self.date
                    or not (start_date <= self.date <= end_date)):
                if start_date <= today:
                    if today <= end_date:
                        self.date = today
                    else:
                        self.date = end_date
                else:
                    self.date = start_date


class CreateMoveKeywords(ModelView):
    __no_slots__ = True
    __name__ = 'account.move.template.create.keywords'
