#!/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 [discovery] scan [OPTIONS] [HOSTS ...]

Print information about cylc suites currently running on scanned hosts. The
list of hosts to scan is determined by the global configuration "[suite host
scanning]" setting, or hosts can be specified explicitly on the command line.

By default, just your own suites are listed (this assumes your username is the
same on all scanned hosts). Use -a/--all-suites to see all suites on all hosts,
or restrict suites displayed with the -o/--owner and -n/--name options (with
--name the default owner restriction (i.e. just your own suites) is disabled.

Suite passphrases are not needed to get identity information (name and owner).
Titles, descriptions, state totals, and cycle point state totals may also be
revealed publicly, depending on global and suite authentication settings. Suite
passphrases still grant full access regardless of what is revealed publicly.

WARNING: a suite suspended with Ctrl-Z will cause port scans to hang until the
connection times out (see --comms-timeout)."""

import sys
if "--use-ssh" in sys.argv[1:]:
    sys.argv.remove("--use-ssh")
    from cylc.remote import remrun
    if remrun().execute():
        sys.exit(0)

import re

from cylc.network.port_scan import scan_all
from cylc.option_parsers import CylcOptionParser as COP
from cylc.cfgspec.globalcfg import GLOBAL_CFG
from cylc.suite_host import get_user
from cylc.task_state import TASK_STATUSES_ORDERED
from cylc.task_state_prop import get_status_prop


NO_BOLD = False


def bold(line):
    """Add terminal control characters for bold text."""
    global NO_BOLD
    if NO_BOLD:
        return line
    else:
        return "\033[1m" + line + "\033[0m"


def main():
    """Implement "cylc scan"."""
    parser = COP(
        __doc__,
        comms=True,
        noforce=True,
        argdoc=[(
            "[HOSTS ...]", "Hosts to scan instead of the configured hosts.")]
    )

    parser.add_option(
        "-a", "--all", "--all-suites",
        help="List all suites found on all scanned hosts (the default is "
             "just your own suites).",
        action="store_true", default=False, dest="all_suites")

    parser.add_option(
        "-n", "--name",
        metavar="PATTERN",
        help="List suites with name matching PATTERN (regular expression). "
             "Defaults to any name. Can be used multiple times.",
        action="append", dest="patterns_name", default=[])

    parser.add_option(
        "-o", "--suite-owner",
        metavar="PATTERN",
        help="List suites with owner matching PATTERN (regular expression). "
             "Defaults to just your own suites. Can be used multiple times.",
        action="append", dest="patterns_owner", default=[])

    parser.add_option(
        "-d", "--describe",
        help="Print suite titles and descriptions if available.",
        action="store_true", default=False, dest="describe")

    parser.add_option(
        "-s", "--state-totals",
        help="Print number of tasks in each state if available "
             "(total, and by cycle point).",
        action="store_true", default=False, dest="print_totals")

    parser.add_option(
        "-f", "--full",
        help="Print all available information about each suite.",
        action="store_true", default=False, dest="print_full")

    parser.add_option(
        "-c", "--color", "--colour",
        help="Print task state summaries using terminal color control codes.",
        action="store_true", default=False, dest="color")

    parser.add_option(
        "-b", "--no-bold",
        help="Don't use any bold text in the command output.",
        action="store_true", default=False, dest="no_bold")

    parser.add_option(
        "--print-ports",
        help="Print the port range from the site config file "
             "($CYLC_DIR/conf/global.rc).",
        action="store_true", default=False, dest="print_ports")

    parser.add_option(
        "--comms-timeout", metavar="SEC",
        help="Set a timeout for network connections "
             "to each running suite. The default is 5 seconds.",
        action="store", default=None, dest="comms_timeout")

    parser.add_option(
        "--old", "--old-format",
        help='Legacy output format ("suite owner host port").',
        action="store_true", default=False, dest="old_format")

    parser.add_option(
        "-r", "--raw", "--raw-format",
        help='Parsable format ("suite|owner|host|property|value")',
        action="store_true", default=False, dest="raw_format"
    )

    options, args = parser.parse_args()

    if options.print_ports:
        base = GLOBAL_CFG.get(["communication", "base port"])
        max_num_ports = GLOBAL_CFG.get(
            ["communication", "maximum number of ports"])
        print base, "<= port <=", base + max_num_ports
        sys.exit(0)

    indent = "   "

    global NO_BOLD
    if options.no_bold:
        NO_BOLD = True

    if options.print_full:
        options.describe = options.print_totals = True

    if options.color:
        options.print_totals = True

    if options.raw_format and (options.old_format or options.describe or
                               options.print_totals or options.color):
        parser.error(
            "--raw-format cannot be used with other format options.")

    if options.all_suites:
        if options.patterns_name != []:
            parser.error("-a and -n are mutually exclusive.")
        if options.patterns_owner != []:
            parser.error("-a and -o are mutually exclusive.")
        patterns_name = ['.*']  # Any name.
        patterns_owner = ['.*']  # Any owner.
    else:
        if options.patterns_name:
            patterns_name = options.patterns_name
        else:
            # Any suite name.
            patterns_name = ['.*']
        if options.patterns_owner:
            patterns_owner = options.patterns_owner
        else:
            if options.patterns_name:
                # Any suite owner.
                patterns_owner = ['.*']
            else:
                # Just the user's suites.
                patterns_owner = [get_user()]
    pattern_name = "(" + ")|(".join(patterns_name) + ")"
    pattern_owner = "(" + ")|(".join(patterns_owner) + ")"

    state_legend = ""
    if options.color:
        n_states = len(TASK_STATUSES_ORDERED)
        for index, state in enumerate(TASK_STATUSES_ORDERED):
            state_legend += get_status_prop(state, 'ascii_ctrl')
            if index == n_states / 2:
                state_legend += "\n"
        state_legend = state_legend.rstrip()

    skip_one = True
    for host, port, suite_identity in scan_all(args, options.comms_timeout):
        name = suite_identity['name']
        owner = suite_identity['owner']

        if not (re.match(pattern_name, name) and
                re.match(pattern_owner, owner)):
            continue

        if options.old_format:
            print name, owner, host, port
            continue

        if options.raw_format:
            print "%s|%s|%s|port|%s" % (name, owner, host, port)
            for property in ["title", "description", "update-time"]:
                value = suite_identity.get(property, None)
                if value:
                    print "%s|%s|%s|%s|%s" % (
                        name, owner, host, property,
                        str(value).replace("\n", " ")
                    )
            totals = suite_identity.get('states', None)
            if totals is None:
                continue
            point_state_lines = get_point_state_count_lines(
                *totals, use_color=options.color)
            for point, state_line in point_state_lines:
                property = "states"
                if point:
                    property = "states:%s" % point
                print "%s|%s|%s|%s|%s" % (
                    name, owner, host, property, state_line)
            continue

        line = '%s %s@%s:%s' % (name, owner, host, port)
        if options.describe or options.print_totals:
            if skip_one:
                skip_one = False
                if state_legend != "":
                    print state_legend + "\n"
            else:
                print
            print bold(line)
        else:
            print line

        if options.describe:
            title = suite_identity.get('title', None)
            if title is None:
                print indent + bold("(description and state totals withheld)")
                continue
            print indent + bold("Title:")
            if title == "":
                line = "(no title)"
            else:
                line = '"%s"' % title
            print indent * 2 + line

            description = suite_identity.get('description', None)
            print indent + bold("Description:")
            if description == "":
                lines = "(no description)"
            else:
                lines = '"%s"' % description
            line1 = True
            for line in lines.split('\n'):
                line = line.lstrip()
                if not line1:
                    # Indent under the double quote.
                    line = " " + line
                line1 = False
                print indent * 2 + line

        totals = suite_identity.get('states', None)
        if totals is not None:
            state_count_totals, state_count_cycles = totals

        if options.print_totals:
            if totals is None:
                print indent + bold("(state totals withheld)")
                continue
            print indent + bold("Task state totals:")
            point_state_lines = get_point_state_count_lines(
                *totals, use_color=options.color)
            for point, state_line in point_state_lines:
                point_prefix = ""
                if point:
                    point_prefix = "%s " % point
                print indent * 2 + "%s%s" % (point_prefix, state_line)


def get_point_state_count_lines(state_count_totals, state_count_cycles,
                                use_color=False):
    """Yield (point, state_summary_text) tuples."""
    line = ""
    for state, tot in sorted(state_count_totals.items()):
        if use_color:
            subst = " %d " % tot
            line += get_status_prop(state, 'ascii_ctrl', subst)
        else:
            line += '%s:%d ' % (state, tot)
    yield ("", line.strip())

    for point_string in sorted(state_count_cycles.keys()):
        line = ""
        for state, tot in sorted(state_count_cycles[point_string].items()):
            if use_color:
                subst = " %d " % tot
                line += get_status_prop(state, 'ascii_ctrl', subst)
            else:
                line += '%s:%d ' % (state, tot)
        yield (point_string, line.strip())


if __name__ == "__main__":
    main()
