from __future__ import annotations

import functools
import re

from typing import TYPE_CHECKING

from poetry.core.constraints.version.exceptions import ParseConstraintError
from poetry.core.version.exceptions import InvalidVersionError


if TYPE_CHECKING:
    from poetry.core.constraints.version.version import Version
    from poetry.core.constraints.version.version_constraint import VersionConstraint


@functools.cache
def parse_constraint(constraints: str) -> VersionConstraint:
    return _parse_constraint(constraints=constraints)


def parse_marker_version_constraint(
    constraints: str, *, pep440: bool = True
) -> VersionConstraint:
    return _parse_constraint(
        constraints=constraints, is_marker_constraint=True, pep440=pep440
    )


def _parse_constraint(
    constraints: str, *, is_marker_constraint: bool = False, pep440: bool = True
) -> VersionConstraint:
    if constraints == "*":
        from poetry.core.constraints.version.version_range import VersionRange

        return VersionRange()

    or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip())
    or_groups = []
    for constraints in or_constraints:
        # allow trailing commas for robustness (even though it may not be
        # standard-compliant it seems to occur in some packages)
        constraints = constraints.rstrip(",").rstrip()
        and_constraints = re.split(
            r"(?<!^)(?<![\^~=>< ,]) *(?<!-)[, ](?!-) *(?!,|$)", constraints
        )
        constraint_objects = []

        if len(and_constraints) > 1:
            for constraint in and_constraints:
                constraint_objects.append(
                    parse_single_constraint(
                        constraint,
                        is_marker_constraint=is_marker_constraint,
                        pep440=pep440,
                    )
                )
        else:
            constraint_objects.append(
                parse_single_constraint(
                    and_constraints[0],
                    is_marker_constraint=is_marker_constraint,
                    pep440=pep440,
                )
            )

        if len(constraint_objects) == 1:
            constraint = constraint_objects[0]
        else:
            constraint = constraint_objects[0]
            for next_constraint in constraint_objects[1:]:
                constraint = constraint.intersect(next_constraint)

        or_groups.append(constraint)

    if len(or_groups) == 1:
        return or_groups[0]
    else:
        from poetry.core.constraints.version.version_union import VersionUnion

        return VersionUnion.of(*or_groups)


def parse_single_constraint(
    constraint: str, *, is_marker_constraint: bool = False, pep440: bool = True
) -> VersionConstraint:
    from poetry.core.constraints.version.patterns import BASIC_CONSTRAINT
    from poetry.core.constraints.version.patterns import BASIC_RELEASE_CONSTRAINT
    from poetry.core.constraints.version.patterns import CARET_CONSTRAINT
    from poetry.core.constraints.version.patterns import TILDE_CONSTRAINT
    from poetry.core.constraints.version.patterns import TILDE_PEP440_CONSTRAINT
    from poetry.core.constraints.version.patterns import X_CONSTRAINT
    from poetry.core.constraints.version.version import Version
    from poetry.core.constraints.version.version_range import VersionRange
    from poetry.core.constraints.version.version_union import VersionUnion

    m = re.match(r"(?i)^v?[xX*](\.[xX*])*$", constraint)
    if m:
        return VersionRange()

    # Tilde range
    m = TILDE_CONSTRAINT.match(constraint)
    if m:
        try:
            version = Version.parse(m.group("version"))
        except InvalidVersionError as e:
            raise ParseConstraintError(
                f"Could not parse version constraint: {constraint}"
            ) from e

        high = version.stable.next_minor()
        if version.release.precision == 1:
            high = version.stable.next_major()

        return VersionRange(version, high, include_min=True)

    # PEP 440 Tilde range (~=)
    m = TILDE_PEP440_CONSTRAINT.match(constraint)
    if m:
        try:
            version = Version.parse(m.group("version"))
        except InvalidVersionError as e:
            raise ParseConstraintError(
                f"Could not parse version constraint: {constraint}"
            ) from e

        if version.release.precision == 2:
            high = version.stable.next_major()
        else:
            high = version.stable.next_minor()

        return VersionRange(version, high, include_min=True)

    # Caret range
    m = CARET_CONSTRAINT.match(constraint)
    if m:
        try:
            version = Version.parse(m.group("version"))
        except InvalidVersionError as e:
            raise ParseConstraintError(
                f"Could not parse version constraint: {constraint}"
            ) from e

        return VersionRange(version, version.next_breaking(), include_min=True)

    # X Range
    m = X_CONSTRAINT.match(constraint)
    if m:
        op = m.group("op")

        try:
            return _make_x_constraint_range(
                version=Version.parse(m.group("version")),
                invert=op == "!=",
                is_marker_constraint=is_marker_constraint,
            )
        except ValueError:
            raise ValueError(f"Could not parse version constraint: {constraint}")

    # Basic comparator
    m = BASIC_CONSTRAINT.match(constraint)
    if m:
        op = m.group("op")
        version_string = m.group("version")

        if version_string == "dev":
            version_string = "0.0-dev"

        try:
            version = Version.parse(version_string)
        except InvalidVersionError as e:
            raise ParseConstraintError(
                f"Could not parse version constraint: {constraint}"
            ) from e

        if op == "<":
            return VersionRange(max=version)
        if op == "<=":
            return VersionRange(max=version, include_max=True)
        if op == ">":
            return VersionRange(min=version)
        if op == ">=":
            return VersionRange(min=version, include_min=True)

        if m.group("wildcard") is not None:
            return _make_x_constraint_range(
                version=version,
                invert=op == "!=",
                is_marker_constraint=is_marker_constraint,
            )

        if op == "!=":
            return VersionUnion(VersionRange(max=version), VersionRange(min=version))

        return version

    # These below should be reserved for comparing non python packages such as OS
    # versions using `platform_release`
    if not pep440 and (m := BASIC_RELEASE_CONSTRAINT.match(constraint)):
        op = m.group("op")
        release_string = m.group("release")
        build = m.group("build")

        try:
            version = Version(
                release=Version.parse(release_string).release,
                local=build,
            )
        except InvalidVersionError as e:
            raise ParseConstraintError(
                f"Could not parse version constraint: {constraint}"
            ) from e

        if op == "<":
            return VersionRange(max=version)
        if op == "<=":
            return VersionRange(max=version, include_max=True)
        if op == ">":
            return VersionRange(min=version)
        if op == ">=":
            return VersionRange(min=version, include_min=True)
        if op == "!=":
            return VersionUnion(VersionRange(max=version), VersionRange(min=version))
        return version

    raise ParseConstraintError(f"Could not parse version constraint: {constraint}")


def _make_x_constraint_range(
    version: Version, *, invert: bool = False, is_marker_constraint: bool = False
) -> VersionConstraint:
    from poetry.core.constraints.version.version_range import VersionRange

    if version.is_postrelease():
        _next = version.next_postrelease()
    elif version.is_stable():
        _next = version.next_stable()
    elif version.is_prerelease():
        _next = version.next_prerelease()
    elif version.is_devrelease():
        _next = version.next_devrelease()
    else:
        raise RuntimeError("version is neither stable, nor pre-release nor dev-release")

    _min = version
    _max = _next

    if not is_marker_constraint:
        _min = _min.first_devrelease()
        if not _max.is_devrelease():
            _max = _max.first_devrelease()

    result = VersionRange(_min, _max, include_min=True)

    if invert:
        return VersionRange().difference(result)

    return result
