# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import logging
from time import sleep

from requests import Timeout
from six import text_type, iteritems, string_types
from six.moves import map

from ...docker_api import HostConfig, NetworkingConfig, EndpointConfig
from ...functional import resolve_value
from ...utils import format_image_tag
from ..action import Action
from ..config.client import USE_HC_MERGE
from ..input import ItemType, NotSet
from ..policy.utils import init_options, extract_user
from . import AbstractRunner
from .attached import AttachedPreparationMixin
from .cmd import ExecMixin
from .image import ImageMixin
from .network import NetworkUtilMixin
from .script import ScriptMixin
from .signal_stop import SignalMixin
from .utils import update_kwargs, get_volumes, get_volumes_from, get_host_binds, get_port_bindings

log = logging.getLogger(__name__)


class DockerBaseRunnerMixin(object):
    action_method_names = [
        (ItemType.NETWORK, Action.CREATE, 'create_network'),
        (ItemType.NETWORK, Action.REMOVE, 'remove_network'),

        (ItemType.VOLUME, Action.CREATE, 'create_volume'),
        (ItemType.VOLUME, Action.REMOVE, 'remove_volume'),

        (ItemType.CONTAINER, Action.CREATE, 'create_container'),
        (ItemType.CONTAINER, Action.START, 'start_container'),
        (ItemType.CONTAINER, Action.RESTART, 'restart'),
        (ItemType.CONTAINER, Action.STOP, 'stop'),
        (ItemType.CONTAINER, Action.REMOVE, 'remove_container'),
        (ItemType.CONTAINER, Action.UPDATE, 'update_container'),
        (ItemType.CONTAINER, Action.KILL, 'kill'),
        (ItemType.CONTAINER, Action.WAIT, 'wait'),
    ]

    def create_network(self, action, n_name, **kwargs):
        """
        Creates a configured network.

        :param action: Action configuration.
        :type action: dockermap.map.runner.ActionConfig
        :param n_name: Network name.
        :type n_name: unicode | str
        :param kwargs: Additional keyword arguments to complement or override the configuration-based values.
        :type kwargs: dict
        """
        c_kwargs = self.get_network_create_kwargs(action, n_name, **kwargs)
        res = action.client.create_network(**c_kwargs)
        self._policy.network_names[action.client_name][n_name] = res['Id']
        return res

    def remove_network(self, action, n_name, **kwargs):
        """
        Removes a network.

        :param action: Action configuration.
        :type action: dockermap.map.runner.ActionConfig
        :param n_name: Network name or id.
        :type n_name: unicode | str
        :param kwargs: Additional keyword arguments.
        :type kwargs: dict
        """
        c_kwargs = self.get_network_remove_kwargs(action, n_name, **kwargs)
        res = action.client.remove_network(**c_kwargs)
        del self._policy.network_names[action.client_name][n_name]
        return res

    def create_volume(self, action, v_name, **kwargs):
        if action.client_config.features['volumes']:
            c_kwargs = self.get_volume_create_kwargs(action, v_name, kwargs=kwargs)
            res = action.client.create_volume(**c_kwargs)
            self._policy.volume_names[action.client_name].add(v_name)
            return res
        c_kwargs = self.get_attached_container_create_kwargs(action, v_name, kwargs=kwargs)
        res = action.client.create_container(**c_kwargs)
        self._policy.container_names[action.client_name][v_name] = res['Id']
        if action.client_config.features['host_config']:
            action.client.start(v_name)
        else:
            c_kwargs = self.get_attached_container_host_config_kwargs(action, v_name, kwargs=kwargs)
            action.client.start(**c_kwargs)
        return res

    def remove_volume(self, action, v_name, **kwargs):
        if action.client_config.features['volumes']:
            c_kwargs = self.get_volume_create_kwargs(action, v_name, kwargs=kwargs)
            res = action.client.remove_volume(**c_kwargs)
            self._policy.volume_names[action.client_name].discard(v_name)
            return res
        c_kwargs = self.get_container_remove_kwargs(action, v_name, kwargs=kwargs)
        res = action.client.remove_container(**c_kwargs)
        del self._policy.container_names[action.client_name][v_name]
        return res

    def create_container(self, action, c_name, **kwargs):
        c_kwargs = self.get_container_create_kwargs(action, c_name, kwargs=kwargs)
        res = action.client.create_container(**c_kwargs)
        self._policy.container_names[action.client_name][c_name] = res['Id']
        return res

    def start_container(self, action, c_name, **kwargs):
        if action.client_config.features['host_config']:
            res = action.client.start(c_name)
        else:
            c_kwargs = self.get_container_host_config_kwargs(action, c_name, kwargs=kwargs)
            res = action.client.start(**c_kwargs)
        start_delay = action.config.start_delay
        if start_delay:
            log.debug("Sleeping %s seconds after container %s start.", start_delay, c_name)
            sleep(start_delay)
        return res

    def restart(self, action, c_name, **kwargs):
        c_kwargs = self.get_container_restart_kwargs(action, c_name, kwargs=kwargs)
        return action.client.restart(**c_kwargs)

    def stop(self, action, c_name, **kwargs):
        c_kwargs = self.get_container_stop_kwargs(action, c_name, kwargs=kwargs)
        try:
            return action.client.stop(**c_kwargs)
        except Timeout:
            log.warning("Container %s did not stop in time - sent SIGKILL.", c_name)
            try:
                action.client.wait(c_name, timeout=c_kwargs.get('timeout', 10))
            except Timeout:
                pass
        return None

    def remove_container(self, action, c_name, **kwargs):
        c_kwargs = self.get_container_remove_kwargs(action, c_name, kwargs=kwargs)
        res = action.client.remove_container(**c_kwargs)
        del self._policy.container_names[action.client_name][c_name]
        return res

    def update_container(self, action, c_name, update_values, **kwargs):
        c_kwargs = self.get_container_update_kwargs(action, c_name, update_values, kwargs=kwargs)
        return action.client.update_container(**c_kwargs)

    def kill(self, action, c_name, **kwargs):
        c_kwargs = self.get_container_kill_kwargs(action, c_name, kwargs=kwargs)
        return action.client.kill(**c_kwargs)

    def wait(self, action, c_name, **kwargs):
        c_kwargs = self.get_container_wait_kwargs(action, c_name, kwargs=kwargs)
        return action.client.wait(**c_kwargs)


class DockerConfigMixin(object):
    def get_container_create_kwargs(self, action, container_name, kwargs=None):
        """
        Generates keyword arguments for the Docker client to create a container.

        :param action: Action configuration.
        :type action: ActionConfig
        :param container_name: Container name.
        :type container_name: unicode | str
        :param kwargs: Additional keyword arguments to complement or override the configuration-based values.
        :type kwargs: dict | NoneType
        :return: Resulting keyword arguments.
        :rtype: dict
        """
        policy = self._policy
        client_config = action.client_config
        container_map = action.container_map
        container_config = action.config
        image_tag = container_map.get_image(container_config.image or action.config_id.config_name)
        default_paths = policy.default_volume_paths[action.config_id.map_name]
        c_kwargs = dict(
            name=container_name,
            image=format_image_tag(image_tag),
            volumes=get_volumes(container_map, container_config, default_paths,
                                client_config.features['volumes']),
            user=extract_user(container_config.user),
            ports=[resolve_value(port_binding.exposed_port)
                   for port_binding in container_config.exposes if port_binding.exposed_port],
            domainname=resolve_value(client_config.get('domainname', container_map.default_domain)) or None,
        )
        if container_map.set_hostname or container_map.set_hostname is NotSet:
            c_kwargs['hostname'] = policy.get_hostname(container_name, action.client_name)
        if container_config.network_mode == 'none':
            c_kwargs['network_disabled'] = True
        elif client_config.features['networks'] and container_config.networks:
            first_network = container_config.networks[0]
            c_kwargs['networking_config'] = NetworkingConfig({
                policy.nname(action.config_id.map_name, first_network.network_name): EndpointConfig(
                    client_config.version, **self.get_network_create_endpoint_kwargs(action, first_network)
                )
            })
        if client_config.features['stop_signal'] and container_config.stop_signal:
            c_kwargs['stop_signal'] = container_config.stop_signal
        hc_extra_kwargs = kwargs.pop('host_config', None) if kwargs else None
        use_host_config = client_config.features['host_config']
        if use_host_config:
            hc_kwargs = self.get_container_host_config_kwargs(action, None, kwargs=hc_extra_kwargs)
            if hc_kwargs:
                if use_host_config == USE_HC_MERGE:
                    c_kwargs.update(hc_kwargs)
                else:
                    c_kwargs['host_config'] = HostConfig(version=client_config.version, **hc_kwargs)
        if client_config.features['stop_timeout'] and container_config.stop_timeout:
            c_kwargs['stop_timeout'] = container_config.stop_timeout
        if client_config.features['healthcheck'] and container_config.healthcheck:
            c_kwargs['healthcheck'] = container_config.healthcheck._asdict()
        update_kwargs(c_kwargs, init_options(container_config.create_options), kwargs)
        return c_kwargs

    def get_container_host_config_kwargs(self, action, container_name, kwargs=None):
        """
        Generates keyword arguments for the Docker client to set up the HostConfig or start a container.

        :param action: Action configuration.
        :type action: ActionConfig
        :param container_name: Container name or id. Set ``None`` when included in kwargs for ``create_container``.
        :type container_name: unicode | str | NoneType
        :param kwargs: Additional keyword arguments to complement or override the configuration-based values.
        :type kwargs: dict | NoneType
        :return: Resulting keyword arguments.
        :rtype: dict
        """
        container_map = action.container_map
        container_config = action.config
        client_config = action.client_config
        config_id = action.config_id
        map_name = config_id.map_name
        policy = self._policy
        cname = policy.cname
        supports_volumes = client_config.features['volumes']

        c_kwargs = dict(
            links=[(cname(map_name, l_name), alias or policy.get_hostname(l_name))
                   for l_name, alias in container_config.links],
            binds=get_host_binds(container_map, config_id.config_name, container_config, config_id.instance_name,
                                 policy, supports_volumes),
            volumes_from=get_volumes_from(container_map, config_id.config_name, container_config,
                                          policy, not supports_volumes),
            port_bindings=get_port_bindings(container_config, client_config),
        )
        network_mode = container_config.network_mode
        if isinstance(network_mode, tuple):
            c_kwargs['network_mode'] = 'container:{0}'.format(cname(map_name, *network_mode))
        elif isinstance(network_mode, string_types):
            c_kwargs['network_mode'] = network_mode
        if container_name:
            c_kwargs['container'] = container_name
        update_kwargs(c_kwargs, init_options(container_config.host_config), kwargs)
        return c_kwargs

    def get_attached_container_create_kwargs(self, action, container_name, kwargs=None):
        """
        Generates keyword arguments for the Docker client to create an attached container.

        :param action: Action configuration.
        :type action: ActionConfig
        :param container_name: Container name.
        :type container_name: unicode | str
        :param kwargs: Additional keyword arguments to complement or override the configuration-based values.
        :type kwargs: dict | NoneType
        :return: Resulting keyword arguments.
        :rtype: dict
        """
        client_config = action.client_config
        policy = self._policy
        config_id = action.config_id
        path = resolve_value(policy.default_volume_paths[config_id.map_name][config_id.instance_name])
        user = extract_user(action.config.user)
        c_kwargs = dict(
            name=container_name,
            image=self._policy.base_image,
            volumes=[path],
            user=user,
            network_disabled=True,
        )
        hc_extra_kwargs = kwargs.pop('host_config', None) if kwargs else None
        use_host_config = client_config.features['host_config']
        if use_host_config:
            hc_kwargs = self.get_attached_container_host_config_kwargs(action, None, kwargs=hc_extra_kwargs)
            if hc_kwargs:
                if use_host_config == USE_HC_MERGE:
                    c_kwargs.update(hc_kwargs)
                else:
                    c_kwargs['host_config'] = HostConfig(version=client_config.version, **hc_kwargs)
        update_kwargs(c_kwargs, kwargs)
        return c_kwargs

    def get_attached_container_host_config_kwargs(self, action, container_name, kwargs=None):
        """
        Generates keyword arguments for the Docker client to set up the HostConfig or start an attached container.

        :param action: Action configuration.
        :type action: ActionConfig
        :param container_name: Container name or id. Set ``None`` when included in kwargs for ``create_container``.
        :type container_name: unicode | str | NoneType
        :param kwargs: Additional keyword arguments to complement or override the configuration-based values.
        :type kwargs: dict | NoneType
        :return: Resulting keyword arguments.
        :rtype: dict
        """
        if container_name:
            c_kwargs = {'container': container_name}
        else:
            c_kwargs = {}
        update_kwargs(c_kwargs, kwargs)
        return c_kwargs

    def get_container_update_kwargs(self, action, container_name, update_values, kwargs=None):
        """
        Generates keyword arguments for the Docker client to update the HostConfig of an existing container.

        :param action: Action configuration.
        :type action: ActionConfig
        :param container_name: Container name or id.
        :type container_name: unicode | str
        :param update_values: Dictionary of values to update; i.e. keyword arguments to the Docker client.
        :type update_values: dict[unicode | str, unicode | str | int | float | decimal.Decimal]
        :param kwargs: Additional keyword arguments to complement or override the configuration-based values.
        :type kwargs: dict | NoneType
        :return: Resulting keyword arguments.
        :rtype: dict
        """
        c_kwargs = dict(container=container_name)
        update_kwargs(c_kwargs, update_values, kwargs)
        return c_kwargs

    def get_container_restart_kwargs(self, action, container_name, kwargs=None):
        """
        Generates keyword arguments for the Docker client to restart a container.

        :param action: Action configuration.
        :type action: ActionConfig
        :param container_name: Container name or id.
        :type container_name: unicode | str
        :param kwargs: Additional keyword arguments to complement or override the configuration-based values.
        :type kwargs: dict
        :return: Resulting keyword arguments.
        :rtype: dict
        """
        c_kwargs = dict(container=container_name)
        stop_timeout = action.config.stop_timeout
        if stop_timeout is NotSet:
            timeout = action.client_config.get('stop_timeout')
            if timeout is not None:
                c_kwargs['timeout'] = timeout
        elif stop_timeout is not None:
            c_kwargs['timeout'] = stop_timeout
        update_kwargs(c_kwargs, kwargs)
        return c_kwargs

    def get_container_wait_kwargs(self, action, container_name, kwargs=None):
        """
        Generates keyword arguments for the Docker client to wait for a container.

        :param action: Action configuration.
        :type action: ActionConfig
        :param container_name: Container name or id.
        :type container_name: unicode | str
        :param kwargs: Additional keyword arguments to complement or override the configuration-based values.
        :type kwargs: dict
        :return: Resulting keyword arguments.
        :rtype: dict
        """
        c_kwargs = dict(container=container_name)
        timeout = action.client_config.get('wait_timeout')
        if timeout is not None:
            c_kwargs['timeout'] = timeout
        update_kwargs(c_kwargs, kwargs)
        return c_kwargs

    def get_container_stop_kwargs(self, action, container_name, kwargs=None):
        """
        Generates keyword arguments for the Docker client to stop a container.

        :param action: Action configuration.
        :type action: ActionConfig
        :param container_name: Container name or id.
        :type container_name: unicode | str
        :param kwargs: Additional keyword arguments to complement or override the configuration-based values.
        :type kwargs: dict
        :return: Resulting keyword arguments.
        :rtype: dict
        """
        c_kwargs = dict(
            container=container_name,
        )
        stop_timeout = action.config.stop_timeout