"""QCodes Library Interface for BOSS/HEROS."""

from boss.helper import log, add_class_descriptor, get_or_create_dynamic_subclass
import inspect
from typing import Any


class QCodesParam:
    """HEROS representation of a QCoDeS parameter.

    This class acts as a descriptor that provides access to QCoDeS parameters
    through a HEROS interface. It handles getting and setting parameter values
    and manages parameter naming with QCoDes submodule prefixes.

    `QCoDeS parameter <https://microsoft.github.io/Qcodes/api/parameters/index.html#qcodes.parameters.Parameter>`_

    Attributes:
        target_cls: The QCoDeS instrument class containing the parameter
        sub_mod_prefix: Prefix for submodule parameters
        name: Name of the parameter
    """

    def __init__(self, target_cls: Any, sub_mod_prefix: str) -> None:
        """Initialize QCodesParam descriptor.

        Args:
            target_cls: The QCoDeS instrument or submodule class containing the parameter
            sub_mod_prefix: Prefix for submodule parameters to distinguish QCoDeS and HEROS name
        """
        self.sub_mod_prefix = sub_mod_prefix
        self.target_cls = target_cls

    def __set_name__(self, owner: Any, name: str) -> None:
        """Set name of QCoDeS parameter to name of the attribute in the owner class.

        This method is called when the descriptor is assigned to a class attribute.

        Args:
            owner: The class that owns this descriptor
            name: The name of the attribute in the owner class
        """
        self.name = name.removeprefix(self.sub_mod_prefix)

    def __get__(self, obj: Any, *_args) -> Any:
        """Get underlying QCoDeS parameter value.

        Args:
            obj: Instance of the owner class

        Returns:
            The current value of the QCoDeS parameter, or None if an error occurs
        """
        if obj is None:
            return self
        log.info("Accessing %r", self.name)
        param = self.target_cls.parameters[self.name]
        try:
            value = param.get()
        except (RuntimeError, AttributeError, KeyError):
            log.exception("Failed to get parameter %s from %s", self.name, obj)
            value = None
        return value

    def __set__(self, obj: Any, value: Any) -> None:
        """Set underlying QCoDeS parameter value.

        Args:
            obj: Instance of the owner class
            value: Value to set for the QCoDeS parameter
        """
        try:
            self.target_cls.parameters[self.name].set(value)
        except (RuntimeError, AttributeError, KeyError):
            log.exception("Failed to set parameter %s to %s at device %s", self.name, value, self.name)


class QCodesDeviceWrapper:
    """HEROS representation of a generic QCoDeS Instrument.

    This class wraps QCoDeS instruments to make them accessible through HEROS.
    It provides methods to resolve parameters (including those in submodules)
    and build dynamic subclasses that expose QCoDeS functionality.

    Attributes:
        _qcodes_instance: The wrapped QCoDeS instrument instance
    """

    _qcodes_instance: type
    name: str

    @staticmethod
    def _resolve_parameter(qcodes_instance: Any, parameter_path: str) -> tuple:
        """Resolve a parameter path to a QCoDeS parameter instance.

        This method takes a parameter path (e.g., "submodule.param_name") and
        navigates through the QCoDeS instrument's submodules to find the
        specified parameter.

        Args:
            qcodes_instance: The QCoDeS instrument instance
            parameter_path: Dot-separated path to the parameter (e.g., "submodule.param_name")

        Returns:
            A tuple of (target_instance, parameter) where target_instance is the
            QCoDeS instance (device or submodule) containing the parameter, and parameter is the parameter itself

        Raises:
            AttributeError: If the submodule path is invalid or the parameter doesn't exist
        """
        param_path_list = parameter_path.split(".")
        param_name = param_path_list.pop(-1)
        target_instance = qcodes_instance
        for sub_mod_name in param_path_list:
            if sub_mod_name in qcodes_instance.submodules:
                target_instance = target_instance.submodules[sub_mod_name]
            else:
                msg = f"{sub_mod_name} is not a valid submodule for {type(qcodes_instance)}"
                raise AttributeError(msg)

        if param_name in target_instance.parameters:
            return target_instance, target_instance.parameters[param_name]

        msg = f"{param_name} is not a valid parameter for {target_instance}"
        raise AttributeError(msg)

    @classmethod
    def _build(cls, source_class: type, name: str, arg_dict: dict) -> type:
        """Build a dynamic subclass that wraps a QCoDeS instrument.

        This method creates a new class that inherits from QCodesDeviceWrapper
        and wraps the specified QCoDeS instrument class. It exposes selected
        parameters as attributes and copies methods from the QCoDeS instrument to be exposed to HEROS.

        Args:
            source_class: The QCoDeS instrument class to wrap
            name: Name of the HERO used as an internal name for QCoDeS
            arg_dict: Dictionary of arguments for the QCoDeS instrument constructor,
                     including a 'parameters' key with a list of parameter paths to expose
        """

        parameters = arg_dict["parameters"] if "parameters" in arg_dict else []
        arg_dict.pop("parameters", None)

        qcodes_cls_path = f"{source_class.__module__}.{source_class.__name__}"
        log.debug("Building QCoDeS wrapper for %s", qcodes_cls_path)
        new_cls: type[QCodesDeviceWrapper] = get_or_create_dynamic_subclass(cls, qcodes_cls_path, name, **arg_dict)
        new_cls._qcodes_instance = source_class(name=name, **arg_dict)
        new_cls.name = name
        for cap in inspect.getmembers(new_cls._qcodes_instance, inspect.ismethod):
            if not cap[0].startswith("_"):
                setattr(new_cls, *cap)
        for param in parameters:
            param_path_list = param.split(".")
            param_name = param_path_list.pop(-1)
            prefix = "_".join(param_path_list)
            prefix = prefix + "_" if prefix else ""
            target_instance, _ = cls._resolve_parameter(new_cls._qcodes_instance, param)
            add_class_descriptor(new_cls, f"{prefix}{param_name}", QCodesParam(target_instance, prefix))
        return new_cls

    def __init__(self, **_kwargs) -> None:
        """Initialize QCodesDeviceWrapper.

        Used to remove spurious kwargs that where passed to _build.

        Args:
            **_kwargs: Any additional arguments (ignored)
        """

    def get_parameter_spec(self, parameter_path: str) -> dict:
        """Get the parameter specification of a parameter.

        This method retrieves the specification dictionary for a QCoDeS parameter,
        which contains metadata about the parameter such as its type, units, etc.

        Args:
            parameter_path: Dot-separated path to the parameter (e.g., "submodule.param_name")

        Returns:
            Dictionary containing the parameter specification

        Raises:
            AttributeError: If the parameter path is invalid
        """
        _, param = self._resolve_parameter(self._qcodes_instance, parameter_path)
        return dict(param.param_spec._to_dict())
