'''Various checks for making sure a user can or can't run docker

Uses:
from opencos.utils.docker_checks import docker_ok

if docker_ok():
  # do docker stuff
else
  # print messages saying can't do docker stuff, fail
'''

from importlib import import_module
import os
from pathlib import Path
import shutil
import subprocess
import sys

from opencos import __version__ as opencos_version
from opencos.util import Colors, debug, warning, warn_once, info_once, get_thread_budget
from opencos.utils.str_helpers import strip_outer_quotes
from opencos.utils.subprocess_helpers import IS_LINUX, IS_MACOS, IS_WINDOWS

DOCKER_EXE = shutil.which('docker')
DOCKER_GROUP = 'docker'

_DOCKER_VERSION = ''
_DOCKER_OK = None
_CAN_RUN_DOCKER_WITHOUT_SUDO = None


grp = None # pylint: disable=invalid-name
if IS_LINUX:
    grp = import_module('grp') # pylint: disable=invalid-name


def get_error_str_cant_run_docker() -> str:
    '''Returns a somewhat O/S specific messsage about you couldn't run docker'''

    is_installed = bool(DOCKER_EXE)
    if not is_installed:
        return (
            'Current user cannot run docker, it does not appear to be installed or is not in PATH'
        )
    if not not_in_docker_already():
        return (
            'Current user cannot run docker, it appears we are already in a docker ENV'
            ' (./dockerenv exists). Docker-in-docker is not yet supported within opencos-eda.'
        )
    if IS_LINUX:
        return (
            'Current user cannot run docker! (user is not a member of',
            ' docker group, or requires sudo to run docker).'
        )
    if IS_MACOS:
        return (
            'Current macos user cannot run docker! (user may not have Docker Desktop'
            ' running, it needs to be. Also check if user can run docker without sudo.)'
        )
    if IS_WINDOWS:
        return (
            'Current windows user cannot run docker! (This may not be correctly implemented'
            f' for windows in opencos-eda version: {opencos_version})'
        )
    return (
        'Current user cannot run docker! (unknown operating system, not supported in'
        f' opencos-eda version: {opencos_version} -- platform: {sys.platform})'
    )

def docker_info_for_pull(
        tool: str, image: str, name: str = '_docker_pull_image', **kwargs
) -> None:
    '''
    A standard info for tools that may optionally (not required to) support docker,
    just as a reminder we don't have version information b/c image not pulled
    '''
    txt = (
        f'For {Colors.off}{Colors.cyan}--tool={tool}{Colors.info}, the default image is not',
        f'present on this system. Use: {Colors.off}{Colors.yellow}docker pull {image}{Colors.info}',
        f'or run with a different image using {Colors.off}{Colors.cyan}--docker-image=',
        f'{Colors.yellow}IMAGE{Colors.info},',
        f'see {Colors.off}{Colors.cyan}--help{Colors.info} for other options.'
    )
    info_once(*txt, info_name=f'{tool}{name}', **kwargs)


def docker_info_for_tool(
        tool: str, image: str, name: str = '_get_versions_docker', is_warning: bool = False,
        **kwargs
) -> None:
    '''
    A standard info for tools that may optionally (not required to) support docker,
    just as a reminder that they can run with --docker-run and --docker-image
    '''
    txt = (
        f'{Colors.off}{Colors.cyan}"{tool}"{Colors.info} not in path. To run with docker, use:',
        f'{Colors.off}{Colors.cyan}--docker-run{Colors.info}',
        f'{Colors.off}{Colors.cyan}--docker-image={Colors.yellow}{image}{Colors.info},',
        f'see {Colors.off}{Colors.cyan}--help{Colors.info} for other options.'
    )
    if is_warning:
        warn_once(*txt, warn_name=f'{tool}{name}', **kwargs)
    else:
        info_once(*txt, info_name=f'{tool}{name}', **kwargs)


def docker_warning_for_tool(
        tool: str, image: str, name: str = '_get_versions_docker', **kwargs
) -> None:
    '''
    A standard warning for tools that may optionally (not required to) support docker,
    just as a reminder that they can run with --docker-run and --docker-image
    '''
    docker_info_for_tool(tool, image, name, warning=True, **kwargs)



def is_docker_installed() -> bool:
    '''Checks if DOCKER_EXE is non-blank'''
    return bool(DOCKER_EXE)

def not_in_docker_already() -> bool:
    '''Returns True if we're not in docker (False if we are in docker).

    We do not yet support DinD situations, so if we are already in docker,
    make sure docker_ok() returns overall False.
    '''
    return not Path('/.dockerenv').exists()


def get_docker_version() -> str:
    '''Returns docker version (or blank str if not installed) and sets global _DOCKER_VERSION'''
    global _DOCKER_VERSION # pylint: disable=global-statement
    if _DOCKER_VERSION:
        return _DOCKER_VERSION
    if not is_docker_installed():
        return ''

    version_ret = subprocess.run(
        [DOCKER_EXE, '--version'],
        capture_output=True,
        check=False
    )
    # Docker version 29.2.0, build 0b9d198
    stdout = version_ret.stdout.decode('utf-8', errors='replace')
    debug(f'{DOCKER_EXE=} {version_ret=}')
    words = stdout.split()
    if len(words) < 1:
        warning(
            f'{DOCKER_EXE} --version: returned unexpected string {version_ret=}'
        )
    _DOCKER_VERSION = words[2].rstrip(',')
    return _DOCKER_VERSION


def can_run_docker_without_sudo() -> bool:
    '''Checks if the user can run 'docker' with sudo

    Note this command is slow until it has been run and cached.
    '''
    global _CAN_RUN_DOCKER_WITHOUT_SUDO # pylint: disable=global-statement
    if _CAN_RUN_DOCKER_WITHOUT_SUDO is not None:
        return _CAN_RUN_DOCKER_WITHOUT_SUDO
    if not is_docker_installed():
        _CAN_RUN_DOCKER_WITHOUT_SUDO = False

    else:
        try:
            # 'docker info' requires connection to the daemon
            # Using check_call ensures we catch non-zero exit codes
            subprocess.check_call([DOCKER_EXE, 'info'],
                                 stdout=subprocess.DEVNULL,
                                 stderr=subprocess.DEVNULL)
            _CAN_RUN_DOCKER_WITHOUT_SUDO = True
        except (subprocess.CalledProcessError, FileNotFoundError):
            _CAN_RUN_DOCKER_WITHOUT_SUDO = False

    return _CAN_RUN_DOCKER_WITHOUT_SUDO


def is_in_docker_group_or_non_linux() -> bool:
    '''Checks if the user is in a group named 'docker'.

    Note this applies to Linux-only. Macos supports groups but they don't need
    docker group for Docker Desktop'''
    if not IS_LINUX:
        return True
    if not grp:
        return False
    try:
        docker_gid = grp.getgrnam(DOCKER_GROUP).gr_gid
        return docker_gid in os.getgroups() # pylint: disable=no-member
    except KeyError:
        # Group 'docker' doesn't even exist on this system
        return False


def is_docker_ok_windows() -> bool:
    '''Checks for docker support in Windows'''
    if not IS_WINDOWS:
        return True

    return False


def docker_ok() -> bool:
    '''Sets DOCKER_OK to True/False if unset, returns _DOCKER_OK'''
    global _DOCKER_OK # pylint: disable=global-statement
    if _DOCKER_OK is None:
        _DOCKER_OK = all(
            (
                is_docker_installed(),
                not_in_docker_already(),
                get_docker_version(),
                can_run_docker_without_sudo(),
                is_in_docker_group_or_non_linux(),
                is_docker_ok_windows(),
            )
        )
    return _DOCKER_OK


def get_docker_env_from_config(config: dict, tool: str, image: str) -> dict:
    '''Gets the env from config.tools.TOOL for this docker image'''
    tool_cfg = config.get('tools', {}).get(tool, {})
    if not tool_cfg or not image:
        return {}
    if not tool_cfg.get('docker-support', False):
        return {}
    image_no_tag = image.split(':')[0]
    return tool_cfg.get('docker-env', {}).get(image_no_tag, {})


def get_docker_env(cmd_des_obj: object) -> dict:
    '''Gets the env from cmd_des_obj.config.tools.TOOL and cmd_des_obj.args[docker-image]'''
    config = getattr(cmd_des_obj, 'config', None)
    tool = getattr(cmd_des_obj, '_TOOL', None)
    image = getattr(cmd_des_obj, 'args', {}).get('docker-image', '')
    if not config or not tool:
        return {}
    if not image:
        # try the Tool.DEFAULT_DOCKER_IMAGE:
        image = getattr(cmd_des_obj, 'DEFAULT_DOCKER_IMAGE', '')
    if not image:
        return {}
    return get_docker_env_from_config(config=config, tool=tool, image=image)


def get_docker_run_command( # pylint: disable=dangerous-default-value
        image: str,
        docker_command: str = 'run', # or 'exec'
        user: str = '$(id -u):$(id -g)',
        env_dict: dict = {},
        ro_volumes: list = [],
        rw_volumes: list = [],
        entrypoint: str = '/bin/bash',
        commands: list = [],
        threads: int = 1
) -> list:
    '''Returns list (suitable for subprocess) of: docker run ... or docker exec ...

    If using commands, for python subprocess, you may want to use:
        commands=[
            '-c', 'Put All Bash Command Here no escaped single quotes'
        ]

    If using this to write to a .sh script, you will want commands to be
        [
            '-c', '\'cd somewhere && ./run_thing.sh\''
        ]
    '''

    ret = [
        'docker', docker_command, '--rm', f'--cpus={max(1, (min(threads, get_thread_budget())))}'
    ]
    if user and not IS_WINDOWS:
        ret.extend([
            '--user', user
        ])
    for k,v in env_dict.items():
        ret.extend(['-e', f'"{strip_outer_quotes(k)}={strip_outer_quotes(v)}"'])
    for v in ro_volumes:
        assert isinstance(v, (str, Path, tuple)), f'{type(v)=} {v=} must be Path|str|tuple'
        if isinstance(v, tuple):
            assert len(v) == 2, f'Need src, dst in ro_volumes entry {v}'
            ret.extend(['-v', f'{v[0]}:{v[1]}:ro'])
        else:
            ret.extend(['-v', f'{v}:{v}:ro'])
    for v in rw_volumes:
        assert isinstance(v, (str, Path, tuple)), f'{type(v)=} {v=} must be Path|str|tuple'
        if isinstance(v, tuple):
            assert len(v) == 2, f'Need src, dst in rw_volumes entry {v}'
            ret.extend(['-v', f'{v[0]}:{v[1]}:rw'])
        else:
            ret.extend(['-v', f'{v}:{v}:rw'])

    if entrypoint:
        ret.extend([
            '--entrypoint', entrypoint
        ])

    assert image, f'docker {image=} is missing, must set'
    ret.append(image)

    if commands:
        ret.extend(commands)

    return ret


def check_image_available(image: str) -> bool:
    '''subprocess check if the docker image has been pulled and present on this system'''

    result = subprocess.run(["docker", "image", "inspect", image],
                            capture_output=True, check=False)
    return result.returncode == 0


def run_docker_for_version_info( # pylint: disable=dangerous-default-value
        image: str,
        docker_command: str = 'run', # or 'exec'
        user: str = '',
        env_dict: dict = {},
        entrypoint: str = '/bin/bash',
        commands: list = []
) -> list:
    '''Returns list (suitable for subprocess) of: docker run ... or docker exec ...

    Avoids volume mapping, runs 'docker run' with --rm, runs as default (root) user'''

    cmd_list = get_docker_run_command(
        image=image, docker_command=docker_command,
        user=user, env_dict=env_dict, entrypoint=entrypoint,
        commands=commands
    )
    ret = subprocess.run(cmd_list, capture_output=True, check=False)
    if ret:
        return ret.stdout.decode('utf-8', errors='replace').strip()
    return ''
