from abc import abstractmethod, ABC
from base64 import b64encode
from dataclasses import dataclass
from pathlib import Path
from typing import List, Union, Optional, Tuple, Any, Protocol, Iterable, TYPE_CHECKING

from pkm.api.dependencies.dependency import Dependency
from pkm.api.packages.package import Package
from pkm.api.packages.package_metadata import PackageMetadata
from pkm.api.versions.version_specifiers import AllowAllVersions
from pkm.utils.iterators import partition

if TYPE_CHECKING:
    from pkm.api.environments.environment import Environment
    from pkm.api.repositories.repository_management import RepositoryManagement


class Repository(Protocol):

    @property
    @abstractmethod
    def name(self) -> str:
        ...

    @abstractmethod
    def match(self, dependency: Union[Dependency, str], env: "Environment") -> List[Package]:
        """
        :param dependency: the dependency to match (or a pep508 string representing it)
        :param env: the environment that the returned packages should be compatible with
        :return: list of all the packages in this repository that match the given `dependency`
        """

    def list(self, package_name: str, env: "Environment") -> List[Package]:
        """
        :param package_name: the package to match
        :param env: the environment that the returned packages should be compatible with
        :return: list of all the packages that match the given `package_name`
        """
        return self.match(Dependency(package_name, AllowAllVersions), env)

    @property
    @abstractmethod
    def publisher(self) -> Optional["RepositoryPublisher"]:
        """
        :return: if this repository is 'publishable' returns its publisher
        """

    # noinspection PyMethodMayBeStatic
    def accepted_url_protocols(self) -> Iterable[str]:
        """
        :return: sequence of url-dependency protocols that this repository can handle
        """
        return ()

    # noinspection PyMethodMayBeStatic
    def accept_non_url_packages(self) -> bool:
        """
        :return: True if this repository should be used for non url packages
        """
        return True


# noinspection PyMethodMayBeStatic
class AbstractRepository(Repository, ABC):

    def __init__(self, name: str):
        self._name = name

    @property
    def name(self) -> str:
        return self._name

    def match(self, dependency: Union[Dependency, str], env: "Environment") -> List[Package]:
        if isinstance(dependency, str):
            dependency = Dependency.parse(dependency)

        matched = [d for d in self._do_match(dependency, env) if d.is_compatible_with(env)]
        filtered = self._filter_prereleases(matched, dependency)
        return self._sort_by_priority(dependency, filtered)

    @property
    def publisher(self) -> Optional["RepositoryPublisher"]:
        return None

    def _filter_prereleases(self, packages: List[Package], dependency: Dependency) -> List[Package]:
        if dependency.version_spec.allows_pre_or_dev_releases():
            return packages
        pre_release, rest = partition(packages, lambda it: it.version.is_pre_or_dev_release())
        return rest or packages

    def _sort_by_priority(self, dependency: Dependency, packages: List[Package]) -> List[Package]:
        """
        sorts `matches` by the required priority
        :param dependency: the dependency that resulted in the given `packages`
        :param packages: the packages that were the result `_do_match(dependency)`
        :return: sorted packages by priority (first is more important than last)
        """
        packages.sort(key=lambda it: it.version, reverse=True)
        return packages

    @abstractmethod
    def _do_match(self, dependency: Dependency, env: "Environment") -> List[Package]:
        """
        IMPLEMENTATION NOTICE:
            you don't have to filter pre-releases or packages based on the given environment
            it is handled for you in the `match` method that call this one.

        :param dependency: the dependency to match
        :param env: the environment that the returned packages should be applicable with
        :return: list of all the packages in this repository that match the given `dependency` version spec
        """


class RepositoryPublisher:
    def __init__(self, repository_name: str):
        self.repository_name = repository_name

    # noinspection PyMethodMayBeStatic
    def required_authentication_fields(self) -> List[str]:
        return ['username', 'password']

    @abstractmethod
    def publish(self, auth: "Authentication", package_meta: PackageMetadata, distribution: Path):
        """
        publish a `distribution` belonging to the given `package_meta` into the repository (registering it if needed)
        :param auth: authentication object filled with the fields that were
                     returned by the method `required_authentication_fields`
        :param package_meta: metadata for the package that this distribution belongs to
        :param distribution: the distribution archive (e.g., wheel, sdist)
        """


@dataclass(frozen=True, eq=True)
class Authentication:
    username: str
    password: str

    def as_basic_auth_header(self) -> Tuple[str, str]:
        return 'Authorization', f'Basic {b64encode(f"{self.username}:{self.password}".encode()).decode("ascii")}'


class RepositoryBuilder(ABC):

    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def build(self, name: str, packages_limit: Optional[List[str]],
              **kwargs: Any) -> Repository:
        """
        build a new repository instance using the given `kwargs`
        :param name: name for the created repository
        :param packages_limit: list of packages the user ask the repository to be limited to
                (or None if no such request was made)
        :param kwargs: arguments for the instance creation, may be defined by derived classes
        :return: the created instance
        """


class HasAttachedRepository(ABC):

    @property
    @abstractmethod
    def repository_management(self) -> "RepositoryManagement":
        ...

    @property
    def attached_repository(self) -> Repository:
        """
        :return: the repository that is attached to this artifact
        """
        return self.repository_management.attached_repo
