import sys
import os
import platform
from pathlib import Path


class Venv:
    def __init__(self, path: Path) -> None:
        self._path = path
        if platform.system() == "Windows":
            self._bin_path = self._path / "Scripts"
        else:
            self._bin_path = self._path / "bin"

    @property
    def path(self) -> Path:
        return self._path

    def exists(self) -> bool:
        return self.path.exists() and self._bin_path.exists()

    def create(self, with_python_exe: str):
        cmd = f"{with_python_exe} -m venv {self.path}"
        print(f"Creating venv {self.path}: {cmd}")
        os.system(cmd)

    def get_exe(self, name: str):
        exe = self._bin_path / name
        if platform.system() == "Windows":
            # the '.exe' suffix is not needed to execute
            # but the caller may want to use `exe.exists()``
            # so we need to add the extension if on windows :/
            exe_suffix = exe.with_suffix(".exe")
            if exe_suffix.exists():
                # special case for .bat files
                exe = exe_suffix
        return exe

    def install_uv(self):
        return self.install_package("uv", use_uv=False)

    def install_package(
        self,
        requirement,
        update: bool = True,
        use_uv: bool = True,
        index: str | None = None,
    ) -> bool:
        """
        Returns True if the package has been successfully installed.

        if `index` is given, it will be passed to pip:
            - as -f/--find-links it it is a path (Note: relative path are discouraged).
            - as -i/--index-url otherwise
        """
        if use_uv:
            # NB: we're using python -m uv instead of uv directly so that uv pip installs in the same env
            # see "If uv is installed in a Python environment" in https://docs.astral.sh/uv/pip/environments/#using-arbitrary-python-environments
            exe = f'{self.get_exe("python")} -m uv pip'
        else:
            exe = f'{self.get_exe("python")} -m pip'

        index_flags = ""
        if index is not None:
            if "://" in index:
                index_flags = f"--index-url {index}"
            else:
                index_flags = f"--find-links {index}"

        update_flag = ""
        if update:
            update_flag = "-U"

        cmd = f"{exe} install {index_flags} {update_flag} {requirement}"
        print(f"Installing package {requirement}: {cmd}")
        ret = os.system(cmd)
        return not ret

    def execute_cmd(self, cmd: str) -> bool:
        """
        Returns True if the command was been successfully executed.
        """
        print("Executing venv cmd:", cmd)
        ret = os.system(cmd)
        return not ret

    def run_cmd(self, cmd_name: str, cmd_args: list[str]) -> bool:
        """
        Returns True if the command was been successfully executed.
        """
        # TODO: thread this.
        exe = self.get_exe(cmd_name)
        cmd = str(exe) + " " + " ".join(cmd_args)
        return self.execute_cmd(cmd)

    def get_cmd_names(self) -> list[str]:
        raise NotImplementedError()


class PackageManager:
    def __init__(self, root: Path) -> None:
        self._root = root

    @property
    def root(self) -> Path:
        return self._root

    def get_venv(self, venv_name: str, group: str) -> Venv:
        return Venv(self.root / group / venv_name)

    def _create_bat_shortcut(self, exe_path: Path | str, shortcut_path: Path | str):
        exe_path = Path(exe_path)
        if not exe_path.is_absolute() and not str(exe_path).startswith("./"):
            exe_path = f".\\{exe_path}"

        shortcut_path = Path(shortcut_path)
        if shortcut_path.suffix != ".bat":
            shortcut_path = f"{shortcut_path}.bat"

        content = [
            "@echo off",
            f"REM Shortcut to {exe_path}",
            "",
            f"{exe_path} %*",
        ]
        with open(shortcut_path, "w") as fp:
            fp.write("\n".join(content))

    def create_shortcut(
        self, exe_path: Path | str, shortcut_path: Path | str, relative: bool = True
    ):
        """
        Create a shortcut to exe_path at shortcut_path.

        If `relative` is True, `exe_path` will be modified to be relative
        to `shortcut_path`.

        The shortcut will be a symlink to the exe, unless the OS does not
        allow it in which case a .bat file is created.
        """
        if relative:
            exe_path = Path(exe_path).relative_to(Path(shortcut_path).parent)

        if platform.system() == "Linux":
            os.symlink(exe_path, shortcut_path, target_is_directory=False)
        else:
            self._create_bat_shortcut(exe_path, shortcut_path)

    def create_venv(self, venv_name: str, group: str, exist_ok: bool = False) -> Venv:
        venv = self.get_venv(venv_name, group)
        if venv.exists() and not exist_ok:
            raise ValueError(f"Virtual Env {venv.path} already exists!")

        python_exe = sys.executable
        # TODO: assert sys.executable works with pyinstaller
        # TODO: detect if we can use uv instead of "python -m venv" and use it when possible.
        venv.create(python_exe)
        venv.install_uv()
        return venv
