/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import groovy.json.JsonOutput

import java.security.KeyStore;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import java.security.SecureRandom;
import javax.net.ssl.HttpsURLConnection;

/**
 * simple http rest client for groovy
 * by dlukyanov@ukr.net 
 */
@groovy.transform.CompileStatic
public class HTTP{
	//default response handler
	public static Closure TEXT_RECEIVER = {InputStream instr,Map ctx->
		return instr.getText( (String)ctx.encoding );
	}
	
	public static Closure JSON_RECEIVER = { InputStream instr, Map ctx-> 
		return new groovy.json.JsonSlurper().parse(instr,(String)ctx.encoding);
	}
	
	public static Closure FILE_RECEIVER(File f){
		return { InputStream instr, Map ctx-> 
			f<<instr;
			return f;
		}
	}
	
	public static Map<String,Object> get(Map<String,Object> ctx)throws IOException{
		ctx.put('method','GET');
		return send(ctx);
	}
	
	public static Map<String,Object> head(Map<String,Object> ctx)throws IOException{
		ctx.put('method','HEAD');
		return send(ctx);
	}
	
	public static Map<String,Object> post(Map<String,Object> ctx)throws IOException{
		ctx.put('method','POST');
		return send(ctx);
	}
	
	public static Map<String,Object> put(Map<String,Object> ctx)throws IOException{
		ctx.put('method','PUT');
		return send(ctx);
	}
	
	public static Map<String,Object> delete(Map<String,Object> ctx)throws IOException{
		ctx.put('method','DELETE');
		return send(ctx);
	}
	
	/**
	 * @param url string where to send request
	 * @param query Map parameters to append to url
	 * @param method http method to be used in request. standard methods: GET, POST, PUT, DELETE, HEAD
	 * @param headers key-value map with headers that should be sent with request
	 * @param body request body/data to send to url (InputStream, CharSequence, or Map for json and x-www-form-urlencoded context types)
	 * @param encoding encoding name to use to send/receive data - default UTF-8
	 * @param receiver Closure that will be called to receive data from server. Defaults: `HTTP.JSON_RECEIVER` for json content-type and `HTTP.TEXT_RECEIVER` otherwise. Available: `HTTP.FILE_RECEIVER(File)` - stores response to file.
	 * @param ssl javax.net.ssl.SSLContext or String that evaluates the javax.net.ssl.SSLContext. example: send( url:..., ssl: "HTTP.getKeystoreSSLContext('./keystore.jks', 'testpass')" )
	 */
	public static Map<String,Object> send(Map<String,Object> ctx)throws IOException{
		String             url      = ctx.url;
		Map<String,String> headers  = (Map<String,String>)ctx.headers;
		String             method   = ctx.method;
		Object             body     = ctx.body;
		String             encoding = ctx.encoding?:"UTF-8";
		Closure            receiver = (Closure)ctx.receiver;
		Map<String,String> query    = (Map<String,String>)ctx.query;
		Object             sslCtxObj= ctx.ssl;
		
		//copy context and set default values
		ctx = [:] + ctx;
		ctx.encoding = encoding;
		String contentType="";
		
		if(query){
			url+="?"+query.collect{k,v-> k+"="+URLEncoder.encode(v,'UTF-8') }.join('&')
		}
		
		HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
		if(sslCtxObj!=null && connection instanceof HttpsURLConnection){
			SSLContext         sslCtx   = null;
			if(sslCtxObj instanceof SSLContext){
				sslCtx = (SSLContext)sslCtxObj;
			}else if(sslCtxObj instanceof CharSequence){
				//assume this is a groovy code to get ssl context
				sslCtx = evaluateSSLContext((CharSequence)sslCtxObj);
			}else{
				throw new IllegalArgumentException("Unsupported ssl parameter ${sslCtxObj.getClass()}")
			}
			((HttpsURLConnection)connection).setSSLSocketFactory(sslCtx.getSocketFactory());
		}
		
        connection.setDoOutput(true);
        connection.setRequestMethod(method);
		if ( headers!=null && !headers.isEmpty() ) {
			//add headers
			for (Map.Entry<String, String> entry : headers.entrySet()) {
				if(entry.getValue()){
					connection.addRequestProperty(entry.getKey(), entry.getValue());
					if("content-type".equals(entry.getKey().toLowerCase()))contentType=entry.getValue();
				}
			}
		}
		
		if(body!=null){
			//write body
			OutputStream out = connection.getOutputStream();
			if( body instanceof Closure ){
				((Closure)body).call(out, ctx);
			}else if(body instanceof InputStream){
				out << (InputStream)body;
			}else if(body instanceof Map){
				if( contentType =~ "(?i)[^/]+/json" ) {
					out.withWriter((String)ctx.encoding){
						it.append( JsonOutput.toJson((Map)body) );
					}
				} else if( contentType =~ "(?i)[^/]+/x-www-form-urlencoded" ) {
					out.withWriter((String)ctx.encoding) {
						it.append( ((Map)body).collect{k,v-> ""+k+"="+URLEncoder.encode((String)v,'UTF-8') }.join('&') )
					}
				} else {
					throw new IOException("Map body type supported only for */json of */x-www-form-urlencoded content-type");
				}
			}else if(body instanceof CharSequence){
				out.withWriter((String)ctx.encoding){
					it.append((CharSequence)body);
				}
			}else{
				throw new IOException("Unsupported body type: "+body.getClass());
			}
			out.flush();
			out.close();
			out=null;
		}
		
		Map response     = [:];
		ctx.response     = response;
		response.code    = connection.getResponseCode();
		response.message = connection.getResponseMessage();
		response.headers = connection.getHeaderFields();
		
		InputStream instr = null;
		
		if( ((int)response.code)>=400 ){
			try{
				instr = connection.getErrorStream();
			}catch(Exception ei){}
		}else{
			try{
				instr = connection.getInputStream();
			}catch(java.io.IOException ei){
				throw new IOException("fail to open InputStream for http code "+response.code+":"+ei);
			}
		}
		
		if(instr!=null) {
			instr = new BufferedInputStream(instr);
			if(receiver==null){
				if( response.headers['Content-Type']?.toString()?.indexOf('/json')>0 ){
					receiver=JSON_RECEIVER;
				} else receiver=TEXT_RECEIVER;
			}
			response.body = receiver(instr,ctx);
			instr.close();
			instr=null;
		}
		return ctx;
	}
	
	@groovy.transform.Memoized
	public static SSLContext getKeystoreSSLContext(String keystorePath, String keystorePass, String keystoreType="JKS", String keyPass = null){
		if(keyPass == null) keyPass=keystorePass;
		KeyStore clientStore = KeyStore.getInstance(keystoreType);
		clientStore.load(new File( keystorePath ).newInputStream(), keystorePass.toCharArray());
		KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
		kmf.init(clientStore, keyPass.toCharArray());
		KeyManager[] kms = kmf.getKeyManagers();
		//init TrustCerts
		TrustManager[] trustCerts = new TrustManager[1];                
		trustCerts[0] = new X509TrustManager() {
			public void checkClientTrusted( final X509Certificate[] chain, final String authType ) { }
			public void checkServerTrusted( final X509Certificate[] chain, final String authType ) { }
			public X509Certificate[] getAcceptedIssuers() {
				return null;
			}	
		}
		SSLContext sslContext = SSLContext.getInstance("TLS");
		sslContext.init(kms, trustCerts, new SecureRandom());
		return sslContext;
	}
    
    @groovy.transform.Memoized
	public static SSLContext getNaiveSSLContext(){
		System.err.println("HTTP.getNaiveSSLContext() used. Must be disabled on prod!");
        KeyManager[] kms = new KeyManager[0];
		TrustManager[] trustCerts = new TrustManager[1];                
		trustCerts[0] = new X509TrustManager() {
			public void checkClientTrusted( final X509Certificate[] chain, final String authType ) { }
			public void checkServerTrusted( final X509Certificate[] chain, final String authType ) { }
			public X509Certificate[] getAcceptedIssuers() {
				return null;
			}	
		}
		SSLContext sslContext = SSLContext.getInstance("TLS");
		sslContext.init(null, trustCerts, new SecureRandom());
		return sslContext;
	}
	
	/**
	 * evaluates code that should return SSLContext
	 */
    @groovy.transform.Memoized
	public static SSLContext evaluateSSLContext(CharSequence code) {
		Object ssl = new GroovyShell( HTTP.class.getClassLoader() ).evaluate( code as String );
		return (SSLContext) ssl;
	}
}
