"""
Expression evaluator for S-expression DSL.

Supports:
- Arithmetic: +, -, *, /, mod, sqrt, pow, abs, floor, ceil, round, min, max, clamp
- Comparison: =, <, >, <=, >=
- Logic: and, or, not
- Predicates: odd?, even?, zero?, nil?
- Conditionals: if, cond, case
- Data: list, dict/map construction, get
- Lambda calls
"""

from typing import Any, Dict, List, Callable
from .parser import Symbol, Keyword, Lambda, Binding


class EvalError(Exception):
    """Error during expression evaluation."""
    pass


# Built-in functions
BUILTINS: Dict[str, Callable] = {}


def builtin(name: str):
    """Decorator to register a builtin function."""
    def decorator(fn):
        BUILTINS[name] = fn
        return fn
    return decorator


@builtin("+")
def add(*args):
    return sum(args)


@builtin("-")
def sub(a, b=None):
    if b is None:
        return -a
    return a - b


@builtin("*")
def mul(*args):
    result = 1
    for a in args:
        result *= a
    return result


@builtin("/")
def div(a, b):
    return a / b


@builtin("mod")
def mod(a, b):
    return a % b


@builtin("sqrt")
def sqrt(x):
    return x ** 0.5


@builtin("pow")
def power(x, n):
    return x ** n


@builtin("abs")
def absolute(x):
    return abs(x)


@builtin("floor")
def floor_fn(x):
    import math
    return math.floor(x)


@builtin("ceil")
def ceil_fn(x):
    import math
    return math.ceil(x)


@builtin("round")
def round_fn(x, ndigits=0):
    return round(x, int(ndigits))


@builtin("min")
def min_fn(*args):
    if len(args) == 1 and isinstance(args[0], (list, tuple)):
        return min(args[0])
    return min(args)


@builtin("max")
def max_fn(*args):
    if len(args) == 1 and isinstance(args[0], (list, tuple)):
        return max(args[0])
    return max(args)


@builtin("clamp")
def clamp(x, lo, hi):
    return max(lo, min(hi, x))


@builtin("=")
def eq(a, b):
    return a == b


@builtin("<")
def lt(a, b):
    return a < b


@builtin(">")
def gt(a, b):
    return a > b


@builtin("<=")
def lte(a, b):
    return a <= b


@builtin(">=")
def gte(a, b):
    return a >= b


@builtin("odd?")
def is_odd(n):
    return n % 2 == 1


@builtin("even?")
def is_even(n):
    return n % 2 == 0


@builtin("zero?")
def is_zero(n):
    return n == 0


@builtin("nil?")
def is_nil(x):
    return x is None


@builtin("not")
def not_fn(x):
    return not x


@builtin("inc")
def inc(n):
    return n + 1


@builtin("dec")
def dec(n):
    return n - 1


@builtin("list")
def make_list(*args):
    return list(args)


@builtin("assert")
def assert_true(condition, message="Assertion failed"):
    if not condition:
        raise RuntimeError(f"Assertion error: {message}")
    return True


@builtin("get")
def get(coll, key, default=None):
    if isinstance(coll, dict):
        # Try the key directly first
        result = coll.get(key, None)
        if result is not None:
            return result
        # If key is a Keyword, also try its string name (for Python dicts with string keys)
        if isinstance(key, Keyword):
            result = coll.get(key.name, None)
            if result is not None:
                return result
        # Return the default
        return default
    elif isinstance(coll, list):
        return coll[key] if 0 <= key < len(coll) else default
    else:
        raise EvalError(f"get: expected dict or list, got {type(coll).__name__}: {str(coll)[:100]}")


@builtin("dict?")
def is_dict(x):
    return isinstance(x, dict)


@builtin("list?")
def is_list(x):
    return isinstance(x, list)


@builtin("nil?")
def is_nil(x):
    return x is None


@builtin("number?")
def is_number(x):
    return isinstance(x, (int, float))


@builtin("string?")
def is_string(x):
    return isinstance(x, str)


@builtin("len")
def length(coll):
    return len(coll)


@builtin("first")
def first(coll):
    return coll[0] if coll else None


@builtin("last")
def last(coll):
    return coll[-1] if coll else None


@builtin("chunk-every")
def chunk_every(coll, n):
    """Split collection into chunks of n elements."""
    n = int(n)
    return [coll[i:i+n] for i in range(0, len(coll), n)]


@builtin("rest")
def rest(coll):
    return coll[1:] if coll else []


@builtin("nth")
def nth(coll, n):
    return coll[n] if 0 <= n < len(coll) else None


@builtin("concat")
def concat(*colls):
    """Concatenate multiple lists/sequences."""
    result = []
    for c in colls:
        if c is not None:
            result.extend(c)
    return result


@builtin("cons")
def cons(x, coll):
    """Prepend x to collection."""
    return [x] + list(coll) if coll else [x]


@builtin("append")
def append(coll, x):
    """Append x to collection."""
    return list(coll) + [x] if coll else [x]


@builtin("range")
def make_range(start, end, step=1):
    """Create a range of numbers."""
    return list(range(int(start), int(end), int(step)))


@builtin("zip-pairs")
def zip_pairs(coll):
    """Zip consecutive pairs: [a,b,c,d] -> [[a,b],[b,c],[c,d]]."""
    if not coll or len(coll) < 2:
        return []
    return [[coll[i], coll[i+1]] for i in range(len(coll)-1)]


@builtin("dict")
def make_dict(*pairs):
    """Create dict from key-value pairs: (dict :a 1 :b 2)."""
    result = {}
    i = 0
    while i < len(pairs) - 1:
        key = pairs[i]
        if isinstance(key, Keyword):
            key = key.name
        result[key] = pairs[i + 1]
        i += 2
    return result


@builtin("keys")
def keys(d):
    """Get the keys of a dict as a list."""
    if not isinstance(d, dict):
        raise EvalError(f"keys: expected dict, got {type(d).__name__}")
    return list(d.keys())


@builtin("vals")
def vals(d):
    """Get the values of a dict as a list."""
    if not isinstance(d, dict):
        raise EvalError(f"vals: expected dict, got {type(d).__name__}")
    return list(d.values())


@builtin("merge")
def merge(*dicts):
    """Merge multiple dicts, later dicts override earlier."""
    result = {}
    for d in dicts:
        if d is not None:
            if not isinstance(d, dict):
                raise EvalError(f"merge: expected dict, got {type(d).__name__}")
            result.update(d)
    return result


@builtin("assoc")
def assoc(d, *pairs):
    """Associate keys with values in a dict: (assoc d :a 1 :b 2)."""
    if d is None:
        result = {}
    elif isinstance(d, dict):
        result = dict(d)
    else:
        raise EvalError(f"assoc: expected dict or nil, got {type(d).__name__}")

    i = 0
    while i < len(pairs) - 1:
        key = pairs[i]
        if isinstance(key, Keyword):
            key = key.name
        result[key] = pairs[i + 1]
        i += 2
    return result


@builtin("dissoc")
def dissoc(d, *keys_to_remove):
    """Remove keys from a dict: (dissoc d :a :b)."""
    if d is None:
        return {}
    if not isinstance(d, dict):
        raise EvalError(f"dissoc: expected dict or nil, got {type(d).__name__}")

    result = dict(d)
    for key in keys_to_remove:
        if isinstance(key, Keyword):
            key = key.name
        result.pop(key, None)
    return result


@builtin("into")
def into(target, coll):
    """Convert a collection into another collection type.

    (into [] {:a 1 :b 2}) -> [["a" 1] ["b" 2]]
    (into {} [[:a 1] [:b 2]]) -> {"a": 1, "b": 2}
    (into [] [1 2 3]) -> [1 2 3]
    """
    if isinstance(target, list):
        if isinstance(coll, dict):
            return [[k, v] for k, v in coll.items()]
        elif isinstance(coll, (list, tuple)):
            return list(coll)
        else:
            raise EvalError(f"into: cannot convert {type(coll).__name__} into list")
    elif isinstance(target, dict):
        if isinstance(coll, dict):
            return dict(coll)
        elif isinstance(coll, (list, tuple)):
            result = {}
            for item in coll:
                if isinstance(item, (list, tuple)) and len(item) >= 2:
                    key = item[0]
                    if isinstance(key, Keyword):
                        key = key.name
                    result[key] = item[1]
                else:
                    raise EvalError(f"into: expected [key value] pairs, got {item}")
            return result
        else:
            raise EvalError(f"into: cannot convert {type(coll).__name__} into dict")
    else:
        raise EvalError(f"into: unsupported target type {type(target).__name__}")


@builtin("filter")
def filter_fn(pred, coll):
    """Filter collection by predicate. Pred must be a lambda."""
    if not isinstance(pred, Lambda):
        raise EvalError(f"filter: expected lambda as predicate, got {type(pred).__name__}")

    result = []
    for item in coll:
        # Evaluate predicate with item
        local_env = {}
        if pred.closure:
            local_env.update(pred.closure)
        local_env[pred.params[0]] = item

        # Inline evaluation of pred.body
        from . import evaluator
        if evaluator.evaluate(pred.body, local_env):
            result.append(item)
    return result


@builtin("some")
def some(pred, coll):
    """Return first truthy value of (pred item) for items in coll, or nil."""
    if not isinstance(pred, Lambda):
        raise EvalError(f"some: expected lambda as predicate, got {type(pred).__name__}")

    for item in coll:
        local_env = {}
        if pred.closure:
            local_env.update(pred.closure)
        local_env[pred.params[0]] = item

        from . import evaluator
        result = evaluator.evaluate(pred.body, local_env)
        if result:
            return result
    return None


@builtin("every?")
def every(pred, coll):
    """Return true if (pred item) is truthy for all items in coll."""
    if not isinstance(pred, Lambda):
        raise EvalError(f"every?: expected lambda as predicate, got {type(pred).__name__}")

    for item in coll:
        local_env = {}
        if pred.closure:
            local_env.update(pred.closure)
        local_env[pred.params[0]] = item

        from . import evaluator
        if not evaluator.evaluate(pred.body, local_env):
            return False
    return True


@builtin("empty?")
def is_empty(coll):
    """Return true if collection is empty."""
    if coll is None:
        return True
    return len(coll) == 0


@builtin("contains?")
def contains(coll, key):
    """Check if collection contains key (for dicts) or element (for lists)."""
    if isinstance(coll, dict):
        if isinstance(key, Keyword):
            key = key.name
        return key in coll
    elif isinstance(coll, (list, tuple)):
        return key in coll
    return False


def evaluate(expr: Any, env: Dict[str, Any] = None) -> Any:
    """
    Evaluate an S-expression in the given environment.

    Args:
        expr: The expression to evaluate
        env: Variable bindings (name -> value)

    Returns:
        The result of evaluation
    """
    if env is None:
        env = {}

    # Literals
    if isinstance(expr, (int, float, str, bool)) or expr is None:
        return expr

    # Symbol - variable lookup
    if isinstance(expr, Symbol):
        name = expr.name
        if name in env:
            return env[name]
        if name in BUILTINS:
            return BUILTINS[name]
        if name == "true":
            return True
        if name == "false":
            return False
        if name == "nil":
            return None
        raise EvalError(f"Undefined symbol: {name}")

    # Keyword - return as-is (used as map keys)
    if isinstance(expr, Keyword):
        return expr.name

    # Lambda - return as-is (it's a value)
    if isinstance(expr, Lambda):
        return expr

    # Binding - return as-is (resolved at execution time)
    if isinstance(expr, Binding):
        return expr

    # Dict literal
    if isinstance(expr, dict):
        return {k: evaluate(v, env) for k, v in expr.items()}

    # List - function call or special form
    if isinstance(expr, list):
        if not expr:
            return []

        head = expr[0]

        # If head is a string/number/etc (not Symbol), treat as data list
        if not isinstance(head, (Symbol, Lambda, list)):
            return [evaluate(x, env) for x in expr]

        # Special forms
        if isinstance(head, Symbol):
            name = head.name

            # if - conditional
            if name == "if":
                if len(expr) < 3:
                    raise EvalError("if requires condition and then-branch")
                cond_result = evaluate(expr[1], env)
                if cond_result:
                    return evaluate(expr[2], env)
                elif len(expr) > 3:
                    return evaluate(expr[3], env)
                return None

            # cond - multi-way conditional
            # Supports both Clojure style: (cond test1 result1 test2 result2 :else default)
            # and Scheme style: (cond (test1 result1) (test2 result2) (else default))
            if name == "cond":
                clauses = expr[1:]
                # Check if Clojure style (flat list) or Scheme style (nested pairs)
                # Scheme style: first clause is (test result) - exactly 2 elements
                # Clojure style: first clause is a test expression like (= x 0) - 3+ elements
                first_is_scheme_clause = (
                    clauses and
                    isinstance(clauses[0], list) and
                    len(clauses[0]) == 2 and
                    not (isinstance(clauses[0][0], Symbol) and clauses[0][0].name in ('=', '<', '>', '<=', '>=', '!=', 'not=', 'and', 'or'))
                )
                if first_is_scheme_clause:
                    # Scheme style: ((test result) ...)
                    for clause in clauses:
                        if not isinstance(clause, list) or len(clause) < 2:
                            raise EvalError("cond clause must be (test result)")
                        test = clause[0]
                        # Check for else/default
                        if isinstance(test, Symbol) and test.name in ("else", ":else"):
                            return evaluate(clause[1], env)
                        if isinstance(test, Keyword) and test.name == "else":
                            return evaluate(clause[1], env)
                        if evaluate(test, env):
                            return evaluate(clause[1], env)
                else:
                    # Clojure style: test1 result1 test2 result2 ...
                    i = 0
                    while i < len(clauses) - 1:
                        test = clauses[i]
                        result = clauses[i + 1]
                        # Check for :else
                        if isinstance(test, Keyword) and test.name == "else":
                            return evaluate(result, env)
                        if isinstance(test, Symbol) and test.name == ":else":
                            return evaluate(result, env)
                        if evaluate(test, env):
                            return evaluate(result, env)
                        i += 2
                return None

            # case - switch on value
            # (case expr val1 result1 val2 result2 :else default)
            if name == "case":
                if len(expr) < 2:
                    raise EvalError("case requires expression to match")
                match_val = evaluate(expr[1], env)
                clauses = expr[2:]
                i = 0
                while i < len(clauses) - 1:
                    test = clauses[i]
                    result = clauses[i + 1]
                    # Check for :else / else
                    if isinstance(test, Keyword) and test.name == "else":
                        return evaluate(result, env)
                    if isinstance(test, Symbol) and test.name in (":else", "else"):
                        return evaluate(result, env)
                    # Evaluate test value and compare
                    test_val = evaluate(test, env)
                    if match_val == test_val:
                        return evaluate(result, env)
                    i += 2
                return None

            # and - short-circuit
            if name == "and":
                result = True
                for arg in expr[1:]:
                    result = evaluate(arg, env)
                    if not result:
                        return result
                return result

            # or - short-circuit
            if name == "or":
                result = False
                for arg in expr[1:]:
                    result = evaluate(arg, env)
                    if result:
                        return result
                return result

            # let and let* - local bindings (both bind sequentially in this impl)
            if name in ("let", "let*"):
                if len(expr) < 3:
                    raise EvalError(f"{name} requires bindings and body")
                bindings = expr[1]

                local_env = dict(env)

                if isinstance(bindings, list):
                    # Check if it's ((name value) ...) style (Lisp let* style)
                    if bindings and isinstance(bindings[0], list):
                        for binding in bindings:
                            if len(binding) != 2:
                                raise EvalError(f"{name} binding must be (name value)")
                            var_name = binding[0]
                            if isinstance(var_name, Symbol):
                                var_name = var_name.name
                            value = evaluate(binding[1], local_env)
                            local_env[var_name] = value
                    # Vector-style [name value ...]
                    elif len(bindings) % 2 == 0:
                        for i in range(0, len(bindings), 2):
                            var_name = bindings[i]
                            if isinstance(var_name, Symbol):
                                var_name = var_name.name
                            value = evaluate(bindings[i + 1], local_env)
                            local_env[var_name] = value
                    else:
                        raise EvalError(f"{name} bindings must be [name value ...] or ((name value) ...)")
                else:
                    raise EvalError(f"{name} bindings must be a list")

                return evaluate(expr[2], local_env)

            # lambda / fn - create function with closure
            if name in ("lambda", "fn"):
                if len(expr) < 3:
                    raise EvalError("lambda requires params and body")
                params = expr[1]
                if not isinstance(params, list):
                    raise EvalError("lambda params must be a list")
                param_names = []
                for p in params:
                    if isinstance(p, Symbol):
                        param_names.append(p.name)
                    elif isinstance(p, str):
                        param_names.append(p)
                    else:
                        raise EvalError(f"Invalid param: {p}")
                # Capture current environment as closure
                return Lambda(param_names, expr[2], dict(env))

            # quote - return unevaluated
            if name == "quote":
                return expr[1] if len(expr) > 1 else None

            # bind - create binding to analysis data
            # (bind analysis-var)
            # (bind analysis-var :range [0.3 1.0])
            # (bind analysis-var :range [0 100] :transform sqrt)
            if name == "bind":
                if len(expr) < 2:
                    raise EvalError("bind requires analysis reference")
                analysis_ref = expr[1]
                if isinstance(analysis_ref, Symbol):
                    symbol_name = analysis_ref.name
                    # Look up the symbol in environment
                    if symbol_name in env:
                        resolved = env[symbol_name]
                        # If resolved is actual analysis data (dict with times/values or
                        # S-expression list with Keywords), keep the symbol name as reference
                        # for later lookup at execution time
                        if isinstance(resolved, dict) and ("times" in resolved or "values" in resolved):
                            analysis_ref = symbol_name  # Use name as reference, not the data
                        elif isinstance(resolved, list) and any(isinstance(x, Keyword) for x in resolved):
                            # Parsed S-expression analysis data ([:times [...] :duration ...])
                            analysis_ref = symbol_name
                        else:
                            analysis_ref = resolved
                    else:
                        raise EvalError(f"bind: undefined symbol '{symbol_name}' - must reference analysis data")

                # Parse optional :range [min max] and :transform
                range_min, range_max = 0.0, 1.0
                transform = None
                i = 2
                while i < len(expr):
                    if isinstance(expr[i], Keyword):
                        kw = expr[i].name
                        if kw == "range" and i + 1 < len(expr):
                            range_val = evaluate(expr[i + 1], env)  # Evaluate to get actual value
                            if isinstance(range_val, list) and len(range_val) >= 2:
                                range_min = float(range_val[0])
                                range_max = float(range_val[1])
                            i += 2
                        elif kw == "transform" and i + 1 < len(expr):
                            t = expr[i + 1]
                            if isinstance(t, Symbol):
                                transform = t.name
                            elif isinstance(t, str):
                                transform = t
                            i += 2
                        else:
                            i += 1
                    else:
                        i += 1

                return Binding(analysis_ref, range_min=range_min, range_max=range_max, transform=transform)

            # Vector literal [a b c]
            if name == "vec" or name == "vector":
                return [evaluate(e, env) for e in expr[1:]]

            # map - (map fn coll)
            if name == "map":
                if len(expr) != 3:
                    raise EvalError("map requires fn and collection")
                fn = evaluate(expr[1], env)
                coll = evaluate(expr[2], env)
                if not isinstance(fn, Lambda):
                    raise EvalError(f"map requires lambda, got {type(fn)}")
                result = []
                for item in coll:
                    local_env = {}
                    if fn.closure:
                        local_env.update(fn.closure)
                    local_env.update(env)
                    local_env[fn.params[0]] = item
                    result.append(evaluate(fn.body, local_env))
                return result

            # map-indexed - (map-indexed fn coll)
            if name == "map-indexed":
                if len(expr) != 3:
                    raise EvalError("map-indexed requires fn and collection")
                fn = evaluate(expr[1], env)
                coll = evaluate(expr[2], env)
                if not isinstance(fn, Lambda):
                    raise EvalError(f"map-indexed requires lambda, got {type(fn)}")
                if len(fn.params) < 2:
                    raise EvalError("map-indexed lambda needs (i item) params")
                result = []
                for i, item in enumerate(coll):
                    local_env = {}
                    if fn.closure:
                        local_env.update(fn.closure)
                    local_env.update(env)
                    local_env[fn.params[0]] = i
                    local_env[fn.params[1]] = item
                    result.append(evaluate(fn.body, local_env))
                return result

            # reduce - (reduce fn init coll)
            if name == "reduce":
                if len(expr) != 4:
                    raise EvalError("reduce requires fn, init, and collection")
                fn = evaluate(expr[1], env)
                acc = evaluate(expr[2], env)
                coll = evaluate(expr[3], env)
                if not isinstance(fn, Lambda):
                    raise EvalError(f"reduce requires lambda, got {type(fn)}")
                if len(fn.params) < 2:
                    raise EvalError("reduce lambda needs (acc item) params")
                for item in coll:
                    local_env = {}
                    if fn.closure:
                        local_env.update(fn.closure)
                    local_env.update(env)
                    local_env[fn.params[0]] = acc
                    local_env[fn.params[1]] = item
                    acc = evaluate(fn.body, local_env)
                return acc

            # for-each - (for-each fn coll) - iterate with side effects
            if name == "for-each":
                if len(expr) != 3:
                    raise EvalError("for-each requires fn and collection")
                fn = evaluate(expr[1], env)
                coll = evaluate(expr[2], env)
                if not isinstance(fn, Lambda):
                    raise EvalError(f"for-each requires lambda, got {type(fn)}")
                for item in coll:
                    local_env = {}
                    if fn.closure:
                        local_env.update(fn.closure)
                    local_env.update(env)
                    local_env[fn.params[0]] = item
                    evaluate(fn.body, local_env)
                return None

        # Function call
        fn = evaluate(head, env)
        args = [evaluate(arg, env) for arg in expr[1:]]

        # Call builtin
        if callable(fn):
            return fn(*args)

        # Call lambda
        if isinstance(fn, Lambda):
            if len(args) != len(fn.params):
                raise EvalError(f"Lambda expects {len(fn.params)} args, got {len(args)}")
            # Start with closure (captured env), then overlay calling env, then params
            local_env = {}
            if fn.closure:
                local_env.update(fn.closure)
            local_env.update(env)
            for name, value in zip(fn.params, args):
                local_env[name] = value
            return evaluate(fn.body, local_env)

        raise EvalError(f"Not callable: {fn}")

    raise EvalError(f"Cannot evaluate: {expr!r}")


def make_env(**kwargs) -> Dict[str, Any]:
    """Create an environment with initial bindings."""
    return dict(kwargs)
