#include "aos_log.h"
#include "aos_util.h"
#include "aos_string.h"
#include "aos_http_io.h"
#include "aos_transport.h"
#include "aos_crc64.h"

static int aos_curl_code_to_status(CURLcode code);
static void aos_init_curl_headers(aos_curl_http_transport_t *t);
static void aos_transport_cleanup(aos_http_transport_t *t);
static int aos_init_curl_url(aos_curl_http_transport_t *t);
static void aos_curl_transport_headers_done(aos_curl_http_transport_t *t);
static int aos_curl_transport_setup(aos_curl_http_transport_t *t);
static void aos_curl_transport_finish(aos_curl_http_transport_t *t);
static void aos_move_transport_state(aos_curl_http_transport_t *t, aos_transport_state_e s);

static size_t aos_curl_default_header_callback(char *buffer, size_t size, size_t nitems, void *userdata);
static size_t aos_curl_default_write_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
static size_t aos_curl_default_read_callback(char *buffer, size_t size, size_t nitems, void *instream);

static void aos_init_curl_headers(aos_curl_http_transport_t *t)
{
    int pos;
    char *header;
    const aos_array_header_t *tarr;
    const aos_table_entry_t *telts;
    union aos_func_u func;

    if (t->req->method == HTTP_PUT || t->req->method == HTTP_POST) {
        header = apr_psprintf(t->pool, "Content-Length: %" APR_INT64_T_FMT, t->req->body_len);
        t->headers = curl_slist_append(t->headers, header);
    }

    tarr = aos_table_elts(t->req->headers);
    telts = (aos_table_entry_t*)tarr->elts;
    for (pos = 0; pos < tarr->nelts; ++pos) {
        header = apr_psprintf(t->pool, "%s: %s", telts[pos].key, telts[pos].val);
        t->headers = curl_slist_append(t->headers, header);
    }
    
    func.func1 = (aos_func1_pt)curl_slist_free_all;
    aos_fstack_push(t->cleanup, t->headers, func, 1);
}

static int aos_init_curl_url(aos_curl_http_transport_t *t)
{
    int rs;
    const char *proto;
    aos_string_t querystr;
    char uristr[3*AOS_MAX_URI_LEN+1];

    uristr[0] = '\0';
    aos_str_null(&querystr);
    
    if ((rs = aos_url_encode(uristr, t->req->uri, AOS_MAX_URI_LEN)) != AOSE_OK) {
        t->controller->error_code = rs;
        t->controller->reason = "uri invalid argument.";
        return rs;
    }

    if ((rs = aos_query_params_to_string(t->pool, t->req->query_params, &querystr)) != AOSE_OK) {
        t->controller->error_code = rs;
        t->controller->reason = "query params invalid argument.";
        return rs;
    }

    proto = strlen(t->req->proto) != 0 ? t->req->proto : AOS_HTTP_PREFIX;
    if (querystr.len == 0) {
        t->url = apr_psprintf(t->pool, "%s%s/%s",
                              proto,
                              t->req->host,
                              uristr);
    } else {
        t->url = apr_psprintf(t->pool, "%s%s/%s%.*s",
                              proto,
                              t->req->host,
                              uristr,
                              querystr.len,
                              querystr.data);
    }
    aos_debug_log("url:%s.", t->url);

    return AOSE_OK;
}

static void aos_transport_cleanup(aos_http_transport_t *t)
{
    int s;
    char buf[256];

    if (t->req->file_buf != NULL && t->req->file_buf->owner) {
        aos_trace_log("close request body file.");
        if ((s = apr_file_close(t->req->file_buf->file)) != APR_SUCCESS) {
            aos_warn_log("apr_file_close failure, %s.", apr_strerror(s, buf, sizeof(buf)));
        }
        t->req->file_buf = NULL;
    }
    
    if (t->resp->file_buf != NULL && t->resp->file_buf->owner) {
        aos_trace_log("close response body file.");
        if ((s = apr_file_close(t->resp->file_buf->file)) != APR_SUCCESS) {
            aos_warn_log("apr_file_close failure, %s.", apr_strerror(s, buf, sizeof(buf)));
        }
        t->resp->file_buf = NULL;
    }
}

aos_http_transport_t *aos_curl_http_transport_create(aos_pool_t *p)
{
    aos_func_u func;
    aos_curl_http_transport_t *t;

    t = (aos_curl_http_transport_t *)aos_pcalloc(p, sizeof(aos_curl_http_transport_t));

    t->pool = p;
    t->options = aos_default_http_transport_options;
    t->cleanup = aos_fstack_create(p, 5);

    func.func1 = (aos_func1_pt)aos_transport_cleanup;
    aos_fstack_push(t->cleanup, t, func, 1);
    
    t->curl = aos_request_get();
    func.func1 = (aos_func1_pt)request_release;
    aos_fstack_push(t->cleanup, t->curl, func, 1);

    t->header_callback = aos_curl_default_header_callback;
    t->read_callback = aos_curl_default_read_callback;
    t->write_callback = aos_curl_default_write_callback;

    return (aos_http_transport_t *)t;
}

static void aos_move_transport_state(aos_curl_http_transport_t *t, aos_transport_state_e s)
{
    if (t->state < s) {
        t->state = s;
    }
}

void aos_curl_response_headers_parse(aos_pool_t *p, aos_table_t *headers, char *buffer, int len)
{
    char *pos;
    aos_string_t str;
    aos_string_t key;
    aos_string_t value;
    
    str.data = buffer;
    str.len = len;

    aos_trip_space_and_cntrl(&str);

    pos = aos_strlchr(str.data, str.data + str.len, ':');
    if (pos == NULL) {
        return;
    }
    key.data = str.data;
    key.len = pos - str.data;

    pos += 1;
    value.len = str.data + str.len - pos;
    value.data = pos;
    aos_strip_space(&value);

    apr_table_addn(headers, aos_pstrdup(p, &key), aos_pstrdup(p, &value));
}

size_t aos_curl_default_header_callback(char *buffer, size_t size, size_t nitems, void *userdata)
{
    int len;
    aos_curl_http_transport_t *t;

    t = (aos_curl_http_transport_t *)(userdata);
    len = size * nitems;

    if (t->controller->first_byte_time == 0) {
        t->controller->first_byte_time = apr_time_now();
    }

    aos_curl_response_headers_parse(t->pool, t->resp->headers, buffer, len);

    aos_move_transport_state(t, TRANS_STATE_HEADER);

    return len;
}

static void aos_curl_transport_headers_done(aos_curl_http_transport_t *t)
{
    long http_code;
    CURLcode code;
    const char *value;

    if (t->controller->error_code != AOSE_OK) {
        aos_debug_log("has error %d.", t->controller->error_code);
        return;
    }
    
    if (t->resp->status > 0) {
        aos_trace_log("http response status %d.", t->resp->status);
        return;
    }

    t->resp->status = 0;
    if ((code = curl_easy_getinfo(t->curl, CURLINFO_RESPONSE_CODE, &http_code)) != CURLE_OK) {
        t->controller->reason = apr_pstrdup(t->pool, curl_easy_strerror(code));
        t->controller->error_code = AOSE_INTERNAL_ERROR;
        return;
    } else {
        t->resp->status = http_code;
    }

    value = apr_table_get(t->resp->headers, "Content-Length");
    if (value != NULL) {
        t->resp->content_length = aos_atoi64(value);
    }
}

size_t aos_curl_default_write_callback(char *ptr, size_t size, size_t nmemb, void *userdata)
{
    int len;
    int bytes;
    aos_curl_http_transport_t *t;

    t = (aos_curl_http_transport_t *)(userdata);
    len = size * nmemb;

    if (t->controller->first_byte_time == 0) {
        t->controller->first_byte_time = apr_time_now();
    }
    
    aos_curl_transport_headers_done(t);

    if (t->controller->error_code != AOSE_OK) {
        aos_debug_log("write callback abort");
        return 0;
    }

    // On HTTP error, we expect to parse an HTTP error response    
    if (t->resp->status < 200 || t->resp->status > 299) {
        bytes = aos_write_http_body_memory(t->resp, ptr, len);
        assert(bytes == len);
        aos_move_transport_state(t, TRANS_STATE_BODY_IN);
        return bytes;
    }

    if (t->resp->type == BODY_IN_MEMORY && t->resp->body_len >= (int64_t)t->controller->options->max_memory_size) {
        t->controller->reason = apr_psprintf(t->pool,
             "receive body too big, current body size: %" APR_INT64_T_FMT ", max memory size: %" APR_INT64_T_FMT,
              t->resp->body_len, t->controller->options->max_memory_size);
        t->controller->error_code = AOSE_OVER_MEMORY;
        aos_error_log("error reason:%s, ", t->controller->reason);
        return 0;
    }

    if ((bytes = t->resp->write_body(t->resp, ptr, len)) < 0) {
        aos_debug_log("write body failure, %d.", bytes);
        t->controller->error_code = AOSE_WRITE_BODY_ERROR;
        t->controller->reason = "write body failure.";
        return 0;
    }

    if (bytes >= 0) {
        // progress callback
        if (NULL != t->resp->progress_callback) {
            t->resp->progress_callback(t->resp->body_len, t->resp->content_length);
        }

        // crc
        if (t->controller->options->enable_crc) {
            t->resp->crc64 = aos_crc64(t->resp->crc64, ptr, bytes);
        }
    }
    
    aos_move_transport_state(t, TRANS_STATE_BODY_IN);
    
    return bytes;
}

size_t aos_curl_default_read_callback(char *buffer, size_t size, size_t nitems, void *instream)
{
    int len;
    int bytes;
    aos_curl_http_transport_t *t;
    
    t = (aos_curl_http_transport_t *)(instream);
    len = size * nitems;

    if (t->controller->error_code != AOSE_OK) {
        aos_debug_log("abort read callback.");
        return CURL_READFUNC_ABORT;
    }

    if ((bytes = t->req->read_body(t->req, buffer, len)) < 0) {
        aos_debug_log("read body failure, %d.", bytes);
        t->controller->error_code = AOSE_READ_BODY_ERROR;
        t->controller->reason = "read body failure.";
        return CURL_READFUNC_ABORT;
    }
    
    if (bytes >= 0) {
        // progress callback
        t->req->consumed_bytes += bytes;
        if (NULL != t->req->progress_callback) {
            t->req->progress_callback(t->req->consumed_bytes, t->req->body_len);
        }

        // crc
        if (t->controller->options->enable_crc) {
            t->req->crc64 = aos_crc64(t->req->crc64, buffer, bytes);
        }
    }

    aos_move_transport_state(t, TRANS_STATE_BODY_OUT);

    return bytes;
}

static int aos_curl_code_to_status(CURLcode code)
{
    switch (code) {
        case CURLE_OUT_OF_MEMORY:
            return AOSE_OUT_MEMORY;
        case CURLE_COULDNT_RESOLVE_PROXY:
        case CURLE_COULDNT_RESOLVE_HOST:
            return AOSE_NAME_LOOKUP_ERROR;
        case CURLE_COULDNT_CONNECT:
            return AOSE_FAILED_CONNECT;
        case CURLE_WRITE_ERROR:
        case CURLE_OPERATION_TIMEDOUT:
            return AOSE_CONNECTION_FAILED;
        case CURLE_PARTIAL_FILE:
            return AOSE_OK;
        case CURLE_SSL_CACERT:
            return AOSE_FAILED_VERIFICATION;
        default:
            return AOSE_INTERNAL_ERROR;
    }
}

static void aos_curl_transport_finish(aos_curl_http_transport_t *t)
{
    aos_curl_transport_headers_done(t);
    
    if (t->cleanup != NULL) {
        aos_fstack_destory(t->cleanup);
        t->cleanup = NULL;
    }
}

int aos_curl_transport_setup(aos_curl_http_transport_t *t)
{
    CURLcode code;

#define curl_easy_setopt_safe(opt, val)                                 \
    if ((code = curl_easy_setopt(t->curl, opt, val)) != CURLE_OK) {    \
            t->controller->reason = apr_pstrdup(t->pool, curl_easy_strerror(code)); \
            t->controller->error_code = AOSE_FAILED_INITIALIZE;         \
            aos_error_log("curl_easy_setopt failed, code:%d %s.", code, t->controller->reason); \
            return AOSE_FAILED_INITIALIZE;                              \
    }

    curl_easy_setopt_safe(CURLOPT_PRIVATE, t);

    curl_easy_setopt_safe(CURLOPT_HEADERDATA, t);
    curl_easy_setopt_safe(CURLOPT_HEADERFUNCTION, t->header_callback);
    
    curl_easy_setopt_safe(CURLOPT_READDATA, t);
    curl_easy_setopt_safe(CURLOPT_READFUNCTION, t->read_callback);
    
    curl_easy_setopt_safe(CURLOPT_WRITEDATA, t);    
    curl_easy_setopt_safe(CURLOPT_WRITEFUNCTION, t->write_callback);

    curl_easy_setopt_safe(CURLOPT_FILETIME, 1);
    curl_easy_setopt_safe(CURLOPT_NOSIGNAL, 1);
    curl_easy_setopt_safe(CURLOPT_NOPROGRESS, 1);
    curl_easy_setopt_safe(CURLOPT_TCP_NODELAY, 1);
    curl_easy_setopt_safe(CURLOPT_NETRC, CURL_NETRC_IGNORED);

    // transport options
    curl_easy_setopt_safe(CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt_safe(CURLOPT_USERAGENT, t->options->user_agent);

    // request options
    curl_easy_setopt_safe(CURLOPT_DNS_CACHE_TIMEOUT, t->controller->options->dns_cache_timeout);
    curl_easy_setopt_safe(CURLOPT_CONNECTTIMEOUT, t->controller->options->connect_timeout);
    curl_easy_setopt_safe(CURLOPT_LOW_SPEED_LIMIT, t->controller->options->speed_limit);
    curl_easy_setopt_safe(CURLOPT_LOW_SPEED_TIME, t->controller->options->speed_time);

    aos_init_curl_headers(t);
    curl_easy_setopt_safe(CURLOPT_HTTPHEADER, t->headers);

    if (t->controller->options->proxy_host != NULL) {
        // proxy
        curl_easy_setopt_safe(CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
        curl_easy_setopt_safe(CURLOPT_PROXY, t->controller->options->proxy_host);
        // authorize
        if (t->controller->options->proxy_auth != NULL) {
            curl_easy_setopt_safe(CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
            curl_easy_setopt_safe(CURLOPT_PROXYUSERPWD, t->controller->options->proxy_auth);
        }
    }

    if (NULL == t->req->signed_url) {
        if (aos_init_curl_url(t) != AOSE_OK) {
            return t->controller->error_code;
        }
    }
    else {
        t->url = t->req->signed_url; 
    }
    curl_easy_setopt_safe(CURLOPT_URL, t->url);

    switch (t->req->method) {
        case HTTP_HEAD:
            curl_easy_setopt_safe(CURLOPT_NOBODY, 1);
            break;
        case HTTP_PUT:
            curl_easy_setopt_safe(CURLOPT_UPLOAD, 1);
            break;
        case HTTP_POST:
            curl_easy_setopt_safe(CURLOPT_POST, 1);
            break;
        case HTTP_DELETE:
            curl_easy_setopt_safe(CURLOPT_CUSTOMREQUEST, "DELETE");
            break;
        default: // HTTP_GET
            break;
    }
    
#undef curl_easy_setopt_safe
    
    t->state = TRANS_STATE_INIT;
    
    return AOSE_OK;
}

int aos_curl_http_transport_perform(aos_http_transport_t *t_)
{
    int ecode;
    CURLcode code;
    aos_curl_http_transport_t *t = (aos_curl_http_transport_t *)(t_);
    ecode = aos_curl_transport_setup(t);
    if (ecode != AOSE_OK) {
        return ecode;
    }

    t->controller->start_time = apr_time_now();
    code = curl_easy_perform(t->curl);
    t->controller->finish_time = apr_time_now();
    aos_move_transport_state(t, TRANS_STATE_DONE);
    
    if ((code != CURLE_OK) && (t->controller->error_code == AOSE_OK)) {
        ecode = aos_curl_code_to_status(code);
        if (ecode != AOSE_OK) {
            t->controller->error_code = ecode;
            t->controller->reason = apr_pstrdup(t->pool, curl_easy_strerror(code));
            aos_error_log("transport failure curl code:%d error:%s", code, t->controller->reason);
        }
    }
    
    aos_curl_transport_finish(t);
    
    return t->controller->error_code;
}
