#
#  Copyright 2016 Red Hat | Ansible
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type


import os
import platform
import re
import sys
from datetime import timedelta
from distutils.version import LooseVersion


from ansible.module_utils.basic import AnsibleModule, env_fallback, missing_required_lib
from ansible.module_utils.common._collections_compat import Mapping, Sequence
from ansible.module_utils.six import string_types
from ansible.module_utils.six.moves.urllib.parse import urlparse
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE, BOOLEANS_FALSE

HAS_DOCKER_PY = True
HAS_DOCKER_PY_2 = False
HAS_DOCKER_PY_3 = False
HAS_DOCKER_ERROR = None

try:
    from requests.exceptions import SSLError
    from docker import __version__ as docker_version
    from docker.errors import APIError, NotFound, TLSParameterError
    from docker.tls import TLSConfig
    from docker import auth

    if LooseVersion(docker_version) >= LooseVersion('3.0.0'):
        HAS_DOCKER_PY_3 = True
        from docker import APIClient as Client
    elif LooseVersion(docker_version) >= LooseVersion('2.0.0'):
        HAS_DOCKER_PY_2 = True
        from docker import APIClient as Client
    else:
        from docker import Client

except ImportError as exc:
    HAS_DOCKER_ERROR = str(exc)
    HAS_DOCKER_PY = False


# The next 2 imports ``docker.models`` and ``docker.ssladapter`` are used
# to ensure the user does not have both ``docker`` and ``docker-py`` modules
# installed, as they utilize the same namespace are are incompatible
try:
    # docker (Docker SDK for Python >= 2.0.0)
    import docker.models  # noqa: F401
    HAS_DOCKER_MODELS = True
except ImportError:
    HAS_DOCKER_MODELS = False

try:
    # docker-py (Docker SDK for Python < 2.0.0)
    import docker.ssladapter  # noqa: F401
    HAS_DOCKER_SSLADAPTER = True
except ImportError:
    HAS_DOCKER_SSLADAPTER = False


try:
    from requests.exceptions import RequestException
except ImportError:
    # Either docker-py is no longer using requests, or docker-py isn't around either,
    # or docker-py's dependency requests is missing. In any case, define an exception
    # class RequestException so that our code doesn't break.
    class RequestException(Exception):
        pass


DEFAULT_DOCKER_HOST = 'unix://var/run/docker.sock'
DEFAULT_TLS = False
DEFAULT_TLS_VERIFY = False
DEFAULT_TLS_HOSTNAME = 'localhost'
MIN_DOCKER_VERSION = "1.8.0"
DEFAULT_TIMEOUT_SECONDS = 60

DOCKER_COMMON_ARGS = dict(
    docker_host=dict(type='str', default=DEFAULT_DOCKER_HOST, fallback=(env_fallback, ['DOCKER_HOST']), aliases=['docker_url']),
    tls_hostname=dict(type='str', default=DEFAULT_TLS_HOSTNAME, fallback=(env_fallback, ['DOCKER_TLS_HOSTNAME'])),
    api_version=dict(type='str', default='auto', fallback=(env_fallback, ['DOCKER_API_VERSION']), aliases=['docker_api_version']),
    timeout=dict(type='int', default=DEFAULT_TIMEOUT_SECONDS, fallback=(env_fallback, ['DOCKER_TIMEOUT'])),
    ca_cert=dict(type='path', aliases=['tls_ca_cert', 'cacert_path']),
    client_cert=dict(type='path', aliases=['tls_client_cert', 'cert_path']),
    client_key=dict(type='path', aliases=['tls_client_key', 'key_path']),
    ssl_version=dict(type='str', fallback=(env_fallback, ['DOCKER_SSL_VERSION'])),
    tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])),
    validate_certs=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY']), aliases=['tls_verify']),
    debug=dict(type='bool', default=False)
)

DOCKER_MUTUALLY_EXCLUSIVE = []

DOCKER_REQUIRED_TOGETHER = [
    ['client_cert', 'client_key']
]

DEFAULT_DOCKER_REGISTRY = 'https://index.docker.io/v1/'
EMAIL_REGEX = r'[^@]+@[^@]+\.[^@]+'
BYTE_SUFFIXES = [
'B', 'KB
', 'MB', 'GB', 'TB', 'PB']


if not HAS_DOCKER_PY:
    docker_version = None

    # No Docker SDK for Python. Create a place holder client to allow
    # instantiation of AnsibleModule and proper error handing
    class Client(object):  # noqa: F811
        def __init__(self, **kwargs):
            pass

    class APIError(Exception):  # noqa: F811
        pass

    class NotFound(Exception):  # noqa: F811
        pass


def is_image_name_id(name):
    """Check whether the given image name is in fact an image ID (hash)."""
    if re.match('^sha256:[0-9a-fA-F]{64}$', name):
        return True
    return False


def is_valid_tag(tag, allow_empty=False):
    """Check whether the given string is a valid docker tag name."""
    if not tag:
        return allow_empty
    # See here ("Extended description") for a definition what tags can be:
    # https://docs.docker.com/engine/reference/commandline/tag/
    return bool(re.match('^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$', tag))


def sanitize_result(data):
    """Sanitize data object for return to Ansible.

    When the data object contains types such as docker.types.containers.HostConfig,
    Ansible will fail when these are returned via exit_json or fail_json.
    HostConfig is derived from dict, but its constructor requires additional
    arguments. This function sanitizes data structures by recursively converting
    everything derived from dict to dict and everything derived from list (and tuple)
    to a list.
    """
    if isinstance(data, dict):
        return dict((k, sanitize_result(v)) for k, v in data.items())
    elif isinstance(data, (list, tuple)):
        return [sanitize_result(v) for v in data]
    else:
        return data


class DockerBaseClass(object):

    def __init__(self):
        self.debug = False

    def log(self, msg, pretty_print=False):
        pass
        # if self.debug:
        #     log_file = open('docker.log', 'a')
        #     if pretty_print:
        #         log_file.write(json.dumps(msg, sort_keys=True, indent=4, separators=(',', ': ')))
        #         log_file.write(u'\n')
        #     else:
        #         log_file.write(msg + u'\n')


def update_tls_hostname(result):
    if result['tls_hostname'] is None:
        # get default machine name from the url
        parsed_url = urlparse(result['docker_host'])
        if ':' in parsed_url.netloc:
            result['tls_hostname'] = parsed_url.netloc[:parsed_url.netloc.rindex(':')]
        else:
            result['tls_hostname'] = parsed_url


def _get_tls_config(fail_function, **kwargs):
    try:
        tls_config = TLSConfig(**kwargs)
        return tls_config
    except TLSParameterError as exc:
        fail_function("TLS config error: %s" % exc)


def get_connect_params(auth, fail_function):
    if auth['tls'] or auth['tls_verify']:
        auth['docker_host'] = auth['docker_host'].replace('tcp://', 'https://')

    if auth['tls_verify'] and auth['cert_path'] and auth['key_path']:
        # TLS with certs and host verification
        if auth['cacert_path']:
            tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']),
                                         ca_cert=auth['cacert_path'],
                                         verify=True,
                                         assert_hostname=auth['tls_hostname'],
                                         ssl_version=auth['ssl_version'],
                                         fail_function=fail_function)
        else:
            tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']),
                                         verify=True,
                                         assert_hostname=auth['tls_hos