from copy import deepcopy
from typing import Tuple, Optional, Any, List

from qiskit.providers import Options
from qiskit.result import MeasLevel, MeasReturnType
from qiskit.transpiler import Target
from qiskit_ibm_runtime.ibm_backend import IBMBackend
from qiskit_ibm_runtime.models import BackendStatus
from qiskit_ibm_runtime.models import QasmBackendConfiguration, BackendProperties
from qiskit_ibm_runtime.utils.backend_converter import convert_to_target
from qiskit_ibm_runtime.utils.backend_decoder import configuration_from_server_data, properties_from_server_data

from qhub.api.quantum import HubQuantumClient, Backend as BackendModel
from qhub.quantum.sdk.exceptions import HubError
from qhub.quantum.sdk.qiskit import HubQiskitBackend
from qhub.quantum.sdk.qiskit.hub_qiskit_runtime_service import HubQiskitRuntimeService

"""
Kipu Quantum Hub IBM Backend Implementation

This class is adapted from the IBM Qiskit Runtime's IBMBackend class
(qiskit_ibm_runtime.ibm_backend.IBMBackend) to provide compatibility
with IBM's backend patterns while integrating with the Kipu Quantum Hub SDK.

Original IBM implementation:
https://github.com/Qiskit/qiskit-ibm-runtime/blob/main/qiskit_ibm_runtime/ibm_backend.py
"""
@HubQiskitRuntimeService.register_backend("ibm.qpu.aachen")
@HubQiskitRuntimeService.register_backend("ibm.qpu.boston")
@HubQiskitRuntimeService.register_backend("ibm.qpu.fez")
@HubQiskitRuntimeService.register_backend("ibm.qpu.kingston")
@HubQiskitRuntimeService.register_backend("ibm.qpu.marrakesh")
@HubQiskitRuntimeService.register_backend("ibm.qpu.miami")
@HubQiskitRuntimeService.register_backend("ibm.qpu.pittsburgh")
@HubQiskitRuntimeService.register_backend("ibm.qpu.torino")
@HubQiskitRuntimeService.register_backend("ibm.qpu.strasbourg")
@HubQiskitRuntimeService.register_backend("ibm.qpu.brussels")
class HubIbmQiskitBackend(HubQiskitBackend, IBMBackend):
    id_warning_issued = False

    def __init__(self,
                 service: "HubQiskitRuntimeService",
                 hub_client: HubQuantumClient,
                 backend_info: BackendModel,
                 configuration: Optional[QasmBackendConfiguration] = None,
                 use_fractional_gates: bool = False,
                 **kwargs):
        """Initialize HubIbmQiskitBackend."""
        self._service = service
        self._properties: Any = None
        self._configuration = configuration
        self._use_fractional_gates = use_fractional_gates

        super().__init__(hub_client=hub_client, backend_info=backend_info, **kwargs)

        if hasattr(self, 'options'):
            self.options.use_fractional_gates = use_fractional_gates

    def _initialize_backend_components(self):
        """Override template method for IBM-specific initialization order."""
        self._configuration = self._ibm_backend_config_to_configuration()
        self.properties()
        self._target = self._hub_backend_to_target()

    def _set_configuration_based_validators(self):
        """Set option validators based on configuration, following IBM's approach."""
        if not hasattr(self, '_configuration') or self._configuration is None:
            return

        if (
                not self._configuration.simulator
                and hasattr(self.options, "noise_model")
                and hasattr(self.options, "seed_simulator")
        ):
            self.options.set_validator("noise_model", type(None))
            self.options.set_validator("seed_simulator", type(None))

        if hasattr(self._configuration, "rep_delay_range"):
            self.options.set_validator(
                "rep_delay",
                (self._configuration.rep_delay_range[0], self._configuration.rep_delay_range[1]),
            )

    @classmethod
    def _default_options(cls) -> Options:
        """Default runtime options."""
        return Options(
            shots=4000,
            memory=False,
            meas_level=MeasLevel.CLASSIFIED,
            meas_return=MeasReturnType.AVERAGE,
            memory_slots=None,
            memory_slot_size=100,
            rep_time=None,
            rep_delay=None,
            init_qubits=True,
            use_measure_esp=None,
            use_fractional_gates=False,
            # Simulator only
            noise_model=None,
            seed_simulator=None,
        )

    def _hub_backend_to_target(self) -> Target:
        return convert_to_target(
            configuration=self._configuration,
            properties=self._properties,
        )

    def _convert_to_job_input(self, job_input, options=None) -> Tuple[str, dict]:
        # Job create is handled by 'HubQiskitRuntimeService'
        pass

    def _get_job_input_format(self) -> str:
        return "QISKIT"

    def _ibm_backend_config_to_configuration(self) -> QasmBackendConfiguration:
        """Create backend configuration following IBM's approach."""

        # Use provided configuration if available
        if self._configuration is not None:
            self.backend_version = self._configuration.backend_version
            self._set_configuration_based_validators()
            return deepcopy(self._configuration)

        # Otherwise create from raw backend config
        raw_config = self._get_backend_config().get("config", None)
        self.backend_version = raw_config.get("backend_version", None)
        raw_config["backend_name"] = self.name

        return configuration_from_server_data(
            raw_config,
            instance="",
            use_fractional_gates=self._use_fractional_gates,
        )

    @property
    def service(self):
        """Return the ``service`` object."""
        return self._service

    @property
    def dtm(self) -> float:
        """Return the system time resolution of output signals."""
        return self._configuration.dtm

    @property
    def meas_map(self) -> List[List[int]]:
        """Return the grouping of measurements which are multiplexed."""
        return self._configuration.meas_map

    def properties(
            self, refresh: bool = False
    ) -> Optional[BackendProperties]:
        """Return the backend properties."""
        # pylint: disable=arguments-differ
        if self._configuration.simulator:
            return None
        if not isinstance(refresh, bool):
            raise TypeError(
                "The 'refresh' argument needs to be a boolean. "
                "{} is of type {}".format(refresh, type(refresh))
            )
        if refresh or self._properties is None:
            api_properties = self._get_backend_config(refresh).get("properties", None)
            if not api_properties:
                return None
            backend_properties = properties_from_server_data(
                api_properties,
                use_fractional_gates=self.options.use_fractional_gates,
            )
            self._properties = backend_properties
        return self._properties

    def refresh(self):
        raise NotImplementedError("refresh method not implemented for Kipu Quantum Hub IBM backends.")

    def status(self) -> BackendStatus:
        """Return the backend status."""
        state_info = self._hub_client.backends.get_backend_status(self.backend_info.id)
        operational = state_info.status == "ONLINE"
        status_msg = "active" if operational else (state_info.status or "unknown").lower()
        pending_jobs = 0

        return BackendStatus.from_dict({'backend_name': self.name,
                                        'backend_version': self.backend_version,
                                        'operational': operational,
                                        'status_msg': status_msg,
                                        'pending_jobs': pending_jobs})

    def target_history(self, datetime=None):
        raise NotImplementedError("target_history method not implemented for Kipu Quantum Hub IBM backends.")

    def retrieve_job(self, job_id: str):
        raise HubError(
            "The retrieve_job() method is not supported for IBM backends. "
            "Use the job() method of HubQiskitRuntimeService to retrieve jobs."
        )

    # No need to implement these methods for IBM backends as IBM's backend decoder is used.
    def _to_gate(self, name: str):
        pass

    def _get_single_qubit_gate_properties(self, instr_name: Optional[str]) -> dict:
        pass

    def _get_multi_qubit_gate_properties(self):
        pass
