package com.sfnt.ldk.api;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.util.UUID;
import java.util.Map;
import java.util.HashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpStatus;
import org.apache.http.client.CookieStore;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;

import com.sfnt.ldk.util.HmacSHA256Util;
import com.sfnt.ldk.util.ServiceUtil;

import com.sfnt.ldk.model.*;
import com.google.gson.Gson;

public class LdkRuntimeRestAPI {
	private static final int TIMEOUT = 30000;
	private static final String URL_COMMON = "/sentinel/ldk_runtime/v1/vendors/";
	private static final String DEFALUT_SECRET = "X-LDK-Identity-WS-V1";
	private static final String PAGE_INDEX = "pageStartIndex=";
	private static final String PAGE_SIZE = "&pageSize=";
	private static final int TOO_MANY_REQUEST = 429;
	private static final String LDK_HEADER = "X-LDK-Instance";
	private static final String HEADER_AUTHORIZATION_IDENTITY = "X-LDK-Identity-WS";
	private static final String HEADER_USER_ID = "X-LDK-User-Id";
	private static final String HEADER_AUTHORIZATION = "Authorization";
	private static final String MEMORY_ID = "memoryId=";
	private static final String OFFSET = "&offset=";
	private static final String LENGTH = "&length=";
	
	private static final Log log = LogFactory.getLog(LdkRuntimeRestAPI.class);

	String serverUrl = "";
	String serverPort = "";
	String identityId = "";
	String identitySecret = "";
	String vendorId = "";
	String sessionId = "";
	String userId = "";
	String accessToken = "";
	long retryAfterTime = 0;
	String instanceId = "";
	String cookieHeader = "";
	CookieStore cookieStore;
	String[] header;

	public LdkRuntimeRestAPI(String vendorId, String clientIdentity, String endpointScheme, String serverAddr,
			String serverPort) {
		if (clientIdentity != null) {
			String[] splitClientIdentity = clientIdentity.split("@");
			if (splitClientIdentity.length == 2) {
				if (serverAddr == null || serverAddr.isEmpty())
					serverAddr = splitClientIdentity[1];
				String identityInfo = splitClientIdentity[0];
				String[] splitIdentityInfo = identityInfo.split(":");
				if (splitIdentityInfo.length == 2) {
					this.identityId = splitIdentityInfo[0];
					this.identitySecret = splitIdentityInfo[1];
				}
			} else {
				String[] splitIdentityInfo = clientIdentity.split(":");
				if (splitIdentityInfo.length == 2) {
					this.identityId = splitIdentityInfo[0];
					this.identitySecret = splitIdentityInfo[1];
				}
			}
		}

		this.vendorId = vendorId;
		if (serverPort != null && !serverPort.isEmpty())
			this.serverUrl = endpointScheme + "://" + serverAddr + ":" + serverPort + URL_COMMON + vendorId;
		else
			this.serverUrl = endpointScheme + "://" + serverAddr + URL_COMMON + vendorId;
		this.instanceId = UUID.randomUUID().toString();
		HttpClientContext context = HttpClientContext.create();
		cookieStore = new BasicCookieStore();
		context.setCookieStore(cookieStore);
		
		header = new String[] {HEADER_AUTHORIZATION_IDENTITY};
	
	}
	
	public LdkRuntimeRestAPI(String vendorId, String accessToken, String userId, String endpointScheme, String serverAddr,
			String serverPort) {
		
		this.vendorId = vendorId;
		this.userId = userId;
		this.accessToken = accessToken;
		
		if (serverPort != null && !serverPort.isEmpty())
			this.serverUrl = endpointScheme + "://" + serverAddr + ":" + serverPort + URL_COMMON + vendorId;
		else
			this.serverUrl = endpointScheme + "://" + serverAddr + URL_COMMON + vendorId;
		this.instanceId = UUID.randomUUID().toString();
		HttpClientContext context = HttpClientContext.create();
		cookieStore = new BasicCookieStore();
		context.setCookieStore(cookieStore);
		
		header = new String[] {HEADER_USER_ID, HEADER_AUTHORIZATION};
	}
	
	private Map <String, String> createMapHeaders(String unigueUrl, String request) {
		
		Map <String, String> mapHeaders = new HashMap<String,String>();
		
		for(int i = 0; i<header.length; i++) {
			switch(header[i]) {
			case HEADER_AUTHORIZATION_IDENTITY:
				try {
					if (!identitySecret.isEmpty())
						mapHeaders.put(HEADER_AUTHORIZATION_IDENTITY, generateHeaderValue(unigueUrl, request));
				}catch (Exception e) {
					log.error("Create headers failed with Exception :" + e.getMessage());
				}
				break;
			case HEADER_USER_ID:
				mapHeaders.put(HEADER_USER_ID, userId);
				break;
			case HEADER_AUTHORIZATION:
				mapHeaders.put(HEADER_AUTHORIZATION, "Bearer " + accessToken);
				break;
			}
		}
		
		return mapHeaders;
		
	}
	
	void getCookiesFromResponse(CloseableHttpResponse response) {
		Header[] responseHeader = response.getHeaders("set-cookie");
		if (responseHeader.length > 0)
			this.cookieHeader = responseHeader[0].getValue();
	}

	// Log the error, and client should wait for some time ( value from response header) to send next request 
	void setRetryAfterTime(CloseableHttpResponse response) {
		Header[] retryHeader = response.getHeaders("Retry-After");
		if (retryHeader != null && retryHeader.length > 0) {
			retryAfterTime = Integer.parseInt(retryHeader[0].getValue()) * 1000;
		}
		log.error("Failed with too many request at server side.");
	}
	
	// Check if last call failed with 429, if yes, wait for some time 
	void isNeedToWait() throws InterruptedException {
		if(retryAfterTime != 0) {
			log.info("Need to wait for " + retryAfterTime / 1000 + " seconds");
			Thread.sleep(retryAfterTime);
			retryAfterTime = 0;
		}
	}
	
	public String getCurrentDate() {
		TimeZone tz = TimeZone.getTimeZone("UTC");
		DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
		df.setTimeZone(tz);
		return df.format(new Date());
	}

	private String generateHeaderValue(String unigueUrl, String request) throws Exception {
		String headerValue = "";
		if (!identitySecret.isEmpty()) {
			String requestDate = getCurrentDate();
			String content = identityId + requestDate + URL_COMMON + vendorId + unigueUrl + "^" + request;
			String signature = generateSignature(identitySecret, content);
			headerValue = "V1, Identity=" + identityId + ", RequestDate=" + requestDate + ", Signature=" + signature;
		}

		return headerValue;
	}

	/**
	 * Sample code for generate signature
	 * 
	 * @return
	 * @throws Exception
	 */
	private String generateSignature(String identitySecret, String content) throws Exception {
		byte[] deriveKey = HmacSHA256Util.hmacSHA256(identitySecret.getBytes(), DEFALUT_SECRET.getBytes());
		return HmacSHA256Util.byteArrayToHexString(HmacSHA256Util.hmacSHA256(deriveKey, content.getBytes()));
	}

	private void handleErrorInfo(CloseableHttpResponse response) throws Exception {
		Gson gson = new Gson();
		String result = ServiceUtil.getDataFromResponse(response);
		ErrorInfo errorResponse = gson.fromJson(result, ErrorInfo.class);
		throw new Exception(errorResponse.getMessage() + ", X-LDK-Trace-Id:" + response.getHeaders("X-LDK-Trace-Id")[0].getValue());
	}
	
	/**
	 * Sample code to execute login WebService
	 * 
	 * @return
	 * @throws Exception
	 */
	public LicenseResponse login(LicenseRequest request) throws Exception {
		LicenseResponse session = null;
		String loginUrl = serverUrl + "/sessions";
		String result = "";
		CloseableHttpClient httpClient = ServiceUtil.createHttpClient(loginUrl);
		HttpPost httpPost = ServiceUtil.createHttpPost(TIMEOUT, loginUrl, request.toString(), createMapHeaders("/sessions", request.toString()));
		httpPost.setHeader(LDK_HEADER, instanceId);	
		// Add cookies to request
		if(cookieHeader != null && !cookieHeader.isEmpty())
			httpPost.addHeader("Cookie", cookieHeader);
		
		isNeedToWait();

		CloseableHttpResponse response = null;
		try {
			response = httpClient.execute(httpPost);
			if (response != null) {
				if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK
						|| response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) {
					result = ServiceUtil.getDataFromResponse(response);
					Gson gson = new Gson();
					session = gson.fromJson(result, LicenseResponse.class);
					sessionId = session.getSessionId();
					getCookiesFromResponse(response);
				} else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
					String errorMessage = "Login failed with status = " + response.getStatusLine().getStatusCode();
					log.error(errorMessage);
					throw new Exception(errorMessage);
				} else if (response.getStatusLine().getStatusCode() == TOO_MANY_REQUEST) {
					setRetryAfterTime(response);
				} else {
					handleErrorInfo(response);
				}
			}
		} catch (Exception e) {
			log.error("Login failed with Exception :" + e.getMessage());
		} finally {
			ServiceUtil.closeHttpClient(httpClient, response);
		}
		return session;
	}

	/**
	 * Sample code to execute logout WebService
	 * 
	 * @return
	 * @throws Exception
	 */
	public LogoutResponse logout() throws Exception {
		LogoutResponse logoutResponse = null;
		String entityPart = "/sessions/" + sessionId;
		String logoutUrl = serverUrl + entityPart;
		String result = "";
		CloseableHttpClient httpClient = ServiceUtil.createHttpClient(logoutUrl);
		HttpDelete httpDelete = ServiceUtil.createHttpDelete(TIMEOUT, logoutUrl, createMapHeaders(entityPart, ""));
		httpDelete.setHeader(LDK_HEADER, instanceId);
		if(cookieHeader != null && !cookieHeader.isEmpty())
			httpDelete.addHeader("Cookie", cookieHeader);
		
		isNeedToWait();

		CloseableHttpResponse response = null;
		try {
			response = httpClient.execute(httpDelete);
			if (response != null) {
				if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT) {
					logoutResponse = new LogoutResponse();
					logoutResponse.setErrorCode(0);
					getCookiesFromResponse(response);
				} else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
					String errorMessage = "Logout failed with status = " + response.getStatusLine().getStatusCode();
					log.error(errorMessage);
					throw new Exception(errorMessage);
				} else if (response.getStatusLine().getStatusCode() == TOO_MANY_REQUEST) {
					setRetryAfterTime(response);
				} else {
					handleErrorInfo(response);
				}
			}
		} catch (Exception e) {
			log.error("Logout failed with Exception :" + e.getMessage());
		} finally {
			ServiceUtil.closeHttpClient(httpClient, response);
		}
		return logoutResponse;
	}

	/**
	 * Sample code to execute refresh session WebService
	 * 
	 * @return
	 * @throws Exception
	 */
	public LicenseResponse refreshSession() throws Exception {
		LicenseResponse session = null;
		String entityPart = "/sessions/" + sessionId + "/refresh";
		String refreshSessionUrl = serverUrl + entityPart;
		String result = "";
		CloseableHttpClient httpClient = ServiceUtil.createHttpClient(refreshSessionUrl);
		HttpPost httpPost = ServiceUtil.createHttpPost(TIMEOUT, refreshSessionUrl, "", createMapHeaders(entityPart, ""));
		httpPost.setHeader(LDK_HEADER, instanceId);
		if(cookieHeader != null && !cookieHeader.isEmpty())
			httpPost.addHeader("Cookie", cookieHeader);
		
		isNeedToWait();

		CloseableHttpResponse response = null;
		try {
			response = httpClient.execute(httpPost);
			if (response != null) {
				if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK
						|| response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) {
					result = ServiceUtil.getDataFromResponse(response);
					Gson gson = new Gson();
					session = gson.fromJson(result, LicenseResponse.class);
					getCookiesFromResponse(response);
				} else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
					String errorMessage = "Refresh session failed with status = " + response.getStatusLine().getStatusCode();
					log.error(errorMessage);
					throw new Exception(errorMessage);
				} else if (response.getStatusLine().getStatusCode() == TOO_MANY_REQUEST) {
					setRetryAfterTime(response);
				} else {
					handleErrorInfo(response);
				}
			}
		} catch (Exception e) {
			log.error("Refresh session failed with Exception :" + e.getMessage());
		} finally {
			ServiceUtil.closeHttpClient(httpClient, response);
		}
		return session;
	}

	/**
	 * Sample code to execute getKeyList webService
	 * 
	 * @return
	 * @throws Exception
	 */
	public KeysResponse getKeyList(int pageStartIndex, int pageSize) throws Exception {
		KeysResponse keys = null;
		String entityPart = "/keys";
		String getKeysUrl = serverUrl + entityPart + "?" + PAGE_INDEX + pageStartIndex + PAGE_SIZE + pageSize;
		String result = "";
		CloseableHttpClient httpClient = ServiceUtil.createHttpClient(getKeysUrl);
		HttpGet httpGet = ServiceUtil.createHttpGet(TIMEOUT, getKeysUrl, createMapHeaders(entityPart, ""));
		httpGet.setHeader(LDK_HEADER, instanceId);
		if(cookieHeader != null && !cookieHeader.isEmpty())
			httpGet.addHeader("Cookie", cookieHeader);
		
		isNeedToWait();

		CloseableHttpResponse response = null;
		try {
			response = httpClient.execute(httpGet);
			if (response != null) {
				if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
					result = ServiceUtil.getDataFromResponse(response);
					Gson gson = new Gson();
					keys = gson.fromJson(result, KeysResponse.class);
					getCookiesFromResponse(response);
				} else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
					String errorMessage = "Get key list failed with status = " + response.getStatusLine().getStatusCode();
					log.error(errorMessage);
					throw new Exception(errorMessage);
				} else if (response.getStatusLine().getStatusCode() == TOO_MANY_REQUEST) {
					setRetryAfterTime(response);
				} else {
					handleErrorInfo(response);
				}
			}
		} catch (Exception e) {
			log.error("Get key list failed with Exception :" + e.getMessage());
		} finally {
			ServiceUtil.closeHttpClient(httpClient, response);
		}
		return keys;
	}

	/**
	 * Sample code to execute getProudctList webService
	 * 
	 * @return
	 * @throws Exception
	 */
	public ProductsResponse getProudctList(int pageStartIndex, int pageSize) throws Exception {
		ProductsResponse products = null;
		String entityPart = "/products";
		String getProductsUrl = serverUrl + entityPart + "?" + PAGE_INDEX + pageStartIndex + PAGE_SIZE + pageSize;
		String result = "";
		CloseableHttpClient httpClient = ServiceUtil.createHttpClient(getProductsUrl);
		HttpGet httpGet = ServiceUtil.createHttpGet(TIMEOUT, getProductsUrl, createMapHeaders(entityPart, ""));
		httpGet.setHeader(LDK_HEADER, instanceId);
		if(cookieHeader != null && !cookieHeader.isEmpty())
			httpGet.addHeader("Cookie", cookieHeader);
		
		isNeedToWait();

		CloseableHttpResponse response = null;
		try {
			response = httpClient.execute(httpGet);
			if (response != null) {
				if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
					result = ServiceUtil.getDataFromResponse(response);
					Gson gson = new Gson();
					products = gson.fromJson(result, ProductsResponse.class);
					getCookiesFromResponse(response);
				}  else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
					String errorMessage = "Get product list failed with status = " + response.getStatusLine().getStatusCode();
					log.error(errorMessage);
					throw new Exception(errorMessage);
				} else if (response.getStatusLine().getStatusCode() == TOO_MANY_REQUEST) {
					setRetryAfterTime(response);
				} else {
					handleErrorInfo(response);
				}
			}
		} catch (Exception e) {
			log.error("Get product list failed with Exception :" + e.getMessage());
		} finally {
			ServiceUtil.closeHttpClient(httpClient, response);
		}
		return products;
	}

	/**
	 * Sample code to execute getFeatureList webService
	 * 
	 * @return
	 * @throws Exception
	 */
	public FeaturesResponse getFeatureList(int pageStartIndex, int pageSize) throws Exception {
		FeaturesResponse features = null;
		String entityPart = "/features";
		String getFeaturesUrl = serverUrl + entityPart + "?" + PAGE_INDEX + pageStartIndex + PAGE_SIZE + pageSize;
		String result = "";
		CloseableHttpClient httpClient = ServiceUtil.createHttpClient(getFeaturesUrl);
		HttpGet httpGet = ServiceUtil.createHttpGet(TIMEOUT, getFeaturesUrl, createMapHeaders(entityPart, ""));
		httpGet.setHeader(LDK_HEADER, instanceId);
		if(cookieHeader != null && !cookieHeader.isEmpty())
			httpGet.addHeader("Cookie", cookieHeader);
		
		isNeedToWait();

		CloseableHttpResponse response = null;
		try {
			response = httpClient.execute(httpGet);
			if (response != null) {
				if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
					result = ServiceUtil.getDataFromResponse(response);
					Gson gson = new Gson();
					features = gson.fromJson(result, FeaturesResponse.class);
					getCookiesFromResponse(response);
				}  else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
					String errorMessage = "Get feature list failed with status = " + response.getStatusLine().getStatusCode();
					log.error(errorMessage);
					throw new Exception(errorMessage);
				} else if (response.getStatusLine().getStatusCode() == TOO_MANY_REQUEST) {
					setRetryAfterTime(response);
				} else {
					handleErrorInfo(response);
				}
			}
		} catch (Exception e) {
			log.error("Get feature list failed with Exception :" + e.getMessage());
		} finally {
			ServiceUtil.closeHttpClient(httpClient, response);
		}
		return features;
	}
	
	/**
	 * Sample code to execute getMemoryList webService
	 * 
	 * @return
	 * @throws Exception
	 */
	public MemoriesResponse getMemoryList(int pageStartIndex, int pageSize) throws Exception {
		MemoriesResponse meories = null;
		String entityPart = "/memories";
		String getMemoriesUrl = serverUrl + entityPart + "?" + PAGE_INDEX + pageStartIndex + PAGE_SIZE + pageSize;
		String result = "";
		CloseableHttpClient httpClient = ServiceUtil.createHttpClient(getMemoriesUrl);
		HttpGet httpGet = ServiceUtil.createHttpGet(TIMEOUT, getMemoriesUrl, createMapHeaders(entityPart, ""));
		httpGet.setHeader(LDK_HEADER, instanceId);
		if(cookieHeader != null && !cookieHeader.isEmpty())
			httpGet.addHeader("Cookie", cookieHeader);
		
		isNeedToWait();

		CloseableHttpResponse response = null;
		try {
			response = httpClient.execute(httpGet);
			if (response != null) {
				if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
					result = ServiceUtil.getDataFromResponse(response);
					Gson gson = new Gson();
					meories = gson.fromJson(result, MemoriesResponse.class);
					getCookiesFromResponse(response);
				}  else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
					String errorMessage = "Get memory list failed with status = " + response.getStatusLine().getStatusCode();
					log.error(errorMessage);
					throw new Exception(errorMessage);
				} else if (response.getStatusLine().getStatusCode() == TOO_MANY_REQUEST) {
					setRetryAfterTime(response);
				} else {
					handleErrorInfo(response);
				}
			}
		} catch (Exception e) {
			log.error("Get memory list failed with Exception :" + e.getMessage());
		} finally {
			ServiceUtil.closeHttpClient(httpClient, response);
		}
		return meories;
	}

	/**
	 * Sample code to execute read memory WebService
	 * 
	 * @return
	 * @throws Exception
	 */
	public ReadMemoryResponse readMemory(int memoryId, int offset, int length) throws Exception {
		ReadMemoryResponse memory = null;
		String entityPart = "/sessions/" + sessionId + "/read";
		String readMemoryUrl = serverUrl + entityPart + "?"  + MEMORY_ID + memoryId + OFFSET + offset + LENGTH + length;
		String result = "";
		CloseableHttpClient httpClient = ServiceUtil.createHttpClient(readMemoryUrl);
		HttpGet httpGet = ServiceUtil.createHttpGet(TIMEOUT, readMemoryUrl, createMapHeaders(entityPart, ""));
		httpGet.setHeader(LDK_HEADER, instanceId);
		if(cookieHeader != null && !cookieHeader.isEmpty())
			httpGet.addHeader("Cookie", cookieHeader);
		
		isNeedToWait();

		CloseableHttpResponse response = null;
		try {
			response = httpClient.execute(httpGet);
			if (response != null) {
				if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
					result = ServiceUtil.getDataFromResponse(response);
					Gson gson = new Gson();
					memory = gson.fromJson(result, ReadMemoryResponse.class);
					getCookiesFromResponse(response);
				}  else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
					String errorMessage = "Read memory failed with status = " + response.getStatusLine().getStatusCode();
					log.error(errorMessage);
					throw new Exception(errorMessage);
				} else if (response.getStatusLine().getStatusCode() == TOO_MANY_REQUEST) {
					setRetryAfterTime(response);
				} else {
					handleErrorInfo(response);
				}
			}
		} catch (Exception e) {
			log.error("Read memory failed with Exception :" + e.getMessage());
		} finally {
			ServiceUtil.closeHttpClient(httpClient, response);
		}
		return memory;
	}
}
