/*
 * @build: make examples
 * @server bin/httpd -s restart -d
 * @usage: bin/curl -v www.baidu.com
 *         bin/curl -v 127.0.0.1:8080
 *         bin/curl -v 127.0.0.1:8080/ping
 *         bin/curl -v 127.0.0.1:8080/echo -d 'hello,world!'
 */

#include "HttpClient.h"
#include "hurl.h"

#ifdef _MSC_VER
#include "misc/win32_getopt.h"
#else
#include <getopt.h>
#endif

static bool verbose         = false;
static const char* method   = NULL;
static const char* url      = "/";
static int  http_version    = 1;
static int  grpc            = 0;
static int  send_count      = 1;
static int  retry_count     = 0;
static int  retry_delay     = 3;
static int  timeout         = 0;

static int lopt = 0;
static const char* http_proxy   = NULL;
static const char* https_proxy  = NULL;
static const char* no_proxy     = NULL;

static const char* options = "hVvX:H:r:d:F:n:";
static const struct option long_options[] = {
    {"help",    no_argument,        NULL,   'h'},
    {"verion",  no_argument,        NULL,   'V'},
    {"verbose", no_argument,        NULL,   'v'},
    {"method",  required_argument,  NULL,   'X'},
    {"header",  required_argument,  NULL,   'H'},
    {"range",   required_argument,  NULL,   'r'},
    {"data",    required_argument,  NULL,   'd'},
    {"form",    required_argument,  NULL,   'F'},
    {"count",   required_argument,  NULL,   'n'},
    {"http2",   no_argument,        &http_version, 2},
    {"grpc",    no_argument,        &grpc,  1},
    \
    {"http-proxy",  required_argument,  &lopt,  1},
    {"https-proxy", required_argument,  &lopt,  2},
    {"no-proxy",    required_argument,  &lopt,  3},
    {"retry",       required_argument,  &lopt,  4},
    {"delay",       required_argument,  &lopt,  5},
    {"timeout",     required_argument,  &lopt,  6},
    \
    {NULL,      0,                  NULL,   0}
};
static const char* help = R"(Options:
    -h|--help           Print this message.
    -V|--version        Print version.
    -v|--verbose        Show verbose infomation.
    -X|--method         Set http method.
    -H|--header         Add http header, -H "Content-Type: application/json"
    -r|--range          Add http header Range:bytes=0-1023
    -d|--data           Set http body.
    -F|--form           Set http form, -F "name=value" -F "file=@filename"
    -n|--count          Send request count, used for test keep-alive
       --http2          Use http2
       --grpc           Use grpc over http2
       --http-proxy     Set http proxy
       --https-proxy    Set https proxy
       --no-proxy       Set no proxy
       --retry          Set fail retry count
       --timeout        Set timeout, unit(s)

Examples:
    curl -v GET  httpbin.org/get
    curl -v POST httpbin.org/post   user=admin pswd=123456
    curl -v PUT  httpbin.org/put    user=admin pswd=123456
    curl -v localhost:8080
    curl -v localhost:8080 -r 0-9
    curl -v localhost:8080/ping
    curl -v localhost:8080/query?page_no=1\&page_size=10
    curl -v localhost:8080/echo     hello,world!
    curl -v localhost:8080/kv       user=admin\&pswd=123456
    curl -v localhost:8080/json     user=admin pswd=123456
    curl -v localhost:8080/form     -F file=@filename
    curl -v localhost:8080/upload   @filename
)";

static void print_usage() {
    fprintf(stderr, "Usage: curl [%s] [METHOD] url [header_field:header_value] [body_key=body_value]\n", options);
}
static void print_version() {
    fprintf(stderr, "curl version 1.0.0\n");
}
static void print_help() {
    print_usage();
    puts(help);
    print_version();
}

static bool is_upper_string(const char* str) {
    const char* p = str;
    while (*p >= 'A' && *p <= 'Z') ++p;
    return *p == '\0';
}

static int parse_data(char* arg, HttpRequest* req) {
    char* pos = NULL;
    // @filename
    if (arg[0] == '@') {
        req->File(arg + 1);
        return 0;
    }

    // k1=v1&k2=v2
    hv::KeyValue kvs = hv::splitKV(arg, '&', '=');
    if (kvs.size() >= 2) {
        if (req->ContentType() == CONTENT_TYPE_NONE) {
            req->content_type = X_WWW_FORM_URLENCODED;
        }
        for (auto& kv : kvs) {
            req->Set(kv.first.c_str(), kv.second);
        }
        return 0;
    }

    // k=v
    if ((pos = strchr(arg, '=')) != NULL) {
        *pos = '\0';
        if (pos[1] == '@') {
            // file=@filename
            req->content_type = MULTIPART_FORM_DATA;
            req->SetFormFile(optarg, pos + 2);
        } else {
            if (req->ContentType() == CONTENT_TYPE_NONE) {
                req->content_type = APPLICATION_JSON;
            }
            req->Set(arg, pos + 1);
        }
        return 0;
    }

    if (req->ContentType() == CONTENT_TYPE_NONE) {
        req->content_type = TEXT_PLAIN;
    }
    req->body = arg;
    return 0;
}

static int parse_cmdline(int argc, char* argv[], HttpRequest* req) {
    int opt;
    int opt_idx;
    char* pos = NULL;
    while ((opt = getopt_long(argc, argv, options, long_options, &opt_idx)) != EOF) {
        switch(opt) {
        case 'h': print_help();     exit(0);
        case 'V': print_version();  exit(0);
        case 'v': verbose = true;   break;
        case 'X': method = optarg;  break;
        case 'H':
            // -H "Content-Type: application/json"
            pos = strchr(optarg, ':');
            if (pos) {
                *pos = '\0';
                req->headers[optarg] = hv::trim(pos + 1);
                *pos = ':';
            }
            break;
        case 'r':
            req->headers["Range"] = std::string("bytes=").append(optarg);
            break;
        case 'd':
            parse_data(optarg, req);
            break;
        case 'F':
            pos = strchr(optarg, '=');
            if (pos) {
                req->content_type = MULTIPART_FORM_DATA;
                *pos = '\0';
                if (pos[1] == '@') {
                    // -F file=@filename
                    req->SetFormFile(optarg, pos + 2);
                } else {
                    // -F name=value
                    req->SetFormData(optarg, pos + 1);
                }
                *pos = '=';
            }
            break;
        case 'n': send_count = atoi(optarg); break;
        case  0 :
        {
            switch (lopt) {
            case  1: http_proxy  = optarg;      break;
            case  2: https_proxy = optarg;      break;
            case  3: no_proxy    = optarg;      break;
            case  4: retry_count = atoi(optarg);break;
            case  5: retry_delay = atoi(optarg);break;
            case  6: timeout     = atoi(optarg);break;
            default: break;
            }
        }
        default: break;
        }
    }

    if (optind == argc) {
        fprintf(stderr, "Missing url\n");
        print_usage();
        exit(-1);
    }

    if (is_upper_string(argv[optind])) {
        method = argv[optind++];
    }
    url = argv[optind++];

    for (int d = optind; d < argc; ++d) {
        char* arg = argv[d];
        if ((pos = strchr(arg, ':')) != NULL) {
            *pos = '\0';
            req->headers[arg] = pos + 1;
        } else {
            parse_data(arg, req);
        }
    }

    // --grpc
    if (grpc) {
        http_version = 2;
        req->content_type = APPLICATION_GRPC;
    }
    // --http2
    if (http_version == 2) {
        req->http_major = 2;
        req->http_minor = 0;
    }
    // --timeout
    if (timeout > 0) {
        req->timeout = timeout;
    }

    return 0;
}

int main(int argc, char* argv[]) {
    if (argc < 2) {
        print_usage();
        return 0;
    }

    int ret = 0;
    HttpRequest req;
    parse_cmdline(argc, argv, &req);
    if (method) {
        req.method = http_method_enum(method);
    } else {
        req.DumpBody();
        if (req.body.empty()) {
            req.method = HTTP_GET;
        } else {
            req.method = HTTP_POST;
        }
    }
    req.url = hv::escapeURL(url);
    req.http_cb = [](HttpMessage* res, http_parser_state state, const char* data, size_t size) {
        if (state == HP_HEADERS_COMPLETE) {
            if (verbose) {
                fprintf(stderr, "%s", res->Dump(true, false).c_str());
            }
        } else if (state == HP_BODY) {
            if (data && size) {
                printf("%.*s", (int)size, data);
                // This program no need to save data to body.
                // res->body.append(data, size);
            }
        }
    };

    hv::HttpClient cli;
    // http_proxy
    if (http_proxy) {
        hv::StringList ss = hv::split(http_proxy, ':');
        const char* host = ss[0].c_str();
        int port = ss.size() == 2 ? hv::from_string<int>(ss[1]) : DEFAULT_HTTP_PORT;
        fprintf(stderr, "* http_proxy=%s:%d\n", host, port);
        cli.setHttpProxy(host, port);
    }
    // https_proxy
    if (https_proxy) {
        hv::StringList ss = hv::split(https_proxy, ':');
        const char* host = ss[0].c_str();
        int port = ss.size() == 2 ? hv::from_string<int>(ss[1]) : DEFAULT_HTTPS_PORT;
        fprintf(stderr, "* https_proxy=%s:%d\n", host, port);
        cli.setHttpsProxy(host, port);
    }
    // no_proxy
    if (no_proxy) {
        hv::StringList ss = hv::split(no_proxy, ',');
        fprintf(stderr, "* no_proxy=");
        for (const auto& s : ss) {
            fprintf(stderr, "%s,", s.c_str());
            cli.addNoProxy(s.c_str());
        }
        fprintf(stderr, "\n");
    }

send:
    if (verbose) {
        fprintf(stderr, "%s\n", req.Dump(true, true).c_str());
    }
    HttpResponse res;
    ret = cli.send(&req, &res);
    if (ret != 0) {
        fprintf(stderr, "* Failed:%s:%d\n", http_client_strerror(ret), ret);
        if (retry_count > 0) {
            fprintf(stderr, "\nretry again later...%d\n", retry_count);
            --retry_count;
            hv_sleep(retry_delay);
            goto send;
        }
    }
    if (--send_count > 0) {
        fprintf(stderr, "\nsend again later...%d\n", send_count);
        hv_sleep(retry_delay);
        goto send;
    }
    return ret;
}
