from __future__ import annotations

import functools
import inspect
from collections import OrderedDict
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, TypeVar, cast

import schema
from pydantic import BaseModel
from schema import Schema
from typing_extensions import ParamSpec

if TYPE_CHECKING:
    from collections import OrderedDict

CONFIG_SCHEMA = Schema(
    {
        schema.Optional("name"): str,
        "description": str,
        schema.Optional("schema"): lambda data: (
            isinstance(data, (Schema, Callable)) or (isinstance(data, type) and issubclass(data, BaseModel))
        ),
    }
)

P = ParamSpec("P")
R = TypeVar("R")


def activity(config: dict) -> Any:
    validated_config = CONFIG_SCHEMA.validate(config)

    validated_config.update({k: v for k, v in config.items() if k not in validated_config})

    if not validated_config.get("schema"):
        validated_config["schema"] = None

    def decorator(func: Callable) -> Any:
        @functools.wraps(func)
        def wrapper(self: Any, params: dict) -> Any:
            return func(self, **_build_kwargs(func, params))

        setattr(wrapper, "name", validated_config.get("name", func.__name__))
        setattr(wrapper, "config", validated_config)
        setattr(wrapper, "is_activity", True)

        return wrapper

    return decorator


def lazy_property(attr_name: str | None = None) -> Callable[[Callable[P, R]], R]:
    def decorator(func: Callable[P, R]) -> R:
        actual_attr_name = f"_{func.__name__}" if attr_name is None else attr_name

        @property
        @functools.wraps(func)
        def wrapper(self: Any) -> R:
            if getattr(self, actual_attr_name) is None:
                setattr(self, actual_attr_name, func(self))  # pyright: ignore[reportCallIssue]
            return getattr(self, actual_attr_name)

        @wrapper.setter
        def wrapper(self: Any, value: Any) -> None:
            setattr(self, actual_attr_name, value)

        return cast("R", wrapper)

    return decorator


def _build_kwargs(func: Callable, params: dict) -> dict:
    func_params = cast("OrderedDict", inspect.signature(func).parameters.copy())
    func_params.popitem(last=False)

    kwarg_var = None
    for param in func_params.values():
        # if there is a **kwargs parameter, we can safely
        # pass all the params to the function
        if param.kind == inspect.Parameter.VAR_KEYWORD:
            kwarg_var = func_params.pop(param.name).name
            break

    # only pass the values that are in the function signature
    # or if there is a **kwargs parameter, pass all the values
    kwargs = {k: v for k, v in params.get("values", {}).items() if k in func_params or kwarg_var is not None}

    # add 'params' and 'values' if they are in the signature
    # or if there is a **kwargs parameter
    if "params" in func_params or kwarg_var is not None:
        kwargs["params"] = params
    if "values" in func_params or kwarg_var is not None:
        kwargs["values"] = params.get("values")

    # set any missing parameters to None
    for param_name, param in func_params.items():
        if param_name not in kwargs and param.default == inspect.Parameter.empty:
            kwargs[param_name] = None

    return kwargs
