#!/usr/bin/env python3
import os
import sys
import logging
import django

DIR_PATH = os.path.dirname(os.path.abspath(__file__))
ROOT_PATH = os.path.abspath(os.path.join(DIR_PATH, "..", ""))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webvirtcloud.settings")
CERT = DIR_PATH + "/cert.pem"

if ROOT_PATH not in sys.path:
    sys.path.append(ROOT_PATH)

django.setup()

from django.conf import settings as django_settings

# socketiod does not authenticate connections yet: any client
# that can reach the port can read and write an open serial console. Refuse
# to start unless an admin explicitly opts in.
if not getattr(django_settings, "SERIAL_CONSOLE_ENABLED", False):
    sys.stderr.write(
        "socketiod: the serial console is disabled (SERIAL_CONSOLE_ENABLED is not "
        "True in settings). It is unauthenticated; only enable it on a trusted "
        "network.\n"
    )
    sys.exit(1)

import codecs
import pty
import select
import struct
import fcntl
import termios
import signal
import socketio
import eventlet

# from six.moves import http_cookies as Cookie
from http import cookies as Cookie
from webvirtcloud.settings import SOCKETIO_PORT, SOCKETIO_HOST
import argparse


def get_parser(default_host=SOCKETIO_HOST, default_port=SOCKETIO_PORT or 6081):
    parser = argparse.ArgumentParser(description="WebVirtCloud Socket.IO daemon")
    parser.add_argument(
        "-v",
        "--verbose",
        dest="verbose",
        action="store_true",
        help="Verbose mode",
        default=False,
    )
    parser.add_argument(
        "-d", "--debug", dest="debug", action="store_true", help="Debug mode", default=False
    )
    parser.add_argument(
        "-H",
        "--host",
        dest="host",
        action="store",
        help="Listen host",
        default=default_host,
    )
    parser.add_argument(
        "-p",
        "--port",
        dest="port",
        action="store",
        type=int,
        help="Listen port",
        default=default_port,
    )
    return parser


options = get_parser().parse_args()

FORMAT = "%(asctime)s - %(name)s - %(levelname)s : %(message)s"
if options.debug:
    logging.basicConfig(level=logging.DEBUG, format=FORMAT)
    options.verbose = True
elif options.verbose:
    logging.basicConfig(level=logging.INFO, format=FORMAT)
else:
    logging.basicConfig(level=logging.WARNING, format=FORMAT)

async_mode = "eventlet"
sio = socketio.Server(async_mode=async_mode, cors_allowed_origins=[])

fd = None
child_pid = None


def get_connection_infos(token):
    from instances.models import Instance
    from vrtManager.instance import wvmInstance

    try:
        temptoken = token.split("-", 1)
        host = int(temptoken[0])
        uuid = temptoken[1]
        instance = Instance.objects.get(compute_id=host, uuid=uuid)
        conn = wvmInstance(
            instance.compute.hostname,
            instance.compute.login,
            instance.compute.password,
            instance.compute.type,
            instance.name,
        )
    except Exception as e:
        logging.error(
            "Fail to retrieve console connection infos for token %s : %s" % (token, e)
        )
        raise
    return (instance, conn)


def set_winsize(fd, row, col, xpix=0, ypix=0):
    winsize = struct.pack("HHHH", row, col, xpix, ypix)
    fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)


def stop_console():
    """Stop the console process (if still running), reap it and close its pty."""
    global fd
    global child_pid

    if child_pid:
        try:
            os.kill(child_pid, signal.SIGKILL)
        except ProcessLookupError:
            pass
        try:
            os.waitpid(child_pid, 0)  # only the child this daemon started
        except ChildProcessError:
            pass
    if fd is not None:
        os.close(fd)
    fd = None
    child_pid = None


def read_and_forward_pty_output():
    max_read_bytes = 1024 * 20
    # a character may be split between two reads
    decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
    # The pty number may be reused by the next console; the pid identifies
    # this one. Every yield (sleep, emit) may end it.
    console_pid, console_fd = child_pid, fd
    while True:
        sio.sleep(0.01)
        if child_pid != console_pid:
            return
        (data_ready, _, _) = select.select([console_fd], [], [], 0)
        if not data_ready:
            continue
        try:
            output = os.read(console_fd, max_read_bytes)
        except OSError:  # EIO: the console process ended
            output = b""
        if not output:
            sio.emit("pty_output", {"output": decoder.decode(b"", final=True) + "\r\n[console closed]\r\n"})
            stop_console()
            for sid, _ in list(sio.manager.get_participants("/", None)):
                sio.disconnect(sid)
            return
        sio.emit("pty_output", {"output": decoder.decode(output)})


@sio.event
def resize(sid, message):
    global fd
    if fd:
        set_winsize(fd, message["rows"], message["cols"])


@sio.event
def pty_input(sid, message):
    global fd
    if fd:
        os.write(fd, message["input"].encode())


@sio.event
def disconnect_request(sid):
    sio.disconnect(sid)


@sio.event
def connect(sid, environ):
    global fd
    global child_pid

    token = None
    hcookie = environ.get("HTTP_COOKIE")
    if hcookie:
        cookie = Cookie.SimpleCookie()
        for hcookie_part in hcookie.split(";"):
            hcookie_part = hcookie_part.lstrip()
            try:
                cookie.load(hcookie_part)
            except Cookie.CookieError:
                logging.warning("Found malformed cookie")
            else:
                if "token" in cookie:
                    token = cookie["token"].value

    if child_pid:
        # already started child process, don't start another
        # write a new line so that when a client refresh the shell prompt is printed
        os.write(fd, str.encode("\n"))
        return

    # create child process attached to a pty we can read from and write to
    (child_pid, fd) = pty.fork()

    if child_pid == 0:
        # Become the console process; never return, which would run this
        # server's code in the child as well. Its output goes to the client.
        try:
            if token is None:
                sys.stderr.write("No console token\n")
            else:
                (instance, conn) = get_connection_infos(token)
                os.execv("conf/daemon/consolecallback", ["consolecallback", conn.wvm.getURI(), conn.get_uuid()])
        except BaseException:
            pass  # get_connection_infos logged it
        finally:
            os._exit(1)
    else:
        # this is the parent process fork.
        sio.start_background_task(target=read_and_forward_pty_output)


@sio.event
def disconnect(sid):
    stop_console()


app = socketio.WSGIApp(sio)

eventlet.wsgi.server(eventlet.listen((options.host, int(options.port))), app)
