import ast
import builtins
import json
import typing
from typing import Dict, Optional, Tuple

from letta.errors import LettaToolCreateError
from letta.types import JsonDict


def resolve_type(annotation: str):
    """
    Resolve a type annotation string into a Python type.
    Previously, primitive support for int, float, str, dict, list, set, tuple, bool.

    Args:
        annotation (str): The annotation string (e.g., 'int', 'list[int]', 'dict[str, int]').

    Returns:
        type: The corresponding Python type.

    Raises:
        ValueError: If the annotation is unsupported or invalid.
    """
    python_types = {**vars(typing), **vars(builtins)}

    if annotation in python_types:
        return python_types[annotation]

    try:
        # Allow use of typing and builtins in a safe eval context
        return eval(annotation, python_types)
    except Exception:
        raise ValueError(f"Unsupported annotation: {annotation}")


# TODO :: THIS MUST BE EDITED TO HANDLE THINGS
def get_function_annotations_from_source(source_code: str, function_name: str) -> Dict[str, str]:
    """
    Parse the source code to extract annotations for a given function name.

    Args:
        source_code (str): The Python source code containing the function.
        function_name (str): The name of the function to extract annotations for.

    Returns:
        Dict[str, str]: A dictionary of argument names to their annotation strings.

    Raises:
        ValueError: If the function is not found in the source code.
    """
    tree = ast.parse(source_code)
    for node in ast.iter_child_nodes(tree):
        if isinstance(node, ast.FunctionDef) and node.name == function_name:
            annotations = {}
            for arg in node.args.args:
                if arg.annotation is not None:
                    annotation_str = ast.unparse(arg.annotation)
                    annotations[arg.arg] = annotation_str
            return annotations
    raise ValueError(f"Function '{function_name}' not found in the provided source code.")


# NOW json_loads -> ast.literal_eval -> typing.get_origin
def coerce_dict_args_by_annotations(function_args: JsonDict, annotations: Dict[str, str]) -> dict:
    coerced_args = dict(function_args)  # Shallow copy

    for arg_name, value in coerced_args.items():
        if arg_name in annotations:
            annotation_str = annotations[arg_name]
            try:
                arg_type = resolve_type(annotation_str)

                # Always parse strings using literal_eval or json if possible
                if isinstance(value, str):
                    try:
                        value = json.loads(value)
                    except json.JSONDecodeError:
                        try:
                            value = ast.literal_eval(value)
                        except (SyntaxError, ValueError) as e:
                            if arg_type is not str:
                                raise ValueError(f"Failed to coerce argument '{arg_name}' to {annotation_str}: {e}")

                origin = typing.get_origin(arg_type)
                if origin in (list, dict, tuple, set):
                    # Let the origin (e.g., list) handle coercion
                    coerced_args[arg_name] = origin(value)
                else:
                    # Coerce simple types (e.g., int, float)
                    coerced_args[arg_name] = arg_type(value)

            except Exception as e:
                raise ValueError(f"Failed to coerce argument '{arg_name}' to {annotation_str}: {e}")

    return coerced_args


def get_function_name_and_docstring(source_code: str, name: Optional[str] = None) -> Tuple[str, str]:
    """Gets the name and docstring for a given function source code by parsing the AST.

    Args:
        source_code: The source code to parse
        name: Optional override for the function name

    Returns:
        Tuple of (function_name, docstring)
    """
    try:
        # Parse the source code into an AST
        tree = ast.parse(source_code)

        # Find the last function definition
        function_def = None
        for node in ast.walk(tree):
            if isinstance(node, ast.FunctionDef):
                function_def = node

        if not function_def:
            raise LettaToolCreateError("No function definition found in source code")

        # Get the function name
        function_name = name if name is not None else function_def.name

        # Get the docstring if it exists
        docstring = ast.get_docstring(function_def)

        if not function_name:
            raise LettaToolCreateError("Could not determine function name")

        if not docstring:
            # For tools with args_json_schema, the docstring is optional
            docstring = f"The {function_name} tool"

        return function_name, docstring

    except Exception as e:
        import traceback

        traceback.print_exc()
        raise LettaToolCreateError(f"Failed to parse function name and docstring: {str(e)}")
