from __future__ import annotations

import logging

from typing import TYPE_CHECKING

from poetry.core.semver.helpers import parse_constraint
from poetry.core.semver.version import Version
from poetry.core.semver.version_constraint import VersionConstraint
from poetry.core.semver.version_range import VersionRange

from poetry.repositories.exceptions import PackageNotFound


if TYPE_CHECKING:
    from packaging.utils import NormalizedName
    from poetry.core.packages.dependency import Dependency
    from poetry.core.packages.package import Package
    from poetry.core.packages.utils.link import Link


class Repository:
    def __init__(self, name: str, packages: list[Package] | None = None) -> None:
        self._name = name
        self._packages: list[Package] = []

        for package in packages or []:
            self.add_package(package)

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

    @property
    def packages(self) -> list[Package]:
        return self._packages

    def find_packages(self, dependency: Dependency) -> list[Package]:
        packages = []
        constraint, allow_prereleases = self._get_constraints_from_dependency(
            dependency
        )
        ignored_pre_release_packages = []

        for package in self._find_packages(dependency.name, constraint):
            if package.yanked and not isinstance(constraint, Version):
                # PEP 592: yanked files are always ignored, unless they are the only
                # file that matches a version specifier that "pins" to an exact
                # version
                continue
            if (
                package.is_prerelease()
                and not allow_prereleases
                and not package.is_direct_origin()
            ):
                if constraint.is_any():
                    # we need this when all versions of the package are pre-releases
                    ignored_pre_release_packages.append(package)
                continue

            packages.append(package)

        self._log(
            f"{len(packages)} packages found for {dependency.name} {constraint!s}",
            level="debug",
        )

        return packages or ignored_pre_release_packages

    def has_package(self, package: Package) -> bool:
        package_id = package.unique_name
        return any(
            package_id == repo_package.unique_name for repo_package in self.packages
        )

    def add_package(self, package: Package) -> None:
        self._packages.append(package)

    def remove_package(self, package: Package) -> None:
        package_id = package.unique_name

        index = None
        for i, repo_package in enumerate(self.packages):
            if package_id == repo_package.unique_name:
                index = i
                break

        if index is not None:
            del self._packages[index]

    def search(self, query: str) -> list[Package]:
        results: list[Package] = []

        for package in self.packages:
            if query in package.name:
                results.append(package)

        return results

    @staticmethod
    def _get_constraints_from_dependency(
        dependency: Dependency,
    ) -> tuple[VersionConstraint, bool]:
        constraint = dependency.constraint
        if constraint is None:
            constraint = "*"

        if not isinstance(constraint, VersionConstraint):
            constraint = parse_constraint(constraint)

        allow_prereleases = dependency.allows_prereleases()
        if isinstance(constraint, VersionRange) and (
            constraint.max is not None
            and constraint.max.is_unstable()
            or constraint.min is not None
            and constraint.min.is_unstable()
        ):
            allow_prereleases = True

        return constraint, allow_prereleases

    def _find_packages(
        self, name: NormalizedName, constraint: VersionConstraint
    ) -> list[Package]:
        return [
            package
            for package in self._packages
            if package.name == name and constraint.allows(package.version)
        ]

    def _log(self, msg: str, level: str = "info") -> None:
        logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
        getattr(logger, level)(f"<c1>Source ({self.name}):</c1> {msg}")

    def __len__(self) -> int:
        return len(self._packages)

    def find_links_for_package(self, package: Package) -> list[Link]:
        return []

    def package(
        self, name: NormalizedName, version: Version, extras: list[str] | None = None
    ) -> Package:
        for package in self.packages:
            if name == package.name and package.version == version:
                return package.clone()

        raise PackageNotFound(f"Package {name} ({version}) not found.")
