import copy

from docker.errors import InvalidArgument, create_unexpected_kwargs_error
from docker.types import ContainerSpec, Placement, ServiceMode, TaskTemplate

from .resource import Collection, Model


class Service(Model):
    """A service."""
    id_attribute = 'ID'

    @property
    def name(self):
        """The service's name."""
        return self.attrs['Spec']['Name']

    @property
    def version(self):
        """
        The version number of the service. If this is not the same as the
        server, the :py:meth:`update` function will not work and you will
        need to call :py:meth:`reload` before calling it again.
        """
        return self.attrs.get('Version').get('Index')

    def remove(self):
        """
        Stop and remove the service.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """
        return self.client.api.remove_service(self.id)

    def tasks(self, filters=None):
        """
        List the tasks in this service.

        Args:
            filters (dict): A map of filters to process on the tasks list.
                Valid filters: ``id``, ``name``, ``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.
        """
        if filters is None:
            filters = {}
        filters['service'] = self.id
        return self.client.api.tasks(filters=filters)

    def update(self, **kwargs):
        """
        Update a service's configuration. Similar to the ``docker service
        update`` command.

        Takes the same parameters as :py:meth:`~ServiceCollection.create`.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """
        # Image is required, so if it hasn't been set, use current image
        if 'image' not in kwargs:
            spec = self.attrs['Spec']['TaskTemplate']['ContainerSpec']
            kwargs['image'] = spec['Image']

        if kwargs.get('force_update') is True:
            task_template = self.attrs['Spec']['TaskTemplate']
            current_value = int(task_template.get('ForceUpdate', 0))
            kwargs['force_update'] = current_value + 1

        create_kwargs = _get_create_service_kwargs('update', kwargs)

        return self.client.api.update_service(
            self.id,
            self.version,
            **create_kwargs
        )

    def logs(self, **kwargs):
        """
        Get log stream for the service.
        Note: This method works only for services with the ``json-file``
        or ``journald`` logging drivers.

        Args:
            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``

        Returns:
            generator: Logs for the service.
        """
        is_tty = self.attrs['Spec']['TaskTemplate']['ContainerSpec'].get(
            'TTY', False
        )
        return self.client.api.service_logs(self.id, is_tty=is_tty, **kwargs)

    def scale(self, replicas):
        """
        Scale service container.

        Args:
            replicas (int): The number of containers that should be running.

        Returns:
            bool: ``True`` if successful.
        """

        if 'Global' in self.attrs['Spec']['Mode'].keys():
            raise InvalidArgument('Cannot scale a global container')

        service_mode = ServiceMode('replicated', replicas)
        return self.client.api.update_service(self.id, self.version,
                                              mode=service_mode,
                                              fetch_current_spec=True)

    def force_update(self):
        """
        Force update the service even if no changes require it.

        Returns:
            bool: ``True`` if successful.
        """

        return self.update(force_update=True, fetch_current_spec=True)


class ServiceCollection(Collection):
    """Services on the Docker server."""
    model = Service

    def create(self, image, command=None, **kwargs):
        """
        Create a service. Similar to the ``docker service create`` command.

        Args:
            image (str): The image name to use for the containers.
            command (list of str or str): Command to run.
            args (list of str): Arguments to the command.
            constraints (list of str): :py:class:`~docker.types.Placement`
                constraints.
            preferences (list of tuple): :py:class:`~docker.types.Placement`
                preferences.
            maxreplicas (int): :py:class:`~docker.types.Placement` maxreplicas
                or (int) representing maximum number of replicas per node.
            platforms (list of tuple): A list of platform constraints
                expressed as ``(arch, os)`` tuples.
            container_labels (dict): Labels to apply to the container.
            endpoint_spec (EndpointSpec): Properties that can be configured to
                access and load balance a service. Default: ``None``.
            env (list of str): Environment variables, in the form
                ``KEY=val``.
            hostname (string): Hostname to set on the container.
            init (boolean): Run an init inside the container that forwards
                signals and reaps processes
            isolation (string): Isolation technology used by the service's
                containers. Only used for Windows containers.
            labels (dict): Labels to apply to the service.
            log_driver (str): Log driver to use for containers.
            log_driver_options (dict): Log driver options.
            mode (ServiceMode): Scheduling mode for the service.
                Default:``None``
            mounts (list of str): Mounts for the containers, in the form
                ``source:target:options``, where options is either
                ``ro`` or ``rw``.
            name (str): Name to give to the service.
            networks (:py:class:`list`): List of network names or IDs or
                :py:class:`~docker.types.NetworkAttachmentConfig` to attach the
                service to. Default: ``None``.
            resources (Resources): Resource limits and reservations.
            restart_policy (RestartPolicy): Restart policy for containers.
            secrets (list of :py:class:`~docker.types.SecretReference`): List
                of secrets accessible to containers for this service.
            stop_grace_period (int): Amount of time to wait for
                containers to terminate before forcefully killing them.
            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``
            user (str): User to run commands as.
            workdir (str): Working directory for commands to run.
            tty (boolean): Whether a pseudo-TTY should be allocated.
            groups (:py:class:`list`): A list of additional groups that the
                container process will run as.
            open_stdin (boolean): Open ``stdin``
            read_only (boolean): Mount the container's root filesystem as read
                only.
            stop_signal (string): Set signal to stop the service's containers
            healthcheck (Healthcheck): Healthcheck
                configuration for this service.
            hosts (:py:class:`dict`): A set of host to IP mappings to add to
                the container's `hosts` file.
            dns_config (DNSConfig): Specification for DNS
                related configurations in resolver configuration file.
            configs (:py:class:`list`): List of
                :py:class:`~docker.types.ConfigReference` that will be exposed
                to the service.
            privileges (Privileges): Security options for the service's
                containers.
            cap_add (:py:class:`list`): A list of kernel capabilities to add to
                the default set for the container.
            cap_drop (:py:class:`list`): A list of kernel capabilities to drop
                from the default set for the container.
            sysctls (:py:class:`dict`): A dict of sysctl values to add to the
                container

        Returns:
            :py:class:`Service`: The created service.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """
        kwargs['image'] = image
        kwargs['command'] = command
        create_kwargs = _get_create_service_kwargs('create', kwargs)
        service_id = self.client.api.create_service(**create_kwargs)
        return self.get(service_id)

    def get(self, service_id, insert_defaults=None):
        """
        Get a service.

        Args:
            service_id (str): The ID of the service.
            insert_defaults (boolean): If true, default values will be merged
                into the output.

        Returns:
            :py:class:`Service`: The service.

        Raises:
            :py:class:`docker.errors.NotFound`
                If the service does not exist.
            :py:class:`docker.errors.APIError`
                If the server returns an error.
            :py:class:`docker.errors.InvalidVersion`
                If one of the arguments is not supported with the current
                API version.
        """
        return self.prepare_model(
            self.client.api.inspect_service(service_id, insert_defaults)
        )

    def list(self, **kwargs):
        """
        List services.

        Args:
            filters (dict): Filters to process on the nodes list. Valid
                filters: ``id``, ``name`` , ``label`` and ``mode``.
                Default: ``None``.
            status (bool): Include the service task count of running and
                desired tasks. Default: ``None``.

        Returns:
            list of :py:class:`Service`: The services.

        Raises:
            :py:class:`docker.errors.APIError`
                If the server returns an error.
        """
        return [
            self.prepare_model(s)
            for s in self.client.api.services(**kwargs)
        ]


# kwargs to copy straight over to ContainerSpec
CONTAINER_SPEC_KWARGS = [
    'args',
    'cap_add',
    'cap_drop',
    'command',
    'configs',
    'dns_config',
    'env',
    'groups',
    'healthcheck',
    'hostname',
    'hosts',
    'image',
    'init',
    'isolation',
    'labels',
    'mounts',
    'open_stdin',
    'privileges',
    'read_only',
    'secrets',
    'stop_grace_period',
    'stop_signal',
    'tty',
    'user',
    'workdir',
    'sysctls',
]

# kwargs to copy straight over to TaskTemplate
TASK_TEMPLATE_KWARGS = [
    'networks',
    'resources',
    'restart_policy',
]

# kwargs to copy straight over to create_service
CREATE_SERVICE_KWARGS = [
    'name',
    'labels',
    'mode',
    'update_config',
    'rollback_config',
    'endpoint_spec',
]

PLACEMENT_KWARGS = [
    'constraints',
    'preferences',
    'platforms',
    'maxreplicas',
]


def _get_create_service_kwargs(func_name, kwargs):
    # Copy over things which can be copied directly
    create_kwargs = {}
    for key in copy.copy(kwargs):
        if key in CREATE_SERVICE_KWARGS:
            create_kwargs[key] = kwargs.pop(key)
    container_spec_kwargs = {}
    for key in copy.copy(kwargs):
        if key in CONTAINER_SPEC_KWARGS:
            container_spec_kwargs[key] = kwargs.pop(key)
    task_template_kwargs = {}
    for key in copy.copy(kwargs):
        if key in TASK_TEMPLATE_KWARGS:
            task_template_kwargs[key] = kwargs.pop(key)

    if 'container_labels' in kwargs:
        container_spec_kwargs['labels'] = kwargs.pop('container_labels')

    placement = {}
    for key in copy.copy(kwargs):
        if key in PLACEMENT_KWARGS:
            placement[key] = kwargs.pop(key)
    placement = Placement(**placement)
    task_template_kwargs['placement'] = placement

    if 'log_driver' in kwargs:
        task_template_kwargs['log_driver'] = {
            'Name': kwargs.pop('log_driver'),
            'Options': kwargs.pop('log_driver_options', {})
        }

    if func_name == 'update':
        if 'force_update' in kwargs:
            task_template_kwargs['force_update'] = kwargs.pop('force_update')

        # fetch the current spec by default if updating the service
        # through the model
        fetch_current_spec = kwargs.pop('fetch_current_spec', True)
        create_kwargs['fetch_current_spec'] = fetch_current_spec

    # All kwargs should have been consumed by this point, so raise
    # error if any are left
    if kwargs:
        raise create_unexpected_kwargs_error(func_name, kwargs)

    container_spec = ContainerSpec(**container_spec_kwargs)
    task_template_kwargs['container_spec'] = container_spec
    create_kwargs['task_template'] = TaskTemplate(**task_template_kwargs)
    return create_kwargs
