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

%enable-all-warnings
%new-style

%requires linenoise
%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 opts) : DebugLinenoiseCommandLine() {
        wscTimeout = opts.response_timeout ?? WSC_TIMEOUT;
        opts.log = \wscLog();
        opts.errlog = \wscErr();
        opts.debuglog = \wscDebug();
        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;
        logger.log(DUV_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) {
        logger.log(DUV_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) {
            wscErr(sprintf("wscEvent: %y", ex));
        }
    }

    public wscLog(s) {
        logger.log(DUV_INFO, s);
    }

    public wscDebug(s) {
        logger.log(DUV_DEBUG, s);
    }

    public wscErr(s) {
        stderr.print(s+"\n");
    }
}


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;
        DebugLogger 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 DebugLogger();
        if (opt.verbose) {
            int verbose;
            if (opt.verbose == 1) {
                verbose = LoggerLevel::INFO;
            } else if (opt.verbose == 2) {
                verbose = LoggerLevel::DEBUG;
            } else if (opt.verbose == 3) {
                verbose = LoggerLevel::TRACE;
            } else {
                verbose = LoggerLevel::ALL;
            }
            logger.verbose = verbose;
        } else {
            logger.verbose = LoggerLevel::WARN;
        }
        logger.log(DUV_INFO, "url: %s, verbose: %d", opt.url, logger.verbose);
        dcl = new DebugCommandLineRemote(opt);
        dcl.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() {
    }
}
