import json
import os
import sys
import time

import docopt
import pkg_resources
import six

import dcoscli
from dcos import cmds, emitting, http, jsonitem, marathon, options, util
from dcos.errors import DCOSException
from dcoscli import tables
from dcoscli.subcommand import default_command_info, default_doc
from dcoscli.util import decorate_docopt_usage

logger = util.get_logger(__name__)
emitter = emitting.FlatEmitter()


def main(argv):
    try:
        return _main(argv)
    except DCOSException as e:
        emitter.publish(e)
        return 1


@decorate_docopt_usage
def _main(argv):
    args = docopt.docopt(
        default_doc("marathon"),
        argv=argv,
        version='dcos-marathon version {}'.format(dcoscli.version))

    return cmds.execute(_cmds(), args)


def _cmds():
    """
    :returns: all the supported commands
    :rtype: dcos.cmds.Command
    """

    subcommand = MarathonSubcommand()

    return [
        cmds.Command(
            hierarchy=['marathon', 'version', 'list'],
            arg_keys=['<app-id>', '--max-count'],
            function=subcommand.version_list),

        cmds.Command(
            hierarchy=['marathon', 'deployment', 'list'],
            arg_keys=['<app-id>', '--json'],
            function=subcommand.deployment_list),

        cmds.Command(
            hierarchy=['marathon', 'deployment', 'rollback'],
            arg_keys=['<deployment-id>'],
            function=subcommand.deployment_rollback),

        cmds.Command(
            hierarchy=['marathon', 'deployment', 'stop'],
            arg_keys=['<deployment-id>'],
            function=subcommand.deployment_stop),

        cmds.Command(
            hierarchy=['marathon', 'deployment', 'watch'],
            arg_keys=['<deployment-id>', '--max-count', '--interval'],
            function=subcommand.deployment_watch),

        cmds.Command(
            hierarchy=['marathon', 'task', 'list'],
            arg_keys=['<app-id>', '--json'],
            function=subcommand.task_list),

        cmds.Command(
            hierarchy=['marathon', 'task', 'stop'],
            arg_keys=['<task-id>', '--wipe'],
            function=subcommand.task_stop),

        cmds.Command(
            hierarchy=['marathon', 'task', 'kill'],
            arg_keys=['<task-ids>', '--scale', '--wipe', '--json'],
            function=subcommand.task_kill),

        cmds.Command(
            hierarchy=['marathon', 'task', 'show'],
            arg_keys=['<task-id>'],
            function=subcommand.task_show),

        cmds.Command(
            hierarchy=['marathon', 'app', 'add'],
            arg_keys=['<app-resource>'],
            function=subcommand.add),

        cmds.Command(
            hierarchy=['marathon', 'app', 'list'],
            arg_keys=['--json'],
            function=subcommand.list),

        cmds.Command(
            hierarchy=['marathon', 'app', 'remove'],
            arg_keys=['<app-id>', '--force'],
            function=subcommand.remove),

        cmds.Command(
            hierarchy=['marathon', 'app', 'show'],
            arg_keys=['<app-id>', '--app-version'],
            function=subcommand.show),

        cmds.Command(
            hierarchy=['marathon', 'app', 'start'],
            arg_keys=['<app-id>', '<instances>', '--force'],
            function=subcommand.start),

        cmds.Command(
            hierarchy=['marathon', 'app', 'stop'],
            arg_keys=['<app-id>', '--force'],
            function=subcommand.stop),

        cmds.Command(
            hierarchy=['marathon', 'app', 'update'],
            arg_keys=['<app-id>', '<properties>', '--force'],
            function=subcommand.update),

        cmds.Command(
            hierarchy=['marathon', 'app', 'restart'],
            arg_keys=['<app-id>', '--force'],
            function=subcommand.restart),

        cmds.Command(
            hierarchy=['marathon', 'app', 'kill'],
            arg_keys=['<app-id>', '--scale', '--host'],
            function=subcommand.kill),

        cmds.Command(
            hierarchy=['marathon', 'group', 'add'],
            arg_keys=['<group-resource>'],
            function=subcommand.group_add),

        cmds.Command(
            hierarchy=['marathon', 'group', 'list'],
            arg_keys=['--json'],
            function=subcommand.group_list),

        cmds.Command(
            hierarchy=['marathon', 'group', 'show'],
            arg_keys=['<group-id>', '--group-version'],
            function=subcommand.group_show),

        cmds.Command(
            hierarchy=['marathon', 'group', 'remove'],
            arg_keys=['<group-id>', '--force'],
            function=subcommand.group_remove),

        cmds.Command(
            hierarchy=['marathon', 'group', 'update'],
            arg_keys=['<group-id>', '<properties>', '--force'],
            function=subcommand.group_update),

        cmds.Command(
            hierarchy=['marathon', 'group', 'scale'],
            arg_keys=['<group-id>', '<scale-factor>', '--force'],
            function=subcommand.group_scale),

        cmds.Command(
            hierarchy=['marathon', 'pod', 'add'],
            arg_keys=['<pod-resource>'],
            function=subcommand.pod_add),

        cmds.Command(
            hierarchy=['marathon', 'pod', 'remove'],
            arg_keys=['<pod-id>', '--force'],
            function=subcommand.pod_remove),

        cmds.Command(
            hierarchy=['marathon', 'pod', 'list'],
            arg_keys=['--json'],
            function=subcommand.pod_list),

        cmds.Command(
            hierarchy=['marathon', 'pod', 'show'],
            arg_keys=['<pod-id>'],
            function=subcommand.pod_show),

        cmds.Command(
            hierarchy=['marathon', 'pod', 'update'],
            arg_keys=['<pod-id>', '--force'],
            function=subcommand.pod_update),

        cmds.Command(
            hierarchy=['marathon', 'pod', 'kill'],
            arg_keys=['<pod-id>', '<instance-ids>'],
            function=subcommand.pod_kill),

        cmds.Command(
            hierarchy=['marathon', 'debug', 'list'],
            arg_keys=['--json'],
            function=subcommand.debug_list),

        cmds.Command(
            hierarchy=['marathon', 'debug', 'summary'],
            arg_keys=['<app-id>', '--json'],
            function=subcommand.debug_summary),

        cmds.Command(
            hierarchy=['marathon', 'debug', 'details'],
            arg_keys=['<app-id>', '--json'],
            function=subcommand.debug_details),

        cmds.Command(
            hierarchy=['marathon', 'about'],
            arg_keys=[],
            function=subcommand.about),

        cmds.Command(
            hierarchy=['marathon'],
            arg_keys=['--config-schema', '--info'],
            function=_marathon)
    ]


def _marathon(config_schema, info):
    """
    :param config_schema: Whether to output the config schema
    :type config_schema: boolean
    :param info: Whether to output a description of this subcommand
    :type info: boolean
    :returns: process return code
    :rtype: int
    """

    if config_schema:
        schema = _cli_config_schema()
        emitter.publish(schema)
    elif info:
        _info()
    else:
        doc = default_command_info("marathon")
        emitter.publish(options.make_generic_usage_message(doc))
        return 1

    return 0


def _info():
    """
    :returns: process return code
    :rtype: int
    """

    emitter.publish(default_command_info("marathon"))
    return 0


class ResourceReader(object):
    """Encapsulates side-effecting methods for loading Marathon resources."""

    @staticmethod
    def get_resource(name):
        """
        :param name: optional filename or http(s) url
        for the application or group resource
        :type name: str | None
        :returns: resource
        :rtype: dict
        """
        if name is not None:
            if os.path.isfile(name):
                with util.open_file(name) as resource_file:
                    return util.load_json(resource_file)
            else:
                try:
                    http.silence_requests_warnings()
                    req = http.get(name)
                    if req.status_code == 200:
                        data = b''
                        for chunk in req.iter_content(1024):
                            data += chunk
                        return util.load_jsons(data.decode('utf-8'))
                    else:
                        raise Exception
                except Exception:
                    logger.exception('Cannot read from resource %s', name)
                    raise DCOSException(
                        "Can't read from resource: {0}.\n"
                        "Please check that it exists.".format(name))

        example = "E.g.: dcos marathon app add < app_resource.json"
        ResourceReader._assert_no_tty(example)

        return util.load_json(sys.stdin)

    @staticmethod
    def get_resource_from_properties(properties):
        """
        :param properties: JSON items in the form key=value
        :type properties: [str]
        :returns: resource JSON
        :rtype: dict
        """

        if len(properties) == 0:
            example =\
                "E.g. dcos marathon app update your-app-id < app_update.json"
            ResourceReader._assert_no_tty(example)

            return util.load_jsons(sys.stdin.read())

        resource_json = {}
        for prop in properties:
            key, value = jsonitem.parse_json_item(prop, None)

            key = jsonitem.clean_value(key)
            if key in resource_json:
                raise DCOSException(
                    'Key {!r} was specified more than once'.format(key))

            resource_json[key] = value
        return resource_json

    @staticmethod
    def _assert_no_tty(command_example):
        if sys.stdin.isatty():
            # We don't support TTY right now.
            # In the future we will start an editor
            template = ("We currently don't support reading from the TTY. "
                        "Please specify an application JSON.\n{}")
            raise DCOSException(template.format(command_example))


class MarathonSubcommand(object):
    """Defines a method for each operation of the `dcos marathon` subcommand.

    :param resource_reader: provides input methods for Marathon resources
    :type resource_reader: ResourceReader
    :param create_marathon_client: a callable that returns an instance of
                                   marathon.Client
    :type create_marathon_client: collections.abc.Callable
    """

    def __init__(self,
                 resource_reader=ResourceReader(),
                 create_marathon_client=marathon.create_client):
        self._resource_reader = resource_reader
        self._create_marathon_client = create_marathon_client

    def about(self):
        """
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()

        emitter.publish(client.get_about())
        return 0

    def add(self, app_resource):
        """
        :param app_resource: optional filename for the application resource
        :type app_resource: str
        :returns: process return code
        :rtype: int
        """
        application_resource = self._resource_reader.get_resource(app_resource)

        # Add application to marathon
        client = self._create_marathon_client()

        # Check that the application doesn't exist
        app_id = util.normalize_marathon_id_path(application_resource['id'])

        try:
            client.get_app(app_id)
        except DCOSException as e:
            logger.exception(e)
        else:
            message = "Application '{}' already exists".format(app_id)
            raise DCOSException(message)

        deployment = client.add_app(application_resource)
        emitter.publish('Created deployment {}'.format(deployment))

        return 0

    def list(self, json_):
        """
        :param json_: output json if True
        :type json_: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        apps = client.get_apps()

        if json_:
            emitter.publish(apps)
        else:
            deployments = client.get_deployments()
            queued_apps = client.get_queued_apps()
            _enhance_row_with_overdue_information(apps, queued_apps)
            table = tables.app_table(apps, deployments)
            output = six.text_type(table)
            if output:
                emitter.publish(output)

        return 0

    def group_list(self, json_):
        """
        :param json_: output json if True
        :type json_: bool
        :rtype: int
        :returns: process return code
        """

        client = self._create_marathon_client()
        groups = client.get_groups()

        emitting.publish_table(emitter, groups, tables.group_table, json_)
        return 0

    def group_add(self, group_resource):
        """
        :param group_resource: optional filename for the group resource
        :type group_resource: str
        :returns: process return code
        :rtype: int
        """

        group_resource = self._resource_reader.get_resource(group_resource)

        client = self._create_marathon_client()

        # Check that the group doesn't exist
        group_id = util.normalize_marathon_id_path(group_resource['id'])

        try:
            client.get_group(group_id)
        except DCOSException as e:
            logger.exception(e)
        else:
            raise DCOSException("Group '{}' already exists".format(group_id))

        deployment = client.create_group(group_resource)
        emitter.publish('Created deployment {}'.format(deployment))

        return 0

    def remove(self, app_id, force):
        """
        :param app_id: ID of the app to remove
        :type app_id: str
        :param force: Whether to override running deployments.
        :type force: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        client.remove_app(app_id, force)
        return 0

    def group_remove(self, group_id, force):
        """
        :param group_id: ID of the app to remove
        :type group_id: str
        :param force: Whether to override running deployments.
        :type force: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        client.remove_group(group_id, force)
        return 0

    def show(self, app_id, version):
        """Show details of a Marathon application.

        :param app_id: The id for the application
        :type app_id: str
        :param version: The version, either absolute (date-time) or relative
        :type version: str
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()

        if version is not None:
            version = _calculate_version(client, app_id, version)

        app = client.get_app(app_id, version=version)

        emitter.publish(app)
        return 0

    def group_show(self, group_id, version=None):
        """Show details of a Marathon application.

        :param group_id: The id for the application
        :type group_id: str
        :param version: The version, either absolute (date-time) or relative
        :type version: str
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()

        app = client.get_group(group_id, version=version)

        emitter.publish(app)
        return 0

    def group_update(self, group_id, properties, force):
        """
        :param group_id: the id of the group
        :type group_id: str
        :param properties: json items used to update group
        :type properties: [str]
        :param force: whether to override running deployments
        :type force: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()

        # Ensure that the group exists
        client.get_group(group_id)

        resource = self._resource_reader.\
            get_resource_from_properties(properties)
        deployment = client.update_group(group_id, resource, force)

        emitter.publish('Created deployment {}'.format(deployment))
        return 0

    def start(self, app_id, instances, force):
        """Start a Marathon application.

        :param app_id: the id for the application
        :type app_id: str
        :param instances: the number of instances to start
        :type instances: str
        :param force: whether to override running deployments
        :type force: bool
        :returns: process return code
        :rtype: int
        """

        # Check that the application exists
        client = self._create_marathon_client()

        desc = client.get_app(app_id)

        if desc['instances'] > 0:
            emitter.publish(
                'Application {!r} already started: {!r} instances.'.format(
                    app_id,
                    desc['instances']))
            return 1

        # Need to add the 'id' because it is required
        app_json = {'id': app_id}

        # Set instances to 1 if not specified
        if instances is None:
            instances = 1
        else:
            instances = util.parse_int(instances)
            if instances <= 0:
                emitter.publish(
                    'The number of instances must be positive: {!r}.'.format(
                        instances))
                return 1

        app_json['instances'] = instances

        deployment = client.update_app(app_id, app_json, force)

        emitter.publish('Created deployment {}'.format(deployment))

        return 0

    def stop(self, app_id, force):
        """Stop a Marathon application

        :param app_id: the id of the application
        :type app_id: str
        :param force: whether to override running deployments
        :type force: bool
        :returns: process return code
        :rtype: int
        """

        # Check that the application exists
        client = self._create_marathon_client()

        desc = client.get_app(app_id)

        if desc['instances'] <= 0:
            emitter.publish(
                'Application {!r} already stopped: {!r} instances.'.format(
                    app_id,
                    desc['instances']))
            return 1

        app_json = {'instances': 0}

        deployment = client.update_app(app_id, app_json, force)

        emitter.publish('Created deployment {}'.format(deployment))

    def update(self, app_id, properties, force):
        """
        :param app_id: the id of the application
        :type app_id: str
        :param properties: json items used to update resource
        :type properties: [str]
        :param force: whether to override running deployments
        :type force: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()

        # Ensure that the application exists
        client.get_app(app_id)

        resource = self._resource_reader.\
            get_resource_from_properties(properties)
        deployment = client.update_app(app_id, resource, force)

        emitter.publish('Created deployment {}'.format(deployment))
        return 0

    def group_scale(self, group_id, scale_factor, force):
        """
        :param group_id: the id of the group
        :type group_id: str
        :param scale_factor: scale factor for application group
        :type scale_factor: str
        :param force: whether to override running deployments
        :type force: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        scale_factor = util.parse_float(scale_factor)
        deployment = client.scale_group(group_id, scale_factor, force)
        emitter.publish('Created deployment {}'.format(deployment))
        return 0

    def restart(self, app_id, force):
        """
        :param app_id: the id of the application
        :type app_id: str
        :param force: whether to override running deployments
        :type force: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()

        desc = client.get_app(app_id)

        if desc['instances'] <= 0:
            app_id = util.normalize_marathon_id_path(app_id)
            emitter.publish(
                'Unable to perform rolling restart of application {!r} '
                'because it has no running tasks'.format(
                    app_id,
                    desc['instances']))
            return 1

        payload = client.restart_app(app_id, force)

        message = 'Created deployment {}'.format(payload['deploymentId'])
        emitter.publish(message)
        return 0

    def kill(self, app_id, scale, host):
        """
        :param app_id: the id of the application
        :type app_id: str
        :param scale: Scale the app down
        :type scale: bool
        :param host: Kill only those tasks running on host specified
        :type host: str
        :returns: process return code
        :rtype: int
        """
        client = self._create_marathon_client()

        payload = client.kill_tasks(app_id, host=host, scale=scale)
        # If scale is provided, the API return a "deploymentResult"
        # https://github.com/mesosphere/marathon/blob/50366c8/src/main/scala/mesosphere/marathon/api/RestResource.scala#L34-L36
        if scale:
            emitter.publish("Started deployment: {}".format(payload))
        else:
            if 'tasks' in payload:
                emitter.publish('Killed tasks: {}'.format(payload['tasks']))
                if len(payload['tasks']) == 0:
                    return 1
            else:
                emitter.publish('Killed tasks: []')
                return 1
        return 0

    def version_list(self, app_id, max_count):
        """
        :param app_id: the id of the application
        :type app_id: str
        :param max_count: the maximum number of version to fetch and return
        :type max_count: str
        :returns: process return code
        :rtype: int
        """

        if max_count is not None:
            max_count = util.parse_int(max_count)

        client = self._create_marathon_client()

        versions = client.get_app_versions(app_id, max_count)

        emitter.publish(versions)
        return 0

    def deployment_list(self, app_id, json_):
        """
        :param app_id: the application id
        :type app_id: str
        :param json_: output json if True
        :type json_: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()

        deployments = client.get_deployments(app_id)

        if not deployments and not json_:
            msg = "There are no deployments"
            if app_id:
                msg += " for '{}'".format(app_id)
            raise DCOSException(msg)

        emitting.publish_table(emitter,
                               deployments,
                               tables.deployment_table,
                               json_)
        return 0

    def deployment_stop(self, deployment_id):
        """
        :param deployment_id: the application id
        :type deployment_id: str
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        client.stop_deployment(deployment_id)

        return 0

    def deployment_rollback(self, deployment_id):
        """
        :param deployment_id: the application id
        :type deployment_id: str
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        deployment = client.rollback_deployment(deployment_id)

        emitter.publish(deployment)
        return 0

    def deployment_watch(self, deployment_id, max_count, interval):
        """
        :param deployment_id: the application id
        :type deployment_id: str
        :param max_count: maximum number of polling calls
        :type max_count: str
        :param interval: wait interval in seconds between polling calls
        :type interval: str
        :returns: process return code
        :rtype: int
        """

        if max_count is not None:
            max_count = util.parse_int(max_count)

        interval = 1 if interval is None else util.parse_int(interval)

        client = self._create_marathon_client()

        count = 0
        while max_count is None or count < max_count:
            deployment = client.get_deployment(deployment_id)

            if deployment is None:
                return 0
            if util.is_windows_platform():
                os.system('cls')
            else:
                if 'TERM' in os.environ:
                    os.system('clear')
            emitter.publish('Deployment update time: '
                            '{} \n'.format(time.strftime("%Y-%m-%d %H:%M:%S",
                                                         time.gmtime())))
            emitter.publish(deployment)
            time.sleep(interval)
            count += 1

        return 0

    def task_list(self, app_id, json_):
        """
        :param app_id: the id of the application
        :type app_id: str
        :param json_: output json if True
        :type json_: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        tasks = client.get_tasks(app_id)

        emitting.publish_table(emitter, tasks, tables.app_task_table, json_)
        return 0

    def task_stop(self, task_id, wipe):
        """Stop a Marathon task

        :param task_id: the id of the task
        :type task_id: str
        :param wipe: whether to wipe persistent data and unreserve resources
        :type wipe: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        task = client.stop_task(task_id, wipe)

        if task is None:
            raise DCOSException("Task '{}' does not exist".format(task_id))

        emitter.publish(task)
        return 0

    def task_kill(self, task_ids, scale, wipe, json_):
        """Kill one or multiple Marathon tasks

        :param task_ids: the id of the task
        :type task_ids: [str]
        :param scale: Scale the app down after killing the specified tasks
        :type scale: bool
        :param wipe: whether remove reservations and persistent volumes.
        :type wipe: bool
        :param json_: output JSON if true
        :type json_: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        payload = client.kill_and_scale_tasks(task_ids, scale, wipe)

        def print_deployment(deployment, json_):
            if json_:
                emitter.publish(deployment)
            else:
                emitter.publish('Created deployment: {}'.format(
                    deployment['deploymentId']))

        def print_killed_tasks(payload, json_):
            if json_:
                emitter.publish(payload)
            else:
                emitter.publish('Killed tasks: {}'.format(
                    [task['id'] for task in payload['tasks']]))

        if scale:
            print_deployment(payload, json_)
        else:
            print_killed_tasks(payload, json_)

            if len(payload['tasks']) == 0:
                raise DCOSException(
                    'Failed to kill tasks. task-ids seems to be unknown')

        return 0

    def task_show(self, task_id):
        """
        :param task_id: the task id
        :type task_id: str
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        task = client.get_task(task_id)

        if task is None:
            raise DCOSException("Task '{}' does not exist".format(task_id))

        emitter.publish(task)
        return 0

    def pod_add(self, pod_resource_path):
        """
        :param pod_resource_path: optional file path for the pod resource
        :type pod_resource_path: str
        :returns: process return code
        :rtype: int
        """

        marathon_client = self._create_marathon_client()
        self._ensure_pods_support(marathon_client)

        pod_json = self._resource_reader.get_resource(pod_resource_path)
        deployment = marathon_client.add_pod(pod_json)
        emitter.publish('Created deployment {}'.format(deployment))
        return 0

    def pod_remove(self, pod_id, force):
        """
        :param pod_id: the Marathon ID of the pod to remove
        :type pod_id: str
        :param force: whether to override running deployments
        :type force: bool
        :returns: process return code
        :rtype: int
        """

        marathon_client = self._create_marathon_client()
        self._ensure_pods_support(marathon_client)

        marathon_client.remove_pod(pod_id, force)
        return 0

    def pod_list(self, json_):
        """
        :param json_: output JSON if true
        :type json_: bool
        :returns: process return code
        :rtype: int
        """

        marathon_client = self._create_marathon_client()
        self._ensure_pods_support(marathon_client)

        pods = marathon_client.list_pod()
        queued_apps = marathon_client.get_queued_apps()
        _enhance_row_with_overdue_information(pods, queued_apps)
        emitting.publish_table(emitter, pods, tables.pod_table, json_)
        return 0

    def pod_show(self, pod_id):
        """Show details of a Marathon pod.

        :param pod_id: the Marathon ID of the pod to show
        :type pod_id: str
        :returns: process return code
        :rtype: int
        """

        marathon_client = self._create_marathon_client()
        self._ensure_pods_support(marathon_client)

        pod_json = marathon_client.show_pod(pod_id)
        emitter.publish(pod_json)
        return 0

    def pod_update(self, pod_id, force):
        """
        :param pod_id: the Marathon ID of the pod to update
        :type pod_id: str
        :param force: whether to override running deployments
        :type force: bool
        :returns: process return code
        :rtype: int
        """

        marathon_client = self._create_marathon_client()
        self._ensure_pods_support(marathon_client)

        # Ensure that the pod exists
        marathon_client.show_pod(pod_id)

        resource = self._resource_reader.get_resource(name=None)
        deployment_id = marathon_client.update_pod(
            pod_id, pod_json=resource, force=force)

        emitter.publish('Created deployment {}'.format(deployment_id))
        return 0

    def pod_kill(self, pod_id, instance_ids):
        """
        :param pod_id: the Marathon ID of the pod to kill instances from
        :type pod_id: str
        :param instance_ids: the instance IDs to kill
        :type instance_ids: [str]
        :returns: process return code
        :rtype: int
        """

        if not instance_ids:
            raise DCOSException('Please provide at least one pod instance ID')

        marathon_client = self._create_marathon_client()
        self._ensure_pods_support(marathon_client)

        marathon_client.kill_pod_instances(pod_id, instance_ids)
        return 0

    def debug_list(self, json_):
        """
        :param json_: output json if True
        :type json_: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        queued_apps = client.get_queued_apps()

        emitting.publish_table(emitter, queued_apps,
                               tables.queued_apps_table, json_)
        return 0

    def debug_details(self, app_id, json_):
        """
        :param app_id: the Marathon ID to display details
        :type app_id: string
        :param json_: output json if True
        :type json_: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        queued_app = client.get_queued_app(app_id)

        if queued_app:
            emitting.publish_table(
                emitter, queued_app,
                tables.queued_app_details_table, json_)
        else:
            raise DCOSException("No apps found in Marathon queue")

        return 0

    def debug_summary(self, app_id, json_):
        """
        :param app_id: the Marathon ID to display details
        :type app_id: string
        :param json_: output json if True
        :type json_: bool
        :returns: process return code
        :rtype: int
        """

        client = self._create_marathon_client()
        queued_app = client.get_queued_app(app_id)

        if queued_app:
            if queued_app.get('processedOffersSummary'):
                emitting.publish_table(
                    emitter, queued_app,
                    tables.queued_app_table, json_)
            else:
                msg = "This command is not supported on your cluster"
                raise DCOSException(msg)
        else:
            raise DCOSException("No apps found in Marathon queue")

        return 0

    @staticmethod
    def _ensure_pods_support(marathon_client):
        """Raises an exception if the given client is communicating with a
        version of Marathon that doesn't support pods.

        :param marathon_client: the Marathon client to check
        :type marathon_client: dcos.marathon.Client
        :rtype: None
        """

        if not marathon_client.pod_feature_supported():
            msg = 'This command is not supported by your version of Marathon'
            raise DCOSException(msg)


def _enhance_row_with_overdue_information(rows, queued_apps):
    """Calculates if configured `backoff` duration for this
    app or pod definition was exceeded. In that case this application
    is marked as `overdue`.
    If the app or pod inside this row should be

    :param rows: list of rows
    :type rows: []
    :param queued_apps: list of
    :type queued_apps: []
    :returns: true if pod is overdue, false otherwise
    :rtype: bool
    """

    for row in rows:
        queued_app = next(
            (app for app in queued_apps
             if row.get('id') == marathon.get_app_or_pod_id(app)),
            None)
        row['overdue'] = queued_app.get('delay', {}) \
            .get('overdue', False) if queued_app else False

    return rows


def _calculate_version(client, app_id, version):
    """
    :param client: Marathon client
    :type client: dcos.marathon.Client
    :param app_id: The ID of the application
    :type app_id: str
    :param version: Relative or absolute version or None
    :type version: str
    :returns: The absolute version as an ISO8601 date-time
    :rtype: str
    """

    # First let's try to parse it as a negative integer
    try:
        value = util.parse_int(version)
    except DCOSException:
        logger.exception('Unable to parse version %s', version)
        return version
    else:
        if value < 0:
            value = -1 * value
            # We have a negative value let's ask Marathon for the last
            # abs(value)
            versions = client.get_app_versions(app_id, value + 1)

            if len(versions) <= value:
                # We don't have enough versions. Return an error.
                msg = "Application {!r} only has {!r} version(s)."
                raise DCOSException(msg.format(app_id, len(versions), value))
            else:
                return versions[value]

        else:
            raise DCOSException(
                'Relative versions must be negative: {}'.format(version))


def _cli_config_schema():
    """
    :returns: schema for marathon cli config
    :rtype: dict
    """
    return json.loads(
        pkg_resources.resource_string(
            'dcos',
            'data/config-schema/marathon.json').decode('utf-8'))
