"""
Fusion Accounting - Python Tax Code Engine
============================================

Extends the standard ``account.tax`` model with a ``python`` computation type.
Users can write arbitrary Python expressions that compute the tax amount based
on contextual variables such as ``price_unit``, ``quantity``, ``product``,
``partner``, and ``company``.

The code is executed inside Odoo's ``safe_eval`` sandbox so that potentially
destructive operations (file I/O, imports, exec, etc.) are blocked while still
providing the flexibility of a full expression language.

Example Python code for a tiered tax::

    # 5% on the first 1000, 8% above
    base = price_unit * quantity
    if base <= 1000:
        result = base * 0.05
    else:
        result = 1000 * 0.05 + (base - 1000) * 0.08

The code must assign the computed tax amount to the variable ``result``.

Copyright (c) Nexa Systems Inc. - All rights reserved.
"""

import logging

from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools.safe_eval import safe_eval, wrap_module

_logger = logging.getLogger(__name__)


# Whitelisted standard library modules available inside Python tax code
_MATH_MODULE = wrap_module(__import__('math'), [
    'ceil', 'floor', 'trunc', 'fabs', 'log', 'log10', 'pow', 'sqrt',
    'pi', 'e', 'inf',
])
_DATETIME_MODULE = wrap_module(__import__('datetime'), [
    'date', 'datetime', 'timedelta',
])


class FusionTaxPython(models.Model):
    """Adds a *Python Code* computation mode to the tax engine.

    When ``amount_type`` is set to ``'python'``, the tax amount is determined
    by executing the user-supplied Python code in a restricted sandbox.
    """

    _inherit = "account.tax"

    # -------------------------------------------------------------------------
    # Fields
    # -------------------------------------------------------------------------
    amount_type = fields.Selection(
        selection_add=[('python', 'Python Code')],
        ondelete={'python': 'set default'},
    )

    python_compute = fields.Text(
        string="Python Code",
        default="# Available variables:\n"
                "#   price_unit  - Unit price of the line\n"
                "#   quantity    - Quantity on the line\n"
                "#   product     - product.product record (or empty)\n"
                "#   partner     - res.partner record (or empty)\n"
                "#   company     - res.company record\n"
                "#   env         - Odoo Environment\n"
                "#   date        - Invoice/transaction date\n"
                "#   currency    - res.currency record\n"
                "#   math        - Python math module (ceil, floor, sqrt, ...)\n"
                "#   datetime    - Python datetime module\n"
                "#\n"
                "# Assign the tax amount to 'result':\n"
                "result = price_unit * quantity * 0.10\n",
        help="Python code executed to compute the tax amount. The code must "
             "assign the final tax amount to the variable ``result``. "
             "Runs inside Odoo's safe_eval sandbox with a restricted set of "
             "built-in functions.",
    )

    python_applicable = fields.Text(
        string="Applicability Code",
        help="Optional Python code that determines whether this tax applies. "
             "Must assign a boolean to ``result``. If not set, the tax always applies.",
    )

    # -------------------------------------------------------------------------
    # Constraints
    # -------------------------------------------------------------------------
    @api.constrains('amount_type', 'python_compute')
    def _check_python_compute(self):
        """Validate that Python-type taxes have non-empty compute code."""
        for tax in self:
            if tax.amount_type == 'python' and not (tax.python_compute or '').strip():
                raise ValidationError(_(
                    "Tax '%(name)s' uses Python computation but the code field is empty.",
                    name=tax.name,
                ))

    # -------------------------------------------------------------------------
    # Python Tax Computation
    # -------------------------------------------------------------------------
    def _compute_tax_python(self, price_unit, quantity, product=None,
                            partner=None, company=None, date=None,
                            currency=None):
        """Execute the Python tax code and return the computed amount.

        The code is evaluated in a restricted namespace containing relevant
        business objects. The evaluated code must assign a numeric value to
        the variable ``result``.

        :param price_unit: Unit price (before tax).
        :param quantity:   Line quantity.
        :param product:    ``product.product`` record or ``None``.
        :param partner:    ``res.partner`` record or ``None``.
        :param company:    ``res.company`` record or ``None``.
        :param date:       Transaction date (``datetime.date`` or ``False``).
        :param currency:   ``res.currency`` record or ``None``.
        :returns:          Computed tax amount as a ``float``.
        :raises UserError: If the code is invalid or fails to set ``result``.
        """
        self.ensure_one()
        if self.amount_type != 'python':
            raise UserError(_(
                "Tax '%(name)s' is not a Python-type tax (amount_type=%(type)s).",
                name=self.name,
                type=self.amount_type,
            ))

        resolved_company = company or self.env.company
        resolved_product = product or self.env['product.product']
        resolved_partner = partner or self.env['res.partner']
        resolved_currency = currency or resolved_company.currency_id

        local_context = {
            'price_unit': price_unit,
            'quantity': quantity,
            'product': resolved_product,
            'partner': resolved_partner,
            'company': resolved_company,
            'env': self.env,
            'date': date or fields.Date.context_today(self),
            'currency': resolved_currency,
            'math': _MATH_MODULE,
            'datetime': _DATETIME_MODULE,
            'result': 0.0,
            # Convenience aliases
            'base_amount': price_unit * quantity,
            'abs': abs,
            'round': round,
            'min': min,
            'max': max,
            'sum': sum,
            'len': len,
            'float': float,
            'int': int,
            'bool': bool,
            'str': str,
        }

        code = (self.python_compute or '').strip()
        if not code:
            return 0.0

        try:
            safe_eval(
                code,
                local_context,
                mode='exec',
                nocopy=True,
            )
        except Exception as exc:
            _logger.error(
                "Python tax computation failed for tax '%s' (id=%s): %s",
                self.name, self.id, exc,
            )
            raise UserError(_(
                "Error executing Python tax code for '%(name)s':\n%(error)s",
                name=self.name,
                error=str(exc),
            ))

        result = local_context.get('result', 0.0)
        if not isinstance(result, (int, float)):
            raise UserError(_(
                "Python tax code for '%(name)s' must assign a numeric value to "
                "'result', but got %(type)s instead.",
                name=self.name,
                type=type(result).__name__,
            ))

        return float(result)

    def _check_python_applicable(self, price_unit, quantity, product=None,
                                 partner=None, company=None, date=None,
                                 currency=None):
        """Evaluate the applicability code to determine if this tax applies.

        If no applicability code is set, the tax is always applicable.

        :returns: ``True`` if the tax should be applied, ``False`` otherwise.
        """
        self.ensure_one()
        code = (self.python_applicable or '').strip()
        if not code:
            return True

        resolved_company = company or self.env.company
        resolved_product = product or self.env['product.product']
        resolved_partner = partner or self.env['res.partner']
        resolved_currency = currency or resolved_company.currency_id

        local_context = {
            'price_unit': price_unit,
            'quantity': quantity,
            'product': resolved_product,
            'partner': resolved_partner,
            'company': resolved_company,
            'env': self.env,
            'date': date or fields.Date.context_today(self),
            'currency': resolved_currency,
            'math': _MATH_MODULE,
            'datetime': _DATETIME_MODULE,
            'result': True,
            'base_amount': price_unit * quantity,
            'abs': abs,
            'round': round,
            'min': min,
            'max': max,
        }

        try:
            safe_eval(code, local_context, mode='exec', nocopy=True)
        except Exception as exc:
            _logger.warning(
                "Python applicability check failed for tax '%s': %s. "
                "Treating as applicable.",
                self.name, exc,
            )
            return True

        return bool(local_context.get('result', True))

    # -------------------------------------------------------------------------
    # Tax Engine Integration
    # -------------------------------------------------------------------------
    def _get_tax_totals(self, base_lines, currency, company, cash_rounding=None):
        """Override to inject Python-computed taxes into the standard totals pipeline.

        For non-Python taxes, delegates entirely to ``super()``. For Python
        taxes, the computed amount is pre-calculated and substituted into the
        standard computation flow.
        """
        # Pre-compute amounts for Python taxes in the base lines
        for base_line in base_lines:
            record = base_line.get('record')
            if not record or not hasattr(record, 'tax_ids'):
                continue
            python_taxes = record.tax_ids.filtered(lambda t: t.amount_type == 'python')
            if not python_taxes:
                continue

            # Retrieve line-level data for context
            price_unit = base_line.get('price_unit', 0.0)
            quantity = base_line.get('quantity', 1.0)
            product = base_line.get('product', self.env['product.product'])
            partner = base_line.get('partner', self.env['res.partner'])
            date = base_line.get('date', False)

            for ptax in python_taxes:
                if ptax._check_python_applicable(
                    price_unit, quantity, product, partner, company, date, currency,
                ):
                    computed = ptax._compute_tax_python(
                        price_unit, quantity, product, partner, company, date, currency,
                    )
                    # Temporarily set the amount on the tax for the standard pipeline
                    ptax.with_context(fusion_python_tax_amount=computed)

        return super()._get_tax_totals(base_lines, currency, company, cash_rounding)
