import copy
import itertools
import operator
from functools import reduce
from typing import Any, List, Optional, Set, Tuple, Union

from packaging.markers import Marker as PackageMarker

from pdm.models.specifiers import PySpecSet
from pdm.utils import join_list_with


class Marker(PackageMarker):
    """A subclass of Marker that supports union and intersection merging."""

    def copy(self) -> "Marker":
        inst = self.__class__('os_name == "nt"')
        inst._markers = copy.deepcopy(self._markers)
        return inst

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, PackageMarker):
            return False
        return str(self) == str(other)

    def split_pyspec(self) -> Tuple[Optional["Marker"], PySpecSet]:
        """Split `python_version` and `python_full_version` from marker string"""
        if _only_contains_python_keys(self._markers):
            return None, _build_pyspec_from_marker(self._markers)
        if "or" in self._markers:
            return self.copy(), PySpecSet()
        py_markers = [
            marker
            for marker in self._markers
            if marker != "and" and _only_contains_python_keys(marker)
        ]
        rest = [
            marker
            for marker in self._markers
            if marker != "and" and marker not in py_markers
        ]
        new_markers = join_list_with(rest, "and")
        if not new_markers:
            marker = None
        else:
            marker = self.copy()
            marker._markers = new_markers
        return marker, _build_pyspec_from_marker(join_list_with(py_markers, "and"))


def get_marker(marker: Union[PackageMarker, Marker, None]) -> Optional[Marker]:
    return Marker(str(marker)) if marker else None


def split_marker_extras(marker: str) -> Tuple[Set[str], str]:
    """An element can be stripped from the marker only if all parts are connected
    with `and` operator. The rest part are returned as a string or `None` if all are
    stripped.
    """

    def extract_extras(submarker: Union[tuple, list]) -> Set[str]:
        if isinstance(submarker, tuple):
            if submarker[0].value == "extra":
                if submarker[1].value == "==":
                    return {submarker[2].value}
                elif submarker[1].value == "in":
                    return {v.strip() for v in submarker[2].value.split(",")}
                else:
                    return set()
            else:
                return set()
        else:
            if "and" in submarker:
                return set()
            pure_extras = [extract_extras(m) for m in submarker if m != "or"]
            if all(pure_extras):
                return set(itertools.chain.from_iterable(pure_extras))
            return set()

    if not marker:
        return set(), marker
    new_marker = PackageMarker(marker)
    submarkers = PackageMarker(marker)._markers
    if "or" in submarkers:
        extras = extract_extras(submarkers)
        if extras:
            return extras, ""
        return set(), marker

    extras = set()
    submarkers_no_extras: List[Union[tuple, list]] = []
    # Below this point the submarkers are connected with 'and'
    for submarker in submarkers:
        if submarker == "and":
            continue
        new_extras = extract_extras(submarker)
        if new_extras:
            if extras:
                # extras are not allowed to appear in more than one parts
                return set(), marker
            extras.update(new_extras)
        else:
            submarkers_no_extras.append(submarker)

    if not submarkers_no_extras:
        return extras, ""
    new_marker._markers = join_list_with(submarkers_no_extras, "and")
    return extras, str(new_marker)


def _only_contains_python_keys(markers: List[Any]) -> bool:
    if isinstance(markers, tuple):
        return markers[0].value in ("python_version", "python_full_version")

    for marker in markers:
        if marker in ("and", "or"):
            continue
        if not _only_contains_python_keys(marker):
            return False
    return True


def _build_pyspec_from_marker(markers: List[Any]) -> PySpecSet:
    def split_version(version: str) -> List[str]:
        if "," in version:
            return [v.strip() for v in version.split(",")]
        return version.split()

    groups = [PySpecSet()]
    for marker in markers:
        if isinstance(marker, list):
            # It is a submarker
            groups[-1] = groups[-1] & _build_pyspec_from_marker(marker)
        elif isinstance(marker, tuple):
            key, op, version = (i.value for i in marker)
            if key == "python_version":
                if op == ">":
                    int_versions = [int(ver) for ver in version.split(".")]
                    int_versions[-1] += 1
                    version = ".".join(str(v) for v in int_versions)
                    op = ">="
                elif op in ("==", "!="):
                    if len(version.split(".")) < 3:
                        version += ".*"
                elif op in ("in", "not in"):
                    version = " ".join(v + ".*" for v in split_version(version))
            if op == "in":
                pyspec = reduce(
                    operator.or_, (PySpecSet(f"=={v}") for v in split_version(version))
                )
            elif op == "not in":
                pyspec = reduce(
                    operator.and_, (PySpecSet(f"!={v}") for v in split_version(version))
                )
            else:
                pyspec = PySpecSet(f"{op}{version}")
            groups[-1] = groups[-1] & pyspec
        else:
            assert marker in ("and", "or")
            if marker == "or":
                groups.append(PySpecSet())
    return reduce(operator.or_, groups)
