import os
import platform
import shutil
import sys
from pathlib import Path
from collections.abc import Iterator

from ctypes.util import find_library

from ..types import StrOrPath

from .runtime_spec import DotnetCoreRuntimeSpec


def find_dotnet_cli() -> Path | None:
    dotnet_path = shutil.which("dotnet")
    if not dotnet_path:
        return None
    else:
        return Path(dotnet_path)


def find_dotnet_root() -> Path:
    """Try to discover the .NET Core root directory

    If the environment variable ``DOTNET_ROOT`` is defined, we will use that.
    Otherwise, we probe the default installation paths on Windows and macOS.

    If none of these lead to a result, we try to discover the ``dotnet`` CLI
    tool and use its (real) parent directory.

    Otherwise, this function raises an exception.

    :return: Path to the .NET Core root
    """

    dotnet_root = os.environ.get("DOTNET_ROOT", None)
    if dotnet_root is not None:
        return Path(dotnet_root)

    if sys.platform == "win32":
        # On Windows, the host library is stored separately from dotnet.exe for x86
        prog_files = os.environ.get("ProgramFiles")
        if not prog_files:
            raise RuntimeError("Could not find ProgramFiles")
        prog_files = Path(prog_files)
        dotnet_root = prog_files / "dotnet"
    elif sys.platform == "darwin":
        if "ARM64" in os.uname().version and platform.machine() == "x86_64":
            # Apple Silicon in Rosetta 2 mode
            dotnet_root = Path("/usr/local/share/dotnet/x64")
        else:
            dotnet_root = Path("/usr/local/share/dotnet")

    if dotnet_root is not None and dotnet_root.is_dir():
        return dotnet_root

    # Try to discover dotnet from PATH otherwise
    dotnet_cli = find_dotnet_cli()
    if not dotnet_cli:
        raise RuntimeError("Can not determine dotnet root")

    return dotnet_cli.resolve().parent


def find_runtimes_using_cli(dotnet_cli: Path) -> Iterator[DotnetCoreRuntimeSpec]:
    import re
    from subprocess import check_output

    out = check_output([str(dotnet_cli), "--list-runtimes"], encoding="UTF8")
    runtime_re = re.compile(r"(?P<name>\S+) (?P<version>\S+) \[(?P<path>[^\]]+)\]")

    for line in out.splitlines():
        m = re.match(runtime_re, line)
        if m:
            path = Path(m.group("path"))
            version = m.group("version")
            if path.is_dir():
                yield DotnetCoreRuntimeSpec(m.group("name"), version, path / version)


def find_runtimes_in_root(dotnet_root: Path) -> Iterator[DotnetCoreRuntimeSpec]:
    shared = dotnet_root / "shared"
    for runtime in shared.iterdir():
        if runtime.is_dir():
            name = runtime.name
            for version_path in runtime.iterdir():
                if version_path.is_dir():
                    yield DotnetCoreRuntimeSpec(name, version_path.name, version_path)


def find_runtimes() -> Iterator[DotnetCoreRuntimeSpec]:
    """Find installed .NET Core runtimes

    If the ``dotnet`` CLI can be found, we will call it as ``dotnet
    --list-runtimes`` and parse the result.

    If it is not available, we try to discover the dotnet root directory using
    :py:func:`find_dotnet_root` and enumerate the runtimes installed in the
    ``shared`` subdirectory.

    :return: Iterable of :py:class:`DotnetCoreRuntimeSpec` objects
    """
    dotnet_cli = find_dotnet_cli()
    if dotnet_cli is not None:
        return find_runtimes_using_cli(dotnet_cli)
    else:
        dotnet_root = find_dotnet_root()
        return find_runtimes_in_root(dotnet_root)


def find_libmono(
    *, assembly_dir: StrOrPath | None = None, sgen: bool = True
) -> Path | None:
    """Find a suitable libmono dynamic library

    On Windows, we check the default installation directory.
    On macOS, we check Homebrew (with architecture awareness) and the framework.
    On Unix-like systems, we check Homebrew and system locations.

    :param assembly_dir:
        Optional directory to search for libmono
    :param sgen:
        Whether to look for an SGen or Boehm GC instance. This parameter is
        ignored on Windows, as only ``sgen`` is installed with the default
        installer
    :return:
        Path to usable ``libmono``
    """

    if sys.platform == "win32":
        return _find_mono_windows()

    else:
        return _find_mono_unix(assembly_dir=assembly_dir, sgen=sgen)


def _find_mono_windows() -> Path | None:
    if sys.maxsize > 2**32:
        prog_files = os.environ.get("ProgramFiles")
    else:
        prog_files = os.environ.get("ProgramFiles(x86)")

    if prog_files is None:
        raise RuntimeError("Could not determine Program Files location")

    # Ignore sgen on Windows, the main installation only contains this DLL
    return Path(prog_files) / "Mono/bin/mono-2.0-sgen.dll"


def _find_mono_unix(
    *, assembly_dir: StrOrPath | None = None, sgen: bool = True
) -> Path | None:
    macos = sys.platform == "darwin"

    unix_name = f"mono{'sgen' if sgen else ''}-2.0"
    ext = ".dylib" if macos else ".so"

    lib_filename = f"lib{unix_name}{ext}"

    if assembly_dir is not None:
        candidate = Path(assembly_dir) / "lib" / lib_filename
        if candidate.exists():
            return candidate

    if res := find_library(unix_name):
        return Path(res)

    if macos:
        res = (
            Path("/Library/Frameworks/Mono.framework/Versions/Current/lib")
            / lib_filename
        )
        if res.exists():
            return res

        # Use HOMEBREW_PREFIX environment variable if available
        if homebrew_prefix := os.environ.get("HOMEBREW_PREFIX"):
            res = Path(homebrew_prefix) / "opt/mono/lib" / lib_filename
            if res.exists():
                return res

        # Check for native Apple Silicon (arm64)
        if platform.machine() == "arm64":
            res = Path("/opt/homebrew/opt/mono/lib") / lib_filename
            if res.exists():
                return res
        else:
            res = Path("/usr/local/opt/mono/lib") / lib_filename
            if res.exists():
                return res

    raise RuntimeError("Could not find libmono")
