"""Safe expression evaluator for dynamic computations.

Uses Python's ``ast`` module for safe expression parsing and evaluation
with a configurable whitelist of allowed names and builtins.

Usage::

    from swifty.expressionator.manager import ExpressionatorManager

    em = ExpressionatorManager(data={"price": 100, "tax_rate": 0.1})
    total = em.evaluate("price * (1 + tax_rate)")  # 110.0
"""

import ast
from typing import Any, Dict, Optional

from swifty.expressionator.namespace import (
    ExpressionatorNamespace,
    ExpressionatorException,
    ALLOWED_BUILTINS,
    ALLOWED_NAMES,
    ALLOWED_AST_NODES,
)
from swifty.logging.logger import SwiftyLoggerMixin
from swifty.utils.objects import DictObject


class ExpressionatorManager(SwiftyLoggerMixin):
    """Safe expression evaluator with configurable namespace.

    Evaluates mathematical and logical expressions in a restricted
    environment. Uses AST node whitelisting for security instead of
    ``eval()`` on arbitrary code.

    Attributes:
        safe_locals: Combined namespace of allowed names, builtins, data, and custom functions.
    """

    _compiled_cache: Dict[str, Any] = {}

    def __init__(
        self,
        data: Optional[Dict] = None,
        namespace: type = ExpressionatorNamespace,
    ) -> None:
        """Initialize with data context and optional custom namespace.

        Args:
            data: Dictionary of variables available in expressions.
            namespace: Namespace class providing custom safe functions.
        """
        self.expr_data = data or {}
        self.safe_expr_namespace = namespace().safe_expr_namespace or {}
        self.safe_locals = {
            **ALLOWED_NAMES,
            **ALLOWED_BUILTINS,
            **self.expr_data,
            **self.safe_expr_namespace,
        }

    @property
    def expr_data(self):
        """Get the expression data context."""
        return self.__expr_data

    @expr_data.setter
    def expr_data(self, data):
        """Set expression data, converting dicts to DictObject for attribute access.

        Args:
            data: Dict or DictObject of expression variables.

        Raises:
            ExpressionatorException: If data is not a dict or DictObject.
        """
        if isinstance(data, DictObject):
            self.__expr_data = data
        elif isinstance(data, dict):
            self.__expr_data = DictObject(dict_data=data)
        else:
            raise ExpressionatorException("Invalid formatted data")

    def _evaluate_expression(
        self, expression: str, default: Any = None, strict: bool = False
    ) -> Any:
        """Evaluate an expression with error handling.

        Args:
            expression: The expression string to evaluate.
            default: Value to return on non-strict failure.
            strict: If True, re-raise exceptions instead of returning default.

        Returns:
            The evaluation result, or default on failure.
        """
        try:
            return self.evaluate(expression)
        except (ExpressionatorException, NameError) as error:
            self.logger.error(
                "An error occurred before evaluating the expression",
                expression=expression,
                default=default,
                error=error,
            )
        except Exception as error:
            self.logger.error(
                "An error occurred when evaluating the expression",
                expression=expression,
                default=default,
                error=error,
            )
            if strict:
                raise error
        return default

    def get_value(self, field_name: str) -> Any:
        """Get the value of an expression or simple field reference.

        Args:
            field_name: Expression string or field name to evaluate.

        Returns:
            The evaluated value.
        """
        return self._evaluate_expression(expression=field_name)

    def initialator(self, initialization_data: Dict[str, Any]) -> Any:
        """Evaluate an initialization expression.

        Args:
            initialization_data: Dict with ``expression`` and optional ``default`` keys.

        Returns:
            The evaluated result, or the default value.
        """
        expression = initialization_data.get("expression")
        default = initialization_data.get("default")
        return self._evaluate_expression(expression=expression, default=default)

    def calculator(
        self,
        calculation_data: Dict[str, Any],
        data: Optional[Dict] = None,
    ) -> Any:
        """Evaluate a calculation expression, optionally with new data.

        Args:
            calculation_data: Dict with ``expression`` and optional ``default`` keys.
            data: Optional new data context to use for this calculation.

        Returns:
            The calculated result, or the default value.
        """
        if data is not None:
            self.expr_data = data
            self.safe_locals.update(self.expr_data)
        expression = calculation_data.get("expression")
        default = calculation_data.get("default")
        return self._evaluate_expression(expression=expression, default=default)

    def validator(self, validation_data: Dict[str, Any], value: Any) -> bool:
        """Validate a value against an expression.

        Args:
            validation_data: Dict with ``expression`` key containing validation logic.
            value: The value to validate (available as ``value`` in the expression).

        Returns:
            True if validation passes, False otherwise.
        """
        self.safe_locals.update({"value": value})
        expression = validation_data.get("expression")
        return self._evaluate_expression(expression=expression, default=False)

    def evaluate(self, expression: str) -> Any:
        """Safely evaluate a mathematical/logical expression.

        Uses AST node whitelisting to prevent execution of arbitrary code.
        Compiled expressions are cached for performance.

        Args:
            expression: The expression string to evaluate.

        Raises:
            ExpressionatorException: If the expression uses disallowed constructs or names.

        Returns:
            The result of the expression.
        """
        # Validate allowed AST nodes
        try:
            tree = ast.parse(expression, mode="eval")
        except SyntaxError as exc:
            raise ExpressionatorException(
                f"Invalid expression syntax: {exc}"
            ) from exc

        self._validate_ast_nodes(tree)

        # Use cached compiled code when available
        if expression not in self._compiled_cache:
            self._compiled_cache[expression] = compile(
                tree, "<expression>", "eval"
            )

        code = self._compiled_cache[expression]

        # Validate names in the compiled code
        for name in code.co_names:
            if name not in self.safe_locals:
                raise ExpressionatorException(f"The use of '{name}' is not allowed")

        return eval(code, {"__builtins__": {}}, self.safe_locals)

    @staticmethod
    def _validate_ast_nodes(tree: ast.AST) -> None:
        """Validate that all AST nodes in the expression are allowed.

        Args:
            tree: The parsed AST tree.

        Raises:
            ExpressionatorException: If a disallowed node type is found.
        """
        for node in ast.walk(tree):
            if type(node) not in ALLOWED_AST_NODES:
                raise ExpressionatorException(
                    f"Disallowed expression construct: {type(node).__name__}"
                )
