from .. import auth, errors, utils
from ..types import ServiceMode


def _check_api_features(version, task_template, update_config, endpoint_spec,
                        rollback_config):

    def raise_version_error(param, min_version):
        raise errors.InvalidVersion(
            '{} is not supported in API version < {}'.format(
                param, min_version
            )
        )

    if update_config is not None:
        if utils.version_lt(version, '1.25'):
            if 'MaxFailureRatio' in update_config:
                raise_version_error('UpdateConfig.max_failure_ratio', '1.25')
            if 'Monitor' in update_config:
                raise_version_error('UpdateConfig.monitor', '1.25')

        if utils.version_lt(version, '1.28'):
            if update_config.get('FailureAction') == 'rollback':
                raise_version_error(
                    'UpdateConfig.failure_action rollback', '1.28'
                )

        if utils.version_lt(version, '1.29'):
            if 'Order' in update_config:
                raise_version_error('UpdateConfig.order', '1.29')

    if rollback_config is not None:
        if utils.version_lt(version, '1.28'):
            raise_version_error('rollback_config', '1.28')

        if utils.version_lt(version, '1.29'):
            if 'Order' in update_config:
                raise_version_error('RollbackConfig.order', '1.29')

    if endpoint_spec is not None:
        if utils.version_lt(version, '1.32') and 'Ports' in endpoint_spec:
            if any(p.get('PublishMode') for p in endpoint_spec['Ports']):
                raise_version_error('EndpointSpec.Ports[].mode', '1.32')

    if task_template is not None:
        if 'ForceUpdate' in task_template and utils.version_lt(
                version, '1.25'):
                raise_version_error('force_update', '1.25')

        if task_template.get('Placement'):
            if utils.version_lt(version, '1.30'):
                if task_template['Placement'].get('Platforms'):
                    raise_version_error('Placement.platforms', '1.30')
            if utils.version_lt(version, '1.27'):
                if task_template['Placement'].get('Preferences'):
                    raise_version_error('Placement.preferences', '1.27')

        if task_template.get('ContainerSpec'):
            container_spec = task_template.get('ContainerSpec')

            if utils.version_lt(version, '1.25'):
                if container_spec.get('TTY'):
                    raise_version_error('ContainerSpec.tty', '1.25')
                if container_spec.get('Hostname') is not None:
                    raise_version_error('ContainerSpec.hostname', '1.25')
                if container_spec.get('Hosts') is not None:
                    raise_version_error('ContainerSpec.hosts', '1.25')
                if container_spec.get('Groups') is not None:
                    raise_version_error('ContainerSpec.groups', '1.25')
                if container_spec.get('DNSConfig') is not None:
                    raise_version_error('ContainerSpec.dns_config', '1.25')
                if container_spec.get('Healthcheck') is not None:
                    raise_version_error('ContainerSpec.healthcheck', '1.25')

            if utils.version_lt(version, '1.28'):
                if container_spec.get('ReadOnly') is not None:
                    raise_version_error('ContainerSpec.dns_config', '1.28')
                if container_spec.get('StopSignal') is not None:
                    raise_version_error('ContainerSpec.stop_signal', '1.28')

            if utils.version_lt(version, '1.30'):
                if container_spec.get('Configs') is not None:
                    raise_version_error('ContainerSpec.configs', '1.30')
                if container_spec.get('Privileges') is not None:
                    raise_version_error('ContainerSpec.privileges', '1.30')

            if utils.version_lt(version, '1.35'):
                if container_spec.get('Isolation') is not None:
                    raise_version_error('ContainerSpec.isolation', '1.35')

            if utils.version_lt(version, '1.38'):
                if container_spec.get('Init') is not None:
                    raise_version_error('ContainerSpec.init', '1.38')

        if task_template.get('Resources'):
            if utils.version_lt(version, '1.32'):
                if task_template['Resources'].get('GenericResources'):
                    raise_version_error('Resources.generic_resources', '1.32')


def _merge_task_template(current, override):
    merged = current.copy()
    if override is not None:
        for ts_key, ts_value in override.items():
            if ts_key == 'ContainerSpec':
                if 'ContainerSpec' not in merged:
                    merged['ContainerSpec'] = {}
                for cs_key, cs_value in override['ContainerSpec'].items():
                    if cs_value is not None:
                        merged['ContainerSpec'][cs_key] = cs_value
            elif ts_value is not None:
                merged[ts_key] = ts_value
    return merged


class ServiceApiMixin(object):
    @utils.minimum_version('1.24')
    def create_service(
            self, task_template, name=None, labels=None, mode=None,
            update_config=None, networks=None, endpoint_config=None,
            endpoint_spec=None, rollback_config=None
    ):
        """
        Create a service.

        Args:
            task_template (TaskTemplate): Specification of the task to start as
                part of the new service.
            name (string): User-defined name for the service. Optional.
            labels (dict): A map of labels to associate with the service.
                Optional.
            mode (ServiceMode): Scheduling mode for the service (replicated
                or global). Defaults to replicated.
            update_config (UpdateConfig): Specification for the update strategy
                of the service. Default: ``None``
            rollback_config (RollbackConfig): Specification for the rollback
                strategy of the service. Default: ``None``
            networks (:py:class:`list`): List of network names or IDs to attach
                the service to. Default: ``None``.
            endpoint_spec (EndpointSpec): Properties that can be configured to
                access and load balance a service. Default: ``None``.

        Returns:
            A dictionary containing an ``ID`` key for the newly created
            service.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """

        _check_api_features(
            self._version, task_template, update_config, endpoint_spec,
            rollback_config
        )

        url = self._url('/services/create')
        headers = {}
        image = task_template.get('ContainerSpec', {}).get('Image', None)
        if image is None:
            raise errors.DockerException(
                'Missing mandatory Image key in ContainerSpec'
            )
        if mode and not isinstance(mode, dict):
            mode = ServiceMode(mode)

        registry, repo_name = auth.resolve_repository_name(image)
        auth_header = auth.get_config_header(self, registry)
        if auth_header:
            headers['X-Registry-Auth'] = auth_header
        if utils.version_lt(self._version, '1.25'):
            networks = networks or task_template.pop('Networks', None)
        data = {
            'Name': name,
            'Labels': labels,
            'TaskTemplate': task_template,
            'Mode': mode,
            'Networks': utils.convert_service_networks(networks),
            'EndpointSpec': endpoint_spec
        }

        if update_config is not None:
            data['UpdateConfig'] = update_config

        if rollback_config is not None:
            data['RollbackConfig'] = rollback_config

        return self._result(
            self._post_json(url, data=data, headers=headers), True
        )

    @utils.minimum_version('1.24')
    @utils.check_resource('service')
    def inspect_service(self, service, insert_defaults=None):
        """
        Return information about a service.

        Args:
            service (str): Service name or ID.
            insert_defaults (boolean): If true, default values will be merged
                into the service inspect output.

        Returns:
            (dict): A dictionary of the server-side representation of the
                service, including all relevant properties.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """
        url = self._url('/services/{0}', service)
        params = {}
        if insert_defaults is not None:
            if utils.version_lt(self._version, '1.29'):
                raise errors.InvalidVersion(
                    'insert_defaults is not supported in API version < 1.29'
                )
            params['insertDefaults'] = insert_defaults

        return self._result(self._get(url, params=params), True)

    @utils.minimum_version('1.24')
    @utils.check_resource('task')
    def inspect_task(self, task):
        """
        Retrieve information about a task.

        Args:
            task (str): Task ID

        Returns:
            (dict): Information about the task.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """
        url = self._url('/tasks/{0}', task)
        return self._result(self._get(url), True)

    @utils.minimum_version('1.24')
    @utils.check_resource('service')
    def remove_service(self, service):
        """
        Stop and remove a service.

        Args:
            service (str): Service name or ID

        Returns:
            ``True`` if successful.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """

        url = self._url('/services/{0}', service)
        resp = self._delete(url)
        self._raise_for_status(resp)
        return True

    @utils.minimum_version('1.24')
    def services(self, filters=None):
        """
        List services.

        Args:
            filters (dict): Filters to process on the nodes list. Valid
                filters: ``id``, ``name`` , ``label`` and ``mode``.
                Default: ``None``.

        Returns:
            A list of dictionaries containing data about each service.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """
        params = {
            'filters': utils.convert_filters(filters) if filters else None
        }
        url = self._url('/services')
        return self._result(self._get(url, params=params), True)

    @utils.minimum_version('1.25')
    @utils.check_resource('service')
    def service_logs(self, service, details=False, follow=False, stdout=False,
                     stderr=False, since=0, timestamps=False, tail='all',
                     is_tty=None):
        """
            Get log stream for a service.
            Note: This endpoint works only for services with the ``json-file``
            or ``journald`` logging drivers.

            Args:
                service (str): ID or name of the service
                details (bool): Show extra details provided to logs.
                    Default: ``False``
                follow (bool): Keep connection open to read logs as they are
                    sent by the Engine. Default: ``False``
                stdout (bool): Return logs from ``stdout``. Default: ``False``
                stderr (bool): Return logs from ``stderr``. Default: ``False``
                since (int): UNIX timestamp for the logs staring point.
                    Default: 0
                timestamps (bool): Add timestamps to every log line.
                tail (string or int): Number of log lines to be returned,
                    counting from the current end of the logs. Specify an
                    integer or ``'all'`` to output all log lines.
                    Default: ``all``
                is_tty (bool): Whether the service's :py:class:`ContainerSpec`
                    enables the TTY option. If omitted, the method will query
                    the Engine for the information, causing an additional
                    roundtrip.

            Returns (generator): Logs for the service.
        """
        params = {
            'details': details,
            'follow': follow,
            'stdout': stdout,
            'stderr': stderr,
            'since': since,
            'timestamps': timestamps,
            'tail': tail
        }

        url = self._url('/services/{0}/logs', service)
        res = self._get(url, params=params, stream=True)
        if is_tty is None:
            is_tty = self.inspect_service(
                service
            )['Spec']['TaskTemplate']['ContainerSpec'].get('TTY', False)
        return self._get_result_tty(True, res, is_tty)

    @utils.minimum_version('1.24')
    def tasks(self, filters=None):
        """
        Retrieve a list of tasks.

        Args:
            filters (dict): A map of filters to process on the tasks list.
                Valid filters: ``id``, ``name``, ``service``, ``node``,
                ``label`` and ``desired-state``.

        Returns:
            (:py:class:`list`): List of task dictionaries.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """

        params = {
            'filters': utils.convert_filters(filters) if filters else None
        }
        url = self._url('/tasks')
        return self._result(self._get(url, params=params), True)

    @utils.minimum_version('1.24')
    @utils.check_resource('service')
    def update_service(self, service, version, task_template=None, name=None,
                       labels=None, mode=None, update_config=None,
                       networks=None, endpoint_config=None,
                       endpoint_spec=None, fetch_current_spec=False,
                       rollback_config=None):
        """
        Update a service.

        Args:
            service (string): A service identifier (either its name or service
                ID).
            version (int): The version number of the service object being
                updated. This is required to avoid conflicting writes.
            task_template (TaskTemplate): Specification of the updated task to
                start as part of the service.
            name (string): New name for the service. Optional.
            labels (dict): A map of labels to associate with the service.
                Optional.
            mode (ServiceMode): Scheduling mode for the service (replicated
                or global). Defaults to replicated.
            update_config (UpdateConfig): Specification for the update strategy
                of the service. Default: ``None``.
            rollback_config (RollbackConfig): Specification for the rollback
                strategy of the service. Default: ``None``
            networks (:py:class:`list`): List of network names or IDs to attach
                the service to. Default: ``None``.
            endpoint_spec (EndpointSpec): Properties that can be configured to
                access and load balance a service. Default: ``None``.
            fetch_current_spec (boolean): Use the undefined settings from the
                current specification of the service. Default: ``False``

        Returns:
            A dictionary containing a ``Warnings`` key.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """

        _check_api_features(
            self._version, task_template, update_config, endpoint_spec,
            rollback_config
        )

        if fetch_current_spec:
            inspect_defaults = True
            if utils.version_lt(self._version, '1.29'):
                inspect_defaults = None
            current = self.inspect_service(
                service, insert_defaults=inspect_defaults
            )['Spec']

        else:
            current = {}

        url = self._url('/services/{0}/update', service)
        data = {}
        headers = {}

        data['Name'] = current.get('Name') if name is None else name

        data['Labels'] = current.get('Labels') if labels is None else labels

        if mode is not None:
            if not isinstance(mode, dict):
                mode = ServiceMode(mode)
            data['Mode'] = mode
        else:
            data['Mode'] = current.get('Mode')

        data['TaskTemplate'] = _merge_task_template(
            current.get('TaskTemplate', {}), task_template
        )

        container_spec = data['TaskTemplate'].get('ContainerSpec', {})
        image = container_spec.get('Image', None)
        if image is not None:
            registry, repo_name = auth.resolve_repository_name(image)
            auth_header = auth.get_config_header(self, registry)
            if auth_header:
                headers['X-Registry-Auth'] = auth_header

        if update_config is not None:
            data['UpdateConfig'] = update_config
        else:
            data['UpdateConfig'] = current.get('UpdateConfig')

        if rollback_config is not None:
            data['RollbackConfig'] = rollback_config
        else:
            data['RollbackConfig'] = current.get('RollbackConfig')

        if networks is not None:
            converted_networks = utils.convert_service_networks(networks)
            if utils.version_lt(self._version, '1.25'):
                data['Networks'] = converted_networks
            else:
                data['TaskTemplate']['Networks'] = converted_networks
        elif utils.version_lt(self._version, '1.25'):
            data['Networks'] = current.get('Networks')
        elif data['TaskTemplate'].get('Networks') is None:
            current_task_template = current.get('TaskTemplate', {})
            current_networks = current_task_template.get('Networks')
            if current_networks is None:
                current_networks = current.get('Networks')
            if current_networks is not None:
                data['TaskTemplate']['Networks'] = current_networks

        if endpoint_spec is not None:
            data['EndpointSpec'] = endpoint_spec
        else:
            data['EndpointSpec'] = current.get('EndpointSpec')

        resp = self._post_json(
            url, data=data, params={'version': version}, headers=headers
        )
        return self._result(resp, json=True)
