from __future__ import annotations

import logging
import os
import subprocess
import typing as t
from abc import ABC
from abc import abstractmethod
from pathlib import Path
from shutil import which
from tempfile import NamedTemporaryFile

import attr

from ...exceptions import BentoMLException
from ..bento.bento import Bento
from ..configuration import get_debug_mode

logger = logging.getLogger(__name__)


@attr.define
class Environment(ABC):
    name: str
    path: Path = attr.field(converter=Path)
    # path to bento's /env dir
    bento: Bento
    env_exe: str = attr.field(init=False)

    def __attrs_post_init__(self):
        self.env_exe = self.get_executable()
        if not self.path.exists():
            self.create()

    @abstractmethod
    def get_executable(self) -> str:
        """
        Returns executable path responsible for running this environment.

        Make sure that said executable is available in PATH.
        """
        ...

    @abstractmethod
    def create(self):
        """
        Create the environment with the files from bento.
        """
        ...

    @abstractmethod
    def run(self, commands: list[str]):
        """
        run the commands in an activated environment.
        """
        ...

    @staticmethod
    def run_script_subprocess(
        script_file_path: t.Union[str, os.PathLike[str]],
        capture_output: bool,
        debug_mode: bool,
    ):
        shell_path = which("bash")
        if shell_path is None:
            raise BentoMLException("Unable to locate a valid shell")

        safer_bash_args: list[str] = []

        # but only work in bash
        if debug_mode:
            safer_bash_args = ["-euxo", "pipefail"]
        result = subprocess.run(
            [shell_path, *safer_bash_args, script_file_path],
            capture_output=capture_output,
        )
        if result.returncode != 0:
            if result.stdout:
                logger.debug(result.stdout.decode())
            if result.stderr:
                logger.error(result.stderr.decode())
            raise BentoMLException(
                "Subprocess call returned non-zero value. Reffer logs for more details"
            )


class Conda(Environment):
    def get_executable(self) -> str:
        conda_exe = os.environ.get("CONDA_EXE")
        if conda_exe is None:
            raise BentoMLException(
                "Conda|Miniconda executable not found! Make sure any one is installed and environment is activated."
            )
        return conda_exe

    def create(self):
        # install conda deps
        from ..bento.build_config import CONDA_ENV_YAML_FILE_NAME

        # create a env under $BENTOML_HOME/env
        # setup conda with bento's environment.yml file and python/install.sh file
        conda_env_path = self.path / self.name
        python_version: str
        with open(self.bento.path_of("/env/python/version.txt"), "r") as pyver_file:
            py_version = pyver_file.read().split(".")[:2]
            python_version = ".".join(py_version)
        conda_environment_file = self.bento.path_of(
            f"/env/conda/{CONDA_ENV_YAML_FILE_NAME}"
        )
        create_script = f"""\
eval "$({self.get_executable()} shell.posix hook)"

conda create -p {conda_env_path} python={python_version} --yes

if [ -f {conda_environment_file} ]; then
    conda config --set pip_interop_enabled True
    conda env update -p {conda_env_path} --file {conda_environment_file}
fi

conda activate {conda_env_path}
bash -euxo pipefail {self.bento.path_of("/env/python/install.sh")}
"""
        with NamedTemporaryFile(mode="w", delete=False) as script_file:
            script_file.write(create_script)

        logger.info("Creating Conda env and installing dependencies...")
        self.run_script_subprocess(
            script_file.name,
            capture_output=not get_debug_mode(),
            debug_mode=get_debug_mode(),
        )

    def run(self, commands: list[str]):
        """
        Run commands in the activated environment.
        """
        conda_env_path = self.path / self.name
        create_script = f"""\
eval "$({self.env_exe} shell.posix hook)"
conda activate {conda_env_path}
{" ".join(commands)}
"""
        with NamedTemporaryFile(mode="w", delete=False) as script_file:
            script_file.write(create_script)
        self.run_script_subprocess(
            script_file.name, capture_output=False, debug_mode=get_debug_mode()
        )
