"""
NovaLang Interpreter
Executes NovaLang programs by traversing and evaluating the AST.
"""

from typing import Any, Dict, List, Optional, Callable
import os
import requests
import json

from parser import (
    ASTNode, Program, LiteralNode, IdentifierNode, BinaryOpNode, UnaryOpNode,
    AssignmentNode, VariableDeclarationNode, IfStatementNode, WhileStatementNode,
    BlockStatementNode, FunctionDeclarationNode, FunctionCallNode, ReturnStatementNode,
    ExpressionStatementNode, ForStatementNode, PrintStatementNode, AskStatementNode,
    LambdaNode
)
from array_nodes import ArrayLiteralNode, IndexAccessNode
from array_assign_node import ArrayAssignmentNode


class ReturnException(Exception):
    """Exception used to handle return statements."""
    def __init__(self, value):
        self.value = value


class Environment:
    """Environment for variable and function storage."""
    
    def __init__(self, parent: Optional['Environment'] = None):
        self.parent = parent
        self.variables: Dict[str, Any] = {}
        self.constants: set = set()
    
    def define(self, name: str, value: Any, is_const: bool = False, var_type: str = None):
        """Define a variable in this environment."""
        if name in self.variables:
            raise RuntimeError(f"Variable '{name}' already defined in this scope")
        self.variables[name] = value
        if is_const:
            self.constants.add(name)
    
    def get(self, name: str) -> Any:
        """Get a variable value."""
        if name in self.variables:
            return self.variables[name]
        
        if self.parent:
            return self.parent.get(name)
        
        raise RuntimeError(f"Undefined variable '{name}'")
    
    def set(self, name: str, value: Any):
        """Set a variable value."""
        if name in self.variables:
            if name in self.constants:
                raise RuntimeError(f"Cannot assign to const variable '{name}'")
            self.variables[name] = value
            return
        if self.parent:
            self.parent.set(name, value)
            return
        raise RuntimeError(f"Undefined variable '{name}'")


class NovaFunction:
    """Represents a user-defined function."""
    
    def __init__(self, declaration, closure: Environment):
        self.declaration = declaration
        self.closure = closure
    
    def call(self, interpreter: 'Interpreter', arguments: List[Any]) -> Any:
        """Call the function with given arguments."""
        # Handle both FunctionDeclarationNode and LambdaNode
        if hasattr(self.declaration, 'name'):
            # Regular function
            name = self.declaration.name.name
            parameters = self.declaration.parameters
            body = self.declaration.body
        else:
            # Lambda function
            name = "<lambda>"
            parameters = self.declaration.parameters
            body = self.declaration.body
        
        if len(arguments) != len(parameters):
            raise RuntimeError(
                f"Function '{name}' expects "
                f"{len(parameters)} arguments, got {len(arguments)}"
            )
        
        env = Environment(self.closure)
        for param, arg in zip(parameters, arguments):
            env.define(param.name, arg)
        
        try:
            interpreter.execute_block(body.statements, env)
            return None
        except ReturnException as ret:
            return ret.value


class BuiltinFunction:
    """Represents a built-in function."""
    
    def __init__(self, func: Callable):
        self.func = func
    
    def call(self, interpreter: 'Interpreter', arguments: List[Any]) -> Any:
        """Call the built-in function."""
        return self.func(*arguments)


class Interpreter:
    """NovaLang interpreter."""
    
    def __init__(self):
        self.environment = Environment()
        self.module_exports = None
        self._setup_builtins()
    
    def _setup_builtins(self):
        """Setup built-in functions using StandardLibrary."""
        try:
            from stdlib import StandardLibrary
            # Load standard library functions
            builtins = StandardLibrary.setup_builtins()
            for name, func in builtins.items():
                self.environment.define(name, func)
        except ImportError:
            # Fallback to basic functions if stdlib not available
            self.environment.define("abs", BuiltinFunction(abs))
            self.environment.define("max", BuiltinFunction(max))
            self.environment.define("min", BuiltinFunction(min))
            self.environment.define("round", BuiltinFunction(round))
            self.environment.define("len", BuiltinFunction(len))
            self.environment.define("str", BuiltinFunction(str))
            self.environment.define("upper", BuiltinFunction(lambda s: str(s).upper()))
            self.environment.define("lower", BuiltinFunction(lambda s: str(s).lower()))
            
            # Array functions with interpreter context
            self.environment.define("map", BuiltinFunction(self._builtin_map))
            self.environment.define("filter", BuiltinFunction(self._builtin_filter))
            self.environment.define("reduce", BuiltinFunction(self._builtin_reduce))
    
    def _builtin_map(self, arr, mapper):
        """Built-in map function with interpreter context."""
        if not isinstance(arr, list):
            raise RuntimeError("map() can only be called on arrays")
        
        result = []
        for item in arr:
            if isinstance(mapper, NovaFunction):
                mapped_value = mapper.call(self, [item])
            elif isinstance(mapper, BuiltinFunction):
                mapped_value = mapper.call([item])
            elif callable(mapper):
                mapped_value = mapper(item)
            else:
                raise RuntimeError("map() requires a function as second argument")
            result.append(mapped_value)
        
        return result
    
    def _builtin_filter(self, arr, predicate):
        """Built-in filter function with interpreter context."""
        if not isinstance(arr, list):
            raise RuntimeError("filter() can only be called on arrays")
        
        result = []
        for item in arr:
            if isinstance(predicate, NovaFunction):
                should_include = predicate.call(self, [item])
            elif isinstance(predicate, BuiltinFunction):
                should_include = predicate.call([item])
            elif callable(predicate):
                should_include = predicate(item)
            else:
                raise RuntimeError("filter() requires a function as second argument")
            
            if should_include:
                result.append(item)
        
        return result
    
    def _builtin_reduce(self, arr, reducer, initial=None):
        """Built-in reduce function with interpreter context."""
        if not isinstance(arr, list):
            raise RuntimeError("reduce() can only be called on arrays")
        
        if initial is not None:
            result = initial
            start = 0
        else:
            if not arr:
                raise RuntimeError("reduce() of empty array with no initial value")
            result = arr[0]
            start = 1
        
        for i in range(start, len(arr)):
            if isinstance(reducer, NovaFunction):
                result = reducer.call(self, [result, arr[i]])
            elif isinstance(reducer, BuiltinFunction):
                result = reducer.call([result, arr[i]])
            elif callable(reducer):
                result = reducer(result, arr[i])
            else:
                raise RuntimeError("reduce() requires a function as second argument")
        
        return result
    
    def interpret(self, ast):
        """Evaluate all statements in the AST."""
        for stmt in getattr(ast, 'statements', []):
            result = self.evaluate(stmt)
    
    def execute_block(self, statements: List[ASTNode], environment: Environment = None):
        """Execute a block of statements."""
        previous_env = self.environment
        if environment:
            self.environment = environment
        
        try:
            for stmt in statements:
                self.evaluate(stmt)
        except ReturnException:
            raise
        finally:
            self.environment = previous_env
    
    def evaluate_binary_op(self, node):
        """Evaluate binary operations."""
        left = self.evaluate(node.left)
        right = self.evaluate(node.right)
        op = node.operator
        
        from lexer import TokenType
        
        # Handle both TokenType and string representations
        op_val = op.value if hasattr(op, 'value') else str(op)
        
        if op_val == '+':
            if isinstance(left, str) or isinstance(right, str):
                return str(left) + str(right)
            return left + right
        elif op_val == '-':
            return left - right
        elif op_val == '*':
            return left * right
        elif op_val == '/':
            return left / right
        elif op_val == '%':
            return left % right
        elif op_val == '==':
            return left == right
        elif op_val == '!=':
            return left != right
        elif op_val == '<':
            return left < right
        elif op_val == '<=':
            return left <= right
        elif op_val == '>':
            return left > right
        elif op_val == '>=':
            return left >= right
        elif op_val == '&&':
            return bool(left) and bool(right)
        elif op_val == '||':
            return bool(left) or bool(right)
        else:
            raise RuntimeError(f"Unknown binary operator: {op_val}")
    
    def evaluate(self, node: ASTNode) -> Any:
        """Evaluate an AST node."""
        
        # Literals
        if isinstance(node, LiteralNode):
            return node.value
        
        # Identifiers
        elif isinstance(node, IdentifierNode):
            return self.environment.get(node.name)
        
        # Binary operations
        elif isinstance(node, BinaryOpNode):
            return self.evaluate_binary_op(node)
        
        # Unary operations
        elif isinstance(node, UnaryOpNode):
            operand = self.evaluate(node.operand)
            from lexer import TokenType
            op_val = node.operator.value if hasattr(node.operator, 'value') else str(node.operator)
            if op_val == '-':
                return -operand
            elif op_val == '!':
                return not bool(operand)
            else:
                raise RuntimeError(f"Unknown unary operator: {op_val}")
        
        # Assignments
        elif isinstance(node, AssignmentNode):
            value = self.evaluate(node.value)
            self.environment.set(node.identifier.name, value)
            return value
        
        # Variable declarations
        elif isinstance(node, VariableDeclarationNode):
            value = self.evaluate(node.value) if node.value else None
            is_const = getattr(node, 'is_const', False)
            self.environment.define(node.identifier.name, value, is_const)
            return value
        
        # Function declarations
        elif isinstance(node, FunctionDeclarationNode):
            func = NovaFunction(node, self.environment)
            self.environment.define(node.name.name, func)
            return func
        
        # Function calls
        elif isinstance(node, FunctionCallNode):
            func = self.evaluate(node.function)
            args = [self.evaluate(arg) for arg in node.arguments]
            
            if isinstance(func, (NovaFunction, BuiltinFunction)):
                return func.call(self, args)
            else:
                raise RuntimeError(f"'{func}' is not a function")
        
        # Print statements
        elif isinstance(node, PrintStatementNode):
            value = self.evaluate(node.value)
            print(value)
            return None
        
        # Control flow
        elif isinstance(node, IfStatementNode):
            condition = self.evaluate(node.condition)
            if condition:
                return self.evaluate(node.then_statement)
            elif node.else_statement:
                return self.evaluate(node.else_statement)
            return None
        
        elif isinstance(node, WhileStatementNode):
            while self.evaluate(node.condition):
                try:
                    self.evaluate(node.body)
                except ReturnException:
                    raise
            return None
        
        elif isinstance(node, ForStatementNode):
            if node.initializer:
                self.evaluate(node.initializer)
            
            while node.condition is None or self.evaluate(node.condition):
                try:
                    self.evaluate(node.body)
                except ReturnException:
                    raise
                
                if node.increment:
                    self.evaluate(node.increment)
            return None
        
        elif isinstance(node, BlockStatementNode):
            env = Environment(self.environment)
            self.execute_block(node.statements, env)
            return None
        
        elif isinstance(node, ReturnStatementNode):
            value = self.evaluate(node.value) if node.value else None
            raise ReturnException(value)
        
        elif isinstance(node, ExpressionStatementNode):
            return self.evaluate(node.expression)
        
        # Arrays
        elif isinstance(node, ArrayLiteralNode):
            return [self.evaluate(elem) for elem in node.elements]
        
        elif isinstance(node, IndexAccessNode):
            obj = self.evaluate(node.object)
            index = self.evaluate(node.index)
            
            if isinstance(obj, list):
                if isinstance(index, int):
                    if 0 <= index < len(obj):
                        return obj[index]
                    else:
                        raise RuntimeError(f"Array index {index} out of bounds")
                else:
                    raise RuntimeError("Array index must be an integer")
            elif isinstance(obj, dict):
                return obj.get(str(index))
            else:
                raise RuntimeError(f"Cannot index into {type(obj)}")
        
        elif isinstance(node, ArrayAssignmentNode):
            obj = self.evaluate(node.object)
            index = self.evaluate(node.index)
            value = self.evaluate(node.value)
            
            if isinstance(obj, list):
                if isinstance(index, int):
                    if 0 <= index < len(obj):
                        obj[index] = value
                    else:
                        raise RuntimeError(f"Array index {index} out of bounds")
                else:
                    raise RuntimeError("Array index must be an integer")
            elif isinstance(obj, dict):
                obj[str(index)] = value
            else:
                raise RuntimeError(f"Cannot index into {type(obj)}")
            
            return value
        
        # Lambda functions
        elif isinstance(node, LambdaNode):
            # Create a function-like object from lambda
            return NovaFunction(node, self.environment)
        
        else:
            raise RuntimeError(f"Unknown AST node type: {type(node)}")
