#! /usr/bin/python
# Copyright 2016, 2017 Cumulus Networks, Inc.  All Rights reserved.
#
# This software is subject to the Cumulus Networks End User License Agreement
# available at the following locations:
#
# Internet: https://cumulusnetworks.com/downloads/eula/latest/view/
# Cumulus Linux systems: /usr/share/cumulus/EULA.txt

"""
This will be called a lot so it needs to be fast. Our job is very simple:
- connect to netqd
- TX sys.argv to netqd
- RX the reply from netqd
- print the reply

netqd does all of the heavy lifting.

"""

from json import dumps as json_dumps, loads as json_loads
from network_docopt import get_bash_prompt, get_network_docopt_info
from select import select
from socket import socket as socket_socket, error as socket_error
from socket import AF_UNIX, SOCK_STREAM
from subprocess import Popen
import os
import re
import sys
from netq_apps.lib.netq import NetQ
from netq_lib.common.enums import (
    BgColors,
)
from netq_lib.common import utils
from netq_apps.modules.core.trace.resolve import resolve_output


def cli_netq_resolve(argv):
    ''''''
    rc = 0
    vrf = vlan = '*'
    start_time = end_time = None

    while argv[2:]:
        ele = argv.pop(2)
        if ele == 'vrf':
            vrf = argv.pop(2) if len(argv) >= 3 else '*'
        elif ele == 'vlan':
            try:
                vlan = int(argv.pop(2)) if len(argv) >= 3 else '*'
                if vlan < 1 or vlan > 4096:
                    print(
                        utils.color_wrap(
                            BgColors.RED,
                            'VLAN must be an integer between 1-4096'))
                    sys.exit(1)
            except ValueError:
                print(
                    utils.color_wrap(
                        BgColors.RED,
                        'VLAN must be an integer between 1-4096'))
                sys.exit(1)
        elif ele == 'around':
            start_time = argv.pop(2) if len(argv) >= 4 else None

    netq = NetQ(None, start_time, None)

    if not netq:
        print(
            utils.color_wrap(
                BgColors.RED,
                'No connection to backend server'))
        sys.exit(1)
    if not netq.get_config_server_addr():
        print(
            utils.color_wrap(
                BgColors.RED,
                'No backend server specified'))
        sys.exit(1)

    ctx = {}
    pmtu_error = ''
    if not sys.stdin.isatty():
        try:
            for line in sys.stdin:
                line = resolve_output(netq, line, vrf, vlan, ctx)
                print line,
                if 'pmtu_error' in ctx and not pmtu_error:
                    pmtu_error = ctx['pmtu_error']

            if pmtu_error:
                print(
                    utils.color_wrap(
                        BgColors.RED,
                        '* - Links with mismatched MTU from origin %s' %
                        ctx['mtu_string'])
                )
            if 'traceroute' in ctx:
                if ctx['mtu'] == 0:
                    print('Path MTU couldn\'t be determined')
                else:
                    print('Path MTU is %d' % ctx['mtu'])
        except KeyboardInterrupt:
            sys.exit(0)

    sys.exit(rc)


def posix_get_window_size():
    """
    Return (width, height) of the console terminal on a POSIX system.
    Return (0, 0) upon raising IOError, which happens when no console is
    allocated.
    """
    # See README.txt for reference information.
    # http://www.kernel.org/doc/man-pages/online/pages/man4/tty_ioctl.4.html
    from fcntl import ioctl
    from termios import TIOCGWINSZ
    from array import array

    winsize = array("H", [0] * 4)

    try:
        ioctl(sys.stdout.fileno(), TIOCGWINSZ, winsize)
    except IOError:
        # For example IOError: [Errno 25] Inappropriate ioctl for device
        # when reply is redirected.
        pass

    return (winsize[1], winsize[0])


def rx_reply(sock):
    """
    RX the reply, and close the socket.
    """
    total_data = []
    timeout = 60
    sock.setblocking(0)

    while True:
        ready = select([sock], [], [], timeout)

        if ready[0]:
            data = sock.recv(4096)
            if data:
                total_data.append(data)
            else:
                break
        else:
            total_data.append('ERROR: did not receive a response within %d '
                              'seconds' % timeout)
            break

    sock.close()
    return ''.join(total_data)


def get_keywords(reply_with_help_text):
    """
    Extract the keywords from the reply containing help text.
    """
    keywords = []
    re_key_value = re.compile(r"""^(.*?):(.*)$""")

    for line in reply_with_help_text.splitlines():
        line = line.strip()

        if line:  # If the line is not the empty string . . .
            re_line = re_key_value.match(line)

            if re_line is not None:
                keywords.append(re_line.group(1))
            else:
                keywords.append(line)

    return keywords


def display_reply(rx_output):

    if not rx_output:
        return

    try:
        json_out = json_loads(rx_output)
        rc = json_out['rc']
        reply = json_out['output']
    except ValueError:
        reply = rx_output
        rc = 0

    pager = os.getenv('PAGER')
    is_json = False
    tab_options = 'USE_STDERR' in reply

    # Determine if the reply is json.
    if reply.startswith('{'):
        try:
            json_loads(reply)
            is_json = True
        except ValueError:
            pass  # Not json

    # Print the entire reply if it is json, if it will fit in the display,
    # or if paging is not an option.
    if (not pager or tab_options or is_json or
            len(reply.splitlines()) < posix_get_window_size()[1]):

        if tab_options:
            reply = reply.replace('USE_STDERR', '')
            stdout_env = os.getenv('NETQ_TAB_STDOUT')

            stdout_env = ((stdout_env is not None) and
                          (stdout_env == '1' or stdout_env.lower() == 'true'))

            if stdout_env:
                try:
                    sys.stdout.write(' '.join(get_keywords(reply)))
                except IOError as e:
                    sys.stderr.write("Failed to write output due to '{0}'.\n".format(e))

            else:
                # Printing TAB complete options with help text.
                cmd = os.path.basename(sys.argv[0])  # get exec name

                sys.stderr.write(reply)
                argv = sys.argv[1:-2]

                if argv:
                    ended_with_space = get_network_docopt_info(sys.argv)[1]
                    sys.stderr.write('\n{0}{1} {2}{3}'.format(
                        get_bash_prompt(), cmd, ' '.join(argv),
                        ' ' if ended_with_space else ''))
                else:
                    sys.stderr.write('\n{0}{1} '.format(get_bash_prompt(), cmd))

        else:  # No tab_options
            use_stdout = True

            # Use write() to avoid adding an extra newline.
            if use_stdout:

                # If the user pipes the output to something like "head -n 10"
                # and there are more than 10 lines of output to display we will
                # get a "Broken pipe" error because STDOUT was closed.  It is
                # safe to silently ignore this error.
                try:
                    sys.stdout.write(reply + '\n')
                except IOError:
                    pass
            else:
                sys.stderr.write(reply)

    else:
        # We have more text than can fit on the screen.  Write the reply to a
        # tmp file, and then point 'less' at that file. Use mkstemp
        # for filename in case multiple users are calling "netq" at the same
        # time.
        f_tmp, filename = tempfile.mkstemp()
        f_tmp.write(reply)

        # Run whatever executable "pager" references, which is usually 'less'.
        Popen([pager, filename], stdout=None, stdin=None).wait()
        os.remove(filename)

    # Long term, we need netqd to tell us what our return code should be. For
    # now, look for text that indicates a failure, and exit(1) if we see any
    # of them.
    #
    # We would not want to exit(1) if the user did something silly, like name a
    # "route-map ERROR:", so look for lines that start with "ERROR: ".
    sys.exit(rc)


if __name__ == '__main__':
    """
    # uncomment to enable logging
    import logging
    logging.basicConfig(level=logging.INFO,
    format='%(asctime)s %(levelname)7s %(filename)12s: %(message)s')
    log = logging.getLogger(__name__)

    # Color the errors and warnings in red
    logging.addLevelName(logging.ERROR, "\033[91m  %s\033[0m" %
    logging.getLevelName(logging.ERROR))
    logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" %
    logging.getLevelName(logging.WARNING))
    """

    # log.info('socket connect start')
    # Connect to netqd's unix domain socket.
    sock = socket_socket(AF_UNIX, SOCK_STREAM)
    try:
        sock.connect('/var/run/netqd/uds')
    except socket_error:
        sys.stderr.write("""
ERROR: net could not connect to netqd

Try starting netqd with:
sudo systemctl start netqd

To configure netqd to start when the box boots:
sudo systemctl enable netqd
""")
        sys.exit(1)

    # If the user enters something crazy like "netq show '' config" we must
    # remove the '', it confuses network-docopt
    sys.argv = [x for x in sys.argv if x != '']

    # TX our command line args, RX the reply, and display the reply.
    if (len(sys.argv) >= 2 and sys.argv[-2] != '--completions' and
            sys.argv[1] == 'resolve' and sys.argv[-1] != 'help'):
        # We handle netq resolve locally due to the way it works
        cli_netq_resolve(sys.argv)
    else:
        sock.send(json_dumps(sys.argv))
        reply = rx_reply(sock)
        display_reply(reply)
