#! /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
from time import sleep
import os
import re
import sys
import tempfile
from collections import OrderedDict

from netq_lib.common.enums import (
    BgColors,
)
from netq_lib.common import utils

from netq_apps.cmd.netq import display_stream
from netq_apps.lib.netq import NetQ
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.is_config_server_addr_present():
        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.
    """
    print_data = OrderedDict()   # This is data that was printed at netqd end
    stream_data = OrderedDict()  # This is the data we need to process
    timeout = 180
    data = ''
    envelope = ''
    sock.setblocking(0)

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

        if ready[0]:
            rxdata = sock.recv(4096)
            # Streaming causes all sorts of munging to happen
            if not rxdata:
                break
            else:
                data += rxdata
        else:
            print('ERROR: did not receive a response within %d seconds' %
                  timeout)
            return None, None, envelope

    sock.close()
    # Now that we have everything, lets try and make sense of this
    # Every send from the daemon is separated by '\n'
    module = 'default'
    for line in data.split('\r'):
        try:
            json_data = json_loads(line)
            if 'data' in json_data:
                if isinstance(json_data['data'], dict):
                    module = json_data['data']['module']
                    if json_data['data'].get('jsonify', False):
                        envelope = json_data['data'].get('envelope', '')
                    stream_data[module] = []
                    print_data[module] = []
                    stream_data[module].append(json_data['data'])
                else:
                    for ele in json_data['data']:
                        if isinstance(ele, dict):
                            module = ele['module']
                            if ele['data'].get('jsonify', False):
                                envelope = ele['data'].get('envelope', '')
                            stream_data[module] = []
                            stream_data[module].append(ele['data'])
                        else:
                            stream_data[module].append(ele)
            else:
                if module not in print_data:
                    print_data[module] = []
                    if module not in stream_data:
                        stream_data[module] = []
                print_data[module].append(line)
        except ValueError:
            print 'Parsing error in data received'
            return None, None, ''
    return stream_data, print_data, envelope


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(stream_data, print_data, envelope):

    if not print_data:
        return                  # Should never happen

    rc = 0
    print_trailer = False
    first = True
    for key in stream_data:
        # This is to print the "," between individual modules when we're
        # dumping JSON and tere's an envelope enclosing the individual key
        # outputs. This piece is here to avoid printing a trailing "," for
        # the last key.
        if print_trailer:
            print ','
        output = ''.join(print_data[key])
        try:
            json_out = json_loads(output)
            rc = json_out['rc']
            reply = json_out['output']
        except ValueError:
            reply = output

        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 and reply:

                    # 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
                elif reply:
                    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
            # for filename in case multiple users are calling "netq" at the
            # 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)

        if stream_data[key]:
            try:
                if first and envelope:
                    print '{{\n "{}": ['.format(envelope)
                    print_trailer = True
                display_stream(stream_data[key])
            except (IOError, KeyboardInterrupt):
                sys.exit(rc)
        first = False

    if print_trailer:
        print ']\n}'
    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)
    for i in range(3):
        # Three retries because when netqd is started and a netq cmd
        # issued, the command fails because netqd is still loading the
        # parser. Three retries is too much, but what the...
        try:
            sock.connect('/var/run/netqd/uds')
            break
        except socket_error:
            sleep(1)
            pass
    else:
        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))
        stream_data, reply, envelope = rx_reply(sock)
        if reply is None:
            sys.exit(1)         # Happens only if there was an error in rx
        display_reply(stream_data, reply, envelope)
