// 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.
package com.cloud.stack;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

/**
 * CloudStackCommand wraps command properties that are being sent to CloudStack
 * 
 */
public class CloudStackCommand {
	Map<String, String> _params = new HashMap<String, String>(); 
	
	public CloudStackCommand(String cmdName) {
		this(cmdName, "json");
	}
	
	public CloudStackCommand(String cmdName, String responseType) {
		_params.put("command", cmdName);
		if(responseType != null)
			_params.put("response", responseType);
	}
	
	public CloudStackCommand setParam(String paramName, String paramValue) {
		assert(paramName != null);
		assert(paramValue != null);
	
		_params.put(paramName, paramValue);
		return this;
	}
	
	public String signCommand(String apiKey, String secretKey) throws SignatureException {
		assert(_params.get("command") != null);
		
		List<String> paramNames = new ArrayList<String>();
		for(String paramName : _params.keySet())
			paramNames.add(paramName);
		
		paramNames.add("apikey");
		Collections.sort(paramNames);

		StringBuffer sb = new StringBuffer();
		for(String name : paramNames) {
			String value;
			if("apikey".equals(name))
				value = apiKey;
			else
				value = _params.get(name);
			
			assert(value != null);
			
			value = urlSafe(value);
			
			if(sb.length() == 0) {
				sb.append(name).append("=").append(value);
			} else {
				sb.append("&").append(name).append("=").append(value);
			}
		}
		
		String signature = calculateRFC2104HMAC(sb.toString().toLowerCase(), secretKey);
		return composeQueryString(apiKey, signature);
	}
	
	private String composeQueryString(String apiKey, String signature) {
		StringBuffer sb = new StringBuffer();
		String name;
		String value;
		
		// treat command specially (although not really necessary )
		name = "command";
		value = _params.get(name);
		if(value != null) {
			value = urlSafe(value);
			sb.append(name).append("=").append(value);
		}
		
		for(Map.Entry<String, String> entry : _params.entrySet()) {
			name = entry.getKey();
			
			if(!"command".equals(name)) {
				value = urlSafe(entry.getValue());
				
				if(sb.length() == 0)
					sb.append(name).append("=").append(value);
				else
					sb.append("&").append(name).append("=").append(value);
			}
		}
		
		sb.append("&apikey=").append(urlSafe(apiKey));
		sb.append("&signature=").append(urlSafe(signature));
		
		return sb.toString();
	}
	
    private String calculateRFC2104HMAC( String signIt, String secretKey ) throws SignatureException {
   	    String result = null;
   	    try { 	 
   	    	SecretKeySpec key = new SecretKeySpec( secretKey.getBytes(), "HmacSHA1" );
   	        Mac hmacSha1 = Mac.getInstance( "HmacSHA1" );
   	        hmacSha1.init( key ); 
            byte [] rawHmac = hmacSha1.doFinal( signIt.getBytes());
            result = new String( Base64.encodeBase64( rawHmac ));
   	    } catch( Exception e ) {
   		    throw new SignatureException( "Failed to generate keyed HMAC on soap request: " + e.getMessage());
   	    }
   	    return result.trim();
    }
   
    private String urlSafe(String value) {
    	try {
    		if (value != null)
    			return URLEncoder.encode(value, "UTF-8").replaceAll("\\+", "%20");
    		else 
    			return null;
		} catch (UnsupportedEncodingException e) {
			assert(false);
		}
		
		return value;
    }
}
