"""
Interpreter for the Alloy Programming Language

This module provides the main interpreter that executes Alloy ASTs.
It handles all language constructs including variables, tools, agents,
control flow, and AI model interactions.
"""

import os
from typing import Any, Dict, List, Tuple, Optional, Union
from dotenv import load_dotenv
import openai

from .environment import Environment, RuntimeError
from .parser import AlloyParser
from .lexer import AlloyLexer


class Interpreter:
    """
    Main interpreter for the Alloy programming language.
    
    Executes abstract syntax trees (ASTs) and manages the runtime environment
    including variables, tools, agents, and AI model interactions.
    """
    
    def __init__(self, use_gpt4_mini: bool = True):
        """
        Initialize the interpreter.
        
        Args:
            use_gpt4_mini: Whether to use gpt-4o-mini as default model
        """
        self.global_env = Environment()
        self.default_model = "gpt-4o-mini" if use_gpt4_mini else "gpt-4"
        
        # Load environment variables from .env file
        load_dotenv()
        
        # Get OpenAI API key from environment
        api_key = os.getenv('OPENAI_API_KEY')
        if api_key:
            self.openai_client = openai.OpenAI(api_key=api_key)
        else:
            self.openai_client = None
            print("Warning: OPENAI_API_KEY not found in environment. Agent blocks will not work.")
            print("Create a .env file with: OPENAI_API_KEY=your_key_here")

    def interpret(self, ast: Union[tuple, str]) -> Any:
        """
        Interpret an AST and return the result.
        
        Args:
            ast: Abstract syntax tree to interpret
            
        Returns:
            Result of interpretation
        """
        return self.visit(ast, self.global_env)

    def visit(self, node: Union[tuple, str, Any], env: Environment) -> Any:
        """
        Visit and execute an AST node.
        
        Args:
            node: AST node to visit
            env: Current environment
            
        Returns:
            Result of node execution
        """
        # Handle the case where node is just a string (identifier)
        if isinstance(node, str):
            if node == 'return':
                return None  # Handle return keyword
            return env.get(node)
        
        if not isinstance(node, tuple):
            return node
            
        node_type = node[0]
        
        if node_type == 'program':
            return self._visit_program(node, env)
        elif node_type == 'assign':
            return self._visit_assign(node, env)
        elif node_type == 'tuple_assign':
            return self._visit_tuple_assign(node, env)
        elif node_type == 'agent_def':
            return self._visit_agent_def(node, env)
        elif node_type == 'command_def':
            return self._visit_command_def(node, env)
        elif node_type == 'tool':
            return self._visit_tool_def(node, env)
        elif node_type == 'when':
            return self._visit_when(node, env)
        elif node_type == 'for':
            return self._visit_for(node, env)
        elif node_type == 'while':
            return self._visit_while(node, env)
        elif node_type == 'block':
            return self._visit_block(node, env)
        elif node_type == 'string':
            return node[1]
        elif node_type == 'number':
            return node[1]
        elif node_type == 'bool':
            return node[1]
        elif node_type == 'null':
            return None
        elif node_type == 'array':
            return [self.visit(e, env) for e in node[1] if e is not None]
        elif node_type == 'object':
            return {k: self.visit(v, env) for k, v in node[1].items()}
        elif node_type == 'tuple':
            return tuple(self.visit(e, env) for e in node[1] if e is not None)
        elif node_type == 'tool_call':
            return self._visit_tool_call(node, env)
        elif node_type == 'agent_command' or node_type == 'agent_block':
            return self.execute_agent(node, env)
        elif node_type == 'binary':
            return self._visit_binary(node, env)
        elif node_type == 'unary':
            return self._visit_unary(node, env)
        elif node_type == 'chain':
            return self._visit_chain(node, env)
        else:
            raise RuntimeError(f"Unknown node type: {node_type}")

    def _visit_program(self, node: tuple, env: Environment) -> Any:
        """Execute a program (list of statements)"""
        statements = node[1]
        
        # Pre-process statements to detect parsing issues
        statements = self._detect_single_command_shorthand(statements)
        statements = self._detect_tool_call_pattern(statements)
        statements = self._detect_then_chaining_pattern(statements)
        
        # Two-pass execution: 
        # Pass 1: Process all definitions (tools, agents, commands) first
        # Pass 2: Execute all other statements
        
        definition_types = {'tool', 'agent_def', 'command_def'}
        
        # Pass 1: Process definitions
        for stmt in statements:
            if isinstance(stmt, tuple) and stmt[0] in definition_types:
                self.visit(stmt, env)
        
        # Pass 2: Execute other statements
        result = None
        for stmt in statements:
            if not (isinstance(stmt, tuple) and stmt[0] in definition_types):
                result = self.visit(stmt, env)
        return result
    
    def _detect_single_command_shorthand(self, statements: List[Any]) -> List[Any]:
        """
        Detect and transform single command shorthand patterns.
        
        Pattern: tuple_assign with agent identifier + two following identifiers
        Example: result, error = claude + Analyze + text
        Transforms to: result, error = claude Analyze text (agent_command)
        """
        new_statements = []
        i = 0
        
        while i < len(statements):
            stmt = statements[i]
            
            # Check for the pattern: tuple_assign + identifier + identifier
            if (isinstance(stmt, tuple) and stmt[0] == 'tuple_assign' and
                isinstance(stmt[2], str) and  # Assignment value is just an identifier
                i + 2 < len(statements) and
                isinstance(statements[i + 1], str) and  # Next statement is identifier
                isinstance(statements[i + 2], str)):    # Statement after that is identifier
                
                # Extract the components
                var_names = stmt[1]  # ['result', 'error']
                agent_name = stmt[2]  # 'claude'
                command_name = statements[i + 1]  # 'Analyze'
                data_name = statements[i + 2]  # 'text'
                
                # Create agent command
                agent_ref = ('agent_var', agent_name)
                agent_cmd = ('agent_command', agent_ref, command_name, data_name)
                
                # Create new tuple assignment with agent command
                new_stmt = ('tuple_assign', var_names, agent_cmd)
                new_statements.append(new_stmt)
                
                # Skip the next two statements as they're now part of the agent command
                i += 3
            else:
                new_statements.append(stmt)
                i += 1
        
        return new_statements
    
    def _detect_tool_call_pattern(self, statements: List[Any]) -> List[Any]:
        """
        Detect and transform tool call patterns.
        
        Pattern: assignment with identifier + tuple literal
        Example: result = tool_name + ("arg1", "arg2")
        Transforms to: result = tool_name("arg1", "arg2") (tool_call)
        """
        new_statements = []
        i = 0
        
        while i < len(statements):
            stmt = statements[i]
            
            # Check for pattern: assign + tuple literal
            if (isinstance(stmt, tuple) and stmt[0] == 'assign' and
                isinstance(stmt[2], str) and  # Assignment value is just an identifier
                i + 1 < len(statements) and
                isinstance(statements[i + 1], tuple) and
                statements[i + 1][0] == 'tuple'):  # Next statement is a tuple literal
                
                # Extract components
                var_name = stmt[1]  # 'result'
                tool_name = stmt[2]  # 'analyze_and_format'
                tuple_args = statements[i + 1][1]  # [('string', 'sales data')]
                
                # Create tool call
                tool_call = ('tool_call', tool_name, tuple_args)
                
                # Create new assignment with tool call
                new_stmt = ('assign', var_name, tool_call)
                new_statements.append(new_stmt)
                
                # Skip the next statement as it's now part of the tool call
                i += 2
            else:
                new_statements.append(stmt)
                i += 1
        
        return new_statements

    def _detect_then_chaining_pattern(self, statements: List[Any]) -> List[Any]:
        """
        Detect and transform then chaining patterns that were incorrectly parsed.
        
        Pattern: tuple_assign with agent_command (with None param) + chain with string + tool
        Example: result, error = claude Research (None) + chain("AI trends", process_analysis)
        Transforms to: result, error = chain(claude Research "AI trends", process_analysis)
        """
        new_statements = []
        i = 0
        
        while i < len(statements):
            stmt = statements[i]
            
            # Check for pattern: tuple_assign with agent_command (None param) + chain
            if (isinstance(stmt, tuple) and stmt[0] == 'tuple_assign' and
                isinstance(stmt[2], tuple) and stmt[2][0] == 'agent_command' and
                stmt[2][3] is None and  # Agent command has None as parameter
                i + 1 < len(statements) and
                isinstance(statements[i + 1], tuple) and
                statements[i + 1][0] == 'chain' and
                isinstance(statements[i + 1][1], tuple) and
                statements[i + 1][1][0] == 'string'):  # Chain starts with a string
                
                # Extract components
                var_names = stmt[1]  # ['final_result', 'error']
                agent_ref = stmt[2][1]  # ('agent_var', 'claude')
                command = stmt[2][2]  # 'Research'
                string_param = statements[i + 1][1][1]  # 'AI trends'
                tool_name = statements[i + 1][2]  # 'process_analysis'
                
                # Create corrected agent command with proper parameter
                corrected_agent_cmd = ('agent_command', agent_ref, command, string_param)
                
                # Create the chain with the corrected agent command
                corrected_chain = ('chain', corrected_agent_cmd, tool_name)
                
                # Create new tuple assignment with corrected chain
                new_stmt = ('tuple_assign', var_names, corrected_chain)
                new_statements.append(new_stmt)
                
                # Skip the next statement as it's now part of the chain
                i += 2
            else:
                new_statements.append(stmt)
                i += 1
        
        return new_statements

    def _visit_assign(self, node: tuple, env: Environment) -> Any:
        """Execute a variable assignment"""
        name, value_node = node[1], node[2]
        value = self.visit(value_node, env)
        
        # Special handling for agent blocks: if the value is a tuple (result, error)
        # and we're doing a single assignment (not tuple assignment), unwrap it
        if (isinstance(value_node, tuple) and value_node[0] == 'agent_block' and
            isinstance(value, tuple) and len(value) == 2):
            # For single assignment of agent block, assign just the result
            result, error = value
            env.set(name, result)
            return result
        else:
            env.set(name, value)  # Use set() instead of define() for proper scoping
            return value

    def _visit_tuple_assign(self, node: tuple, env: Environment) -> Any:
        """Execute a tuple assignment (e.g., result, error = tool_call())"""
        identifiers, value_node = node[1], node[2]
        value = self.visit(value_node, env)
        
        # Handle tuple assignment for result, error patterns
        if isinstance(value, tuple) and len(value) == len(identifiers):
            for i, ident in enumerate(identifiers):
                env.set(ident, value[i])  # Use set() instead of define() for proper scoping
            return value
        else:
            # If not a tuple or wrong length, assign None to error variables
            for i, ident in enumerate(identifiers):
                if i == 0:
                    env.set(ident, value)  # Use set() instead of define() for proper scoping
                else:
                    env.set(ident, None)   # Use set() instead of define() for proper scoping
            return value

    def _visit_agent_def(self, node: tuple, env: Environment) -> Any:
        """Execute an agent definition"""
        name, model, tools, system_prompts = node[1], node[2].strip('"\''), node[3], node[4]
        
        # Synthesize system prompt from must/should statements
        system_prompt = self._synthesize_system_prompt(system_prompts)
        
        # Store agent with system prompt
        env.define_agent(name, {
            'model': model,
            'tools': tools,
            'system_prompt': system_prompt
        })
        
        # Also define as a regular variable for reference
        env.define(name, ('agent_var', name))
        return ('agent_var', name)

    def _visit_command_def(self, node: tuple, env: Environment) -> Any:
        """Process command definition"""
        name, description = node[1], node[2].strip('"\'')
        env.define_command(name, description)
        # Also define as a variable so it can be referenced in must/should statements
        env.define(name, ('command_ref', name))
        return None

    def _visit_tool_def(self, node: tuple, env: Environment) -> Any:
        """Execute a tool definition"""
        name, params, ret_name, body = node[1], node[2], node[3], node[4]
        
        def tool_func(*args):
            tool_env = Environment(env)
            for param, arg in zip(params, args):
                tool_env.define(param, arg)
            
            # Pre-define the return variable as None to ensure it exists in tool scope
            tool_env.define(ret_name, None)
            
            # Execute the body statements directly in tool environment
            # to avoid creating additional nested environments
            if isinstance(body, tuple) and body[0] == 'block':
                # Execute block statements directly in tool environment
                result = None
                for stmt in body[1]:
                    if isinstance(stmt, str) and stmt == 'return':
                        continue  # Skip 'return' keyword
                    result = self.visit(stmt, tool_env)
            else:
                # Fallback for non-block bodies
                self.visit(body, tool_env)
            
            # Get the return value from the return variable
            try:
                result = tool_env.get(ret_name)
            except:
                # If return variable not set, return None
                result = None
            
            # If result is already a tuple, return it as-is (for tuple assignment)
            # Otherwise, wrap it in (result, None) pattern
            if isinstance(result, tuple):
                return result
            else:
                return result, None  # (result, error)
        
        env.define_tool(name, tool_func)
        return tool_func

    def _visit_when(self, node: tuple, env: Environment) -> Any:
        """Execute a when/then/otherwise statement"""
        condition, then_stmt, otherwise_stmt = node[1], node[2], node[3]
        cond = self.visit(condition, env)
        if cond:
            return self.visit(then_stmt, env)
        else:
            return self.visit(otherwise_stmt, env)

    def _visit_for(self, node: tuple, env: Environment) -> Any:
        """Execute a for loop"""
        var_name, iterable_node, body = node[1], node[2], node[3]
        iterable = self.visit(iterable_node, env)
        results = []
        
        for item in iterable:
            loop_env = Environment(env)
            loop_env.define(var_name, item)
            results.append(self.visit(body, loop_env))
        
        return results

    def _visit_while(self, node: tuple, env: Environment) -> Any:
        """Execute a while loop"""
        condition_node, body = node[1], node[2]
        results = []
        
        while self.visit(condition_node, env):
            results.append(self.visit(body, env))
        
        return results

    def _visit_block(self, node: tuple, env: Environment) -> Any:
        """Execute a block of statements"""
        block_env = Environment(env)
        result = None
        
        for stmt in node[1]:
            if isinstance(stmt, str) and stmt == 'return':
                continue  # Skip 'return' keyword
            result = self.visit(stmt, block_env)
        
        return result

    def _visit_tool_call(self, node: tuple, env: Environment) -> Any:
        """Execute a tool call"""
        tool_name, args_nodes = node[1], node[2]
        tool = env.get_tool(tool_name)
        args = [self.visit(arg, env) for arg in args_nodes]
        return tool(*args)

    def _visit_binary(self, node: tuple, env: Environment) -> Any:
        """Execute a binary operation"""
        operator, left_node, right_node = node[1], node[2], node[3]
        left = self.visit(left_node, env)
        right = self.visit(right_node, env)
        
        if operator == 'add':
            return left + right
        elif operator == 'sub':
            return left - right
        elif operator == 'mul':
            return left * right
        elif operator == 'gt':
            return left > right
        elif operator == 'ge':
            return left >= right
        elif operator == 'lt':
            return left < right
        elif operator == 'le':
            return left <= right
        elif operator == 'eq':
            return left == right
        elif operator == 'ne':
            return left != right
        else:
            raise RuntimeError(f"Unknown binary operator: {operator}")

    def _visit_unary(self, node: tuple, env: Environment) -> Any:
        """Execute a unary operation"""
        operator, operand_node = node[1], node[2]
        operand = self.visit(operand_node, env)
        
        if operator == 'neg':
            # Handle tuple case - if it's a single-element tuple, treat as grouped expression
            if isinstance(operand, tuple) and len(operand) == 1:
                return -operand[0]
            else:
                return -operand
        else:
            raise RuntimeError(f"Unknown unary operator: {operator}")

    def _visit_chain(self, node: tuple, env: Environment) -> Any:
        """Execute a chain operation with proper data flow"""
        left_node, right_node = node[1], node[2]
        
        # Execute first operation
        left_result = self.visit(left_node, env)
        
        # Handle tuple unpacking - some operations return tuples, others don't
        if isinstance(left_result, tuple) and len(left_result) == 2:
            res1, err1 = left_result
        else:
            res1, err1 = left_result, None
        
        if err1:
            return None, err1
        
        # Pass result to second operation
        # Handle different types of right-side operations
        if isinstance(right_node, str):
            # Right side is a tool name (e.g., "add_ten")
            # Call the tool with res1 as argument
            tool = env.get_tool(right_node)
            right_result = tool(res1)
            if isinstance(right_result, tuple) and len(right_result) == 2:
                res2, err2 = right_result
            else:
                res2, err2 = right_result, None
            return res2, err2
        elif isinstance(right_node, tuple):
            node_type = right_node[0]
            
            if node_type == 'tool_call':
                # Right side is a tool call - inject res1 as first parameter
                tool_name = right_node[1]
                existing_args = right_node[2]
                tool = env.get_tool(tool_name)
                # Prepend res1 to existing arguments
                args = [res1] + [self.visit(arg, env) for arg in existing_args]
                right_result = tool(*args)
                if isinstance(right_result, tuple) and len(right_result) == 2:
                    res2, err2 = right_result
                else:
                    res2, err2 = right_result, None
                return res2, err2
            elif node_type == 'agent_command':
                # Right side is an agent command - pass res1 as data
                agent_ref = right_node[1]
                command = right_node[2]
                # Create a new agent command node with res1 as data
                modified_node = ('agent_command', agent_ref, command, res1)
                right_result = self.execute_agent(modified_node, env)
                if isinstance(right_result, tuple) and len(right_result) == 2:
                    res2, err2 = right_result
                else:
                    res2, err2 = right_result, None
                return res2, err2
            elif node_type == 'agent_block':
                # Right side is an agent block - pass res1 as implicit data
                agent_ref = right_node[1]
                statements = right_node[2]
                # Add the chained data as an implicit data statement
                enhanced_statements = statements + [('data', 'chained_input', res1)]
                modified_node = ('agent_block', agent_ref, enhanced_statements)
                right_result = self.execute_agent(modified_node, env)
                if isinstance(right_result, tuple) and len(right_result) == 2:
                    res2, err2 = right_result
                else:
                    res2, err2 = right_result, None
                return res2, err2
            elif node_type == 'chain':
                # Right side is another chain - pass res1 to it
                # Need to modify the leftmost node of the chain to receive res1
                right_result = self._visit_chain_with_input(right_node, env, res1)
                if isinstance(right_result, tuple) and len(right_result) == 2:
                    res2, err2 = right_result
                else:
                    res2, err2 = right_result, None
                return res2, err2
            else:
                # Other node types - just visit normally
                right_result = self.visit(right_node, env)
                if isinstance(right_result, tuple) and len(right_result) == 2:
                    res2, err2 = right_result
                else:
                    res2, err2 = right_result, None
                return res2, err2
        else:
            # Fallback - just visit the right node
            right_result = self.visit(right_node, env)
            if isinstance(right_result, tuple) and len(right_result) == 2:
                res2, err2 = right_result
            else:
                res2, err2 = right_result, None
            return res2, err2
    
    def _visit_chain_with_input(self, chain_node: tuple, env: Environment, input_value: Any) -> Any:
        """Visit a chain with a predefined input value"""
        left_node, right_node = chain_node[1], chain_node[2]
        
        # Handle the left side with the input value
        if isinstance(left_node, str):
            # Left side is a tool name - call with input
            tool = env.get_tool(left_node)
            left_result = tool(input_value)
        elif isinstance(left_node, tuple) and left_node[0] == 'tool_call':
            # Left side is a tool call - inject input as first parameter
            tool_name = left_node[1]
            existing_args = left_node[2]
            tool = env.get_tool(tool_name)
            args = [input_value] + [self.visit(arg, env) for arg in existing_args]
            left_result = tool(*args)
        else:
            # Other cases - just visit normally (may not use input)
            left_result = self.visit(left_node, env)
        
        # Handle tuple unpacking
        if isinstance(left_result, tuple) and len(left_result) == 2:
            res1, err1 = left_result
        else:
            res1, err1 = left_result, None
        
        if err1:
            return None, err1
        
        # Continue with the right side
        # Create a proper chain where the left side is the computed result
        # We need to pass res1 to the right side directly
        if isinstance(right_node, str):
            # Right side is a tool name
            tool = env.get_tool(right_node)
            return tool(res1)
        elif isinstance(right_node, tuple) and right_node[0] == 'chain':
            # Right side is another chain, recursively handle it
            return self._visit_chain_with_input(right_node, env, res1)
        else:
            # Other cases, just visit the right node normally
            return self.visit(right_node, env)

    def execute_agent(self, node: tuple, env: Environment) -> Tuple[Any, Optional[str]]:
        """
        Execute an agent command or block.
        
        Args:
            node: Agent AST node
            env: Current environment
            
        Returns:
            Tuple of (result, error)
        """
        agent_ref = node[1]
        
        # Determine the agent model, tools, and system prompt
        if agent_ref[0] == 'alloy':
            model = self.default_model  # Use default model for alloy
            tools = []
            system_prompt = "You are the Alloy agent, a helpful programming assistant."
        elif agent_ref[0] == 'agent_var':
            # Reference to an agent variable (clean syntax)
            agent_name = agent_ref[1]
            agent_info = env.get_agent(agent_name)
            model = agent_info['model']
            tools = agent_info['tools']
            system_prompt = agent_info.get('system_prompt', "You are a helpful assistant.")
        else:
            # Old syntax: agent "model-name"
            model = agent_ref[1]
            tools = []
            system_prompt = "You are a helpful assistant."
        
        # Construct the prompt based on node type
        if node[0] == 'agent_command':
            prompt = self._build_command_prompt(node, env)
        else:  # agent_block
            prompt = self._build_block_prompt(node, env)
        
        # Execute the AI request
        return self._execute_ai_request(model, system_prompt, prompt, tools)

    def _build_command_prompt(self, node: tuple, env: Environment) -> str:
        """Build prompt for agent command"""
        command = node[2]
        data = node[3] if len(node) > 3 else None
        
        # Check if command is capitalized (Command) vs lowercase (tool)
        if isinstance(command, str):
            if command[0].isupper():
                # This is a Command
                prompt = f"Execute command: {command}"
                if data:
                    # Check if data is a string literal or a variable reference
                    if isinstance(data, str):
                        if data.startswith('"') and data.endswith('"'):
                            # String literal - use as is, stripping quotes
                            data_value = data[1:-1]
                        else:
                            # Variable reference - look it up
                            data_value = env.get(data)
                    else:
                        # AST node - evaluate it
                        data_value = self.visit(data, env)
                    prompt += f" with data: {data_value}"
            else:
                # This is a tool call - would need to check if agent has this tool
                prompt = f"Use tool '{command}'"
                if data:
                    # Check if data is a string literal or a variable reference
                    if isinstance(data, str):
                        if data.startswith('"') and data.endswith('"'):
                            # String literal - use as is, stripping quotes
                            data_value = data[1:-1]
                        else:
                            # Variable reference - look it up
                            data_value = env.get(data)
                    else:
                        # AST node - evaluate it
                        data_value = self.visit(data, env)
                    prompt += f" with data: {data_value}"
        else:
            # String command
            strip_chars = '"\''
            prompt = f"Execute: {command.strip(strip_chars)}"
        
        return prompt

    def _build_block_prompt(self, node: tuple, env: Environment) -> str:
        """Build prompt for agent block with implicit variable access"""
        statements = node[2]
        musts, shoulds, data_items = [], [], {}
        
        # Add implicit variable context from current environment
        available_vars = self._get_available_variables(env)
        if available_vars:
            data_items['available_variables'] = available_vars
        
        for stmt in statements:
            if stmt[0] == 'must':
                # Process must statement with variable resolution
                command = self._resolve_command_variables(stmt, env)
                musts.append(command)
            elif stmt[0] == 'should':
                # Process should statement with variable resolution  
                command = self._resolve_command_variables(stmt, env)
                shoulds.append(command)
            elif stmt[0] == 'simple_command':
                # Process simple command as a should directive
                command = stmt[1]
                shoulds.append(command)
            elif stmt[0] == 'data':
                if len(stmt) == 2:
                    # data: expression
                    data_items['data'] = self.visit(stmt[1], env)
                else:
                    # identifier: expression
                    # Check if stmt[2] is already a resolved value (from chaining)
                    # rather than an AST node that needs visiting
                    if isinstance(stmt[2], (str, int, float, bool, list, dict)) and not isinstance(stmt[2], tuple):
                        # Already a resolved value, use it directly
                        data_items[stmt[1]] = stmt[2]
                    else:
                        # AST node, visit it
                        data_items[stmt[1]] = self.visit(stmt[2], env)
        
        # Build prompt with explicit structure
        prompt_parts = []
        
        if musts:
            prompt_parts.append("MUST:\n" + "\n".join(f"- {cmd}" for cmd in musts))
        
        if shoulds:
            prompt_parts.append("SHOULD:\n" + "\n".join(f"- {cmd}" for cmd in shoulds))
        
        if data_items:
            prompt_parts.append("DATA:\n" + "\n".join(f"{k}: {v}" for k, v in data_items.items()))
        
        return "\n\n".join(prompt_parts) if prompt_parts else "Process request"
    
    def _get_available_variables(self, env: Environment) -> Dict[str, Any]:
        """Get all available variables from the environment chain"""
        variables = {}
        current_env = env
        
        while current_env:
            # Add variables from this environment level
            for name, value in current_env.vars.items():
                if name not in variables:  # Don't override with parent values
                    # Convert complex objects to string representation
                    if isinstance(value, (str, int, float, bool)):
                        variables[name] = value
                    elif value is None:
                        variables[name] = "null"
                    else:
                        variables[name] = f"<{type(value).__name__}>"
            
            current_env = current_env.parent
        
        return variables

    def _resolve_command_variables(self, stmt: tuple, env: Environment) -> str:
        """Resolve variables in must/should commands"""
        if len(stmt) == 2:
            # Simple command: must "string" or must identifier
            command = stmt[1]
            if isinstance(command, str) and command.startswith('"'):
                return command.strip('"\'')
            else:
                # Try to resolve as variable
                try:
                    value = env.get(command)
                    return f"{command} ({value})"
                except:
                    return command
        elif len(stmt) == 3:
            # Command with data: must identifier expression
            command = stmt[1]
            try:
                data_value = self.visit(stmt[2], env)
                # Format complex data structures better for AI understanding
                if isinstance(data_value, dict):
                    formatted_data = self._format_dict_for_ai(data_value)
                    return f"{command} with data: {formatted_data}"
                elif isinstance(data_value, list):
                    formatted_data = self._format_list_for_ai(data_value)
                    return f"{command} with data: {formatted_data}"
                else:
                    return f"{command} with data: {data_value}"
            except:
                return f"{command} {stmt[2]}"
        else:
            return str(stmt[1]) if len(stmt) > 1 else "process"
    
    def _format_dict_for_ai(self, data: dict) -> str:
        """Format dictionary data for AI understanding"""
        formatted_items = []
        for key, value in data.items():
            if isinstance(value, dict):
                formatted_items.append(f"{key}: {{{', '.join(f'{k}: {v}' for k, v in value.items())}}}")
            elif isinstance(value, list):
                formatted_items.append(f"{key}: [{', '.join(str(v) for v in value)}]")
            else:
                formatted_items.append(f"{key}: {value}")
        return "{" + ", ".join(formatted_items) + "}"
    
    def _format_list_for_ai(self, data: list) -> str:
        """Format list data for AI understanding"""
        return "[" + ", ".join(str(item) for item in data) + "]"

    def _synthesize_system_prompt(self, system_prompts: List[tuple]) -> str:
        """
        Synthesize system prompt from must/should statements.
        
        Args:
            system_prompts: List of (type, content) tuples
            
        Returns:
            Synthesized system prompt
        """
        if not system_prompts:
            return "You are a helpful assistant."
        
        musts = []
        shoulds = []
        
        for prompt_type, content in system_prompts:
            content = content.strip('"\'')  # Remove quotes
            if prompt_type == 'must':
                musts.append(content)
            elif prompt_type == 'should':
                shoulds.append(content)
        
        prompt_parts = []
        if musts:
            prompt_parts.append("You must: " + " ".join(musts))
        if shoulds:
            prompt_parts.append("You should: " + " ".join(shoulds))
        
        return " ".join(prompt_parts)

    def _execute_ai_request(self, model: str, system_prompt: str, user_prompt: str, tools: List[str]) -> Tuple[Any, Optional[str]]:
        """
        Execute an AI request using OpenAI API.
        
        Args:
            model: Model name to use
            system_prompt: System prompt for the agent
            user_prompt: User prompt to send
            tools: List of available tools
            
        Returns:
            Tuple of (result, error)
        """
        if not self.openai_client:
            return None, "OpenAI API key not configured. Please set OPENAI_API_KEY in your .env file."
            
        try:
            response = self.openai_client.chat.completions.create(
                model=model,
                messages=[
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": user_prompt}
                ]
            )
            return response.choices[0].message.content, None
        except Exception as e:
            return None, str(e)


def create_interpreter(parser: Optional[AlloyParser] = None) -> Interpreter:
    """
    Factory function to create a new interpreter instance.
    
    Args:
        parser: Parser instance to use
        
    Returns:
        Interpreter instance
    """
    return Interpreter()


# Example usage function
def run_alloy_code(code: str, debug: bool = False) -> Any:
    """
    Convenience function to run Alloy code.
    
    Args:
        code: Alloy source code
        debug: Enable debug output
        
    Returns:
        Result of execution
    """
    parser = AlloyParser()
    interpreter = Interpreter()
    
    try:
        ast = parser.parse(code, debug=debug)
        return interpreter.interpret(ast)
    except Exception as e:
        print(f"Error executing Alloy code: {e}")
        return None


if __name__ == "__main__":
    # Test the interpreter
    test_code = '''
    tool add_numbers takes a, b returns sum {
        sum = a + b
        return sum
    }
    
    result = add_numbers(5, 3)
    '''
    
    print("Testing Alloy Interpreter")
    print("=" * 40)
    print(f"Code: {test_code}")
    print("-" * 40)
    
    result = run_alloy_code(test_code)
    print(f"Result: {result}")