import contextlib
import inspect
import io
import queue
import re
import socket
import threading
import types
from typing import Any

from langchain_core.tools import tool

from universal_mcp.agents.codeact0.utils import derive_context


def eval_unsafe(
    code: str, _locals: dict[str, Any], add_context: dict[str, Any]
) -> tuple[str, dict[str, Any], dict[str, Any]]:
    # print(_locals)
    EXCLUDE_TYPES = (
        types.ModuleType,  # modules
        type(re.match("", "")),
        type(threading.Lock()),  # instead of threading.Lock
        type(threading.RLock()),  # reentrant lock
        threading.Event,  # events
        threading.Condition,  # condition vars
        threading.Semaphore,  # semaphores
        queue.Queue,  # thread-safe queues
        socket.socket,  # network sockets
        io.IOBase,  # file handles (and StringIO/BytesIO)
    )
    try:
        with contextlib.redirect_stdout(io.StringIO()) as f:
            # Execute the code in the provided locals context
            # Using exec to allow dynamic code execution
            # This is a simplified version; in production, consider security implications
            exec(code, _locals, _locals)
        result = f.getvalue()
        if not result:
            result = "<code ran, no output printed to stdout>"
    except Exception as e:
        result = f"Error during execution: {repr(e)}"

    # Return all variables in locals except __builtins__ and unpicklable objects (including tools)
    all_vars = {}
    for key, value in _locals.items():
        if key == "__builtins__":
            continue

        # Skip coroutines, async generators, and coroutine functions
        if inspect.iscoroutine(value) or inspect.iscoroutinefunction(value):
            continue
        if inspect.isasyncgen(value) or inspect.isasyncgenfunction(value):
            continue

        # Skip "obviously unpicklable" types
        if isinstance(value, EXCLUDE_TYPES):
            continue

        # Keep if it's not a callable OR if it has no __name__ attribute
        if not callable(value) or not hasattr(value, "__name__"):
            all_vars[key] = value

    new_add_context = derive_context(code, add_context)
    return result, all_vars, new_add_context


@tool(parse_docstring=True)
def execute_ipython_cell(snippet: str) -> str:
    """
    Executes Python code in an IPython notebook cell:
    * The output generated by the notebook cell is returned by this tool
    * State is persistent across executions and discussions with the user
    * The input code may reference variables created in previous executions

    Args:
        snippet: The Python code to execute.

    Returns:
        String containing the execution output or error message.

    Raises:
        ValueError: If snippet is empty.
    """
    # Validate required parameters
    if not snippet or not snippet.strip():
        raise ValueError("Parameter 'snippet' is required and cannot be empty or whitespace")

    # Your actual execution logic would go here
    return f"Successfully executed {len(snippet)} characters of Python code"
