from __future__ import annotations

from abc import abstractmethod, ABC
from copy import copy
from typing import Union, Optional, Dict, Any, TYPE_CHECKING

from braket.ahs import AnalogHamiltonianSimulation
from braket.circuits import Circuit
from qiskit import QuantumCircuit

from qhub.api.quantum import HubQuantumClient, Backend as BackendModel, BackendStateInfo, CalibrationResponse
from qhub.api.quantum.core import ApiError
from qhub.quantum.sdk.job import HubBaseJob, JobRequest

if TYPE_CHECKING:
    from qhub.quantum.sdk.qiskit.job import HubQiskitJob
    from qhub.quantum.sdk.braket import HubAwsQuantumTask


class HubBackend(ABC):
    def __init__(  # pylint: disable=too-many-arguments
            self,
            backend_info: BackendModel,
            hub_client: HubQuantumClient,
    ):
        """HubBackend for executing inputs against Kipu Quantum Hub devices.

        Example:
            provider = HubQiskitProvider()
            backend = provider.get_backend("azure.ionq.simulator")
            input = ... # Can be Qiskit circuit
            backend.run(input, shots=10).result().get_counts()
            {"100": 10, "001": 10}

        Args:
            backend_info: Kipu Quantum Hub actual infos
            hub_client: Kipu Quantum Hub API client
        """

        self._hub_client = hub_client
        if self._hub_client is None:
            raise RuntimeError("hub_client must not be None")

        self._backend_info = backend_info

    @property
    def backend_info(self) -> BackendModel:
        return self._backend_info

    @property
    def min_shots(self):
        return self.backend_info.configuration.shots_range.min

    @property
    def max_shots(self):
        return self.backend_info.configuration.shots_range.max

    def calibration(self) -> Optional[CalibrationResponse]:
        """
        Retrieve the currently effective calibration data of the backend.

        Note: Backend calibrations are unavailable for simulators.

        Returns:
            Optional[CalibrationResponse]: Backend calibration data or None if no calibration data is available.
            The data format is backend specific.
        """
        try:
            response = self._hub_client.backends.get_backend_calibration(self.backend_info.id)
            return response if response else None
        except ApiError as e:
            if e.status_code == 204:
                return None
            raise

    def run(self, job_input: Union[QuantumCircuit, Circuit, AnalogHamiltonianSimulation], shots: Optional[int] = None,
            verbatim: bool = False,
            sdk_provider: str = None,
            **kwargs: object) -> Union[HubQiskitJob, HubAwsQuantumTask]:
        """Runs an experiment on the backend as job.

        Args:
            job_input (QuantumCircuit, AnalogHamiltonianSimulation): job input to run, e.g. a Qiskit circuit or a hamiltonian. Currently only a single input can be executed per job.
            shots (int): the number of shots
            verbatim (bool): If True, submit circuit without transpilation (AWS Braket backends only).
                Use this when the circuit already contains native gates.
            sdk_provider (str): the SDK provider used for the execution
            **kwargs: additional arguments for the execution
        Returns:
            The job object representing the experiment
        """

        if isinstance(job_input, (list, tuple)):
            if len(job_input) > 1:
                raise ValueError("Multi-experiment jobs are not supported")
            job_input = job_input[0]

        # add kwargs, if defined as options, to a copy of the options
        options = {}
        if hasattr(self, 'options'):
            options = copy(self.options)

        if kwargs:
            for field in kwargs:
                if field in options.data:
                    options[field] = kwargs[field]

        shots = shots if shots else self.backend_info.configuration.shots_range.min
        options['shots'] = shots
        options['verbatim'] = verbatim  # Used by AWS Braket backends for native gate execution
        job_input_format = self._get_job_input_format()
        converted_job_input = self._convert_to_job_input(job_input, options)
        input_params = self._convert_to_job_params(job_input, options)

        job_request = JobRequest(
            backend_id=self.backend_info.id,
            shots=shots,
            input=converted_job_input,
            input_format=job_input_format,
            input_params=input_params,
            sdk_provider=sdk_provider,
            name=kwargs.get('name', None),
        )

        return self._run_job(job_request)

    @abstractmethod
    def _convert_to_job_input(self, job_input: Union[QuantumCircuit, AnalogHamiltonianSimulation], options=None) -> dict:
        pass

    def _convert_to_job_params(self, job_input: QuantumCircuit = None, options=None) -> dict:
        return {}

    @abstractmethod
    def _get_job_input_format(self) -> str:
        pass

    @abstractmethod
    def _run_job(self, job_request: JobRequest) -> HubBaseJob:
        pass

    @property
    def backend_provider(self) -> str:
        """Return the provider offering the quantum backend resource."""
        return self.backend_info.provider

    def _get_backend_config(self, refresh: bool = False) -> Dict[str, Any]:
        backend_config = getattr(self, "_backend_config", None)
        if backend_config is None or refresh:
            self._backend_config = self._hub_client.backends.get_backend_config(self.backend_info.id)
        return self._backend_config

    def _get_backend_state(self) -> BackendStateInfo:
        return self._hub_client.backends.get_backend_status(self.backend_info.id)
