#!/usr/bin/python
"""
Copyright (C) 2016, 2017, 2018 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 netd
- TX sys.argv to netd
- RX the reply from netd
- print the reply

netd does all of the heavy lifting.
"""

from json import dumps as json_dumps, loads as json_loads
from network_docopt import get_network_docopt_info
from network_docopt.tab import get_bash_prompt
from select import select
from socket import socket as socket_socket, error as socket_error, AF_UNIX, SOCK_STREAM
from subprocess import Popen
from tempfile import mkstemp
import os
import re
import sys

# Amount of time (seconds) to wait for a response from netd
NETD_TIMEOUT = 720

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 = []
    sock.setblocking(0)

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

        if ready[0]:
            data = sock.recv(4096)
            if data:
                total_data.append(data)
            else:
                break
        else:
            sys.exit("ERROR: No response received within {0} seconds.".format(NETD_TIMEOUT))

    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(reply):

    if not reply:
        return

    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

    if (pager is None) or tab_options or is_json or (reply.count('\n') + 1) < posix_get_window_size()[1]:
        # Print the entire reply because either paging is not an option, the reply is json, or the reply will fit in the display.

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

            if (stdout_env is not None) and (stdout_env == '1' or stdout_env.lower() == "true"):
                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.

                # Get the executable name without the full path.
                cmd = os.path.basename(sys.argv[0])

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

                if argv:
                    ended_with_space = get_network_docopt_info(sys.argv)[1]  # Boolean
                    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 = False if reply.startswith("ERROR") else True

            if use_stdout:
                print reply,  # No newline
            else:
                sys.stderr.write(reply)

    else:
        # The reply will not fit on the screen.  Write it to a temp file.
        f_tmp, filename = mkstemp()
        f_tmp = os.fdopen(f_tmp, 'w')
        f_tmp.write(reply)

        # Run whatever executable "pager" references, which is usually "less".
        try:
            Popen([pager, filename], stdout=None, stdin=None).wait()
        except ValueError:
            # Can't paginate.  Dump the reply to standard output.
            print reply

        f_tmp.close()
        os.remove(filename)

    if not reply.endswith('\n'):
        # Output a newline.
        print ''

    # Long term, we need netd 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: ".
    if re.search(r"""^\s*ERROR:""", reply, re.MULTILINE) is not None:
        sys.exit(1)


if __name__ == "__main__":

    # Connect to netd's Unix domain socket.
    sock = socket_socket(AF_UNIX, SOCK_STREAM)
    try:
        sock.connect("/run/nclu/uds")
    except socket_error:
        sys.exit("""
ERROR: net could not connect to netd

Try starting netd with:
sudo systemctl start netd

To configure netd to start when the box boots:
sudo systemctl enable netd
""")

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

    # TX the command line args to netd.
    sock.send(json_dumps(sys.argv))

    # RX the reply from netd.
    reply = rx_reply(sock)

    if "Press <ENTER> to confirm connectivity." in reply:
        raw_input(reply)

        sock = socket_socket(AF_UNIX, SOCK_STREAM)
        sock.connect("/run/nclu/uds")

        sys.argv.append("Press <ENTER> to confirm connectivity.")
        sock.send(json_dumps(sys.argv))
        reply = rx_reply(sock)

    display_reply(reply)
