from __future__ import annotations

import json
from dataclasses import dataclass
from typing import Any, Dict

from celpy import Environment, adapter, celtypes


def _fn_to_json(value: Any) -> str:
    return json.dumps(value)


def _fn_json(value: Any) -> Any:
    text = str(value)
    return adapter.json_to_cel(json.loads(text))


@dataclass
class CelEngine:
    _env: Environment

    @classmethod
    def default(cls) -> "CelEngine":
        return cls(_env=Environment())

    def eval(self, expr: str, ctx: Dict[str, Any]) -> Any:
        source = (expr or "").strip()
        if not source:
            return None
        ast = self._env.compile(source)
        program = self._env.program(
            ast,
            functions={
                "to_json": _fn_to_json,
                "json": _fn_json,
            },
        )
        cel_ctx = adapter.json_to_cel(ctx)
        value = program.evaluate(cel_ctx)
        return _to_python(value)


def _to_python(value: Any) -> Any:
    if isinstance(value, celtypes.BoolType):
        return bool(value)
    if isinstance(value, celtypes.IntType):
        return int(value)
    if isinstance(value, celtypes.DoubleType):
        return float(value)
    if isinstance(value, celtypes.StringType):
        return str(value)
    if isinstance(value, celtypes.ListType):
        return [_to_python(v) for v in value]
    if isinstance(value, celtypes.MapType):
        return {str(_to_python(k)): _to_python(v) for k, v in value.items()}
    # cel-python values generally cast cleanly through JSON for scalars/containers.
    try:
        dumped = json.dumps(value, default=str)
        return json.loads(dumped)
    except Exception:
        return value
