import contextlib
import logging
import multiprocessing
import os
import random
import signal
import socket
import string
import struct
import subprocess
import sys
import tempfile
import traceback

from libosue import *
from libosue import CSnippet as C
from libosue import FunctionPrototype as FP


################################################################################
# INITIALIZE THE EXAM OBJECT
################################################################################

# These functions are not allowed.
not_allowed = [
    "DEMO_setup_connection",
    "DEMO_server_request_handler",
    "DEMO_execute_command",
]

prototypes = {
    "parse_arguments": FP("void")("int argc", "char **argv", "struct arguments *args"),
    "DEMO_setup_connection": FP("int")("const char *port_str"),
    "DEMO_server_request_handler": FP("void")("int sockfd", "const char *command", "int flags"),
    "DEMO_execute_command": FP("FILE *")("const char *command", "const char *argument"),
}

PORT = 692

exam.initialize(
    "./server",
    server_port=7220,
    not_allowed=not_allowed + exam.DEFAULT_RESTRICTED_FUNCTIONS,
    call_limit=200,
    prototypes=prototypes,
    report_file="report.txt",
    result_file="result.txt",
    debug_file="libosue.dbg",
    make_targets=["clean", "all"],
    initialize_stack=["setup_connection", "server_request_handler", "execute_command"],
)


class FunctionAborted(EvalError):
    """Raised when a function should be aborted."""

    pass


# Kill programs on port 692.
# This is slow, so we only do it once every deliver call and not for every session.
utils.free_port(PORT)


################################################################################
# TASK 1: SETUP CONNECTION
################################################################################


def bind_cb(s, args):
    """Call setsockopt(...) before binding."""
    reuse = Value.from_type("char")
    reuse.value = 1
    s.c.setsockopt(args["sockfd"], socket.SOL_SOCKET, socket.SO_REUSEADDR, reuse.address, 1)


with exam.task("Task 1: setup connection", 10) as task:
    with task.subtask("initialize server socket", 8) as st:
        with exam.session([]) as s:
            # Setup tracing for hints.
            s.c.socket.tracing = True
            s.c.bind.tracing = True
            s.c.listen.tracing = True

            # Make sure setsockopt(...) is called before bind().
            s.c.bind.call_cb = bind_cb

            # Call the student's setup_connection connection function
            sockfd = s.c.setup_connection(str(PORT).encode())

            # Check if the server socket works and creates valid connection sockets.
            utils.check_server_socket(s, sockfd, PORT)

    # Give the students some hints for their implementation
    if st.points != 8:
        if "socket" not in s.trace:
            exam.log("[INFO] socket() was never called")
        if "bind" not in s.trace:
            exam.log("[INFO] bind() was never called")
        if "listen" not in s.trace:
            exam.log("[INFO] listen() was never called")

        exam.log("The following subtasks are skipped because of errors:")
        exam.log("* error handling (2 points)")
        task.skip_subtasks()

    with task.subtask("error handling", 2) as st:
        with st.subtask("bind()", 1):
            with exam.session([]) as s:
                s.c.listen.tracing = True
                s.c.bind.override_return = C("((int) -1)")

                with exam.raises(ExitError, "program did not exit after bind() failed") as excinfo:
                    sockfd = s.c.setup_connection(str(PORT).encode())
                assert excinfo.value.exitcode == 1, "wrong exit code"
                assert "listen" not in s.trace, "listen() called after bind() failed"

        with st.subtask("listen()", 1):
            with exam.session([]) as s:
                s.c.listen.override_return = C("((int) -1)")

                with exam.raises(ExitError, "program did not exit after listen() failed") as excinfo:
                    sockfd = s.c.setup_connection(str(PORT).encode())
                assert excinfo.value.exitcode == 1, "wrong exit code"


with exam.task("Task 2: handle requests", 20) as task:
    with task.subtask("read argument and execute command", 7) as st:
        # Check if the program correctly accepts a connection and reads the argument
        # from the connection socket. Execution stops when task 3 is called.
        def accept_return_cb(s, rv):
            rv = rv.convert()
            if rv > 0:
                s["connfd"] = rv

        def read_call_cb(s, args):
            connfd = args.get("sockfd") or args.get("fd")
            s["read"] = connfd.convert() == s["connfd"]
            s["buffer"] = args.get("buf")

        def read_return_cb(s, rv):
            if s["read"]:
                n = rv.convert()
                s["result"] += s["buffer"].convert(shape=n, cast="char *")

        def execute_command_cb(s, args):
            s["ec_called"] = True
            try:
                s["command"] = args["command"].convert()
                s["argument"] = args["argument"].convert()
            except InvalidMemoryError:
                pass
            raise FunctionAborted("This is where execution ends.")

        with exam.session([]) as s:
            s.c.accept.return_cb = accept_return_cb
            s.c.read.call_cb = read_call_cb
            s.c.read.return_cb = read_return_cb
            s.c.recv.call_cb = read_call_cb
            s.c.recv.return_cb = read_return_cb

            s.c.execute_command.call_cb = execute_command_cb
            s.c.DEMO_execute_command.call_cb = execute_command_cb
            s.c.DEMO_execute_command.allowed = True

            # Setup the tracing variables.
            s["connfd"] = None
            s["result"] = b""
            s["ec_called"] = False
            s["command"] = None
            s["argument"] = None

            argument = "".join(random.choices(string.ascii_letters, k=24)).encode()
            sockfd = s.c.DEMO_setup_connection(str(PORT).encode())
            with utils.SingleShotClient(PORT, argument) as client:
                with contextlib.suppress(FunctionAborted):
                    s.c.server_request_handler(sockfd, b"./osue-tool")
            assert s["result"], "no argument read from client"
            assert s["result"] == argument, "read wrong argument from client"
            assert s["ec_called"], "execute_command() not called"
            assert s["command"] == b"./osue-tool", "invalid command passt to execute_command()"
            assert (
                s["argument"] == argument
            ), "Invalid argument passed to execute_command()\nDid you terminate it?"

    with task.subtask("response", 8) as st:
        # Execute the full function and check if the client receives the correct result.
        def execute_command_cb(s, args):
            with s.c.fork.save():
                s.c.fork.allowed = True
                return s.c.DEMO_execute_command(args["command"], args["argument"])

        with st.subtask("single line output", 4) as pt:
            with exam.session([]) as s:
                s.c.execute_command.call_cb = execute_command_cb
                s.c.DEMO_execute_command.call_cb = execute_command_cb
                s.c.DEMO_execute_command.allowed = True

                sockfd = s.c.DEMO_setup_connection(str(PORT).encode())

                argument = "".join(random.choices(string.ascii_letters, k=24)).encode()
                expected = argument + b"\n"
                with utils.SingleShotClient(PORT, argument) as client:
                    s.c.server_request_handler(sockfd, b"./osue-tool")
                assert client.response, "didn't receive a response for single-line output"
                assert client.response == expected, "received wrong response for single-line output"

        with st.subtask("multi line output", 4) as pt:
            with exam.session([]) as s:
                s.c.execute_command.call_cb = execute_command_cb
                s.c.DEMO_execute_command.call_cb = execute_command_cb
                s.c.DEMO_execute_command.allowed = True

                sockfd = s.c.DEMO_setup_connection(str(PORT).encode())

                argument = "".join(random.choices(string.ascii_letters, k=128)).encode()
                expected = b"\n".join(argument[i : i + 32] for i in range(0, len(argument), 32)) + b"\n"
                with utils.SingleShotClient(PORT, argument) as client:
                    s.c.server_request_handler(sockfd, b"./osue-tool")
                assert client.response, "didn't receive a response for multi-line output"
                assert client.response == expected, "received wrong response for multi-line output"

    if task.points != 15:
        exam.log("The following subtasks are skipped because of errors:")
        exam.log("* repeated write/send calls (5 points)")
        task.skip_subtasks()

    with task.subtask("repeated write/send calls", 5) as st:

        def accept_return_cb(s, rv):
            s["connfd"] = rv.convert()

        def write_cb(s, args):
            fd = (args.get("fd") or args.get("sockfd")).convert()

            if fd == s["connfd"]:
                n = args.get("count") or args.get("len")
                buf = args.get("buf")
                s["messages"].append(buf.convert(shape=n.convert(), cast="char *"))
                return 1

        with exam.session([]) as s:
            s.c.execute_command.call_cb = execute_command_cb
            s.c.DEMO_execute_command.call_cb = execute_command_cb
            s.c.DEMO_execute_command.allowed = True

            s.c.accept.return_cb = accept_return_cb
            s.c.write.call_cb = write_cb
            s.c.send.call_cb = write_cb

            s["connfd"] = None
            s["messages"] = []

            argument = "".join(random.choices(string.ascii_letters, k=24)).encode()
            expected = argument + b"\n"
            sockfd = s.c.DEMO_setup_connection(str(PORT).encode())
            with utils.SingleShotClient(PORT, argument) as client:
                s.c.server_request_handler(sockfd, b"./osue-tool")

            assert len(s["messages"]) > 1, "write/send not called multiple times"
            message = b"".join(m[:1] for m in s["messages"])
            assert message == expected, "received wrong response"


with exam.task("Task 3: execute command", 20) as task:
    with task.subtask("fork/exec pattern", 5) as st:
        pattern = utils.ForkNode(parent=None, child=utils.ExecNode("./osue-tool"))
        with utils.ForkExecTraces(pattern) as tracegen:
            for fork_exec_run in tracegen:
                with exam.session() as s:
                    with fork_exec_run(s):
                        s.c.execute_command(b"./osue-tool", b"just a random string")

        assert tracegen.correct, tracegen.default_message

    if task.points != 5:
        exam.log("The following subtasks are skipped because of errors:")
        exam.log("* wait for child process to finish  (5 points)")
        exam.log("* result                            (10 points)")
        task.skip_subtasks()

    with task.subtask("wait for child process to finish", 5) as st:
        # Correctly wait for the child process to finish.
        # Follow the parent process and trace the return value of fork() and wait().
        def fork_return_cb(s, rv):
            s["fork_result"] = rv.convert()

        def wait_return_cb(s, rv):
            s["wait_result"] = rv.convert()

        with exam.session([]) as s:
            s.c.fork.allowed = True
            s.c.fork.return_cb = fork_return_cb
            s.c.wait.return_cb = wait_return_cb
            s.c.waitpid.return_cb = wait_return_cb
            s.c.execute_command(b"./osue-tool", b"just a random string")

            assert "wait_result" in s, "wait never called"
            assert s.get("wait_result") == s.get("fork_result"), "program did not wait for child process"

    if task.points != 10:
        exam.log("The following subtasks are skipped because of errors:")
        exam.log("* result (10 points)")
        task.skip_subtasks()

    with task.subtask("result", 10) as st:
        # Handle a single request and check the result.
        with exam.session([]) as s:
            s.c.fork.allowed = True

            argument = "".join(random.choices(string.ascii_letters, k=24)).encode()
            expected = argument + b"\n"

            fp = s.c.execute_command(b"./osue-tool", argument)
            assert fp.intaddr(), "returned file pointer not valid"

            bufsize = 8192
            try:
                buf = s.c.malloc(bufsize, cast="char *")
                n = s.c.fread(buf, 1, bufsize, fp)
            except FunctionKilledError:
                st.fail("reading from returned file pointer never terminated")
            except SignalError:
                st.fail("reading from returned file pointer failed")

            result = buf.convert(shape=n.convert())
            assert result == expected, "invalid response"
