import abc
from typing import Optional
from .apptypes import PackageAlias, PackageSpec, PackageDefinition, PackageReleaseId


class PackageImportLoopError(ValueError):
    """Indicates that a cycle existed in the package dependency graph. We don't want to allow that."""

    def __init__(self, release_id: PackageReleaseId, parent: Optional[PackageReleaseId]) -> None:
        super().__init__()
        self.release_id: PackageReleaseId = release_id
        self.parent_chain: list[PackageReleaseId] = []
        if parent:
            self.parent_chain.append(parent)

    def __str__(self) -> str:
        first_import = self.parent_chain[0] if self.parent_chain else None
        source_text = "While importing a package"
        if first_import:
            source_text = f"While importing package release {first_import}"
        middle = f"found package release {self.release_id} in its own dependency graph twice"
        details = " -> ".join(self.parent_chain)

        return f"{source_text}, {middle}. Import chain: {details}"


class BasePackageRegistry(abc.ABC):
    """The abstract base class of a package registry."""

    @abc.abstractmethod
    def add_package(self, spec: PackageSpec) -> PackageReleaseId:
        """Add the given package to the local dict registry, returning its release id"""
        raise NotImplementedError("not implemented")

    @abc.abstractmethod
    def get_package(self, release_id: PackageReleaseId) -> PackageSpec:
        """Return the package with the given release id, or throw a KeyError if it is not registered."""
        raise NotImplementedError("not implemented")

    @abc.abstractmethod
    def check_dependencies(self, release_id: PackageReleaseId) -> bool:
        """Return True if all dependencies (and recursively, their dependencies) are known and False if they are not.

        raise an exception if there a dependency cycle is encountered. This halts detection early if a dependency is
            unresolved and therefore is not a suitable cycle detector.
        """
        raise NotImplementedError("not implemented")

    def spec_from_definitions(self, pkgdef: PackageDefinition) -> PackageSpec:
        """Given a package definition, return a package spec where all dependencies have been recursively added to the
        registry.
        """
        dependencies: dict[PackageAlias, PackageReleaseId] = {}
        for dep_alias, dep in pkgdef.dependencies.items():
            if isinstance(dep, str):
                dependencies[dep_alias] = dep
            else:
                if dep.alias != dep_alias:
                    raise ValueError(f"Package alias mismatch: {dep.alias} != {dep_alias}")
                spec = self.spec_from_definitions(dep)
                dependencies[dep_alias] = self.add_package(spec)
        return PackageSpec(
            PackageReleaseId(pkgdef.release_id), PackageAlias(pkgdef.alias), pkgdef.version, pkgdef.macros, dependencies
        )

    def alias_ids_from_packages(
        self, packages: dict[PackageAlias, PackageDefinition | PackageReleaseId]
    ) -> dict[PackageAlias, PackageReleaseId]:
        """Get an alias: release_id map for the given packages dict, and update the registry accordingly."""
        result: dict[PackageAlias, PackageReleaseId] = {}
        for alias, dep in packages.items():
            if isinstance(dep, str):
                result[alias] = dep
            else:
                spec = self.spec_from_definitions(dep)
                result[alias] = self.add_package(spec)
        return result


class DictPackageRegistry(BasePackageRegistry):
    """A dictionary-based implementation of a package registry."""

    def __init__(self) -> None:
        self.registry: dict[PackageReleaseId, PackageSpec] = {}

    def add_package(self, spec: PackageSpec) -> PackageReleaseId:
        """Add the given package to the local dict registry, returning a package release id"""
        self.registry[spec.release_id] = spec
        return spec.release_id

    def get_package(self, release_id: PackageReleaseId) -> PackageSpec:
        """Return the package with the given release id, or throw a KeyError if it is not registered."""
        return self.registry[release_id]

    def _check_dependencies(
        self, release_id: PackageReleaseId, parent: Optional[PackageReleaseId], seen: set[PackageReleaseId]
    ) -> bool:
        """Return True if all dependencies (and recursively, their dependencies) are known and False if they are not.

        raise an exception if there a dependency cycle is encountered. This halts detection early if a dependency is
            unresolved and therefore is not a suitable cycle detector.
        """
        if release_id in seen:
            raise PackageImportLoopError(release_id, parent)

        try:
            package = self.get_package(release_id)
        except KeyError:
            return False

        for dep_release_id in package.dependencies.values():
            try:
                if not self._check_dependencies(dep_release_id, release_id, seen | {release_id}):
                    return False
            except PackageImportLoopError as exc:
                if parent:
                    exc.parent_chain.append(parent)
                raise exc
        return True

    def check_dependencies(self, release_id: PackageReleaseId) -> bool:
        return self._check_dependencies(release_id, None, set())
