"""
Kipu Quantum Hub Qiskit Runtime Service V2 Implementation

This class is adapted from the IBM Qiskit Runtime's QiskitRuntimeService class
(qiskit_ibm_runtime.qiskit_runtime_service.QiskitRuntimeService) to provide compatibility
with IBM's runtime service patterns while integrating with the Kipu Quantum Hub SDK.

0Original IBM implementation:
https://github.com/Qiskit/qiskit-ibm-runtime/blob/main/qiskit_ibm_runtime/qiskit_runtime_service.py
"""
import json
import warnings
from datetime import datetime
from typing import Dict, Callable, Optional, Union, List, Any, Type, Sequence

from qiskit.providers import QiskitBackendNotFoundError
from qiskit.providers.backend import BackendV2 as Backend
from qiskit_ibm_runtime import ibm_backend, RuntimeOptions, QiskitRuntimeService, RuntimeEncoder
from qiskit_ibm_runtime.accounts import ChannelType
from qiskit_ibm_runtime.runtime_job_v2 import RuntimeJobV2
from qiskit_ibm_runtime.utils.result_decoder import ResultDecoder

from qhub.quantum.sdk.job import JobRequest
from qhub.quantum.sdk.qiskit.hub_qiskit_runtime_job import HubRuntimeJobV2
from qhub.quantum.sdk.qiskit.provider import HubQiskitProvider
from qhub.quantum.sdk.qiskit.providers.ibm.runtime_session_client_adapter import RuntimeSessionClientAdapter


class HubQiskitRuntimeService(HubQiskitProvider, QiskitRuntimeService):
    """Kipu Quantum Hub Runtime Service V2 with full IBM QiskitRuntimeService API compatibility."""

    # Channel constant for Kipu Quantum Hub
    _CHANNEL = "Kipu Quantum Hub"

    # Backend mapping for registered IBM backends
    _backend_mapping: Dict[str, Type["HubIbmQiskitBackend"]] = {}

    @classmethod
    def register_backend(cls, backend_id: str):
        """For internal use only. Binds a backend class to a Kipu Quantum Hub IBM backend id."""

        def decorator(backend_cls: Type["HubIbmQiskitBackend"]):
            cls._backend_mapping[backend_id] = backend_cls
            return backend_cls

        return decorator

    def _get_backend_class(self, backend_id: str) -> Type["HubIbmQiskitBackend"]:
        return self._backend_mapping.get(backend_id)

    def __init__(
            self, access_token: Optional[str] = None, organization_id: Optional[str] = None, _client=None
    ) -> None:
        """HubQiskitRuntimeServiceV2 constructor."""
        super().__init__(access_token=access_token, organization_id=organization_id, _client=_client)

    def _api_client(self) -> RuntimeSessionClientAdapter:
        """API client for IBM Session compatibility."""
        return self._get_api_client()

    def _get_api_client(self, instance: Optional[str] = None) -> RuntimeSessionClientAdapter:
        """Return the API client for session operations."""
        return RuntimeSessionClientAdapter(self._client)

    def active_account(self) -> Optional[Dict[str, str]]:
        """Return the account currently in use for the session."""
        if not self._client:
            return None

        account_info = {
            "channel": self._CHANNEL,
        }

        return account_info

    def backend(
        self,
        name: str,
        instance: Optional[str] = None,
        use_fractional_gates: Optional[bool] = False,
    ) -> "HubIbmQiskitBackend":
        """Return a single backend matching the Kipu Quantum Hub backend id."""
        backend_model = self._client.backends.get_backend(name)

        if backend_model.provider != "IBM":
            raise QiskitBackendNotFoundError(
                f"Backend '{name}' is not from IBM. Qiskit Runtime only supports IBM backends.")

        backend_class = self._get_backend_class(name)
        if backend_class:
            return backend_class(
                service=self,
                hub_client=self._client,
                backend_info=backend_model,
                use_fractional_gates=use_fractional_gates
            )
        else:
            raise QiskitBackendNotFoundError(f"IBM backend '{name}' is not registered with HubQiskitRuntimeService.")

    def backends(self, provider: str = None) -> List[str]:
        """Return all IBM backends accessible via this service that are registered."""
        all_ibm_backend_ids = super().backends(provider="IBM")

        registered_backend_ids = [
            backend_id for backend_id in all_ibm_backend_ids
            if backend_id in self._backend_mapping
        ]

        return registered_backend_ids

    def check_pending_jobs(self) -> None:
        raise NotImplementedError("check_pending_jobs method not yet implemented")

    @staticmethod
    def delete_account(
            filename: Optional[str] = None,
            name: Optional[str] = None,
            channel: Optional[ChannelType] = None,
    ) -> bool:
        raise NotImplementedError("delete_account method not supported. Use Kipu Quantum Hub CLI: qhub logout")

    def delete_job(self, job_id: str) -> None:
        raise NotImplementedError("delete_job method not yet implemented")

    def instances(self) -> List[str]:
        raise NotImplementedError("instances method not implemented. Instance management is handled by Kipu Quantum Hub")

    @classmethod
    def _get_shots(cls, inputs: Dict, backend) -> int:
        """Retrieve shots from inputs with fallback logic."""
        default_shots = backend.options.get('shots', backend.min_shots) if hasattr(backend, 'options') else backend.min_shots
        options = inputs.get('options', {})
        if 'default_shots' in options:
            default_shots = options['default_shots']

        shots = default_shots

        pubs = inputs.get('pubs', [])
        if pubs and len(pubs) > 0:
            first_pub = pubs[0]
            if hasattr(first_pub, 'shots') and first_pub.shots is not None:
                shots = first_pub.shots

        if shots == default_shots:
            run_options = inputs.get('run_options')
            if run_options is not None:
                shots = run_options.get('shots', default_shots)

        return shots

    def _run(
            self,
            program_id: str,
            inputs: Dict,
            options: Optional[Union[RuntimeOptions, Dict]] = None,
            callback: Optional[Callable] = None,
            result_decoder: Optional[Union[Type[ResultDecoder], Sequence[Type[ResultDecoder]]]] = None,
            session_id: Optional[str] = None,
            start_session: Optional[bool] = False,
            calibration_id: Optional[str] = None,
    ) -> HubRuntimeJobV2:
        """Execute the runtime program."""

        if calibration_id is not None:
            warnings.warn(
                "The 'calibration_id' parameter is currently not supported by Kipu Quantum Hub and will be ignored.",
                UserWarning
            )

        qrt_options: RuntimeOptions = options
        if options is None:
            qrt_options = RuntimeOptions()
        elif isinstance(options, Dict):
            qrt_options = RuntimeOptions(**options)

        qrt_options.validate(channel=self.channel)

        backend = qrt_options.backend
        if isinstance(backend, str):
            backend = self.backend(name=qrt_options.get_backend_name())

        status = backend.status()
        if status.operational is True and status.status_msg != "active":
            warnings.warn(
                f"The backend {backend.name} currently has a status of {status.status_msg}."
            )

        version = inputs.get("version", 1) if inputs else 1

        input_params = {
            "program_id": program_id,
            "backend_name": qrt_options.get_backend_name(),
            "image": qrt_options.image,
            "log_level": qrt_options.log_level,
            "job_tags": qrt_options.job_tags,
            "max_execution_time": qrt_options.max_execution_time,
            "start_session": start_session,
            "session_time": qrt_options.session_time,
            "version": version,
            "private": qrt_options.private,
        }

        backend_id = backend.name
        shots = self._get_shots(inputs, backend)

        # Encode inputs with RuntimeEncoder for Qiskit serialization, then parse back to dict
        encoded_input_json = json.dumps(inputs, cls=RuntimeEncoder)
        parsed_inputs = json.loads(encoded_input_json)

        job_request = JobRequest(
            backend_id=backend_id,
            shots=shots,
            input=parsed_inputs,
            input_format="QISKIT_QPY",
            input_params=input_params,
            sdk_provider="QISKIT",
            session_id=session_id,
        )

        return HubRuntimeJobV2(backend=backend, job_details=job_request, result_decoder=result_decoder, hub_client=self._client, provider=self)

    def job(self, job_id: str) -> RuntimeJobV2:
        """Retrieve a runtime job."""
        return self.retrieve_job(job_id)

    def jobs(
            self,
            limit: Optional[int] = 10,
            skip: int = 0,
            backend_name: Optional[str] = None,
            pending: bool = None,
            program_id: str = None,
            instance: Optional[str] = None,
            job_tags: Optional[List[str]] = None,
            session_id: Optional[str] = None,
            created_after: Optional[datetime] = None,
            created_before: Optional[datetime] = None,
            descending: bool = True,
    ) -> List[RuntimeJobV2]:
        raise NotImplementedError("jobs method not yet implemented")

    def least_busy(
            self,
            min_num_qubits: Optional[int] = None,
            instance: Optional[str] = None,
            filters: Optional[Callable[["ibm_backend.IBMBackend"], bool]] = None,
            **kwargs: Any,
    ) -> "ibm_backend.IBMBackend":
        raise NotImplementedError("least_busy method not yet implemented")

    @staticmethod
    def save_account(
            token: Optional[str] = None,
            url: Optional[str] = None,
            instance: Optional[str] = None,
            channel: Optional[ChannelType] = None,
            filename: Optional[str] = None,
            name: Optional[str] = None,
            proxies: Optional[dict] = None,
            verify: Optional[bool] = None,
            overwrite: Optional[bool] = False,
            set_as_default: Optional[bool] = None,
            private_endpoint: Optional[bool] = False,
            region: Optional[str] = None,
            plans_preference: Optional[str] = None,
            tags: Optional[List[str]] = None,
    ) -> None:
        raise NotImplementedError("save_account method not supported. Use Kipu Quantum Hub CLI: qhub login")

    @staticmethod
    def saved_accounts(
            default: Optional[bool] = None,
            channel: Optional[ChannelType] = None,
            filename: Optional[str] = None,
            name: Optional[str] = None,
    ) -> dict:
        raise NotImplementedError("saved_accounts method not supported. Use Kipu Quantum Hub CLI: qhub get-context")

    def usage(self) -> Dict[str, Any]:
        raise NotImplementedError("usage method not yet implemented")

    @property
    def channel(self) -> str:
        return self._CHANNEL

    def active_instance(self) -> str:
        raise NotImplementedError("active_instance method not supported. Instance management is handled by Kipu Quantum Hub.")
