from __future__ import annotations

import operator as op
from pathlib import Path
from typing import (
    Any,
    NamedTuple,
    Optional,
    Sequence,
    TypeAlias,
    Union,
    cast,
    Callable,
    TypeGuard,
)
from .config_var_def import ConfigVarDef
from numbers import Number
from .utils import _undef
import json
import inspect

PathType = Union[Path, ConfigVarDef, "ConfigVarExpression"]
""" Type that support Path operation """
PathOperand = Union[str, PathType]
""" operand that can be combined with a PathType in a Path operation """
NumericType = Union[Number, ConfigVarDef, "ConfigVarExpression"]
""" Type that support numeric operations """
NumericOperand = NumericType
""" operand that can be combined with a NumericType in a numeric operation """
StringType = Union[str, ConfigVarDef, "ConfigVarExpression"]
""" Type that support string operations """
StringOperand = StringType
""" operand that can be combined with a StringType in a string operation """
OperandType: TypeAlias = Union[
    PathOperand,
    NumericOperand,
    StringOperand,
]
""" Types that are supported as operand """


UnaryOperator = Callable[[OperandType], Any]
# Unary Operator type
BinaryOperator = Callable[[OperandType, OperandType], Any]
# Binary Operator type


def _is_operand(value: Any) -> TypeGuard[OperandType]:
    """Return True when value is a runtime-valid expression operand.

    Runtime checks cannot use ``isinstance(value, OperandType)`` safely because
    OperandType includes forward references through typing aliases.
    """
    return isinstance(value, (str, Path, Number, ConfigVarDef, ConfigVarExpression))


class ExpressionDecorator(NamedTuple):
    """Decorator of an expression node with the value resulting of applying
    the operator on  operands and the type of the result.
    """

    value: Any
    type_info: type


def config_path_expression(
    left: PathOperand, *others: PathOperand
) -> ConfigVarExpression:
    """Factory function to create a ConfigVarExpression from PathOperands.

    Used :
        - By Jinja filter and function 'p' and 'P'.
        - To provide backward compatibility with deprecated SegmentedPath.
    """

    if isinstance(left, ConfigVarExpression):
        left_expr = left
    elif isinstance(left, Path):
        left_expr = left
    elif isinstance(left, str):
        left_expr = Path(left)
    elif isinstance(left, ConfigVarDef):
        left_expr = left
    else:
        raise TypeError(
            f"ConfigVarDef {left.name} with type {left.type_info} is not compatible with Path operation"
        )

    if len(others) > 0:
        right = (
            config_path_expression(others[0], *others[1:])
            if len(others) > 1
            else others[0]
        )
        return ConfigVarExpression(left_expr, right, op.truediv, is_unary=False)
    else:
        return ConfigVarExpression(
            left_expr, None, lambda x: x, is_unary=True  # identity function
        )


class ConfigVarExpression:
    """Class representing an expression combining several configuration variables with different operators."""

    _expression: ConfigVarOperator
    """ The expression tree representing the operations to perfom at resolution time.
    """
    _resolved_value: Any = _undef
    """ The resolved value of this expression, set at resolution time. """
    _resolved_type: Optional[type] = None
    """ The resolved type of this expression, set at resolution time. """

    def __init__(
        self,
        left: OperandType,
        right: Optional[OperandType],
        operator: UnaryOperator | BinaryOperator,
        *,
        is_unary: bool = False,
    ) -> None:
        """Create a ConfigVarExpression that can be used in ConfigVar operation."""
        self._expression = ConfigVarOperator(
            left=left,
            right=right,
            operator=operator,
            is_unary=is_unary,
        )

    def resolve(self, substitutor: Callable[[Any], Any]) -> Any:
        """Resolve this expression to a concrete value by resolving the expression tree with the provided substitutor."""
        if self._resolved_value is not _undef:
            return self._resolved_value
        value, type_info = self._expression.resolve(substitutor)
        self._resolved_value = value
        self._resolved_type = type_info
        return value

    def _binary(
        self,
        other: OperandType,
        operator_fn: BinaryOperator,
    ) -> ConfigVarExpression:
        """Helper method used to create binary operation with self on the left and other on the right of the operator"""
        if not _is_operand(other):
            return NotImplemented
        return ConfigVarExpression(self, other, operator_fn)

    def _rbinary(
        self,
        other: OperandType,
        operator_fn: BinaryOperator,
    ) -> ConfigVarExpression:
        """Helper method used to create binary operation with self on the right and other on the left of the operator"""
        if not _is_operand(other):
            return NotImplemented
        return ConfigVarExpression(other, self, operator_fn)

    def __add__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for + operator between this ConfigVarExpression and another supported operand, returning a new ConfigVarExpression
        representing the addition operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._binary(other, op.add)

    def __radd__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for + operator between another supported operand and this ConfigVarExpression, returning a new ConfigVarExpression
        representing the addition operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._rbinary(other, op.add)

    def __sub__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for - operator between this ConfigVarExpression and another supported operand, returning a new ConfigVarExpression
        representing the subtraction operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._binary(other, op.sub)

    def __rsub__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for - operator between another supported operand and this ConfigVarExpression, returning a new ConfigVarExpression
        representing the subtraction operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._rbinary(other, op.sub)

    def __mul__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for * operator between this ConfigVarExpression and another supported operand returning a new ConfigVarExpression
        representing the multiplication operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._binary(other, op.mul)

    def __rmul__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for * operator between another supported operand and this ConfigVarExpression, returning a new ConfigVarExpression
        representing the multiplication operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._rbinary(other, op.mul)

    def __truediv__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for / operator between another supported operand and this ConfigVarExpression, returning a new ConfigVarExpression
        representing the division operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._binary(other, op.truediv)

    def __rtruediv__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for / operator between another supported operand and this ConfigVarExpression, returning a new ConfigVarExpression
        representing the division operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._rbinary(other, op.truediv)

    def __floordiv__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for // operator between this ConfigVarExpression and another supported operand, returning a new ConfigVarExpression
        representing the floor division operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._binary(other, op.floordiv)

    def __rfloordiv__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for // operator between another supported operand and this ConfigVarExpression, returning a new ConfigVarExpression
        representing the floor division operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._rbinary(other, op.floordiv)

    def __mod__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for % operator between this ConfigVarExpression and another supported operand, returning a new ConfigVarExpression
        representing the modulo operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._binary(other, op.mod)

    def __rmod__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for % operator between another supported operand and this ConfigVarExpression, returning a new ConfigVarExpression
        representing the modulo operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._rbinary(other, op.mod)

    def __pow__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for ** operator between this ConfigVarExpression and another supported operand, returning a new ConfigVarExpression
        representing the exponentiation operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._binary(other, op.pow)

    def __rpow__(self, other: OperandType) -> ConfigVarExpression:
        """Provide support for ** operator between another supported operand and this ConfigVarExpression, returning a new ConfigVarExpression
        representing the exponentiation operation."""
        if not _is_operand(other):
            return NotImplemented
        return self._rbinary(other, op.pow)

    def __neg__(self) -> ConfigVarExpression:
        """Provide support for the unary - operator on this ConfigVarExpression, returning a new ConfigVarExpression
        representing the negation operation."""
        return ConfigVarExpression(
            self,
            None,
            cast(UnaryOperator, op.neg),
            is_unary=True,
        )

    def __pos__(self) -> ConfigVarExpression:
        """Provide support for the unary + operator on this ConfigVarExpression, returning a new ConfigVarExpression
        representing the positive operation."""
        return ConfigVarExpression(
            self,
            None,
            cast(UnaryOperator, op.pos),
            is_unary=True,
        )

    def __abs__(self) -> ConfigVarExpression:
        """Provide support for the abs() function on this ConfigVarExpression, returning a new ConfigVarExpression
        representing the absolute value operation."""
        return ConfigVarExpression(
            self,
            None,
            cast(UnaryOperator, op.abs),
            is_unary=True,
        )

    def __json__(self) -> dict[str, Any]:
        """Json representation of the tree representing the expression."""
        return self._expression.__json__()

    # def __str__(self) -> str:
    #     """String representation of this ConfigVarExpression.
    #     If the expression is resolved, return the resolved value and type, otherwise return the expression tree
    #     """
    #     if self._resolved_value is not _undef:
    #         return f"{self._resolved_value}/{self._resolved_type}"
    #     return json.dumps(self.__json__(), indent=2)


class ConfigVarOperator:
    """Class representing a numeric operation that can be applied to ConfigVarExpression values and that can
       be resolved with variable resolution before applying the operator.
    Note: Resolution implies jinja template rendering and resolution of the value.
    """

    _right: Any
    """ right operand"""
    _left: Any
    """ left operand"""
    _unary_operator: Optional[UnaryOperator]
    """ unary operator"""
    _binary_operator: Optional[BinaryOperator]
    """ binary operator"""
    _is_unary: bool
    """ flag indicating if the operator is unary """
    _type_info: Optional[type]
    """ type of the result of this operation, computed at resolution time based on the operator and the type of the operands """
    _resolved_value: Any
    """ resolved value of this operation, computed at resolution time by applying the operator on the resolved operands """

    def __init__(
        self,
        left: Any,
        right: Any | None,
        operator: UnaryOperator | BinaryOperator,
        *,
        is_unary: bool = False,
    ) -> None:
        """Create a SegmentedVarOperator representing either a unary or binary operation.

        IMPORTANT: we use Any for left and right in order to avoid confusing the type checker with the ConfigVarDef
        assignement that would be interpreted as a property assignement (ConfigVarDef defines __set__()) not compatible
        with the OperandType.

        """
        self._left = left
        self._right = right
        self._is_unary = is_unary
        self._unary_operator = cast(UnaryOperator, operator) if is_unary else None
        self._binary_operator = None if is_unary else cast(BinaryOperator, operator)

    @staticmethod
    def compute_type(
        operator: UnaryOperator | BinaryOperator,
        left_type: type,
        right_type: Optional[type] = None,
    ) -> type:
        """Compute the type of the result of applying the operator on left and right operator.

        right is None for unary operattion.

        The operators that pose problem are:
        - division: it can be used in Path operations and in numeric operations.
        - addition: it can be used in string concatenation and in numeric addition.
        For all other operators we return numbers.Number as the result type.

        For division if one operand is a Number , we return numbers.Number as the result type, otherwise Path.
        For addition if one operand is a Number , we return numbers.Number as the result type, otherwise str.
        """
        if right_type is None:
            if operator in (op.neg, op.pos, op.abs):
                return Number
            else:
                return left_type
        if operator == op.truediv:
            if (
                isinstance(left_type, type)
                and issubclass(left_type, Number)
                or isinstance(right_type, type)
                and issubclass(right_type, Number)
            ):
                return Number
            else:
                return Path
        if operator == op.add:
            if (
                isinstance(left_type, type)
                and issubclass(left_type, Number)
                or isinstance(right_type, type)
                and issubclass(right_type, Number)
            ):
                return Number
            else:
                return str
        return Number

    def _decorate_operand(
        self,
        operand: OperandType,
        substitutor: Callable[[Any], Any],
    ) -> ExpressionDecorator:
        if operand is None:
            return ExpressionDecorator(None, type(None))
        elif isinstance(operand, ConfigVarExpression):
            value, type_info = operand._expression.resolve(substitutor)
            return ExpressionDecorator(value, type_info)
        elif isinstance(operand, ConfigVarDef):
            value = substitutor(operand)
            type_info = operand.type_info
            return ExpressionDecorator(value, type_info)
        elif isinstance(operand, str):
            value = substitutor(operand)
            type_info = type(value)
            return ExpressionDecorator(value, type_info)
        else:
            value = substitutor(operand)
            type_info = type(value)
            return ExpressionDecorator(value, type_info)

    def resolve(self, substitutor: Callable[[Any], Any]) -> ExpressionDecorator:
        """Resolve and evaluate this operator tree to a concrete value.

        The resolution is done by decorating the expression tree with the intermediate values and types of the operands and then computing the result and its type
        based on the decorated operands and the operator.
        We use a substitutor function to resolve the value of ConfigVarDef and to render the jinja template if the operand is a string.
        """

        # first decorate the tree with type information and intermediate values.
        right_value, right_type = None, None
        left_value, left_type = self._decorate_operand(self._left, substitutor)
        if not self._is_unary:
            right_value, right_type = self._decorate_operand(self._right, substitutor)

        if self._is_unary:
            if self._unary_operator is None:
                raise ValueError("Unary operation requires a unary operator")
            # we don't check the type of operand because we want to let the interpreter do the casting if possible
            res_type = self.compute_type(self._unary_operator, left_type)
            res = self._unary_operator(left_value)
            return ExpressionDecorator(res, res_type)

        if (
            self._right is None
            or self._binary_operator is None
            or right_value is None
            or right_type is None
        ):
            raise ValueError(
                "Binary operation requires a right operand and a binary operator"
            )

        res_type = self.compute_type(self._binary_operator, left_type, right_type)
        if self._binary_operator == op.add:
            if isinstance(res_type, type) and issubclass(res_type, str):
                res = f"{str(left_value)}{str(right_value)}"
            else:
                # we don't check the type of operand because we want to let the interpreter do the casting if possible
                res = self._binary_operator(left_value, right_value)
        elif self._binary_operator == op.truediv:
            if isinstance(res_type, type) and issubclass(res_type, Path):
                res = Path(str(left_value), str(right_value))
            else:
                # we don't check the type of operand because we want to let the interpreter do the casting if possible
                res = self._binary_operator(left_value, right_value)
        else:
            # we don't check the type of operand because we want to let the interpreter do the casting if possible
            res = self._binary_operator(left_value, right_value)
        return ExpressionDecorator(res, res_type)

    @staticmethod
    def _operator_str(operator: UnaryOperator | BinaryOperator) -> str:
        """Helper method to get a string representation of an operator function."""
        if operator == op.add:
            return "+"
        elif operator == op.sub:
            return "-"
        elif operator == op.mul:
            return "*"
        elif operator == op.truediv:
            return "/"
        elif operator == op.floordiv:
            return "//"
        elif operator == op.mod:
            return "%"
        elif operator == op.pow:
            return "**"
        elif operator == op.neg:
            return "-"
        elif operator == op.pos:
            return "+"
        elif operator == op.abs:
            return "abs"
        else:
            if callable(operator) and getattr(operator, "__name__", "") == "<lambda>":
                try:
                    return inspect.getsource(operator).strip()
                except (OSError, TypeError):
                    pass
            return str(operator)

    def __json__(self) -> dict[str, Any]:
        """JSON representation of this ConfigVarOperator."""
        operator_str: str = ""
        if self._is_unary and self._unary_operator is not None:
            operator_str = self._operator_str(self._unary_operator)
        elif self._binary_operator is not None:
            operator_str = self._operator_str(self._binary_operator)
        else:
            raise ValueError(
                "Operator must be either unary or binary with a valid operator function"
            )
        res: dict[str, Any] = {
            "operator": operator_str,
        }
        res["left"] = (
            self._left._expression.__json__()
            if isinstance(self._left, ConfigVarExpression)
            else str(self._left)
        )
        if not self._is_unary and self._right is not None:
            res["right"] = (
                self._right._expression.__json__()
                if isinstance(self._right, ConfigVarExpression)
                else str(self._right)
            )
        return res

    def __str__(self) -> str:
        """String representation of this ConfigVarExpression.
        print the tree reprenting this expression
        """
        return json.dumps(self.__json__(), indent=2)
