import docopt
import six

import dcoscli
from dcos import cmds, emitting, marathon, mesos, subprocess, util
from dcos.errors import DCOSException, DefaultError
from dcoscli import log, tables
from dcoscli.subcommand import default_command_info, default_doc
from dcoscli.util import decorate_docopt_usage

from ..task import main as task_main

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("service"),
        argv=argv,
        version="dcos-service version {}".format(dcoscli.version))

    return cmds.execute(_cmds(), args)


def _cmds():
    """
    :returns: All of the supported commands
    :rtype: [Command]
    """

    return [

        cmds.Command(
            hierarchy=['service', 'log'],
            arg_keys=['--follow', '--lines', '--ssh-config-file', '<service>',
                      '<file>'],
            function=_log),

        cmds.Command(
            hierarchy=['service', 'shutdown'],
            arg_keys=['<service-id>'],
            function=_shutdown),

        cmds.Command(
            hierarchy=['service', '--info'],
            arg_keys=[],
            function=_info),

        cmds.Command(
            hierarchy=['service'],
            arg_keys=['--inactive', '--completed', '--json'],
            function=_service),
    ]


def _info():
    """Print services cli information.

    :returns: process return code
    :rtype: int
    """

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


def _service(inactive, completed, is_json):
    """List dcos services

    :param inactive: If True, include completed tasks
    :type inactive: bool
    :param is_json: If true, output json.
        Otherwise, output a human readable table.
    :type is_json: bool
    :returns: process return code
    :rtype: int
    """

    services = mesos.get_master().frameworks(
        inactive=inactive,
        completed=completed)

    if is_json:
        emitter.publish([service.dict() for service in services])
    else:
        table = tables.service_table(services)
        output = six.text_type(table)
        if output:
            emitter.publish(output)

    return 0


def _shutdown(service_id):
    """Shuts down a service

    :param service_id: the id for the service
    :type service_id: str
    :returns: process return code
    :rtype: int
    """

    mesos.DCOSClient().shutdown_framework(service_id)
    return 0


def _log(follow, lines, ssh_config_file, service, file_):
    """Prints the contents of the logs for a given service.  The service
    task is located by first identifying the marathon app running a
    framework named `service`.

    :param follow: same as unix tail's -f
    :type follow: bool
    :param lines: number of lines to print
    :type lines: int
    :param ssh_config_file: SSH config file.  Used for marathon.
    :type ssh_config_file: str | None
    :param service: service name
    :type service: str
    :param file_: file path to read
    :type file_: str
    :returns: process return code
    :rtype: int
    """

    if lines is None:
        lines = 10
    lines = util.parse_int(lines)

    if service == 'marathon':
        if file_:
            raise DCOSException('The <file> argument is invalid for marathon.'
                                ' The systemd journal is always used for the'
                                ' marathon log.')

        return _log_marathon(follow, lines, ssh_config_file)
    else:
        if ssh_config_file:
            raise DCOSException(
                'The `--ssh-config-file` argument is invalid for non-marathon '
                'services. SSH is not used.')
        return _log_service(follow, lines, service, file_)


def _log_service(follow, lines, service, file_):
    """Prints the contents of the logs for a given service.  Used for
    non-marathon services.

    :param follow: same as unix tail's -f
    :type follow: bool
    :param lines: number of lines to print
    :type lines: int
    :param service: service name
    :type service: str
    :param file_: file path to read
    :type file_: str
    :returns: process return code
    :rtype: int
    """

    if file_ is None:
        file_ = 'stdout'

    task = _get_service_task(service)

    # if journald logging is disabled, read from files API.
    if not log.dcos_log_enabled():
        return _log_task(task['id'], follow, lines, file_)

    if 'id' not in task:
        raise DCOSException('Missing `id` in task. {}'.format(task))

    task_id = task['id']
    task_main._log(follow, False, lines, task_id, file_)
    return 0


def _log_task(task_id, follow, lines, file_):
    """Prints the contents of the logs for a given task ID.

    :param task_id: task ID
    :type task_id: str
    :param follow: same as unix tail's -f
    :type follow: bool
    :param lines: number of lines to print
    :type lines: int
    :param file_: file path to read
    :type file_: str
    :returns: process return code
    :rtype: int
    """

    dcos_client = mesos.DCOSClient()
    task = mesos.get_master(dcos_client).task(task_id)
    mesos_file = mesos.MesosFile(file_, task=task, dcos_client=dcos_client)
    return log.log_files([mesos_file], follow, lines)


def _get_service_task(service_name):
    """Gets the task running the given service.  If there is more than one
    such task, throws an exception.

    :param service_name: service name
    :type service_name: str
    :returns: The marathon task dict
    :rtype: dict
    """

    marathon_client = marathon.create_client()
    app = _get_service_app(marathon_client, service_name)
    tasks = marathon_client.get_app(app['id'])['tasks']
    if len(tasks) != 1:
        raise DCOSException(
            ('We expected marathon app [{}] to be running 1 task, but we ' +
             'instead found {} tasks').format(app['id'], len(tasks)))
    return tasks[0]


def _get_service_app(marathon_client, service_name):
    """Gets the marathon app running the given service.  If there is not
    exactly one such app, throws an exception.

    :param marathon_client: marathon client
    :type marathon_client: marathon.Client
    :param service_name: service name
    :type service_name: str
    :returns: marathon app
    :rtype: dict
    """

    apps = marathon_client.get_apps_for_framework(service_name)

    if len(apps) > 1:
        raise DCOSException(
            'Multiple marathon apps found for service name [{}]: {}'.format(
                service_name,
                ', '.join('[{}]'.format(app['id']) for app in apps)))
    elif len(apps) == 0:
        raise DCOSException(
            'No marathon apps found for service [{}]'.format(service_name))
    else:
        return apps[0]


def _log_marathon(follow, lines, ssh_config_file):
    """Prints the contents of the marathon logs. Proxy through the master
    because marathon only runs on the master.

    :param follow: same as unix tail's -f
    :type follow: bool
    :param lines: number of lines to print
    :type lines: int
    :param ssh_config_file: SSH config file.
    :type ssh_config_file: str | None
    ;:returns: process return code
    :rtype: int
    """

    ssh_options = util.get_ssh_options(ssh_config_file, [])

    journalctl_args = ''
    if follow:
        journalctl_args += '-f '
    if lines:
        journalctl_args += '-n {} '.format(lines)

    leader_ip = marathon.create_client().get_leader().split(':')[0]
    user_string = 'core@'
    if ssh_config_file:
        user_string = ''

    dcos_client = mesos.DCOSClient()
    master_public_ip = dcos_client.metadata().get('PUBLIC_IPV4')
    service = 'dcos-marathon'

    cmd = "ssh -At {0}{1}{2} ssh -At {0}{1}{3} journalctl {4}-u {5}".format(
        ssh_options,
        user_string,
        master_public_ip,
        leader_ip,
        journalctl_args,
        service)

    emitter.publish(DefaultError("Running `{}`".format(cmd)))

    return subprocess.Subproc().call(cmd, shell=True)
