# built-in
import subprocess
import sys
from argparse import ArgumentParser
from os import environ, name as os_name, pathsep
from pathlib import Path
from shutil import rmtree
from venv import create


POST_MESSAGE = """DepHell is installed now. Great!

DepHell was added in your PATH. Please, restart your shell.
"""

POST_MESSAGE_NO_MODIFY_PATH = """DepHell is installed now. Great!

Please, add DepHell's bin directory ({dephell_home_bin}) in your `PATH`
environment variable.
"""


# https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
# https://github.com/pydanny/cached-property/blob/master/cached_property.py
class cached_property(object):  # noqa: N801
    """
    A property that is only computed once per instance and then replaces itself
    with an ordinary attribute. Deleting the attribute resets the property.
    """

    def __init__(self, func):
        self.__doc__ = func.__doc__
        self.func = func

    def __get__(self, obj, cls):
        if obj is None:
            return self
        value = obj.__dict__[self.func.__name__] = self.func(obj)
        return value


class Context:
    @cached_property
    def is_windows(self) -> bool:
        if sys.platform.startswith('win'):
            return True
        if sys.platform == 'cli' and os_name == 'nt':
            return True
        return False

    @cached_property
    def dephell_name(self) -> str:
        if self.is_windows:
            return 'dephell.exe'
        return 'dephell'

    @cached_property
    def python_name(self) -> str:
        if self.is_windows:
            return 'python.exe'
        return 'python3'

    @cached_property
    def parser(self):
        parser = ArgumentParser()
        parser.add_argument('--branch', help='install dephell from git from given branch')
        parser.add_argument('--version', help='install specified version')
        parser.add_argument('--slug', default='dephell/dephell',
                            help='repository slug to use when installing from Github')
        return parser

    @cached_property
    def args(self):
        return self.parser.parse_args()

    @cached_property
    def pip_main(self):
        # install
        try:
            import pip  # noQA: F401
        except ImportError:
            print('install pip')
            from ensurepip import bootstrap
            bootstrap()

        # import
        try:
            from pip._internal.main import main
        except ImportError:
            try:
                from pip._internal import main
            except ImportError:
                from pip import main

        return main

    @cached_property
    def data_dir(self) -> Path:
        try:
            from appdirs import user_data_dir
        except ImportError:

            # linux
            path = Path.home() / '.local' / 'share'
            if path.exists():
                return path / 'dephell'

            # mac os
            path = Path.home() / 'Library' / 'Application Support'
            if path.exists():
                return path / 'dephell'

            self.pip_main(['install', 'appdirs'])
            from appdirs import user_data_dir

        return Path(user_data_dir('dephell'))

    @cached_property
    def venv_path(self):
        return self.data_dir / 'venvs' / 'dephell'

    @cached_property
    def python_path(self):
        """path to python in venv
        """
        pythons = self.venv_path.glob('*/{}'.format(self.python_name))
        return list(pythons)[0]

    @cached_property
    def bin_dir(self) -> Path:
        """Global directory from PATH to simlink dephell's binary
        """
        path = Path.home() / '.local' / 'bin'
        if path.exists():
            return path
        paths = [Path(path) for path in environ.get('PATH', '').split(pathsep)]
        for path in paths:
            if path.exists() and '.local' in path.parts:
                return path
        for path in paths:
            if path.exists():
                return path
        raise LookupError('cannot find place to install binary', paths)

    # actions

    def make_venv(self) -> None:
        self.pip_main  # install pip before all to have it in the venv
        if self.venv_path.exists():
            rmtree(str(self.venv_path))
        create(str(self.venv_path), with_pip=True)

    def upgrade_pip(self) -> None:
        command = [str(self.python_path), '-m', 'pip', 'install', '-U', 'pip']
        result = subprocess.run(command)
        if result.returncode != 0:
            # try again, pip is nightly
            result = subprocess.run(command)
            if result.returncode != 0:
                exit(result.returncode)

    def install_dephell(self):
        if self.args.branch:
            name = 'git+https://github.com/{slug}.git@{branch}#egg=dephell[full]'
            name = name.format(
                slug=self.args.slug,
                branch=self.args.version or self.args.branch,
            )
        elif self.args.version:
            name = 'dephell[full]=={version}'.format(version=self.args.version)
        else:
            name = 'dephell[full]'
        result = subprocess.run([str(self.python_path), '-m', 'pip', 'install', name])
        if result.returncode != 0:
            exit(result.returncode)

    def copy_binary(self):
        dephell_home_bin = self.python_path.parent
        local_path = dephell_home_bin / self.dephell_name
        if not local_path.exists():
            print('DepHell binary not found')
            exit(1)
        global_path = self.bin_dir / self.dephell_name
        if global_path.exists() or global_path.is_symlink():
            global_path.unlink()
        try:
            global_path.symlink_to(local_path)
        except OSError:
            return False
        return True


if __name__ == '__main__':
    context = Context()
    print('make venv')
    context.make_venv()
    print('upgrade pip')
    context.upgrade_pip()
    print('install dephell')
    context.install_dephell()
    print('copy binary dephell')
    modified_path = context.copy_binary()

    kwargs = {
        'dephell_home_bin': context.python_path.parent,
    }
    if modified_path:
        print(POST_MESSAGE.format(**kwargs))
    else:
        print(POST_MESSAGE_NO_MODIFY_PATH.format(**kwargs))