from __future__ import annotations
from dataclasses import dataclass
from distutils.version import Version
from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional

from semver import VersionInfo
from loguru import logger


@dataclass
class Package:
    name: str
    version: str
    dependencies: Dict[str, DepConstraints]
    dev_dependencies: Dict[str, DepConstraints]

    # below are properties that can be from the package file, but may be modified heavily
    # so that you should never use them to override previous package definition
    type: PackageType
    location: Path
    # list of glob patterns to be included in the final package
    include: List[str]
    # a list of glob patterns to be excluded in the final package
    exclude: List[str]


class PackageType(str, Enum):
    Poetry = "poetry"


@dataclass
class DepConstraint:
    """Constraint of a dependency.

    The two important fields are rule (for comparing between versions) and constraint
    (for distinguish between different platforms/os).

    To reconstruct/save the constraint,
    back to the original package specification, update the `origin_spec` with the a new key
    stored in `version_field` and value from `version`. The reason for this behaviour is to
    support cases such as where the dependency is from git (`version_field` = 'git').
    """

    # rule for matching version of dependency, e.g. "^1.0.0" or ">= 1.0.0", the rule sometimes depends on what package's type
    version_spec: str
    # an identifier for the condition that this version is applicable to.
    # none mean there is no other constraint.
    constraint: Optional[str] = None
    # name of the rule field in origin specification
    # none if the spec is just a string
    version_spec_field: Optional[str] = None
    # the original specification without the version
    # none if the spec is just a string
    origin_spec: Optional[dict] = None


# see: https://python-poetry.org/docs/dependency-specification/
# the constraints always sorted by constraint
DepConstraints = List[DepConstraint]


@dataclass
class VersionSpec:
    lowerbound: Optional[VersionInfo]
    upperbound: Optional[VersionInfo]
    is_lowerbound_inclusive: bool
    is_upperbound_inclusive: bool

    def is_version_compatible(self, version: VersionInfo) -> bool:
        """Check if the given version is compatible with the given rule

        Args:
            version: the version to check
        """
        incompatible = (
            self.lowerbound is not None
            and (
                (self.is_lowerbound_inclusive and self.lowerbound > version)
                or (not self.is_lowerbound_inclusive and self.lowerbound >= version)
            )
        ) or (
            self.upperbound is not None
            and (
                (self.is_upperbound_inclusive and self.upperbound < version)
                or (not self.is_upperbound_inclusive and self.upperbound <= version)
            )
        )
        return not incompatible

    def intersect(self, version_spec: VersionSpec) -> VersionSpec:
        """Intersect two version specs. Result in a stricter version spec.

        Raise exception if the intersection is empty.

        Examples:
            - "^1.0.0" and "^2.0.0" -> Exception
            - "^1.1.0" and "^1.2.0" -> "^1.2.0"
        """
        lb = self.lowerbound
        is_lb_inclusive = self.is_lowerbound_inclusive

        if version_spec.lowerbound is not None:
            if lb is None:
                lb = version_spec.lowerbound
                is_lb_inclusive = version_spec.is_lowerbound_inclusive
            elif version_spec.lowerbound > lb:
                lb = version_spec.lowerbound
                is_lb_inclusive = version_spec.is_lowerbound_inclusive
            elif version_spec.lowerbound == lb:
                is_lb_inclusive = (
                    is_lb_inclusive and version_spec.is_lowerbound_inclusive
                )

        ub = self.upperbound
        is_ub_inclusive = self.is_upperbound_inclusive

        if version_spec.upperbound is not None:
            if ub is None:
                ub = version_spec.upperbound
                is_ub_inclusive = version_spec.is_upperbound_inclusive
            elif version_spec.upperbound < ub:
                ub = version_spec.upperbound
                is_ub_inclusive = version_spec.is_upperbound_inclusive
            elif version_spec.upperbound == ub:
                is_ub_inclusive = (
                    is_ub_inclusive and version_spec.is_upperbound_inclusive
                )

        if lb is not None and ub is not None and lb > ub:
            raise ValueError(
                "Can't intersect two version specs: {} and {} because it results in empty spec".format(
                    self, version_spec
                )
            )

        return VersionSpec(
            lowerbound=lb,
            upperbound=ub,
            is_lowerbound_inclusive=is_lb_inclusive,
            is_upperbound_inclusive=is_ub_inclusive,
        )
