"""
Math expression checker: safely evaluates mathematical expressions.

Uses ast.parse to create a safe sandbox — NO eval() or exec().
Supports: +, -, *, /, //, %, ** (and ^ as alias for **), parentheses.
"""

import ast
import operator
import math


# Allowed binary operators
_BINARY_OPS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
    ast.FloorDiv: operator.floordiv,
    ast.Mod: operator.mod,
    ast.Pow: operator.pow,
    ast.BitXor: operator.pow,  # ^ as alias for **
}

# Allowed unary operators
_UNARY_OPS = {
    ast.UAdd: operator.pos,
    ast.USub: operator.neg,
}


def _safe_eval(node):
    """Recursively evaluate an AST node safely."""
    if isinstance(node, ast.Expression):
        return _safe_eval(node.body)

    if isinstance(node, ast.Constant):
        if isinstance(node.value, (int, float)):
            return node.value
        raise ValueError(f"Unsupported constant: {node.value!r}")

    # Python 3.7 compat: ast.Num (removed in 3.12+)
    if hasattr(ast, "Num") and isinstance(node, ast.Num):
        return node.n

    if isinstance(node, ast.BinOp):
        op_type = type(node.op)
        if op_type not in _BINARY_OPS:
            raise ValueError(f"Unsupported operator: {op_type.__name__}")
        left = _safe_eval(node.left)
        right = _safe_eval(node.right)
        # Guard against absurdly large exponents
        if op_type in (ast.Pow, ast.BitXor) and isinstance(right, (int, float)):
            if abs(right) > 1000:
                raise ValueError("Exponent too large")
        return _BINARY_OPS[op_type](left, right)

    if isinstance(node, ast.UnaryOp):
        op_type = type(node.op)
        if op_type not in _UNARY_OPS:
            raise ValueError(f"Unsupported unary operator: {op_type.__name__}")
        return _UNARY_OPS[op_type](_safe_eval(node.operand))

    raise ValueError(f"Unsupported expression: {type(node).__name__}")


def check(value):
    """Check if value is a math expression that evaluates to 42."""
    if not isinstance(value, str):
        return None

    s = value.strip()
    if not s:
        return None

    # Must contain at least one operator to be an "expression"
    # (plain "42" is handled by other checkers)
    has_operator = False
    for ch in s:
        if ch in "+-*/%^()":
            has_operator = True
            break
    if not has_operator:
        return None

    # Replace ^ with ** for Python parsing (but not ^^, etc.)
    # Simple approach: replace single ^ with **
    expr = ""
    i = 0
    while i < len(s):
        if s[i] == "^":
            expr += "**"
        else:
            expr += s[i]
        i += 1

    try:
        tree = ast.parse(expr, mode="eval")
        result = _safe_eval(tree)
    except Exception:
        return None

    try:
        if isinstance(result, float):
            if math.isnan(result) or math.isinf(result):
                return None
            if abs(result - 42) < 1e-9:
                return f"math expression: {s} = 42"
        elif isinstance(result, int):
            if result == 42:
                return f"math expression: {s} = 42"
    except (TypeError, ValueError):
        return None

    return None
