package com.windowsazure.messaging;

import java.io.IOException;
import java.io.StringWriter;
import java.net.URI;
import java.net.URLEncoder;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;

public class NamespaceManager {
	private static final String IFMATCH_HEADER_NAME = "If-Match";
	private static final String AUTHORIZATION_HEADER_NAME = "Authorization";
	private static final String HUBS_COLLECTION_PATH = "$Resources/NotificationHubs/";
	private static final String APIVERSION = "?api-version=2014-09";
	private static final String SKIP_TOP_PARAM = "&$skip=0&$top=2147483647";
	private String endpoint;
	private String SasKeyName;
	private String SasKeyValue;

	public NamespaceManager(String connectionString) {
		String[] parts = connectionString.split(";");
		if (parts.length != 3)
			throw new RuntimeException("Error parsing connection string: "
					+ connectionString);

		for (int i = 0; i < parts.length; i++) {
			if (parts[i].startsWith("Endpoint")) {
				this.endpoint = "https" + parts[i].substring(11);
			} else if (parts[i].startsWith("SharedAccessKeyName")) {
				this.SasKeyName = parts[i].substring(20);
			} else if (parts[i].startsWith("SharedAccessKey")) {
				this.SasKeyValue = parts[i].substring(16);
			}
		}
	}
	
	public void getNotificationHubAsync(String hubPath, final FutureCallback<NotificationHubDescription> callback){
		try {
			URI uri = new URI(endpoint + hubPath + APIVERSION);
			final HttpGet get = new HttpGet(uri);
			get.setHeader(AUTHORIZATION_HEADER_NAME, generateSasToken(uri));
			
			HttpClientManager.getHttpAsyncClient().execute(get, new FutureCallback<HttpResponse>() {
		        public void completed(final HttpResponse response) {
		        	try{
		        		int httpStatusCode = response.getStatusLine().getStatusCode();
		        		if (httpStatusCode != 200) {
		        			callback.failed(new NotificationHubsException(getErrorString(response), httpStatusCode));
		        			return;
		    			}		    			
		    			
						callback.completed(NotificationHubDescription.parseOne(response.getEntity().getContent()));
		        	} catch (Exception e) {
		        		callback.failed(e);	        		
		        	} finally {
		        		get.releaseConnection();
		    		}
		        }
		        public void failed(final Exception ex) {
		        	get.releaseConnection();
		        	callback.failed(ex);
		        }
		        public void cancelled() {
		        	get.releaseConnection();
		        	callback.cancelled();
		        }
			});			
		} catch (Exception e) {
			throw new RuntimeException(e);
		} 
	}
	
	public NotificationHubDescription getNotificationHub(String hubPath) throws NotificationHubsException{
		SyncCallback<NotificationHubDescription> callback = new SyncCallback<NotificationHubDescription>();
		getNotificationHubAsync(hubPath, callback);
		return callback.getResult();
	}
	
	public void getNotificationHubsAsync(final FutureCallback<List<NotificationHubDescription>> callback){
		try {
			URI uri = new URI(endpoint + HUBS_COLLECTION_PATH + APIVERSION + SKIP_TOP_PARAM);
			final HttpGet get = new HttpGet(uri);
			get.setHeader(AUTHORIZATION_HEADER_NAME, generateSasToken(uri));
			
			HttpClientManager.getHttpAsyncClient().execute(get, new FutureCallback<HttpResponse>() {
		        public void completed(final HttpResponse response) {
		        	try{
		        		int httpStatusCode = response.getStatusLine().getStatusCode();
		        		if (httpStatusCode != 200) {
		        			callback.failed(new NotificationHubsException(getErrorString(response), httpStatusCode));
		        			return;
		    			}			    			
		    			
						callback.completed(NotificationHubDescription.parseCollection(response.getEntity().getContent()));
		        	} catch (Exception e) {
		        		callback.failed(e);	        		
		        	} finally {
		        		get.releaseConnection();
		    		}
		        }
		        public void failed(final Exception ex) {
		        	get.releaseConnection();
		        	callback.failed(ex);
		        }
		        public void cancelled() {
		        	get.releaseConnection();
		        	callback.cancelled();
		        }
			});			
		} catch (Exception e) {
			throw new RuntimeException(e);
		} 
	}
	
	public List<NotificationHubDescription> getNotificationHubs() throws NotificationHubsException{
		SyncCallback<List<NotificationHubDescription>> callback = new SyncCallback<List<NotificationHubDescription>>();
		getNotificationHubsAsync(callback);
		return callback.getResult();
	}
	
	public void createNotificationHubAsync(NotificationHubDescription hubDescription, final FutureCallback<NotificationHubDescription> callback){
		createOrUpdateNotificationHubAsync(hubDescription, false, callback);
	}
	
	public NotificationHubDescription createNotificationHub(NotificationHubDescription hubDescription) throws NotificationHubsException{
		SyncCallback<NotificationHubDescription> callback = new SyncCallback<NotificationHubDescription>();
		createNotificationHubAsync(hubDescription, callback);
		return callback.getResult();
	}
	
	public void updateNotificationHubAsync(NotificationHubDescription hubDescription, FutureCallback<NotificationHubDescription> callback){
		createOrUpdateNotificationHubAsync(hubDescription, true, callback);
	}
	
	public NotificationHubDescription updateNotificationHub(NotificationHubDescription hubDescription) throws NotificationHubsException{
		SyncCallback<NotificationHubDescription> callback = new SyncCallback<NotificationHubDescription>();
		updateNotificationHubAsync(hubDescription, callback);
		return callback.getResult();
	}
	
	private void createOrUpdateNotificationHubAsync(NotificationHubDescription hubDescription, final boolean isUpdate, final FutureCallback<NotificationHubDescription> callback){
		try {
			URI uri = new URI(endpoint + hubDescription.getPath() + APIVERSION);
			final HttpPut put = new HttpPut(uri);
			put.setHeader(AUTHORIZATION_HEADER_NAME, generateSasToken(uri));
			if(isUpdate){
				put.setHeader(IFMATCH_HEADER_NAME, "*");
			}
			
			StringEntity entity = new StringEntity(hubDescription.getXml(), ContentType.APPLICATION_ATOM_XML);
			entity.setContentEncoding("utf-8");
			put.setEntity(entity);
			
			HttpClientManager.getHttpAsyncClient().execute(put, new FutureCallback<HttpResponse>() {
		        public void completed(final HttpResponse response) {
		        	try{
		        		int httpStatusCode = response.getStatusLine().getStatusCode();
		        		if (httpStatusCode != (isUpdate ? 200 : 201)) {
		        			callback.failed(new NotificationHubsException(getErrorString(response) ,httpStatusCode));
		        			return;
		    			}		    			
		    			
						callback.completed(NotificationHubDescription.parseOne(response.getEntity().getContent()));
		        	} catch (Exception e) {
		        		callback.failed(e);	        		
		        	} finally {
		        		put.releaseConnection();
		    		}
		        }
		        public void failed(final Exception ex) {
		        	put.releaseConnection();
		        	callback.failed(ex);
		        }
		        public void cancelled() {
		        	put.releaseConnection();
		        	callback.cancelled();
		        }
			});			
		} catch (Exception e) {
			throw new RuntimeException(e);
		} 
	}	
	
	public void deleteNotificationHubAsync(String hubPath, final FutureCallback<Object> callback){
		try {
			URI uri = new URI(endpoint + hubPath + APIVERSION);
			final HttpDelete delete = new HttpDelete(uri);
			delete.setHeader(AUTHORIZATION_HEADER_NAME, generateSasToken(uri));
			
			HttpClientManager.getHttpAsyncClient().execute(delete, new FutureCallback<HttpResponse>() {
		        public void completed(final HttpResponse response) {
		        	try{
		        		int httpStatusCode = response.getStatusLine().getStatusCode();
		        		if (httpStatusCode != 200 && httpStatusCode != 404) {
		        			callback.failed(new NotificationHubsException(getErrorString(response), httpStatusCode));
		        			return;
		    			}		    			
		    			
						callback.completed(null);
		        	} catch (Exception e) {
		        		callback.failed(e);	        		
		        	} finally {
		        		delete.releaseConnection();
		    		}
		        }
		        public void failed(final Exception ex) {
		        	delete.releaseConnection();
		        	callback.failed(ex);
		        }
		        public void cancelled() {
		        	delete.releaseConnection();
		        	callback.cancelled();
		        }
			});			
		} catch (Exception e) {
			throw new RuntimeException(e);
		} 
	}	
	
	public void deleteNotificationHub(String hubPath) throws NotificationHubsException{
		SyncCallback<Object> callback = new SyncCallback<Object>();
		deleteNotificationHubAsync(hubPath, callback);
		callback.getResult();
	}	 
		
	private String generateSasToken(URI uri) {
		String targetUri;
		try {
			targetUri = URLEncoder
					.encode(uri.toString().toLowerCase(), "UTF-8")
					.toLowerCase();

			long expiresOnDate = System.currentTimeMillis();
			expiresOnDate += SdkGlobalSettings.getAuthorizationTokenExpirationInMinutes() * 60 * 1000;
			long expires = expiresOnDate / 1000;
			String toSign = targetUri + "\n" + expires;

			// Get an hmac_sha1 key from the raw key bytes
			byte[] keyBytes = SasKeyValue.getBytes("UTF-8");
			SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA256");

			// Get an hmac_sha1 Mac instance and initialize with the signing key
			Mac mac = Mac.getInstance("HmacSHA256");
			mac.init(signingKey);

			// Compute the hmac on input data bytes
			byte[] rawHmac = mac.doFinal(toSign.getBytes("UTF-8"));

			// Convert raw bytes to Hex
			String signature = URLEncoder.encode(
					Base64.encodeBase64String(rawHmac), "UTF-8");

			// construct authorization string
			String token = "SharedAccessSignature sr=" + targetUri + "&sig="
					+ signature + "&se=" + expires + "&skn=" + SasKeyName;
			return token;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private String getErrorString(HttpResponse response)
			throws IllegalStateException, IOException {
		StringWriter writer = new StringWriter();
		IOUtils.copy(response.getEntity().getContent(), writer, "UTF-8");
		String body = writer.toString();
		return "Error: " + response.getStatusLine() + " - " + body;
	}
}