import json
from abc import ABC
from typing import List, Dict, Type

from planqk.client.backend_dtos import BackendDto
from planqk.client.client import _PlanqkClient
from planqk.client.job_dtos import JobSummary
from planqk.client.model_enums import Provider
from planqk.exceptions import PlanqkClientError, BackendNotFoundError, PlanqkError
from planqk.qiskit.backend import PlanqkQiskitBackend
from planqk.qiskit.job_factory import PlanqkQiskitJobFactory


class _PlanqkProvider(ABC):
    def __init__(self, access_token: str = None, organization_id: str = None, _client=None):
        """Initialize the PLANQK provider.
              Args:
                    access_token (str): access token used for authentication with PLANQK.
                        If no token is provided, the token is retrieved from the environment variable PLANQK_ACCESS_TOKEN that can be either set
                        manually or by using the PLANQK CLI.
                        Defaults to None.

                    organization_id (str, optional): the ID of a PLANQK organization you are member of.
                        Provide this ID if you want to access quantum
                        backends with an organization account and its associated pricing plan.
                        All backend executions (jobs, tasks etc.) you create are visible to the members of the organization.
                        If the ID is omitted, all backend executions are performed under your personal account.
                        Defaults to None.

                    _client (_PlanqkClient, optional): Client instance used for making requests to the PLANQK API.
                        This parameter is mainly intended for testing purposes.
                        Defaults to None.
        """
        self._client = _client or _PlanqkClient(access_token=access_token, organization_id=organization_id)


class PlanqkQuantumProvider(_PlanqkProvider):
    _backend_mapping: Dict[str, Type[PlanqkQiskitBackend]] = {}

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

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

        return decorator

    def backends(self, provider: Provider = None) -> List[str]:
        """Return the list of backend IDs supported by PLANQK.

       Args:
           provider: returns only the IDs of the backend of the given provider, if specified. Defaults to None.

       Returns:
           List[str]: a list of backend ids that match the filtering criteria.
        """
        backend_dtos = self._client.get_backends()

        supported_backend_ids = [
            backend_info.id for backend_info in backend_dtos
            if (provider is None or backend_info.provider == provider) and backend_info.provider != Provider.DWAVE
        ]
        return supported_backend_ids

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

        Args:
            backend_id: name of the backend.
            sdk: the SDK to use. Note, that backends must be supported by the SDK.
            **kwargs: dict used for filtering.

        Returns:
            Backend: a backend matching the filtering criteria.

        Raises:
            BackendNotFoundError: if no backend could be found or more than one backend matches the filtering criteria.
        """
        try:
            backend_dto = self._client.get_backend(backend_id=backend_id)
        except PlanqkClientError as e:
            if e.response.status_code == 404:
                text = e.response.text
                if text:
                    error_detail = json.loads(e.response.text)
                    raise BackendNotFoundError("No backend matches the criteria. Reason: " + error_detail['error'])
                else:
                    raise BackendNotFoundError("No backend matches the criteria.")
            raise e

        backend_state_dto = self._client.get_backend_state(backend_id=backend_id)
        if backend_state_dto:
            backend_dto.status = backend_state_dto.status

        return self._get_backend_object(backend_dto)

    def _get_backend_object(self, backend_info: BackendDto) -> PlanqkQiskitBackend:
        backend_class = self._get_backend_class(backend_info.id)

        if backend_class:
            return backend_class(planqk_client=self._client, backend_info=backend_info)
        else:
            raise BackendNotFoundError(f"Qiskit backend '{backend_info.id}' is not supported.")

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

    def retrieve_job(self, job_id: str):
        """
        Retrieve a job from the backend.

        Args:
            backend (PlanqkQiskitBackend): the backend that run the job.
            job_id (str): the job id.

        Returns:
            Job: the job from the backend with the given id.
        """
        job_details = self._client.get_job(job_id=job_id)
        backend_id = job_details.backend_id
        backend_class = self._get_backend_class(backend_id)
        if backend_class is None:
            raise PlanqkError(f"Job '{job_id}' was created on backend '{backend_id}'. This backend is not supported by Qiskit.")

        return PlanqkQiskitJobFactory.create_job(backend=None, job_id=job_id, job_details=job_details, planqk_client=self._client)

    def jobs(self) -> List[JobSummary]:
        """
        Returns an overview on all jobs of the user or an organization.

        For each job the following attributes are returned:
            - job_id (str): The unique identifier of the job.
            - provider_name (Provider): The job provider.
            - backend_id (str): The identifier of the backend used.
            - created_at (datetime): The timestamp when the job was created.

        Returns:
            List[JobSummary]: A list of basic job information.
        """
        print("Getting your jobs from PLANQK, this may take a few seconds...")
        job_dtos = self._client.get_jobs()
        return [
            JobSummary(
                id=job_dto.id,
                provider=job_dto.provider,
                backend_id=job_dto.backend_id,
                created_at=job_dto.created_at
            )
            for job_dto in job_dtos
        ]
