import os
import shutil
import subprocess
import time
from pathlib import Path

from .models import ProjectKind, Toolchain

VC_WORKLOAD = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
VS_CMAKE_RELATIVE = Path(
    "Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin"
)


class ToolchainError(RuntimeError):
    pass


def _decode(value):
    if isinstance(value, str):
        return value
    return (value or b"").decode("utf-8", errors="replace")


def _first_path(result, label, argv, cwd):
    text = _decode(result.stdout).strip()
    if result.returncode != 0:
        diagnostic = (
            _decode(result.stdout) + "\n" + _decode(result.stderr)
        ).rstrip()[-8000:]
        diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ")
        message = (
            f"{label} probe failed with exit code {result.returncode}; "
            f"argv={argv!r}; cwd={str(cwd)!r}"
        )
        if diagnostic:
            message = f"{message}; output tail: {diagnostic}"
        raise ToolchainError(
            f"{message}; install Visual Studio Desktop development with C++"
        )
    if not text:
        raise ToolchainError(
            f"{label} probe returned no path; argv={argv!r}; cwd={str(cwd)!r}; "
            "install Visual Studio Desktop development with C++"
        )
    path = Path(text.splitlines()[0]).resolve()
    if not path.exists():
        diagnostic = (
            _decode(result.stdout) + "\n" + _decode(result.stderr)
        ).rstrip()[-8000:]
        diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ")
        message = (
            f"{label} probe returned nonexistent path with exit code 0; "
            f"argv={argv!r}; cwd={str(cwd)!r}"
        )
        if diagnostic:
            message = f"{message}; output tail: {diagnostic}"
        raise ToolchainError(
            f"{message}; install or repair Visual Studio Desktop development with C++"
        )
    return path


def _require_path(path, label):
    if path is None or not path.exists():
        raise ToolchainError(f"{label} not found")
    return path.resolve()


def _remaining(deadline, clock, label):
    if deadline is None:
        return None
    remaining = deadline - clock()
    if remaining <= 0:
        raise ToolchainError(f"{label} probe timed out before it could start")
    return remaining


def _probe(run, argv, label, *, deadline, clock):
    cwd = Path.cwd().resolve()
    kwargs = {
        "cwd": cwd,
        "capture_output": True,
        "check": False,
        "shell": False,
    }
    remaining = _remaining(deadline, clock, label)
    if remaining is not None:
        kwargs["timeout"] = remaining
    try:
        return run(argv, **kwargs)
    except subprocess.TimeoutExpired as exc:
        raise ToolchainError(
            f"{label} probe timed out; argv={argv!r}; cwd={str(cwd)!r}; "
            "install or repair Visual Studio Desktop development with C++"
        ) from exc


def discover_toolchain(
    kind: ProjectKind,
    *,
    env=None,
    which=shutil.which,
    run=subprocess.run,
    deadline=None,
    clock=time.monotonic,
) -> Toolchain:
    environment = os.environ if env is None else env
    vswhere_path = which("vswhere.exe")
    if vswhere_path is None:
        program_files = environment.get("ProgramFiles(x86)")
        if program_files:
            candidate = Path(program_files) / "Microsoft Visual Studio/Installer/vswhere.exe"
            vswhere_path = str(candidate) if candidate.exists() else None
    vswhere = _require_path(
        Path(vswhere_path) if vswhere_path else None,
        "vswhere.exe; install Visual Studio Desktop development with C++",
    )

    install_argv = [
        str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD,
        "-property", "installationPath",
    ]
    install_result = _probe(
        run,
        install_argv,
        "Visual Studio installation",
        deadline=deadline,
        clock=clock,
    )
    probe_cwd = Path.cwd().resolve()
    installation = _require_path(
        _first_path(
            install_result,
            "Visual Studio installation",
            install_argv,
            probe_cwd,
        ),
        "Visual Studio installation",
    )
    msbuild_argv = [
        str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD,
        "-find", r"MSBuild\**\Bin\MSBuild.exe",
    ]
    msbuild_result = _probe(
        run,
        msbuild_argv,
        "MSBuild",
        deadline=deadline,
        clock=clock,
    )
    msbuild = _require_path(
        _first_path(msbuild_result, "MSBuild", msbuild_argv, probe_cwd),
        "MSBuild.exe",
    )

    if kind is not ProjectKind.CMAKE:
        return Toolchain(installation=installation, msbuild=msbuild)

    bundled_cmake_dir = installation / VS_CMAKE_RELATIVE
    cmake = _require_path(
        Path(which("cmake.exe")) if which("cmake.exe") else bundled_cmake_dir / "cmake.exe",
        "cmake.exe",
    )
    ctest = _require_path(
        Path(which("ctest.exe")) if which("ctest.exe") else bundled_cmake_dir / "ctest.exe",
        "ctest.exe",
    )
    return Toolchain(installation=installation, msbuild=msbuild, cmake=cmake, ctest=ctest)
