#!/usr/bin/env python

# THIS FILE IS PART OF THE CYLC SUITE ENGINE.
# Copyright (C) 2008-2017 NIWA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""cylc [control] trigger [OPTIONS] ARGS

Manually trigger one or more tasks. Waiting tasks will be queued (cylc internal
queues) and will submit as normal when released by the queue; queued tasks will
submit immediately even if that violates the queue limit (so you may need to
trigger a queue-limited task twice to get it to submit).

For single tasks you can use "--edit" to edit the generated job script before
it submits, to apply one-off changes. A diff between the original and edited
job script will be saved to the task job log directory.
"""

import sys

if '--host' in sys.argv[1:] and '--edit' in sys.argv[1:]:
    # Edit runs must always be re-invoked on the suite host.
    if '--use-ssh' not in sys.argv[1:]:
        sys.argv[1:].append('--use-ssh')

if '--use-ssh' in sys.argv[1:]:
    sys.argv.remove('--use-ssh')
    from cylc.remote import remrun
    if remrun().execute(force_required=True, forward_x11=True):
        sys.exit(0)

import re
import os
import time
import shutil
import difflib
import subprocess

import cylc.flags
from cylc.prompt import prompt
from cylc.option_parsers import CylcOptionParser as COP
from cylc.network.suite_command_client import SuiteCommandClient
from cylc.network.suite_info_client import SuiteInfoClient
from cylc.cfgspec.globalcfg import GLOBAL_CFG
from cylc.task_id import TaskID


def main():
    """CLI for "cylc trigger"."""
    parser = COP(
        __doc__, comms=True, multitask=True,
        argdoc=[
            ('REG', 'Suite name'),
            ('[TASKID ...]', 'Task identifiers')])

    parser.add_option(
        "-e", "--edit",
        help="Manually edit the job script before running it.",
        action="store_true", default=False, dest="edit_run")

    parser.add_option(
        "-g", "--geditor",
        help="(with --edit) force use of the configured GUI editor.",
        action="store_true", default=False, dest="geditor")

    options, args = parser.parse_args()
    suite = args.pop(0)

    task_ids = args

    msg = 'Trigger task(s) %s in %s' % (args, suite)
    prompt(msg, options.force)

    cmd_client = SuiteCommandClient(
        suite, options.owner, options.host, options.port,
        options.comms_timeout, my_uuid=options.set_uuid,
        print_uuid=options.print_uuid)

    if options.edit_run:
        items = parser.parse_multitask_compat(options, args)
        task_id = items[0]
        # Check that TASK is a unique task.
        info_client = SuiteInfoClient(
            suite, options.owner, options.host, options.port,
            options.comms_timeout, my_uuid=cmd_client.my_uuid)
        success, msg = info_client.get_info(
            'ping_task', task_id=task_id, exists_only=True)
        if not success:
            sys.exit('ERROR: %s' % msg)

        # Get the job filename from the suite daemon - the task cycle point may
        # need standardising to the suite cycle point format.
        jobfile_path = info_client.get_info(
            'get_task_jobfile_path', task_id=task_id)
        if not jobfile_path:
            sys.exit('ERROR: task not found')

        # Note: localhost time and file system time may be out of sync,
        #       so the safe way to detect whether a new file is modified
        #       or is to detect whether time stamp has changed or not.
        #       Comparing the localhost time with the file timestamp is unsafe
        #       and may cause the "while True" loop that follows to sys.exit
        #       with an error message after MAX_TRIES.
        try:
            old_mtime = os.stat(jobfile_path).st_mtime
        except OSError:
            old_mtime = None

        # Tell the suite daemon to generate the job file.
        cmd_client.put_command('dry_run_tasks', items=[task_id])

        # Wait for the new job file to be written. Use mtime because the same
        # file could potentially exist already, left from a previous run.
        count = 0
        MAX_TRIES = 10
        while True:
            count += 1
            try:
                mtime = os.stat(jobfile_path).st_mtime
            except OSError:
                pass
            else:
                if old_mtime is None or mtime > old_mtime:
                    break
            if count > MAX_TRIES:
                sys.exit('ERROR: no job file after %s seconds' % MAX_TRIES)
            time.sleep(1)

        # Make a pre-edit copy to allow a post-edit diff.
        jobfile_copy_path = "%s.ORIG" % jobfile_path
        shutil.copy(jobfile_path, jobfile_copy_path)

        # Edit the new job file.
        if options.geditor:
            editor = GLOBAL_CFG.get(['editors', 'gui'])
        else:
            editor = GLOBAL_CFG.get(['editors', 'terminal'])
        # The editor command may have options, e.g. 'emacs -nw'.
        command_list = re.split(' ', editor)
        command_list.append(jobfile_path)
        command = ' '.join(command_list)
        try:
            # Block until the editor exits.
            retcode = subprocess.call(command_list)
            if retcode != 0:
                sys.exit(
                    'ERROR, command failed with %d:\n %s' % (retcode, command))
        except OSError:
            sys.exit('ERROR, unable to execute:\n %s' % command)

        # Get confirmation after editing is done.
        # Don't allow force-no-prompt in this case.
        if options.geditor:
            # Alert stdout of the dialog window, in case it's missed.
            print "Editing done. I'm popping up a confirmation dialog now."

        # Save a diff to record the changes made.
        log_dir = os.path.dirname(jobfile_path)
        with open("%s-edit.diff" % jobfile_path, 'wb') as diff_file:
            for line in difflib.unified_diff(
                    open(jobfile_copy_path).readlines(),
                    open(jobfile_path).readlines(),
                    fromfile="original",
                    tofile="edited"):
                diff_file.write(line)
        os.unlink(jobfile_copy_path)

        log_dir = os.path.dirname(jobfile_path)
        msg = "Trigger edited task %s?" % task_id
        if not prompt(msg, gui=options.geditor, no_force=True, no_abort=True):
            # Generate placeholder log files for the aborted run.
            for log in ["job.out", "job.err"]:
                lf = os.path.join(log_dir, log)
                with open(lf, 'wb') as log_file:
                    log_file.write("This edit run was aborted\n")
                    print "Run aborted."
                    sys.exit(0)

    # Trigger the task proxy(s).
    items = parser.parse_multitask_compat(options, args)
    cmd_client.put_command('trigger_tasks', items=items)


if __name__ == "__main__":
    try:
        main()
    except Exception as exc:
        if cylc.flags.debug:
            raise
        sys.exit(str(exc))
