# coding=utf-8
"""Author: Konrad Zemek, Jakub Liput
Copyright (C) 2015 ACK CYFRONET AGH
Copyright (C) 2026 Onedata (onedata.org)
This software is released under the MIT license cited in 'LICENSE.txt'

Functions wrapping capabilities of docker binary.
"""

import json
import os
import subprocess
import sys

PULL_DOCKER_IMAGE_RETRIES = 5


# Adds a bind-mount consistency option depending on the container's access_level.
# This option applies to macOS only, otherwise is ignored by Docker. It relaxes
# the consistency guarantees between the host and container:
#   * cached - the host is authoritative, writes on host may not be immediately
#              visible on the container. Improves performance of read-heavy
#              workloads.
#   * delegated - the container is authoritative, writes on the container may
#                 not be immediately visible on the host, but they will be
#                 flushed before the container exit. Improves performance of
#                 write-heavy workloads.
def with_consistency_opt(access_level):
    if access_level == 'ro':
        return 'ro,cached'
    elif access_level == 'rw':
        return 'rw,delegated'


# noinspection PyDefaultArgument
def run(image, docker_host=None, detach=False, dns_list=[], add_host={},
        envs={}, hostname=None, interactive=False, link={}, tty=False,
        rm=False, reflect=[], volumes=[], name=None, workdir=None, user=None,
        group=None, group_add=[], cpuset_cpus=None, privileged=False,
        publish=[], run_params=[], command=None, output=False, stdin=None,
        stdout=None, stderr=None, network=None):
    cmd = ['docker']

    cmd.append('run')

    if detach:
        cmd.append('-d')

    for addr in dns_list:
        cmd.extend(['--dns', addr])

    for key, value in list(add_host.items()):
        cmd.extend(['--add-host', '{0}:{1}'.format(key, value)])

    for key in envs:
        cmd.extend(['-e', '{0}={1}'.format(key, envs[key])])

    if hostname:
        cmd.extend(['-h', hostname])

    if network:
        cmd.extend(['--network', network])

    if detach or sys.__stdin__.isatty():
        if interactive:
            cmd.append('-i')
        if tty:
            cmd.append('-t')

    for container, alias in list(link.items()):
        cmd.extend(['--link', '{0}:{1}'.format(container, alias)])

    if name:
        cmd.extend(['--name', name])

    if rm:
        cmd.append('--rm')

    for path, access_level in reflect:
        vol = '{0}:{0}:{1}'.format(
            os.path.abspath(path), with_consistency_opt(access_level)
        )
        cmd.extend(['-v', vol])

    # Volume can be in one of three forms
    # 1. 'path_on_docker'
    # 2. ('path_on_host', 'path_on_docker', 'ro'/'rw')
    # 3. {'volumes_from': 'volume name'}
    for entry in volumes:
        if isinstance(entry, tuple):
            path, bind, access_level = entry
            vol = '{0}:{1}:{2}'.format(
                os.path.abspath(path), bind, with_consistency_opt(access_level)
            )
            cmd.extend(['-v', vol])
        elif isinstance(entry, dict):
            volume_name = entry['volumes_from']
            cmd.extend(['--volumes-from', volume_name])
        else:
            cmd.extend(['-v', entry])

    if workdir:
        cmd.extend(['-w', os.path.abspath(workdir)])

    if user:
        user_group = '{0}:{1}'.format(user, group) if group else user
        cmd.extend(['-u', user_group])

    for g in group_add:
        cmd.extend(['--group-add', g])

    if privileged:
        cmd.append('--privileged')

    for port in publish:
        if isinstance(port, tuple):
            cmd.extend(['-p', '{0}:{1}'.format(port[0], port[1])])
        else:
            cmd.extend(['-p', '{0}:{0}'.format(port)])

    if cpuset_cpus:
        cmd.extend(['--cpuset-cpus', cpuset_cpus])

    cmd.extend(run_params)
    cmd.append(image)

    cmd = format_command(cmd, command, docker_host)

    if detach or output:
        return subprocess.check_output(cmd, stdin=stdin, stderr=stderr).decode(
            'utf-8').strip()

    return subprocess.call(cmd, stdin=stdin, stderr=stderr, stdout=stdout)


def exec_(container, command, docker_host=None, user=None, group=None,
          detach=False, interactive=False, tty=False, privileged=False,
          output=False, stdin=None, stdout=None, stderr=None):
    cmd = ['docker']

    cmd.append('exec')

    if user:
        user_group = '{0}:{1}'.format(user, group) if group else user
        cmd.extend(['-u', user_group])

    if detach:
        cmd.append('-d')

    if detach or sys.__stdin__.isatty():
        if interactive:
            cmd.append('-i')
        if tty:
            cmd.append('-t')

    if privileged:
        cmd.append('--privileged')

    cmd.append(container)

    cmd = format_command(cmd, command, docker_host)

    if detach or output:
        return subprocess.check_output(cmd, stdin=stdin, stderr=stderr).decode(
            'utf-8').strip()

    return subprocess.call(cmd, stdin=stdin, stderr=stderr, stdout=stdout)


def inspect(container, docker_host=None, timeout=None, stderr=None):
    cmd = ['docker']

    cmd.extend(['inspect', container])

    if docker_host:
        cmd = wrap_in_ssh_call(cmd, docker_host)

    if timeout is not None:
        cmd = add_timeout_cmd(cmd, timeout)

    out = subprocess.check_output(cmd, universal_newlines=True, stderr=stderr)
    return json.loads(out)[0]


def logs(container, docker_host=None):
    cmd = ['docker']

    cmd.extend(['logs', container])

    if docker_host:
        cmd = wrap_in_ssh_call(cmd, docker_host)

    return subprocess.check_output(cmd, universal_newlines=True,
                                   stderr=subprocess.STDOUT)


def remove(containers, docker_host=None, force=False,
           link=False, volumes=False, timeout=None, stderr=None):
    cmd = ['docker']

    cmd.append('rm')

    if force:
        cmd.append('-f')

    if link:
        cmd.append('-l')

    if volumes:
        cmd.append('-v')

    if isinstance(containers, str):
        cmd.append(containers)
    else:
        cmd.extend(containers)

    if docker_host:
        cmd = wrap_in_ssh_call(cmd, docker_host)

    if timeout is not None:
        cmd = add_timeout_cmd(cmd, timeout)

    subprocess.check_call(cmd, stderr=stderr)


def cp(container, src_path, dest_path, to_container=False, docker_host=None):
    """Copying file between docker container and host
    :param container: str, docker id or name
    :param src_path: str
    :param dest_path: str
    :param to_container: bool, if True file will be copied from host to
    container, otherwise from docker container to host
    :param docker_host: dict
    """
    cmd = ["docker", "cp"]
    if to_container:
        cmd.extend([src_path, "{0}:{1}".format(container, dest_path)])
    else:
        cmd.extend(["{0}:{1}".format(container, src_path), dest_path])

    if docker_host:
        cmd = wrap_in_ssh_call(cmd, docker_host)

    subprocess.check_call(cmd)


def login(user, password, repository='hub.docker.com'):
    """Logs into docker repository."""

    subprocess.check_call(['docker', 'login', '-u', user, '-p', password,
                           repository])


def build_image(image, build_args):
    """Builds and tags docker image."""

    subprocess.check_call(['docker', 'build', '--no-cache', '--force-rm', '-t',
                           image] + build_args)


def tag_image(image, tag):
    """Tags docker image."""

    subprocess.check_call(['docker', 'tag', image, tag])


def push_image(image):
    """Pushes docker image to the repository."""

    subprocess.check_call(['docker', 'push', image])


def pull_image(image):
    """Pulls docker image from the repository."""

    subprocess.check_call(['docker', 'pull', image])


def pull_image_with_retries(image, retries=PULL_DOCKER_IMAGE_RETRIES):
    attempts = 0

    while attempts < retries:
        try:
            pull_image(image)
        except subprocess.CalledProcessError as e:
            attempts += 1
            if attempts >= retries:
                print('Could not download image {}. Tried {} times. \n'
                      'Captured output from last call: {} \n'
                      .format(image, retries, e.output))
                raise
        else:
            return


def image_exists(image):
    """Checks whether docker image is in the repository."""

    with open(os.devnull, 'w') as DEVNULL:
        return 0 == subprocess.call(
            ['docker', 'manifest', 'inspect', image],
            stdout=DEVNULL,
            stderr=DEVNULL
        )


def remove_image(image):
    """Removes docker image."""

    subprocess.check_call(['docker', 'rmi', '-f', image])


def create_volume(path, name, image, command):
    cmd = ['docker']

    cmd.append('create')
    cmd.append('-v')
    cmd.append(path)

    cmd.append('--name')
    cmd.append(name)

    cmd.append(image)

    cmd.append(command)

    return subprocess.check_output(cmd, universal_newlines=True,
                                   stderr=subprocess.STDOUT)


def ps(all=False, quiet=False, filters=None):
    """
    List containers
    """
    cmd = ["docker", "ps"]
    if all:
        cmd.append("--all")
    if quiet:
        cmd.append("--quiet")
    if filters:
        for f in filters:
            cmd.extend(['-f', '{}={}'.format(f[0], f[1])])

    return subprocess.check_output(cmd, universal_newlines=True).split()


def list_volumes(quiet=True):
    """
    List volumes
    """
    cmd = ["docker", "volume", "ls"]
    if quiet:
        cmd.append("--quiet")
    return subprocess.check_output(cmd,  universal_newlines=True).split()


def remove_volumes(volumes, timeout=None, stderr=None):
    """
    Remove volumes
    """
    cmd = ["docker", "volume", "rm"]
    if isinstance(volumes, str):
        cmd.append(volumes)
    else:
        cmd.extend(volumes)

    if timeout is not None:
        cmd = add_timeout_cmd(cmd, timeout)

    return subprocess.check_call(cmd, stderr=stderr)


def connect_docker_to_network(network, container):
    """
    Connect docker to the network
    Useful when dockers are in different subnetworks and they need to see each other using IP address
    """

    subprocess.check_call(['docker', 'network', 'connect', network, container])


def format_command(docker_cmd, entry_point, docker_host):
    if isinstance(entry_point, str) and docker_host is not None:
        docker_cmd.extend(['sh', '-c', '"' + entry_point + '"'])
    elif isinstance(entry_point, str):
        docker_cmd.extend(['sh', '-c', entry_point])
    elif isinstance(entry_point, list):
        docker_cmd.extend(entry_point)
    elif entry_point is not None:
        raise ValueError('{0} is not a string nor list'.format(entry_point))

    if docker_host:
        docker_cmd = wrap_in_ssh_call(docker_cmd, docker_host)

    return docker_cmd


def wrap_in_ssh_call(docker_cmd, docker_host):
    username = docker_host['ssh_username']
    hostname = docker_host['ssh_hostname']
    port = docker_host['ssh_port'] if 'ssh_port' in docker_host else 22
    return ['ssh', '-p', port, '{0}@{1}'.format(username, hostname), ' '.join(docker_cmd)]


def add_timeout_cmd(cmd, timeout):
    timeout_cmd = ['timeout', '--kill-after', str(timeout), str(timeout)]
    return timeout_cmd + cmd
