import inspect
import json
from typing import Callable, List, Optional, Union


def serialize(objective: Callable) -> str:
    try:
        return inspect.getsource(objective)
    except (OSError, TypeError) as e:
        raise ValueError(
            "serialize failed: objective function must be saved as a file\n"
            "In REPL/Jupyter Notebook, use %%writefile magic to save locally:\n\n"
            "%%writefile objective.py\n"
            "def objective(trial):\n"
            "    ...\n\n"
            "Then:\n"
            "from objective import objective\n"
            "study.optimize(objective, ...)"
        ) from e


def build_requirements(file_path: Optional[str] = None, reqs: Optional[List[str]] = None) -> str:
    if file_path and reqs:
        raise ValueError("requirements_file and requirements_list cannot be specified together")

    if file_path:
        with open(file_path) as f:
            return f.read()
    elif reqs:
        return "\n".join(reqs)
    else:
        return ""


def object_to_json(obj: Union[object, dict, None]) -> str:
    if obj is None:
        return ""

    if isinstance(obj, dict):
        return json.dumps(obj)

    cls = type(obj)
    module_name = cls.__module__
    class_name = cls.__name__

    if not (module_name.startswith("optuna.") or module_name.startswith("aiauto.")):
        raise ValueError(f"only optuna/aiauto core classes are supported: {class_name}")

    # __init__의 실제 파라미터만 가져오기
    sig = inspect.signature(cls.__init__)
    valid_params = set(sig.parameters.keys()) - {"self"}

    # Optuna 객체들은 __dict__에 _param_name 형태로 저장
    kwargs = {}
    for key, value in obj.__dict__.items():
        if key.startswith("_"):
            param_name = key[1:]  # _ 제거

            # __init__의 실제 파라미터인지 확인
            if param_name in valid_params:
                # PatientPruner의 wrapped_pruner 또는 CmaEs/QMC의 independent_sampler 특별 처리
                is_wrapped_pruner = (
                    class_name == "PatientPruner"
                    and param_name == "wrapped_pruner"
                    and value is not None
                )
                is_independent_sampler = (
                    param_name == "independent_sampler"
                    and value is not None
                    and class_name in ["CmaEsSampler", "QMCSampler"]
                )
                if is_wrapped_pruner or is_independent_sampler:
                    kwargs[param_name] = json.loads(object_to_json(value))
                # Callable 타입은 제외 (gamma, weights 등)
                elif not callable(value):
                    kwargs[param_name] = value

    return json.dumps({"cls": class_name, "kwargs": kwargs})


_FROM_JSON_MAX_DEPTH = 3


def from_json(config_json: str, whitelist: dict, _depth: int = 0):
    if not config_json:
        return None

    if _depth > _FROM_JSON_MAX_DEPTH:
        raise ValueError(
            f"Maximum nesting depth ({_FROM_JSON_MAX_DEPTH}) exceeded for deserialization"
        )

    try:
        config = json.loads(config_json)
        cls_name = config.get("cls")
        kwargs = config.get("kwargs", {})

        if not cls_name:
            raise ValueError("Class name 'cls' not specified in JSON config.")

        if cls_name not in whitelist:
            raise ValueError(f"Class '{cls_name}' is not in the whitelist.")

        cls = whitelist[cls_name]

        # PatientPruner의 wrapped_pruner 특별 처리
        if cls_name == "PatientPruner" and "wrapped_pruner" in kwargs:
            wrapped_config = kwargs["wrapped_pruner"]
            if wrapped_config:
                wrapped_pruner = from_json(json.dumps(wrapped_config), whitelist, _depth + 1)
                kwargs["wrapped_pruner"] = wrapped_pruner

        # CmaEsSampler와 QMCSampler의 independent_sampler 특별 처리
        if cls_name in ["CmaEsSampler", "QMCSampler"] and "independent_sampler" in kwargs:
            sampler_config = kwargs["independent_sampler"]
            if sampler_config:
                independent_sampler = from_json(json.dumps(sampler_config), whitelist, _depth + 1)
                kwargs["independent_sampler"] = independent_sampler

        return cls(**kwargs)
    except (json.JSONDecodeError, TypeError, ValueError):
        raise
