import inspect
import json
import importlib.util
import sys
from typing import Any


def get_type_name(annotation):
    """Helper function: Reads the type name as a string."""
    if annotation is inspect.Parameter.empty:
        return "Any"
    if hasattr(annotation, "__name__"):
        return annotation.__name__
    return str(annotation)


def extract_function_info(func, prefix=""):
    """Extracts all data and metadata of a function or method."""
    identifier = f"{prefix}.{func.__name__}" if prefix else func.__name__
    meta_data = getattr(func, "_meta", {})

    # Automatically adds docstrings as description to metadata if desired
    if func.__doc__ and "description" not in meta_data:
        meta_data["description"] = func.__doc__.strip()

    sig = inspect.signature(func)

    inputs = []
    for name, param in sig.parameters.items():
        if name == "self":
            continue
        inp = {
            "identifier": f"{identifier}.{name}",
            "type": get_type_name(param.annotation)
        }
        # Check if a default value exists
        if param.default is not inspect.Parameter.empty:
            inp["default"] = param.default
        inputs.append(inp)

    output_type = get_type_name(sig.return_annotation)

    func_dict = {"identifier": identifier}

    if meta_data:
        func_dict["metadata"] = meta_data
    if inputs:
        func_dict["input"] = inputs
    if output_type != "Any":
        func_dict["output"] = output_type

    return func_dict


def extract_class_info(cls):
    """Extracts attributes, methods, and metadata of a class."""
    identifier = cls.__name__
    meta_data = getattr(cls, "_meta", {})

    attributes = []
    # Read type annotations of the class
    annotations = getattr(cls, "__annotations__", {})
    for attr_name, attr_type in annotations.items():
        if attr_name.startswith("_"):
            continue

        attr_dict = {
            "identifier": f"{identifier}.{attr_name}",
            "type": get_type_name(attr_type)
        }

        # Check if a default value is directly attached to the class
        default_val = getattr(cls, attr_name, None)
        if default_val is not None and not inspect.isroutine(default_val):
            attr_dict["default"] = default_val

        attributes.append(attr_dict)

    functions = []
    # Read all defined methods of the class
    for name, method in inspect.getmembers(cls, inspect.isroutine):
        if name.startswith("_"):  # Skip methods like __init__
            continue
        functions.append(extract_function_info(method, prefix=identifier))

    cls_dict = {"identifier": identifier}
    if meta_data:
        cls_dict["metadata"] = meta_data
    if attributes:
        cls_dict["attributes"] = attributes
    if functions:
        cls_dict["functions"] = functions

    return cls_dict

def extract(obj: Any) -> dict:
    """
    Universal extraction function.
    Automatically selects between class and function extraction.
    """
    if inspect.isclass(obj):
        return extract_class_info(obj)
    elif inspect.isroutine(obj):
        return extract_function_info(obj)
    else:
        # Fallback for instances or simple variables
        identifier = getattr(obj, "__name__", str(obj))
        meta_data = getattr(obj, "_meta", {})
        return {
            "identifier": identifier,
            "type": type(obj).__name__,
            "metadata": meta_data
        }

def parse_python_file_to_json(file_path: str) -> str:
    """Loads a Python script dynamically and generates the metadata JSON."""
    module_name = "dynamic_loaded_module"

    # Load module dynamically from the file path
    spec = importlib.util.spec_from_file_location(module_name, file_path)
    mod = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = mod
    spec.loader.exec_module(mod)

    result = {
        "classes": [],
        "functions": [],
        "variables": []
    }

    # 1. Extract classes
    for name, obj in inspect.getmembers(mod, inspect.isclass):
        if obj.__module__ == module_name:  # Only classes defined in the script
            result["classes"].append(extract_class_info(obj))

    # 2. Extract global functions
    for name, obj in inspect.getmembers(mod, inspect.isfunction):
        if obj.__module__ == module_name:
            result["functions"].append(extract_function_info(obj))

    # 3. Extract global variables
    for name, obj in inspect.getmembers(mod, lambda x: not inspect.isroutine(x) and not inspect.isclass(
            x) and not inspect.ismodule(x)):
        if not name.startswith("_"):
            meta_data = getattr(obj, "_meta", {})
            var_dict = {
                "identifier": name,
                "type": type(obj).__name__
            }
            if meta_data:
                var_dict["metadata"] = meta_data
            result["variables"].append(var_dict)

    # Remove empty categories
    result = {k: v for k, v in result.items() if v}

    return json.dumps(result, indent=4)