from __future__ import annotations

import sys
from abc import ABC, abstractmethod
from contextlib import contextmanager
from os.path import expandvars

from ...config.constants import AppEnvVars
from ...utils.structures import EnvVars


class EnvironmentInterface(ABC):
    """
    Example usage:

    === ":octicons-file-code-16: plugin.py"

        ```python
        from hatch.env.plugin.interface import EnvironmentInterface


        class SpecialEnvironment(EnvironmentInterface):
            PLUGIN_NAME = 'special'
            ...
        ```

    === ":octicons-file-code-16: hooks.py"

        ```python
        from hatchling.plugin import hookimpl

        from .plugin import SpecialEnvironment


        @hookimpl
        def hatch_register_environment():
            return SpecialEnvironment
        ```
    """

    PLUGIN_NAME = ''
    """The name used for selection."""

    def __init__(self, root, metadata, name, config, data_directory, platform, verbosity, app=None):
        self.__root = root
        self.metadata = metadata
        self.__name = name
        self.__config = config
        self.__data_directory = data_directory
        self.__platform = platform
        self.verbosity = verbosity
        self.__app = app

        self._system_python = None
        self._env_vars = None
        self._env_include = None
        self._env_exclude = None
        self._environment_dependencies_complex = None
        self._environment_dependencies = None
        self._dependencies_complex = None
        self._dependencies = None
        self._platforms = None
        self._skip_install = None
        self._dev_mode = None
        self._features = None
        self._description = None
        self._scripts = None
        self._pre_install_commands = None
        self._post_install_commands = None

    @property
    def app(self):
        """
        An instance of [Application](utilities.md#hatchling.bridge.app.Application).
        """
        if self.__app is None:
            from hatchling.bridge.app import Application

            self.__app = Application().get_safe_application()

        return self.__app

    @property
    def root(self):
        """
        The root of the project tree as a path-like object.
        """
        return self.__root

    @property
    def name(self) -> str:
        """
        The name of the environment.
        """
        return self.__name

    @property
    def platform(self):
        """
        An instance of [Platform](utilities.md#hatch.utils.platform.Platform).
        """
        return self.__platform

    @property
    def data_directory(self):
        """
        The [directory](../config/hatch.md#environments) reserved exclusively for this plugin as a path-like object.
        """
        return self.__data_directory

    @property
    def config(self) -> dict:
        """
        === ":octicons-file-code-16: pyproject.toml"

            ```toml
            [tool.hatch.envs.<ENV_NAME>]
            ```

        === ":octicons-file-code-16: hatch.toml"

            ```toml
            [envs.<ENV_NAME>]
            ```
        """
        return self.__config

    @property
    def system_python(self):
        if self._system_python is None:
            self._system_python = self.platform._shutil.which('python') or sys.executable

        return self._system_python

    @property
    def env_vars(self) -> dict:
        """
        === ":octicons-file-code-16: pyproject.toml"

            ```toml
            [tool.hatch.envs.<ENV_NAME>.env-vars]
            ```

        === ":octicons-file-code-16: hatch.toml"

            ```toml
            [envs.<ENV_NAME>.env-vars]
            ```
        """
        if self._env_vars is None:
            env_vars = self.config.get('env-vars', {})
            if not isinstance(env_vars, dict):
                raise TypeError(f'Field `tool.hatch.envs.{self.name}.env-vars` must be a mapping')

            for key, value in env_vars.items():
                if not isinstance(value, str):
                    raise TypeError(
                        f'Environment variable `{key}` of field `tool.hatch.envs.{self.name}.env-vars` must be a string'
                    )

            env_vars = env_vars.copy()
            env_vars[AppEnvVars.ENV_ACTIVE] = self.name
            self._env_vars = env_vars

        return self._env_vars

    @property
    def env_include(self) -> list[str]:
        """
        === ":octicons-file-code-16: pyproject.toml"

            ```toml
            [tool.hatch.envs.<ENV_NAME>]
            env-include = [...]
            ```

        === ":octicons-file-code-16: hatch.toml"

            ```toml
            [envs.<ENV_NAME>]
            env-include = [...]
            ```
        """
        if self._env_include is None:
            env_include = self.config.get('env-include', [])
            if not isinstance(env_include, list):
                raise TypeError(f'Field `tool.hatch.envs.{self.name}.env-include` must be an array')

            for i, pattern in enumerate(env_include, 1):
                if not isinstance(pattern, str):
                    raise TypeError(f'Pattern #{i} of field `tool.hatch.envs.{self.name}.env-include` must be a string')

            if env_include:
                self._env_include = ['HATCH_BUILD_*', *env_include]
            else:
                self._env_include = env_include

        return self._env_include

    @property
    def env_exclude(self) -> list[str]:
        """
        === ":octicons-file-code-16: pyproject.toml"

            ```toml
            [tool.hatch.envs.<ENV_NAME>]
            env-exclude = [...]
            ```

        === ":octicons-file-code-16: hatch.toml"

            ```toml
            [envs.<ENV_NAME>]
            env-exclude = [...]
            ```
        """
        if self._env_exclude is None:
            env_exclude = self.config.get('env-exclude', [])
            if not isinstance(env_exclude, list):
                raise TypeError(f'Field `tool.hatch.envs.{self.name}.env-exclude` must be an array')

            for i, pattern in enumerate(env_exclude, 1):
                if not isinstance(pattern, str):
                    raise TypeError(f'Pattern #{i} of field `tool.hatch.envs.{self.name}.env-exclude` must be a string')

            self._env_exclude = env_exclude

        return self._env_exclude

    @property
    def environment_dependencies_complex(self):
        if self._environment_dependencies_complex is None:
            from packaging.requirements import InvalidRequirement, Requirement

            dependencies_complex = []
            for option in ('dependencies', 'extra-dependencies'):
                dependencies = self.config.get(option, [])
                if not isinstance(dependencies, list):
                    raise TypeError(f'Field `tool.hatch.envs.{self.name}.{option}` must be an array')

                for i, entry in enumerate(dependencies, 1):
                    if not isinstance(entry, str):
                        raise TypeError(
                            f'Dependency #{i} of field `tool.hatch.envs.{self.name}.{option}` must be a string'
                        )

                    try:
                        dependencies_complex.append(Requirement(entry))
                    except InvalidRequirement as e:
                        raise ValueError(
                            f'Dependency #{i} of field `tool.hatch.envs.{self.name}.{option}` is invalid: {e}'
                        )

            self._environment_dependencies_complex = dependencies_complex

        return self._environment_dependencies_complex

    @property
    def environment_dependencies(self) -> list[str]:
        """
        The list of all [environment dependencies](../config/environment.md#dependencies).
        """
        if self._environment_dependencies is None:
            self._environment_dependencies = [str(dependency) for dependency in self.environment_dependencies_complex]

        return self._environment_dependencies

    @property
    def dependencies_complex(self):
        if self._dependencies_complex is None:
            dependencies_complex = list(self.environment_dependencies_complex)

            # Ensure these are checked last to speed up initial environment creation since
            # they will already be installed along with the project
            if not self.skip_install and self.dev_mode:
                dependencies_complex.extend(self.metadata.core.dependencies_complex)

            self._dependencies_complex = dependencies_complex

        return self._dependencies_complex

    @property
    def dependencies(self) -> list[str]:
        """
        The list of all [project dependencies](../config/metadata.md#dependencies) (if
        [installed](../config/environment.md#skip-install) and in [dev mode](../config/environment.md#dev-mode)) and
        [environment dependencies](../config/environment.md#dependencies).
        """
        if self._dependencies is None:
            self._dependencies = [str(dependency) for dependency in self.dependencies_complex]

        return self._dependencies

    @property
    def platforms(self) -> list[str]:
        """
        All names are stored as their lower-cased version.

        === ":octicons-file-code-16: pyproject.toml"

            ```toml
            [tool.hatch.envs.<ENV_NAME>]
            platforms = [...]
            ```

        === ":octicons-file-code-16: hatch.toml"

            ```toml
            [envs.<ENV_NAME>]
            platforms = [...]
            ```
        """
        if self._platforms is None:
            platforms = self.config.get('platforms', [])
            if not isinstance(platforms, list):
                raise TypeError(f'Field `tool.hatch.envs.{self.name}.platforms` must be an array')

            for i, command in enumerate(platforms, 1):
                if not isinstance(command, str):
                    raise TypeError(f'Platform #{i} of field `tool.hatch.envs.{self.name}.platforms` must be a string')

            self._platforms = [platform.lower() for platform in platforms]

        return self._platforms

    @property
    def skip_install(self) -> bool:
        """
        === ":octicons-file-code-16: pyproject.toml"

            ```toml
            [tool.hatch.envs.<ENV_NAME>]
            skip-install = ...
            ```

        === ":octicons-file-code-16: hatch.toml"

            ```toml
            [envs.<ENV_NAME>]
            skip-install = ...
            ```
        """
        if self._skip_install is None:
            skip_install = self.config.get('skip-install', False)
            if not isinstance(skip_install, bool):
                raise TypeError(f'Field `tool.hatch.envs.{self.name}.skip-install` must be a boolean')