import typing
from abc import ABC, abstractmethod
from typing import Optional, Generic
from typing import TypeVar

from .types import CriteriaTree, Metadata, TaskItemValueMap, StreamMatrixResponse
from .errors import CriteriaSetIdNotFoundError, CriteriaSetVersionNotFoundError

ParametersType = TypeVar("ParametersType", bound=dict)


# A class representing a specific version of a specific criteria set
class ICriteriaSetService(ABC, Generic[ParametersType]):
    # ID of the criteria set
    id: str

    # Specific version of the criteria set this class represents, SemVer-formatted
    version: str

    # Validate the given parameters
    @abstractmethod
    def validate_parameters(self, parameters: ParametersType):
        pass

    # Get the metadata for this criteria set version
    @abstractmethod
    def get_metadata(self) -> Metadata:
        pass

    # Get the metadata for this criteria set version
    @abstractmethod
    def get_data(
            self,
            parameters: Optional[ParametersType] = None,
            values: Optional[TaskItemValueMap] = None,
            locale: Optional[str] = None
    ) -> dict[str, dict[str, typing.Any]]:
        pass

    # Get the criteria tree for the given parameters
    @abstractmethod
    def get_criteria_tree(
            self,
            parameters: Optional[ParametersType] = None,
            values: Optional[TaskItemValueMap] = None,
            locale: Optional[str] = None
    ) -> CriteriaTree:
        pass

    # Stream the matrix for the given parameters
    @abstractmethod
    def stream_matrix(
            self,
            parameters: Optional[ParametersType] = None,
            values: Optional[TaskItemValueMap] = None,
            locale: Optional[str] = None
    ) -> StreamMatrixResponse:
        pass


class ManagerService:
    criteria_set_services: dict[str, list[ICriteriaSetService]]

    def __init__(self, criteria_set_services: Optional[dict[str, list[ICriteriaSetService]]] = None):
        self.criteria_set_services = criteria_set_services if criteria_set_services is not None else dict()

    # Get the criteria sets and versions available to this service
    def get_criteria_sets_and_versions(self) -> dict[str, list[Metadata]]:
        criteria_sets_and_versions = dict()
        for criteria_set_id, services in self.criteria_set_services.items():
            criteria_sets_and_versions[criteria_set_id] = [service.get_metadata() for service in services]
        return criteria_sets_and_versions

    # Get the criteria set with the given ID. If no version is requested, return the latest version
    def get_service_for_criteria_set(self, criteria_set_id: str, version: Optional[str] = None) -> ICriteriaSetService:
        if criteria_set_id not in self.criteria_set_services:
            raise CriteriaSetIdNotFoundError()

        services_for_criteria_set_id = self.criteria_set_services[criteria_set_id]
        if version is None:
            if len(services_for_criteria_set_id):
                return services_for_criteria_set_id[-1]
            raise CriteriaSetIdNotFoundError()

        for service in services_for_criteria_set_id:
            if service.version == version:
                return service
        raise CriteriaSetVersionNotFoundError()
