from __future__ import annotations

import ast
import os
import platform
from dataclasses import dataclass
from typing import Any, Dict, Mapping, Optional

from simpleeval import SimpleEval


@dataclass(frozen=True)
class WhenContext:
    vars: Dict[str, Any]
    env: Dict[str, str]
    ctx: Dict[str, Any]


class WhenEvaluator:
    """
    Constrained evaluator for step.when using simpleeval.

    Security/constraint goals:
    - Only primitives + dict/list of primitives in scope.
    - No attribute access.
    - Only a tiny set of functions.
    - Strict by default: unknown names cause error.
    - Max expression length.
    """

    def __init__(self, *, max_len: int = 512) -> None:
        self.max_len = max_len

    def eval(self, expr: str, wc: WhenContext) -> bool:
        expr = (expr or "").strip()
        if not expr:
            return True
        if len(expr) > self.max_len:
            raise ValueError("when expression too long")

        s = _make_simpleeval(wc)
        val = s.eval(expr)

        if isinstance(val, bool):
            return val
        # allow truthy primitives
        return bool(val)


def _make_simpleeval(wc: WhenContext) -> SimpleEval:
    # Only pass simple data structures
    safe_vars = _sanitize(wc.vars)
    safe_env = _sanitize(wc.env)
    safe_ctx = _sanitize(wc.ctx)

    s = SimpleEval()

    # Disallow attribute access entirely
    s.attr_index = {}  # no allowed attributes
    s.allowed_attrs = {}  # for older versions; safe no-op if unused

    # Strongly restrict names
    s.names = {
        "vars": safe_vars,
        "env": safe_env,
        "ctx": safe_ctx,
        # convenience
        "True": True,
        "False": False,
        "None": None,
    }

    # Strictly restrict functions
    s.functions = {
        "has": _fn_has,
        "get": _fn_get,
        "contains": _fn_contains,
        "starts_with": _fn_starts_with,
        "ends_with": _fn_ends_with,
        "lower": lambda x: str(x).lower(),
        "upper": lambda x: str(x).upper(),
        "len": lambda x: len(x),
    }

    # Restrict operators to basic boolean/comparison + arithmetic (optional)
    s.operators = {
        ast.Add: lambda a, b: a + b,
        ast.Sub: lambda a, b: a - b,
        ast.Mult: lambda a, b: a * b,
        ast.Div: lambda a, b: a / b,
        ast.Mod: lambda a, b: a % b,
        ast.Pow: lambda a, b: a**b,
        ast.USub: lambda a: -a,
        ast.UAdd: lambda a: +a,
        ast.Not: lambda a: not a,
        ast.And: lambda a, b: a and b,
        ast.Or: lambda a, b: a or b,
        ast.Eq: lambda a, b: a == b,
        ast.NotEq: lambda a, b: a != b,
        ast.Lt: lambda a, b: a < b,
        ast.LtE: lambda a, b: a <= b,
        ast.Gt: lambda a, b: a > b,
        ast.GtE: lambda a, b: a >= b,
        ast.In: lambda a, b: a in b,
        ast.NotIn: lambda a, b: a not in b,
    }

    return s


def _sanitize(x: Any) -> Any:
    # Only allow primitives and containers thereof
    if x is None or isinstance(x, (bool, int, float, str)):
        return x
    if isinstance(x, dict):
        return {str(k): _sanitize(v) for k, v in x.items()}
    if isinstance(x, list):
        return [_sanitize(v) for v in x]
    if isinstance(x, tuple):
        return tuple(_sanitize(v) for v in x)
    # Drop anything else to avoid object-model issues
    return str(x)


def _fn_has(m: Any, key: Any) -> bool:
    if isinstance(m, dict):
        return str(key) in m
    return False


def _fn_get(m: Any, key: Any, default: Any = None) -> Any:
    if isinstance(m, dict):
        return m.get(str(key), default)
    return default


def _fn_contains(s: Any, sub: Any) -> bool:
    return str(sub) in str(s)


def _fn_starts_with(s: Any, prefix: Any) -> bool:
    return str(s).startswith(str(prefix))


def _fn_ends_with(s: Any, suffix: Any) -> bool:
    return str(s).endswith(str(suffix))


def default_ctx() -> Dict[str, Any]:
    return {
        "os": platform.system().lower(),
        "platform": platform.platform(),
        "ci": str(os.environ.get("CI", "")).lower() in ("1", "true", "yes"),
    }
