/*********************************************************************
 *   Copyright 2018, UCAR/Unidata
 *   See netcdf/COPYRIGHT file for copying and redistribution conditions.
 *********************************************************************/

#include "d4includes.h"
#include "d4curlfunctions.h"

#define MAX_REDIRECTS 20L

/* Mnemonic */
#define OPTARG uintptr_t

/* Condition on libcurl version */
/* Set up an alias as needed */
#ifndef HAVE_CURLOPT_KEYPASSWD
#define CURLOPT_KEYPASSWD CURLOPT_SSLKEYPASSWD
#endif

#define D4BUFFERSIZE "HTTP.READ.BUFFERSIZE"
#define D4KEEPALIVE "HTTP.KEEPALIVE"

#ifdef HAVE_CURLOPT_BUFFERSIZE
#ifndef CURL_MAX_READ_SIZE
#define CURL_MAX_READ_SIZE  (512*1024)
#endif
#endif

#define SETCURLOPT(state,flag,value) {if(set_curlopt(state,flag,(void*)value) != NC_NOERR) {goto done;}}

/* forward */
static int set_curlflag(NCD4INFO*, int flag);
static int set_curlopt(NCD4INFO*, int flag, void* value);

/*
Set a specific curl flag; primary wrapper for curl_easy_setopt
*/
static int
set_curlopt(NCD4INFO* state, int flag, void* value)
{
    int ret = NC_NOERR;
    CURLcode cstat = CURLE_OK;
    cstat = curl_easy_setopt(state->curl->curl,flag,value);
    if(cstat != CURLE_OK)
	ret = NC_ECURL;
    return THROW(ret);
}

/*
Update a specific flag from state
*/
static int
set_curlflag(NCD4INFO* state, int flag)
{
    int ret = NC_NOERR;
    switch (flag) {
    case CURLOPT_USERPWD: /* Do both user and pwd */
        if(state->auth->creds.user != NULL
           && state->auth->creds.pwd != NULL) {
	    SETCURLOPT(state, CURLOPT_USERNAME, state->auth->creds.user);
	    SETCURLOPT(state, CURLOPT_PASSWORD, state->auth->creds.pwd);
            SETCURLOPT(state, CURLOPT_HTTPAUTH, (OPTARG)CURLAUTH_ANY);
	}
	break;
    case CURLOPT_COOKIEJAR: case CURLOPT_COOKIEFILE:
        if(state->auth->curlflags.cookiejar) {
	    /* Assume we will read and write cookies to same place */
	    SETCURLOPT(state, CURLOPT_COOKIEJAR, state->auth->curlflags.cookiejar);
	    SETCURLOPT(state, CURLOPT_COOKIEFILE, state->auth->curlflags.cookiejar);
        }
	break;
    case CURLOPT_NETRC: case CURLOPT_NETRC_FILE:
	if(state->auth->curlflags.netrc) {
	    SETCURLOPT(state, CURLOPT_NETRC, (OPTARG)CURL_NETRC_REQUIRED);
	    SETCURLOPT(state, CURLOPT_NETRC_FILE, state->auth->curlflags.netrc);
        }
	break;
    case CURLOPT_VERBOSE:
	if(state->auth->curlflags.verbose)
	    SETCURLOPT(state, CURLOPT_VERBOSE, (OPTARG)1L);
	break;
    case CURLOPT_TIMEOUT:
	if(state->auth->curlflags.timeout)
	    SETCURLOPT(state, CURLOPT_TIMEOUT, (OPTARG)((long)state->auth->curlflags.timeout));
	break;
    case CURLOPT_CONNECTTIMEOUT:
	if(state->auth->curlflags.connecttimeout)
	    SETCURLOPT(state, CURLOPT_CONNECTTIMEOUT, (OPTARG)((long)state->auth->curlflags.connecttimeout));
	break;
    case CURLOPT_USERAGENT:
        if(state->auth->curlflags.useragent)
	    SETCURLOPT(state, CURLOPT_USERAGENT, state->auth->curlflags.useragent);
	break;
    case CURLOPT_FOLLOWLOCATION:
        SETCURLOPT(state, CURLOPT_FOLLOWLOCATION, (OPTARG)1L);
	break;
    case CURLOPT_MAXREDIRS:
	SETCURLOPT(state, CURLOPT_MAXREDIRS, (OPTARG)MAX_REDIRECTS);
	break;
    case CURLOPT_ERRORBUFFER:
	SETCURLOPT(state, CURLOPT_ERRORBUFFER, state->curl->errdata.errorbuf);
	break;
    case CURLOPT_ENCODING:
#ifdef CURLOPT_ENCODING
	if(state->auth->curlflags.compress) {
	    SETCURLOPT(state, CURLOPT_ENCODING,"deflate, gzip");
        }
#endif
	break;
    case CURLOPT_PROXY:
	if(state->auth->proxy.host != NULL) {
	    SETCURLOPT(state, CURLOPT_PROXY, state->auth->proxy.host);
	    SETCURLOPT(state, CURLOPT_PROXYPORT, (OPTARG)(long)state->auth->proxy.port);
	    if(state->auth->proxy.user != NULL
	       && state->auth->proxy.pwd != NULL) {
                SETCURLOPT(state, CURLOPT_PROXYUSERNAME, state->auth->proxy.user);
                SETCURLOPT(state, CURLOPT_PROXYPASSWORD, state->auth->proxy.pwd);
#ifdef CURLOPT_PROXYAUTH
	        SETCURLOPT(state, CURLOPT_PROXYAUTH, (long)CURLAUTH_ANY);
#endif
	    }
	}
	break;
    case CURLOPT_USE_SSL:
    case CURLOPT_SSLCERT: case CURLOPT_SSLKEY:
    case CURLOPT_SSL_VERIFYPEER: case CURLOPT_SSL_VERIFYHOST:
    {
        struct ssl* ssl = &state->auth->ssl;
	/* VERIFYPEER == 0 => VERIFYHOST == 0 */
	/* We need to have 2 states: default and a set value */
	/* So -1 => default, >= 0 => use value; */
	if(ssl->verifypeer >= 0)
            SETCURLOPT(state, CURLOPT_SSL_VERIFYPEER, (OPTARG)(ssl->verifypeer));
#ifdef HAVE_LIBCURL_766
	if(ssl->verifyhost >= 0)
            SETCURLOPT(state, CURLOPT_SSL_VERIFYHOST, (OPTARG)(ssl->verifyhost));
#endif
        if(ssl->certificate)
            SETCURLOPT(state, CURLOPT_SSLCERT, ssl->certificate);
        if(ssl->key)
            SETCURLOPT(state, CURLOPT_SSLKEY, ssl->key);
        if(ssl->keypasswd)
            /* libcurl prior to 7.16.4 used 'CURLOPT_SSLKEYPASSWD' */
            SETCURLOPT(state, CURLOPT_KEYPASSWD, ssl->keypasswd);
        if(ssl->cainfo)
            SETCURLOPT(state, CURLOPT_CAINFO, ssl->cainfo);
        if(ssl->capath)
            SETCURLOPT(state, CURLOPT_CAPATH, ssl->capath);
    }
    break;

#ifdef HAVE_CURLOPT_BUFFERSIZE
    case CURLOPT_BUFFERSIZE:
	SETCURLOPT(state, CURLOPT_BUFFERSIZE, (OPTARG)state->curl->buffersize);
	break;
#endif

#ifdef HAVE_CURLOPT_KEEPALIVE
    case CURLOPT_TCP_KEEPALIVE:
	if(state->curl->keepalive.active != 0)
	    SETCURLOPT(state, CURLOPT_TCP_KEEPALIVE, (OPTARG)1L);
	if(state->curl->keepalive.idle > 0)
	    SETCURLOPT(state, CURLOPT_TCP_KEEPIDLE, (OPTARG)state->curl->keepalive.idle);
	if(state->curl->keepalive.interval > 0)
	    SETCURLOPT(state, CURLOPT_TCP_KEEPINTVL, (OPTARG)state->curl->keepalive.interval);
	break;
#endif

    default:
        nclog(NCLOGWARN,"Attempt to update unexpected curl flag: %d",flag);
	break;
    }
done:
    return THROW(ret);
}

/* Set various general curl flags per fetch  */
int
NCD4_set_flags_perfetch(NCD4INFO* state)
{
    int ret = NC_NOERR;
    /* currently none */
    return THROW(ret);
}

/* Set various general curl flags per link */

int
NCD4_set_flags_perlink(NCD4INFO* state)
{
    int ret = NC_NOERR;
    /* Following are always set */
    if(ret == NC_NOERR) ret = set_curlflag(state,CURLOPT_ENCODING);
    if(ret == NC_NOERR) ret = set_curlflag(state,CURLOPT_NETRC);
    if(ret == NC_NOERR) ret = set_curlflag(state,CURLOPT_VERBOSE);
    if(ret == NC_NOERR) ret = set_curlflag(state,CURLOPT_TIMEOUT);
    if(ret == NC_NOERR) ret = set_curlflag(state,CURLOPT_USERAGENT);
    if(ret == NC_NOERR) ret = set_curlflag(state,CURLOPT_COOKIEJAR);
    if(ret == NC_NOERR) ret = set_curlflag(state,CURLOPT_USERPWD);
    if(ret == NC_NOERR) ret = set_curlflag(state,CURLOPT_PROXY);
    if(ret == NC_NOERR) ret = set_curlflag(state,CURLOPT_USE_SSL);
    if(ret == NC_NOERR) ret = set_curlflag(state, CURLOPT_FOLLOWLOCATION);
    if(ret == NC_NOERR) ret = set_curlflag(state, CURLOPT_MAXREDIRS);
    if(ret == NC_NOERR) ret = set_curlflag(state, CURLOPT_ERRORBUFFER);

    /* Optional */
#ifdef HAVE_CURLOPT_BUFFERSIZE
    if(ret == NC_NOERR && state->curl->buffersize > 0)
        ret = set_curlflag(state, CURLOPT_BUFFERSIZE);
#endif
#ifdef HAVE_CURLOPT_KEEPALIVE
    if(ret == NC_NOERR && state->curl->keepalive.active != 0)
        ret = set_curlflag(state, CURLOPT_TCP_KEEPALIVE);
#endif

#if 0
    /* Set the CURL. options */
    if(ret == NC_NOERR) ret = set_curl_options(state);
#endif
    return THROW(ret);
}

#if 0
/**
Directly set any options starting with 'CURL.'
*/
static int
set_curl_options(NCD4INFO* state)
{
    int ret = NC_NOERR;
    NClist* store = NULL;
    int i;
    char* hostport = NULL;

    hostport = NC_combinehostport(state->uri);

    store = ncrc_getglobalstate()->rcinfo.triples;

    for(i=0;i<nclistlength(store);i++) {
        struct CURLFLAG* flag;
	NCTriple* triple = (NCTriple*)nclistget(store,i);
        size_t hostlen = (triple->host ? strlen(triple->host) : 0);
        const char* flagname;
        if(strncmp("CURL.",triple->key,5) != 0) continue; /* not a curl flag */
        /* do hostport prefix comparison */
        if(hostport != NULL) {
	  int t = 0;
	  if(triple->host != NULL)
              t = strncmp(hostport,triple->host,hostlen);
          if(t !=  0) continue;
        }
        flagname = triple->key+5; /* 5 == strlen("CURL."); */
        flag = NCD4_curlflagbyname(flagname);
        if(flag == NULL) {ret = NC_ECURL; goto done;}
        ret = set_curlopt(state,flag->flag,cvt(triple->value,flag->type));
    }
 done:
    nullfree(hostport);
    return THROW(ret);
}

static void*
cvt(char* value, enum CURLFLAGTYPE type)
{
    switch (type) {
    case CF_LONG: {
	/* Try to convert to long value */
	const char* p = value;
	char* q = NULL;
	long longvalue = strtol(p,&q,10);
	if(*q != '\0')
	    return NULL;
	return (void*)longvalue;
	}
    case CF_STRING:
	return (void*)value;
    case CF_UNKNOWN: case CF_OTHER:
	return (void*)value;
    }
    return NULL;
}
#endif

void
NCD4_curl_debug(NCD4INFO* state)
{
    state->auth->curlflags.verbose = 1;
    set_curlflag(state,CURLOPT_VERBOSE);
    set_curlflag(state,CURLOPT_ERRORBUFFER);
}

/* Misc. */

/* Determine if this version of curl supports
       "file://..." &/or "https://..." urls.
*/
void
NCD4_curl_protocols(NCD4INFO* state)
{
    const char* const* proto; /*weird*/
    curl_version_info_data* curldata;
    curldata = curl_version_info(CURLVERSION_NOW);
    for(proto=curldata->protocols;*proto;proto++) {
        if(strcmp("http",*proto)==0) {state->auth->curlflags.proto_https=1;}
    }
#ifdef D4DEBUG
    nclog(NCLOGNOTE,"Curl https:// support = %d",state->auth->curlflags.proto_https);
#endif
}

/*
    Extract state values from .rc file
*/
ncerror
NCD4_get_rcproperties(NCD4INFO* state)
{
    ncerror err = NC_NOERR;
    char* option = NULL;
#ifdef HAVE_CURLOPT_BUFFERSIZE
    option = NC_rclookup(D4BUFFERSIZE,state->uri->uri);
    if(option != NULL && strlen(option) != 0) {
	long bufsize;
	if(strcasecmp(option,"max")==0)
	    bufsize = CURL_MAX_READ_SIZE;
	else if(sscanf(option,"%ld",&bufsize) != 1 || bufsize <= 0)
	    fprintf(stderr,"Illegal %s size\n",D4BUFFERSIZE);
        state->curl->buffersize = bufsize;
    }
#endif
#ifdef HAVE_CURLOPT_KEEPALIVE
    option = NC_rclookup(D4KEEPALIVE,state->uri->uri);
    if(option != NULL && strlen(option) != 0) {
	/* The keepalive value is of the form 0 or n/m,
           where n is the idle time and m is the interval time;
           setting either to zero will prevent that field being set.*/
	if(strcasecmp(option,"on")==0) {
	    state->curl->keepalive.active = 1;
	} else {
	    unsigned long idle=0;
	    unsigned long interval=0;
	    if(sscanf(option,"%lu/%lu",&idle,&interval) != 2)
	        fprintf(stderr,"Illegal KEEPALIVE VALUE: %s\n",option);
	    state->curl->keepalive.idle = idle;
	    state->curl->keepalive.interval = interval;
	    state->curl->keepalive.active = 1;
	}
    }
#endif
    return err;
}

#if 0
/*
"Inverse" of set_curlflag;
Given a flag and value, it updates state.
Update a specific flag from state->curlflags.
*/
int
NCD4_set_curlstate(NCD4INFO* state, int flag, void* value)
{
    int ret = NC_NOERR;
    switch (flag) {
    case CURLOPT_USERPWD:
        if(info->creds.userpwd != NULL) free(info->creds.userpwd);
	info->creds.userpwd = strdup((char*)value);
	break;
    case CURLOPT_COOKIEJAR: case CURLOPT_COOKIEFILE:
        if(info->curlflags.cookiejar != NULL) free(info->curlflags.cookiejar);
	info->curlflags.cookiejar = strdup((char*)value);
	break;
    case CURLOPT_NETRC: case CURLOPT_NETRC_FILE:
        if(info->curlflags.netrc != NULL) free(info->curlflags.netrc);
	info->curlflags.netrc = strdup((char*)value);
	break;
    case CURLOPT_VERBOSE:
	info->curlflags.verbose = (long)value;
	break;
    case CURLOPT_TIMEOUT:
	info->curlflags.timeout = (long)value;
	break;
    case CURLOPT_CONNECTTIMEOUT:
	info->curlflags.connecttimeout = (long)value;
	break;
    case CURLOPT_USERAGENT:
        if(info->curlflags.useragent != NULL) free(info->curlflags.useragent);
        info->curlflags.useragent = strdup((char*)value);
	break;
    case CURLOPT_FOLLOWLOCATION:
	/* no need to store; will always be set */
	break;
    case CURLOPT_MAXREDIRS:
	/* no need to store; will always be set */
	break;
    case CURLOPT_ERRORBUFFER:
	/* no need to store; will always be set */
	break;
    case CURLOPT_ENCODING:
	/* no need to store; will always be set to fixed value */
	break;
    case CURLOPT_PROXY:
	/* We assume that the value is the proxy url */
	if(info->proxy.host != NULL) free(info->proxy.host);
	if(info->proxy.userpwd != NULL) free(info->proxy.userpwd);
	info->proxy.host = NULL;
	info->proxy.userpwd = NULL;
	if(!NCD4_parseproxy(state,(char*)value))
		{ret = NC_EINVAL; goto done;}
	break;
    case CURLOPT_SSLCERT:
	if(info->ssl.certificate != NULL) free(info->ssl.certificate);
	info->ssl.certificate = strdup((char*)value);
	break;
    case CURLOPT_SSLKEY:
	if(info->ssl.key != NULL) free(info->ssl.key);
	info->ssl.key = strdup((char*)value);
	break;
    case CURLOPT_KEYPASSWD:
	if(info->ssl.keypasswd!= NULL) free(info->ssl.keypasswd);
	info->ssl.keypasswd = strdup((char*)value);
	break;
    case CURLOPT_SSL_VERIFYPEER:
      info->ssl.verifypeer = (long)value;
      break;
    case CURLOPT_SSL_VERIFYHOST:
      info->ssl.verifyhost = (long)value;
      break;
    case CURLOPT_CAINFO:
      if(info->ssl.cainfo != NULL) free(info->ssl.cainfo);
      info->ssl.cainfo = strdup((char*)value);
      break;
    case CURLOPT_CAPATH:
	if(info->ssl.capath != NULL) free(info->ssl.capath);
	info->ssl.capath = strdup((char*)value);
	break;

    default: break;
    }
done:
    return THROW(ret);
}
#endif

void
NCD4_curl_printerror(NCD4INFO* state)
{
    fprintf(stderr,"curl error details: %s\n",state->curl->errdata.errorbuf);
}

CURLcode
NCD4_reportcurlerror(CURLcode cstat)
{
    if(cstat != CURLE_OK) {
        fprintf(stderr,"CURL Error: %s\n",curl_easy_strerror(cstat));
    }
    fflush(stderr);
    return cstat;
}
