#!/usr/bin/env qore
# -*- mode: qore; indent-tabs-mode: nil -*-

%enable-all-warnings
%new-style

%requires qore >= 0.8.13
%requires WebSocketClient
%requires yaml >= 0.5
%requires DebugCmdLine
%requires DebugLinenoiseCmdLine
%requires ConnectionProvider
%requires Logger

%exec-class DebugWrapper

class DebugCommandLineRemote inherits DebugLinenoiseCommandLine {
    const WSC_TIMEOUT = 1500ms;
    public {
        Counter counter();
        int pendingUid;
        any recData;
        WebSocketClient wsc;
        timeout wscTimeout;
        bool connecting;
        string serverName;
        string url;
        *hash headers;
    }

    constructor(hash<auto> opts) : DebugLinenoiseCommandLine() {
        wscTimeout = opts.response_timeout ?? WSC_TIMEOUT;
        opts.logger = logger;
        url = opts.url;
        if (opts.header) {
            foreach string h in (opts.header) {
                *list<string> rv = (h =~ x/^([a-zA-Z0-9\-]+)=(.*)$/);
                if (!rv)
                    throw "GETOPT-ERROR", sprintf("Wrong header specification %y", h);
                headers{rv[0]} = rv[1];
            }
        }
        wsc = new WebSocketClient(\wscEvent(), opts);
    }

    public *hash doCommandImpl(hash data) {
        #if (!wsc.isOpen()) throw
        pendingUid = clock_getmicros();
        data.uid = pendingUid;
        string d = make_yaml(data);
        while (counter.getCount() > 0) {
            counter.dec();
        }
        counter.inc();
        recData = NOTHING;
        debug("send: %y", d);
        wsc.send(d);
        if (counter.waitForZero(wscTimeout)) {
            return NOTHING;
        } else {
            return recData;
        }
    }

    public nothing connect() {
        connecting = True;
        counter.inc();
        hash hh = wsc.connect({"hdr": headers});
        *string prot_ver = hh{QoreDebugWsProtocolHeader.lwr()};
        if (!prot_ver.val())
            throw "QORE-DEBUG", sprintf("Connected to %y, but no %y header received in response; check the URI path "
                "and try again", url, QoreDebugWsProtocolHeader);
        if (prot_ver != QoreDebugProtocolVersion)
            throw "QORE-DEBUG", sprintf("Qore debug server at %y claims unsupported protocol version %y; expecting "
                "%y", url, prot_ver, QoreDebugProtocolVersion);
        if (counter.waitForZero(wscTimeout)) {
            throw "QORE-DEBUG", "No response from debug server";
        } else {
            serverName = recData.result;
        }
    }

    public wscEvent(*data msg) {
        debug("received: %y", msg);
        if (!exists msg)
            return;
        try {
            auto d = parse_yaml(msg);
            if (counter.getCount() > 0) {
                if ((d.type == "response" && pendingUid == d.uid && !connecting) ||
                    (d.type == "event" && d.cmd == "welcome" && connecting) ) {
                    recData = d;
                    counter.dec();
                    connecting = False;
                    return;
                }
            }
            printData(d);
        } catch (hash<ExceptionInfo> ex) {
            error("wscEvent: %s", get_exception_string(ex));
        }
    }
}

class DebugWrapper {
    private {
        hash opts = (
            'help': 'h,help',
            'verbose': 'v,verbose:+',
            'url': 'u,url=s',
            'max_redirects': 'm,max-redir=i',
            'header': 'H,header=s@',
            'proxy': 'P,proxy=s',
            'timeout': 't,timeout=i',
            'connect_timeout': 'c,conn-timeout=i',
            'response_timeout': 'w,resp-timeout=i',
            "history": "y,history=s",
            "session": "s,session=s",
        );
        DebugCommandLineRemote dcl;
        Logger logger;
    }

    constructor() {
        hash opt;
        try {
            GetOpt g(opts);
            list a = ARGV;
            opt = g.parse2(\a);
            opt.url = shift a;
            if (a) {
                throw "GETOPT-ERROR", "Only one URL can be specified";
            }
            if (!exists opt.url) {
                throw "GETOPT-ERROR", "Missing URL";
            }
            *string path_suffix;
            string orig_url = opt.url;
            try {
                # check if it's in "connection_name/path" format
                *list l = opt.url =~ x/^([A-Za-z_\-0-9]+)\/([A-Za-z_\-0-9\.]+)$/;
                if (l) {
                    opt.url = get_connection_url(l[0]);
                    path_suffix = l[1];
                } else {
                    opt.url = get_connection_url(opt.url);
                }
            } catch (hash<ExceptionInfo> ex) {
                if (ex.err != "CONNECTION-ERROR") {
                    rethrow;
                }
                if (opt.url !~ /:\/\// && opt.url !~ /:/ && opt.url !~ /\//) {
                    throw "CONNECTION-ERROR", sprintf("URL %y is not a valid URL and no connection can be found with "
                        "this name; connection providers searched (QORE_CONNECTION_PROVIDERS env var): %y", opt.url,
                        ENV.QORE_CONNECTION_PROVIDERS);
                }
            }
            if (orig_url != opt.url) {
                if (opt.url =~ /^https?:\/+[^\/]+$/) {
                    # use "debug" path if none present
                    opt.url += "/debug";
                }
                if (path_suffix.val()) {
                    if (opt.url !~ /\/$/) {
                        opt.url += "/";
                    }
                    opt.url += path_suffix;
                }
                if (opt.url =~ /^https?:/) {
                    opt.url =~ s/^http/ws/;
                }
                if (opt.verbose) {
                    stderr.printf("Using connection %y url: %y\n", orig_url, opt.url);
                }
            }
            switch (opt.url) {
            case /^wss?:\/\//:
                break;
            case /^[a-zA-Z0-9_]+:\/\//:
                throw "GETOPT-ERROR", "Url protocol is not ws://";
            default:
                opt.url = "ws://"+opt.url;
            }
        } catch (hash<ExceptionInfo> ex) {
            stderr.printf("%s: %s\n", ex.err, ex.desc);
            help(-1);
        }

        if (opt.help) {
            help();
        }
        logger = new Logger();
        logger.addAppender(new StdoutAppender());
        if (opt.verbose) {
            if (opt.verbose == 1) {
                logger.setLevel("INFO");
            } else if (opt.verbose == 2) {
                logger.setLevel("DEBUG");
            } else if (opt.verbose == 3) {
                logger.setLevel("TRACE");
            } else {
                logger.setLevel("ALL");
            }
        } else {
            logger.setLevel("WARN");
        }
        logger.info("url: %s verbose: %s", opt.url, logger.getLevel().getStr());
        dcl = new DebugCommandLineRemote(opt + {"logger": logger});
        try {
            dcl.connect();
            # unconditional connection message
            stdout.printf("connected to debug server %y: %s; \"help\" for help\n", opt.url, dcl.serverName);
            *hash sess = dcl.doCommandImpl(('cmd': 'session'));
            if (sess.result) {
                string pgmId = sess.result.firstKey();
                dcl.setContextValue('programId', pgmId);
                if (sess.result{pgmId}.interrupted) {
                    dcl.setContextValue('threadId', sess.result{pgmId}.interrupted[0]);
                }
            }
            dcl.init(opt);
            dcl.runCmdLine();
            dcl.wsc.disconnect();

        } catch (hash<ExceptionInfo> e) {
            stderr.printf("%s: %s\n", e.err, e.desc);
            exit(-1);
        }
    }

    private help(int exCode=1) {
        printf(
            "usage: %s [options] <url>\n"
            "       %s [options] <conn-id>[/<path>]\n"
            "\n"
            "  <url> is the URL of debug server in 'ws://socket=<url_encoded_path>/path' format, ws:// is optional.\n"
            "  ConnectionProvider <conn-id> name is considered and when is resolved then http(s) protocol is changed to ws(s).\n"
            "  The '/debug' path is appended unless a path already exists in the URL and the <path> follows\n"
            "\n"
            "  -v                      verbose\n"
            "  -h                      help\n"
            "  -H,--header=<hdr>=<val> headers for connection request\n"
            "  -m,--max-redir=<num>    the maximum number of redirects before throwing an exception (the default is 5)\n"
            "  -P,--proxy=<url>        the proxy URL for connecting through a proxy\n"
            "  -t,--timeout=<ms>       the timeout\n"
            "  -c,--conn-timeout=<ms>  the timeout for establishing a new socket connection\n"
            "  -w,--resp-timeout=<ms>  the timeout to wait for websocket response, default: %d\n"
            "  -y,--history=file       load command line history from file, default: %s in home directory\n"
            "                          When \".\" name is provided then history is not loaded/saved\n"
            "  -s,--session=file       load session from file\n"

            "\n"
            "Example:\n"
            "  %s -v localhost:8000/debug\n"
            "  %s -v qorus\n"
            "  %s -v qorus/qjob-test_v1.0-1\n"
            "\n"
            ,
            get_script_name(),
            get_script_name(),
            DebugCommandLineRemote::WSC_TIMEOUT,
            DebugLinenoiseCommandLine::defaultHistoryFileName,
            get_script_name(),
            get_script_name(),
            get_script_name(),

        );
        exit(exCode);
    }

    public dummy() {
    }
}
