from __future__ import annotations

import dataclasses
import inspect
from typing import (
  TYPE_CHECKING,
  Annotated,
  Any,
  ParamSpec,
  TypeVar,
  cast,
  get_args,
  get_origin,
  overload,
  override,
)

from loguru import logger
from pydantic import ValidationError, create_model

from hipr.extraction import (
  _extract_dataclass_params,  # pyright: ignore[reportPrivateUsage]
  _extract_hyper_field_info,  # pyright: ignore[reportPrivateUsage]
  _extract_init_params,  # pyright: ignore[reportPrivateUsage]
  _extract_public_fields,  # pyright: ignore[reportPrivateUsage]
  _get_type_hints_with_fallback,  # pyright: ignore[reportPrivateUsage]
)
from hipr.models import ConfigurableIndicator, MakeableModel
from hipr.registry import _config_creation_lock  # pyright: ignore[reportPrivateUsage]

if TYPE_CHECKING:
  from collections.abc import Callable

__all__ = [
  "_configurable_class",
  "_create_class_make_method",
  "_recursive_make",
  "configurable",
  "validate_config",
]

# Expected number of type args for dict (key, value)
_DICT_TYPE_ARGS_COUNT = 2

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


def _recursive_make(value: object, expected_type: type | None = None) -> object:
  """Recursively make configs in nested structures.

  Args:
    value: A value that may be a MakeableModel, list, dict, or plain value
    expected_type: The expected type of the destination field.
                   If this is a Config type, we do NOT make the value.

  Returns:
    The made value (configs become instances, collections are recursed)
  """
  # If expected_type is a Config type, don't make - keep as Config
  if expected_type is not None:
    origin = get_origin(expected_type)
    actual_type = origin if origin else expected_type
    if isinstance(actual_type, type) and issubclass(actual_type, MakeableModel):
      return value

  # If value is a Config, make it
  if isinstance(value, MakeableModel):
    return value.make()  # pyright: ignore[reportUnknownVariableType]

  if isinstance(value, list):
    # Inspect inner type of list if possible
    inner_expected = None
    if expected_type and get_origin(expected_type) is list:
      args = get_args(expected_type)
      if args:
        inner_expected = args[0]  # pyright: ignore[reportAny]

    return [_recursive_make(item, inner_expected) for item in value]  # pyright: ignore[reportUnknownVariableType, reportUnknownArgumentType]

  if isinstance(value, dict):
    # Similar logic for dicts
    inner_expected = None
    if expected_type and get_origin(expected_type) is dict:
      args = get_args(expected_type)
      if len(args) == _DICT_TYPE_ARGS_COUNT:
        inner_expected = args[1]  # pyright: ignore[reportAny]

    return {k: _recursive_make(v, inner_expected) for k, v in value.items()}  # pyright: ignore[reportUnknownVariableType, reportUnknownArgumentType]

  return value


def _create_class_make_method[R](cls: type[R]) -> Callable[[MakeableModel[R]], R]:
  """Create make method for class configs.

  Args:
    cls: The class being configured

  Returns:
    A make method that creates instances with recursive nested making
  """

  def make(self: MakeableModel[R]) -> R:
    """Creates an instance with the configured hyperparameters."""
    if self._instance is not None:  # pyright: ignore[reportPrivateUsage]
      return self._instance  # pyright: ignore[reportPrivateUsage]

    # Get type hints for __init__ to guide recursion
    init_hints = _get_type_hints_with_fallback(cls.__init__)

    kwargs = {}
    for name, value in _extract_public_fields(self).items():
      expected = init_hints.get(name)
      # Unwrap Hyper/Annotated if present
      if get_origin(expected) is Annotated:
        args = get_args(expected)
        if args:
          expected = args[0]  # pyright: ignore[reportAny]

      kwargs[name] = _recursive_make(value, cast("type | None", expected))

    instance = cls(**kwargs)
    self._instance = instance  # pyright: ignore[reportPrivateUsage]
    return instance

  return make


def _configurable_class[R](cls: type[R]) -> type[R]:
  """Apply @configurable decorator to a class.

  Extracts all parameters from either dataclass fields or __init__ signature
  and generates a Config class. Uses double-check locking for thread safety.

  Args:
    cls: Class to decorate

  Returns:
    The same class with a Config attribute attached

  Raises:
    ValueError: If 'model_config' is used as a parameter name
  """
  # Fast path: if already decorated, return immediately without acquiring lock
  if "Config" in cls.__dict__:
    return cls

  # Slow path: acquire lock for decoration
  with _config_creation_lock:
    # Double-check: another thread might have decorated while we waited for lock
    if "Config" in cls.__dict__:
      return cls

    hyper_params: dict[str, Any] = {}  # pyright: ignore[reportExplicitAny]

    if dataclasses.is_dataclass(cls):
      hyper_params = _extract_dataclass_params(cls)
    elif hasattr(cls, "__init__"):
      hyper_params = _extract_init_params(cls)

    # Log configuration creation
    logger.trace(
      "Creating Config for class '{}' with {} hyperparameter(s): {}",
      cls.__name__,
      len(hyper_params),
      list(hyper_params.keys()),
    )

    # Handle model_config name collision
    if "model_config" in hyper_params:
      msg = (
        "The parameter name 'model_config' is reserved by Pydantic. "
        "Please rename this parameter (e.g., to 'model_cfg' or 'config')."
      )
      raise ValueError(msg)

    # Dynamically create Pydantic model from extracted hyperparameters
    config_model = cast(
      "type[MakeableModel[R]]",
      create_model(
        f"{cls.__name__}Config",
        __base__=MakeableModel[R],
        **hyper_params,  # type: ignore[arg-type]
      ),
    )
    config_model.make = _create_class_make_method(cls)  # pyright: ignore[reportAttributeAccessIssue]

    # Dynamically attach Config class to the decorated class
    cls.Config = config_model  # type: ignore[attr-defined]

    # Add to_config method to the class
    def to_config(self: R) -> MakeableModel[R]:
      """Create a Config that wraps this instance."""
      return cls.Config.from_instance(self)  # type: ignore[attr-defined]

    cls.to_config = to_config  # type: ignore[attr-defined]

    return cls


@overload
def configurable[R](fn_or_class: type[R]) -> type[R]: ...  # pyright: ignore[reportOverlappingOverload]


@overload
def configurable[**P, R](
  fn_or_class: Callable[P, R],
) -> ConfigurableIndicator[P, R]: ...


def configurable[**P, R](  # noqa: C901
  fn_or_class: Callable[P, R] | type[R],
) -> ConfigurableIndicator[P, R] | type[R]:
  """Decorator that generates Pydantic config models from Hyper parameters.

  Scans the decorated function/class for parameters annotated with Hyper[T]
  and automatically generates a Config class that can validate and bind these
  hyperparameters.

  Args:
    fn_or_class: Function, method, or class to decorate

  Returns:
    ConfigurableIndicator for functions/methods, or the original class with
    Config attribute attached for classes

  Raises:
    ValueError: If 'model_config' is used as a parameter name
    TypeError: If Hyper parameter lacks a default value

  Example:
    >>> @configurable
    ... def process(data: pd.DataFrame, window: Hyper[int, Ge[1]] = 14) -> float:
    ...     return data.rolling(window).mean().sum()
    >>> config = process.Config(window=7)
    >>> fn = config.make()
    >>> result = fn(my_data)
  """
  # Check if this is a class
  if inspect.isclass(fn_or_class):
    return _configurable_class(fn_or_class)

  fn = fn_or_class  # It's a function/method
  hyper_params = {}

  # Get raw annotations first (without evaluation)
  sig = inspect.signature(fn)

  # Check if this is a method
  params_list = list(sig.parameters.values())
  is_method = params_list and params_list[0].name in {"self", "cls"}

  # Get type hints with fallback for TYPE_CHECKING imports
  type_hints = _get_type_hints_with_fallback(fn)

  for param in sig.parameters.values():
    # Skip self/cls parameters for methods
    if is_method and param.name in {"self", "cls"}:
      continue

    if param.name not in type_hints:
      continue

    annotation = type_hints[param.name]
    default_val: object = cast("object", param.default)

    field_info = _extract_hyper_field_info(annotation, default_val, param.name)
    if field_info:
      hyper_params[param.name] = field_info

  # Log configuration creation
  logger.trace(
    "Creating Config for '{}' with {} hyperparameter(s): {}",
    fn.__name__,
    len(hyper_params),  # pyright: ignore[reportUnknownArgumentType]
    list(hyper_params.keys()),  # pyright: ignore[reportUnknownArgumentType]
  )

  # Handle model_config name collision
  if "model_config" in hyper_params:
    msg = (
      "The parameter name 'model_config' is reserved by Pydantic. "
      "Please rename this parameter (e.g., to 'model_cfg' or 'config')."
    )
    raise ValueError(msg)

  def make(self: MakeableModel[R]) -> Callable[P, R]:
    """Creates a bound function with the given hyperparameters.

    Uses a closure for efficient callable creation.
    """
    kwargs = _extract_public_fields(self)

    # Use a class to allow custom __repr__
    class BoundFunction:
      def __init__(
        self, original_fn: Callable[P, R], config: dict[str, object]
      ) -> None:
        super().__init__()
        self._fn = original_fn
        self._config = config
        self.__name__ = original_fn.__name__
        self.__qualname__ = original_fn.__qualname__
        self.__doc__ = original_fn.__doc__

      def __call__(self, *args: P.args, **kw: P.kwargs) -> R:
        return self._fn(*args, **self._config, **kw)

      @override
      def __repr__(self) -> str:
        config_str = ", ".join(f"{k}={v!r}" for k, v in self._config.items())
        return f"{self._fn.__qualname__}({config_str})"

    return BoundFunction(fn, kwargs)

  # Dynamically create Pydantic model from extracted hyperparameters
  # Convert snake_case to PascalCase (e.g., my_func -> MyFuncConfig)
  config_name = "".join(word.capitalize() for word in fn.__name__.split("_")) + "Config"
  config_model = create_model(
    config_name,
    __base__=MakeableModel[R],
    **hyper_params,  # type: ignore[arg-type]
  )
  config_model.make = make  # pyright: ignore[reportAttributeAccessIssue]

  return ConfigurableIndicator(fn, config_model)


def validate_config[R](
  config_cls: type[MakeableModel[R]], data: dict[str, object]
) -> tuple[bool, list[str]]:
  """Validate config data without creating an instance.

  Args:
    config_cls: The Config class to validate against
    data: Dictionary of field values to validate

  Returns:
    Tuple of (is_valid, error_messages)

  Example:
    >>> @configurable
    ... def func(x: Hyper[int, Ge[0]] = 10) -> int:
    ...     return x
    >>> valid, errors = validate_config(func.Config, {"x": 5})
    >>> assert valid
    >>> valid, errors = validate_config(func.Config, {"x": -1})
    >>> assert not valid
    >>> assert len(errors) > 0

  Raises:
    TypeError: If config_cls is not a MakeableModel subclass or data is not a dict
  """
  try:
    config_cls(**data)
    return (True, [])
  except ValidationError as e:
    errors = [str(err) for err in e.errors()]
    return (False, errors)
