from abc import ABC
from typing import List, Dict, Type, Optional, Union

from qhub.api.quantum import HubQuantumClient, Backend as BackendModel
from qhub.api.quantum.errors import NotFoundError
from qhub.quantum.sdk.client_factory import create_hub_client
from qhub.quantum.sdk.exceptions import BackendNotFoundError, HubError
from qhub.quantum.sdk.qiskit.backend import HubQiskitBackend
from qhub.quantum.sdk.qiskit.job_factory import HubQiskitJobFactory


class _HubProvider(ABC):
    def __init__(self, access_token: str = None, organization_id: str = None, _client=None):
        """Initialize the Kipu Quantum Hub provider.
              Args:
                    access_token (str): access token used for authentication with Kipu Quantum Hub.
                        If no token is provided, the token is retrieved from the environment variable
                        or config file via DefaultCredentialsProvider.
                        Defaults to None.

                    organization_id (str, optional): the ID of a Kipu Quantum Hub organization you are member of.
                        Defaults to None.

                    _client (HubQuantumClient, optional): Client instance used for making requests to the Kipu Quantum Hub API.
                        This parameter is mainly intended for testing purposes.
                        Defaults to None.
        """
        if _client is not None:
            self._client = _client
        else:
            self._client = create_hub_client(access_token, organization_id)


class HubQiskitProvider(_HubProvider):
    _backend_mapping: Dict[str, Type[HubQiskitBackend]] = {}

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

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

        return decorator

    def backends(self, provider: str = None, detailed: bool = False) -> Union[List[str], List[BackendModel]]:
        """Return the list of backends supported by Kipu Quantum Hub.

        Args:
            provider: Filter by cloud provider string (e.g., "AWS"). Defaults to None (all providers).
            detailed: If True, returns Backend model objects.
                     If False (default), returns only backend IDs as strings.

        Returns:
            List[str]: Backend IDs when detailed=False.
            List[BackendModel]: Backend model objects when detailed=True
        """
        backend_models = self._client.backends.get_backends()

        # Filter backends (exclude DWAVE, apply provider filter)
        filtered_backends = [
            backend_info for backend_info in backend_models
            if (provider is None or backend_info.provider == provider) and backend_info.provider != "DWAVE"
        ]

        if not detailed:
            return [backend_info.id for backend_info in filtered_backends]

        return filtered_backends

    def get_backend(self, backend_id: str) -> HubQiskitBackend:
        """Return a single backend matching the specified filtering.

        Args:
            backend_id: name of the backend.

        Returns:
            Backend: a backend matching the filtering criteria.

        Raises:
            BackendNotFoundError: if no backend could be found.
        """
        try:
            backend_model = self._client.backends.get_backend(backend_id)
        except NotFoundError as e:
            raise BackendNotFoundError(f"No backend matches the criteria. Backend '{backend_id}' not found.") from e

        backend_state = self._client.backends.get_backend_status(backend_id)
        if backend_state:
            # Store status on the model for downstream use
            backend_model = _with_status(backend_model, backend_state.status)

        return self._get_backend_object(backend_model)

    def _get_backend_object(self, backend_info: BackendModel) -> HubQiskitBackend:
        backend_class = self._get_backend_class(backend_info.id)

        if backend_class:
            return backend_class(hub_client=self._client, backend_info=backend_info)
        else:
            # Provide helpful error message for IBM backends
            if backend_info.provider == "IBM":
                raise BackendNotFoundError(
                    f"IBM backend '{backend_info.id}' cannot be accessed through HubQiskitProvider. "
                    f"IBM Quantum backends require using HubQiskitRuntimeService instead.\n\n"
                    f"Migration guide:\n"
                    f"  Replace: provider = HubQiskitProvider()\n"
                    f"           backend = provider.get_backend('{backend_info.id}')\n\n"
                    f"  With:    service = HubQiskitRuntimeService()\n"
                    f"           backend = service.backend('{backend_info.id}')\n\n"
                    f"Note: HubQiskitRuntimeService provides full compatibility with IBM's Qiskit Runtime API, "
                    f"including Session and Batch execution modes."
                )
            else:
                raise BackendNotFoundError(f"Qiskit backend '{backend_info.id}' is not supported.")

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

    @classmethod
    def create_registered_backend(cls, backend_id: str, hub_client: HubQuantumClient) -> Optional[HubQiskitBackend]:
        """Create a backend instance from registered backends."""
        backend_class = cls._backend_mapping.get(backend_id)
        if backend_class is None:
            return None

        try:
            backend_model = hub_client.backends.get_backend(backend_id)
            return backend_class(hub_client=hub_client, backend_info=backend_model)
        except Exception:
            return None

    def retrieve_job(self, job_id: str) -> 'HubQiskitJob':
        """Retrieve a job."""
        job_details = self._client.jobs.get_job(job_id)

        backend_id = job_details.backend_id
        backend_class = self._get_backend_class(backend_id)
        if backend_class is None:
            raise HubError(f"Job '{job_id}' was created on backend '{backend_id}'. This backend is not supported by Qiskit.")

        return HubQiskitJobFactory.create_job(backend=None, job_id=job_id, job_details=job_details, hub_client=self._client, provider=self)

    def jobs(self) -> list:
        """Returns an overview on all jobs of the user or an organization."""
        print("Getting your jobs from Kipu Quantum Hub, this may take a few seconds...")
        page_response = self._client.jobs.search_jobs(page=0, size=50)
        return page_response.content or []


def _with_status(backend: BackendModel, status: str) -> BackendModel:
    """Create a copy of Backend with an updated status field.

    qhub-api models are frozen, so we rebuild with the new status.
    """
    data = backend.dict()
    data['status'] = status
    return BackendModel(**data)
