import inspect
from abc import abstractmethod, ABC
from typing import Optional, Type, Union

from qiskit.circuit import Instruction as QiskitInstruction, Delay, Parameter, Reset
from qiskit.circuit import Measure
from qiskit.providers import BackendV2
from qiskit.transpiler import Target

from qhub.api.quantum import HubQuantumClient, Backend as BackendModel, Configuration, Connectivity
from qhub.quantum.sdk.backend import HubBackend
from qhub.quantum.sdk.job import JobRequest
from .job import HubQiskitJob
from .options import OptionsV2


class HubQiskitBackend(HubBackend, BackendV2, ABC):

    def __init__(  # pylint: disable=too-many-arguments
        self,
        hub_client: HubQuantumClient,
        backend_info: BackendModel,
        backend_version: Optional[str] = None,
        **fields,
    ):
        """HubBackend for executing Qiskit circuits against Kipu Quantum Hub devices.

        Args:
            hub_client: Kipu Quantum Hub client for API communication
            backend_info: Kipu Quantum Hub backend infos
            backend_version: Backend version string. Defaults to "2" if not provided.
            **fields: other arguments
        """

        HubBackend.__init__(self, hub_client=hub_client, backend_info=backend_info)
        BackendV2.__init__(self,
                           provider=backend_info.provider,
                           name=backend_info.id,
                           description=f"Kipu Quantum Hub Backend: {backend_info.hardware_provider} {backend_info.id}.",
                           online_date=backend_info.updated_at,
                           backend_version=backend_version or "2",
                           **fields)

        self._normalize_qubit_indices()
        self._initialize_backend_components()
        self._instance = None

    def _initialize_backend_components(self):
        """Template method for initializing target and configuration.

        Subclasses can override this method to customize the initialization order.
        Default implementation follows target-first approach for instruction building.
        """
        self._target = self._hub_backend_to_target()

    @abstractmethod
    def _to_gate(self, name: str) -> Optional[Union[QiskitInstruction, Type[QiskitInstruction]]]:
        """Convert a gate name to a Qiskit gate instance or class."""
        pass

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

    @abstractmethod
    def _get_multi_qubit_gate_properties(self):
        pass

    non_gate_instr_mapping = {
        "delay": Delay(Parameter("t")),
        "measure": Measure(),
        "reset": Reset(),
    }

    def _normalize_qubit_indices(self):
        # Override to handle non-contiguous zero starting qubit indices
        pass

    def _get_gate_names_for_target(self, configuration: Configuration) -> list[str]:
        """Get gate names to include in target construction."""
        return sorted(gate.name.lower() for gate in configuration.gates)

    @property
    def is_simulator(self):
        return self.backend_info.type == "SIMULATOR"

    def _to_non_gate_instruction(self, name: str) -> Optional[QiskitInstruction]:
        instr = self.non_gate_instr_mapping.get(name, None)
        if instr is not None:
            return instr
        return None

    def _hub_backend_to_target(self) -> Target:
        """Converts properties of a Kipu Quantum Hub actual into Qiskit Target object."""

        configuration: Configuration = self.backend_info.configuration

        # Store remapped connectivity for use in target construction (models are frozen)
        self._contiguous_connectivity = self._make_contiguous_connectivity(configuration.connectivity)

        qubit_count: int = self._num_qubits()
        target = Target(description=f"Target for Kipu Quantum Hub backend {self.name}", num_qubits=qubit_count)

        single_qubit_props = self._get_single_qubit_gate_properties()
        multi_qubit_props = self._get_multi_qubit_gate_properties()
        gates_names = self._get_gate_names_for_target(configuration)

        for gate_name in gates_names:
            gate = self._to_gate(gate_name)

            if gate is None:
                continue

            if inspect.isclass(gate):
                target.add_instruction(instruction=gate, properties=None, name=gate_name)
            elif gate.num_qubits == 1:
                target.add_instruction(instruction=gate, properties=single_qubit_props)
            elif gate.num_qubits > 1:
                target.add_instruction(instruction=gate, properties=multi_qubit_props)
            elif gate.num_qubits == 0 and single_qubit_props == {None: None}:
                target.add_instruction(instruction=gate, properties={None: None})

        measure_props = self._get_single_qubit_gate_properties("measure")
        target.add_instruction(Measure(), measure_props)

        non_gate_instructions = set(configuration.instructions).difference(gates_names).difference({'measure'})
        for non_gate_instruction_name in non_gate_instructions:
            instruction = self._to_non_gate_instruction(non_gate_instruction_name)
            if instruction is not None:
                instr_props = self._get_single_qubit_gate_properties(instruction.name)
                target.add_instruction(instruction, instr_props)

        return target

    @staticmethod
    def _make_contiguous_connectivity(connectivity: Connectivity) -> Optional[dict]:
        """Remap noncontiguous qubit indices to contiguous ones for Qiskit transpiler.

        Returns the remapped graph dict, or None if fully connected.
        Does not mutate the frozen connectivity model.
        """
        if connectivity is None or connectivity.fully_connected:
            return None

        connectivity_graph = connectivity.graph
        if not connectivity_graph:
            return None

        indices = sorted(
            int(i)
            for i in set.union(*[{k} | set(v) for k, v in connectivity_graph.items()])
        )
        map_list = list(range(len(indices)))
        mapper = dict(zip(indices, map_list))
        return {
            mapper[int(k)]: [mapper[int(v)] for v in val]
            for k, val in connectivity_graph.items()
        }

    def _num_qubits(self):
        return self.backend_info.configuration.qubit_count

    def _get_qiskit_gate_name(self, provider_gate_name: str) -> Optional[str]:
        """Convert provider gate name to Qiskit gate name."""
        return provider_gate_name

    def __deepcopy__(self, memo):
        # Backends hold non-serializable HTTP resources (httpx.Client connection pools with
        # threading locks) - this causes pennylane pickle errors.
        # Return self — backends are stateful connection holders and
        # deep-copying them makes no semantic sense.
        return self

    @property
    def target(self):
        return self._target

    @property
    def max_circuits(self):
        return None

    @classmethod
    def _default_options(cls):
        return OptionsV2()

    def _run_job(self, job_request: JobRequest) -> Union[HubQiskitJob]:
        from qhub.quantum.sdk.qiskit.job_factory import HubQiskitJobFactory

        job_request.sdk_provider = "QISKIT"
        return HubQiskitJobFactory.create_job(backend=self, job_details=job_request, hub_client=self._hub_client)

    def retrieve_job(self, job_id: str) -> HubQiskitJob:
        """Return a single job."""
        from qhub.quantum.sdk.qiskit.job_factory import HubQiskitJobFactory
        return HubQiskitJobFactory.create_job(backend=self, job_id=job_id, hub_client=self._hub_client)
