from __future__ import annotations

import dataclasses
import operator
from typing import Any, Iterable

from .environment import set_asdf_paths, set_pyenv_paths
from .exceptions import InvalidPythonVersion
from .models.path import PathEntry, SystemPath
from .models.python import PythonVersion
from .utils import version_re


@dataclasses.dataclass(unsafe_hash=True)
class Finder:
    path: str | None = None
    system: bool = False
    global_search: bool = True
    ignore_unsupported: bool = True
    sort_by_path: bool = False
    system_path: SystemPath | None = dataclasses.field(default=None, init=False)

    def __post_init__(self):
        self.system_path = self.create_system_path()

    def create_system_path(self) -> SystemPath:
        # Implementation of set_asdf_paths and set_pyenv_paths might need to be adapted.
        set_asdf_paths()
        set_pyenv_paths()
        return SystemPath.create(
            path=self.path,
            system=self.system,
            global_search=self.global_search,
            ignore_unsupported=self.ignore_unsupported,
        )

    def which(self, exe) -> PathEntry | None:
        return self.system_path.which(exe)

    @classmethod
    def parse_major(
        cls,
        major: str | None,
        minor: int | None = None,
        patch: int | None = None,
        pre: bool | None = None,
        dev: bool | None = None,
        arch: str | None = None,
    ) -> dict[str, Any]:
        major_is_str = major and isinstance(major, str)
        is_num = (
            major
            and major_is_str
            and all(part.isdigit() for part in major.split(".")[:2])
        )
        major_has_arch = (
            arch is None
            and major
            and major_is_str
            and "-" in major
            and major[0].isdigit()
        )
        name = None
        if major and major_has_arch:
            orig_string = f"{major!s}"
            major, _, arch = major.rpartition("-")
            if arch:
                arch = arch.lower().lstrip("x").replace("bit", "")
                if not (arch.isdigit() and (int(arch) & int(arch) - 1) == 0):
                    major = orig_string
                    arch = None
                else:
                    arch = f"{arch}bit"
            try:
                version_dict = PythonVersion.parse(major)
            except (ValueError, InvalidPythonVersion):
                if name is None:
                    name = f"{major!s}"
                    major = None
                version_dict = {}
        elif major and major[0].isalpha():
            return {"major": None, "name": major, "arch": arch}
        elif major and is_num:
            match = version_re.match(major)
            version_dict = match.groupdict() if match else {}
            version_dict.update(
                {
                    "is_prerelease": bool(version_dict.get("prerel", False)),
                    "is_devrelease": bool(version_dict.get("dev", False)),
                }
            )
        else:
            version_dict = {
                "major": major,
                "minor": minor,
                "patch": patch,
                "pre": pre,
                "dev": dev,
                "arch": arch,
            }
        if not version_dict.get("arch") and arch:
            version_dict["arch"] = arch
        version_dict["minor"] = (
            int(version_dict["minor"]) if version_dict.get("minor") is not None else minor
        )
        version_dict["patch"] = (
            int(version_dict["patch"]) if version_dict.get("patch") is not None else patch
        )
        version_dict["major"] = (
            int(version_dict["major"]) if version_dict.get("major") is not None else major
        )
        if not (version_dict["major"] or version_dict.get("name")):
            version_dict["major"] = major
            if name:
                version_dict["name"] = name
        return version_dict

    def find_python_version(
        self,
        major: str | int | None = None,
        minor: int | None = None,
        patch: int | None = None,
        pre: bool | None = None,
        dev: bool | None = None,
        arch: str | None = None,
        name: str | None = None,
        sort_by_path: bool = False,
    ) -> PathEntry | None:
        """
        Find the python version which corresponds most closely to the version requested.

        :param major: The major version to look for, or the full version, or the name of the target version.
        :param minor: The minor version. If provided, disables string-based lookups from the major version field.
        :param patch: The patch version.
        :param pre: If provided, specifies whether to search pre-releases.
        :param dev: If provided, whether to search dev-releases.
        :param arch: If provided, which architecture to search.
        :param name: *Name* of the target python, e.g. ``anaconda3-5.3.0``
        :param sort_by_path: Whether to sort by path -- default sort is by version(default: False)
        :return: A new *PathEntry* pointer at a matching python version, if one can be located.
        """
        minor = int(minor) if minor is not None else minor
        patch = int(patch) if patch is not None else patch

        if (
            isinstance(major, str)
            and pre is None
            and minor is None
            and dev is None
            and patch is None
        ):
            version_dict = self.parse_major(major, minor=minor, patch=patch, arch=arch)
            major = version_dict["major"]
            minor = version_dict.get("minor", minor)
            patch = version_dict.get("patch", patch)
            arch = version_dict.get("arch", arch)
            name = version_dict.get("name", name)
            _pre = version_dict.get("is_prerelease", pre)
            pre = bool(_pre) if _pre is not None else pre
            _dev = version_dict.get("is_devrelease", dev)
            dev = bool(_dev) if _dev is not None else dev
            if "architecture" in version_dict and isinstance(
                version_dict["architecture"], str
            ):
                arch = version_dict["architecture"]
        return self.system_path.find_python_version(
            major=major,
            minor=minor,
            patch=patch,
            pre=pre,
            dev=dev,
            arch=arch,
            name=name,
            sort_by_path=sort_by_path,
        )

    def find_all_python_versions(
        self,
        major: str | int | None = None,
        minor: int | None = None,
        patch: int | None = None,
        pre: bool | None = None,
        dev: bool | None = None,
        arch: str | None = None,
        name: str | None = None,
    ) -> list[PathEntry]:
        version_sort = operator.attrgetter("as_python.version_sort")
        python_version_dict = getattr(self.system_path, "python_version_dict", {})
        if python_version_dict:
            paths = (
                path
                for version in python_version_dict.values()
                for path in version
                if path is not None and path.as_python
            )
            path_list = sorted(paths, key=version_sort, reverse=True)
            return path_list
        versions = self.system_path.find_all_python_versions(
            major=major, minor=minor, patch=patch, pre=pre, dev=dev, arch=arch, name=name
        )
        if not isinstance(versions, Iterable):
            versions = [versions]
        path_list = sorted(
            filter(lambda v: v and v.as_python, versions), key=version_sort, reverse=True
        )
        path_map = {}
        for p in path_list:
            try:
                resolved_path = p.path.resolve(strict=True)
            except (OSError, RuntimeError):
                resolved_path = p.path.absolute()
            if resolved_path not in path_map:
                path_map[resolved_path] = p
        return [path_map[p] for p in path_map]
