package com.nifty.cloud.ncs.service.cli;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.crypto.SecretKey;

import com.nifty.cloud.ncs.NiftyCloudClientException;
import com.nifty.cloud.ncs.NiftyCloudServiceException;
import com.nifty.cloud.ncs.auth.Credentials;
import com.nifty.cloud.ncs.encrypt.EncryptionKeyMaterials;
import com.nifty.cloud.ncs.http.ClientProperties;
import com.nifty.cloud.ncs.service.NcsBucket;
import com.nifty.cloud.ncs.service.NcsObject;
import com.nifty.cloud.ncs.service.NcsObjectContents;
import com.nifty.cloud.ncs.service.ObjectListing;
import com.nifty.cloud.ncs.service.StorageClient;
import com.nifty.cloud.ncs.service.StorageEncryptionClient;
import com.nifty.cloud.ncs.service.StorageHeadersImpl;
import com.nifty.cloud.ncs.service.acl.Policy;
import com.nifty.cloud.ncs.transfer.TransferManager;
import com.nifty.cloud.ncs.transfer.TransferManagerConfiguration;
import com.nifty.cloud.ncs.transfer.UploadMonitor;
import com.nifty.cloud.ncs.util.DateUtils;


/**
 * ニフクラ ストレージ コマンドクラス
 *
 */
public class Command
{
	/** ストレージサービスクライアント */
	private StorageClient client;

	private TransferManager manager;

	private static final String NIFS3_PREFIX = "ncss://";
	private static final int MB = 1024 * 1024;
	private static final long GB = MB * 1024L;
	private static final long MAX_FILE_SIZE = 2L * GB;
	private static final long MAX_MULTI_FILE_SIZE = 5L * GB;
	private static final String KEY_MARKER = "key-marker";
	private static final String MAX_KEYS = "max-keys";
	private static final String PREFIX = "prefix";
	private static final int DEFAULT_MAX_THREADS = 10;
	private static final long DEFAULT_MULTIPART_THRESHOLD = 16L * MB;
	private static final int DEFAULT_PART_SIZE = 5 * MB;
	private static final String DEFAULT_SOCKET_TIMEOUT = "180";

	private int maxThreads = DEFAULT_MAX_THREADS;
	private long multipartThreshold = DEFAULT_MULTIPART_THRESHOLD;
	private int partSize = DEFAULT_PART_SIZE;
	private long multipartMaxSize = MAX_MULTI_FILE_SIZE;

	private String myEndpoint = "https://jp-east-1.storage.api.nifcloud.com";

	/**
	 * コンストラクタ
	 *
	 * @param credentials
	 *  アクセスキー／シークレットキー
	 * @param protocol
	 *  HTTPプロトコル
	 * @param privateKey
	 *  暗号化鍵
	 */
	public Command(Credentials credentials, String protocol, SecretKey privateKey)
	{
		ClientProperties properties = new ClientProperties();

		// エンドポイント設定
		String endpoint = System.getProperty("endpoint");

		// プロキシ設定
		String proxyHost = System.getProperty("http.proxyHost");
		String proxyPort = System.getProperty("http.proxyPort");
		String proxyUser = System.getProperty("http.proxyUser");
		String proxyPass = System.getProperty("http.proxyPass");

		// リトライ設定
		String maxRetry = System.getProperty("http.maxRetry");

		// ソケットタイムアウト設定
		int socketTimeout = Integer.parseInt(System.getProperty("socket.timeout", DEFAULT_SOCKET_TIMEOUT));

		// マルチパートアップロード設定
		String maxThreads = System.getProperty("multipart.maxThreads");
		String multipartThreshold = System.getProperty("multipart.threshold");
		String partSize = System.getProperty("multipart.partSize");
		String maxSize = System.getProperty("multipart.maxSize");

		if (proxyHost != null) {
			properties.setProxyHost(proxyHost);
		}
		if (proxyPort != null) {
			properties.setProxyPort(Integer.parseInt(proxyPort));
		}
		if (proxyUser != null) {
			properties.setProxyUserName(proxyUser);
		}
		if (proxyPass != null) {
			properties.setProxyPassword(proxyPass);
		}

		if (maxRetry != null) {
			properties.setMaxRetry(Integer.parseInt(maxRetry));
		}

		if (endpoint != null) {
			this.myEndpoint = endpoint;
		}
		if (maxThreads != null) {
			this.maxThreads = Integer.parseInt(maxThreads);
		}
		if (multipartThreshold != null) {
			this.multipartThreshold = Long.parseLong(multipartThreshold);
		}
		if (partSize != null) {
			this.partSize = Integer.parseInt(partSize);
		}
		if (maxSize != null) {
			this.multipartMaxSize = Long.parseLong(maxSize);
		}

		properties.setProtocol(protocol);
		properties.setSocketTimeout(socketTimeout * 1000);

		if (privateKey != null) {
			EncryptionKeyMaterials encryptionMaterials = new EncryptionKeyMaterials(privateKey);
			this.client = new StorageEncryptionClient(credentials, properties, encryptionMaterials);
		} else {
			this.client = new StorageClient(credentials, properties);
		}
		this.client.setEndpoint(myEndpoint);
	}

	/**
	 * バケット作成
	 *
	 * @param bucketName
	 *  バケット名
	 * @param policy
	 *  Access Control List
	 * @throws NiftyCloudServiceException
	 * @throws NiftyCloudClientException
	 */
	public void putBucket(String bucketName, Policy policy, String locationConstraint) throws NiftyCloudServiceException, NiftyCloudClientException
	{
		NcsBucket bucket = client.putBucket(bucketName, policy);
		System.out.println(String.format("Bucket '%s%s' created", NIFS3_PREFIX, bucket.getName()));
	}

	/**
	 * バケット削除
	 *
	 * @param bucketName
	 *  バケット名
	 * @throws NiftyCloudServiceException
	 * @throws NiftyCloudClientException
	 */
	public void deleteBucket(String bucketName) throws NiftyCloudServiceException, NiftyCloudClientException
	{
		client.deleteBucket(bucketName);
		System.out.println(String.format("Bucket '%s%s' removed", NIFS3_PREFIX, bucketName));
	}

	/**
	 * オブジェクト アップロード
	 *
	 * @param bucketName
	 *  バケット名
	 * @param objectName
	 *  オブジェクト名
	 * @param localFileName
	 *  アップロード対象ファイル名
	 * @param policy
	 *  Access Control List
	 * @param headers
	 *  リクエストヘッダー
	 * @throws NiftyCloudServiceException
	 * @throws NiftyCloudClientException
	 */
	public void putObject(String bucketName, String objectName, String localFileName, Policy policy, HashMap<String, String> headers) throws NiftyCloudServiceException, NiftyCloudClientException
	{
		// オブジェクト名未指定時は、ローカルファイル名とする
		if (objectName == null) {
			objectName = localFileName;
		}

		NcsObject object = new NcsObject();
		object.setBucketName(bucketName);
		object.setObjectName(objectName);

		for (Entry<String, String> header : headers.entrySet()) {
			object.addHeader(header.getKey(), header.getValue());
		}

		// localFileNameの末尾が「/」の場合は常にディレクトリ作成とする
		if (localFileName.endsWith("/")) {
			client.putObject(object, policy);
		}
		else {
			boolean isMultipartUpload = false;

			// ファイルサイズ制限チェック
			File file;
			try {
				file = new File(localFileName);
				if (file.canRead() == false) {
					throw (new NiftyCloudClientException("No such file: " + localFileName));
				}

				isMultipartUpload = file.length() > multipartThreshold;

				// 通常アップロードは2GB、マルチアップロードは5GBまで
				long maxFileSize = isMultipartUpload ? multipartMaxSize : MAX_FILE_SIZE;
				if ((file.isDirectory() == false) && (file.length() > maxFileSize)) {
					throw (new NiftyCloudClientException("File size over a limit size(" + maxFileSize/GB + "Gbyte): " + file.length()));
				}
			} catch (Exception e) {
				throw (new NiftyCloudClientException(e.getMessage(), e));
			}

			if (isMultipartUpload) {
				uploadMultipart(object, file, policy);
			} else {
				client.putObject(object, file, policy);
			}
		}

		System.out.println(String.format("%s -> %s%s/%s", localFileName, NIFS3_PREFIX, bucketName, objectName));
	}

	/**
	 * マルチパートアップロード
	 *
	 * @param object
	 * @param file
	 * @param policy
	 * @throws NiftyCloudServiceException
	 * @throws NiftyCloudClientException
	 */
	private void uploadMultipart(NcsObject object, File file, Policy policy) throws NiftyCloudServiceException, NiftyCloudClientException
	{
		if (manager == null) {
			TransferManagerConfiguration configuration = new TransferManagerConfiguration();
			configuration.setMultipartUploadThreshold(multipartThreshold);
			configuration.setMinimumUploadPartSize(partSize);
			manager = new TransferManager(client, configuration, maxThreads);
		}

		// アップロード
		UploadMonitor monitor = manager.upload(object, file, policy);

		// アップロードの完了待ち
		try {
			monitor.waitForUploadResult();
		} catch (InterruptedException e) {
			throw (new NiftyCloudClientException(e.getMessage(), e.getCause()));
		} finally {
			manager.shutdownNow();
		}
	}

	/**
	 * マルチパートアップロード破棄
	 *
	 * @param bucketName
	 *  バケット名
	 * @param objectName
	 *  オブジェクト名
	 * @param uploadId
	 *  アップロードID
	 * @throws NiftyCloudServiceException
	 * @throws NiftyCloudClientException
	 */
	public void abortMultipartUpload(String bucketName, String objectName, String uploadId) throws NiftyCloudServiceException, NiftyCloudClientException
	{
		client.abortMultipartUpload(bucketName, objectName, uploadId);

		System.out.println(String.format("%s%s/%s(%s) deleted", NIFS3_PREFIX, bucketName, objectName, uploadId));
	}

	/**
	 * オブジェクト削除
	 *
	 * @param bucketName
	 *  バケット名
	 * @param objectName
	 *  オブジェクト名
	 * @throws NiftyCloudServiceException
	 * @throws NiftyCloudClientException
	 */
	public void deleteObject(String bucketName, String objectName) throws NiftyCloudServiceException, NiftyCloudClientException
	{
		client.deleteObject(bucketName, objectName);

		String type;
		if (objectName.endsWith("/")) {
			type = "Dir";
		} else {
			type = "File";
		}
		System.out.println(String.format("%s %s%s/%s deleted", type, NIFS3_PREFIX, bucketName, objectName));
	}

	/**
	 * オブジェクトダウンロード
	 *
	 * @param bucketName
	 *  バケット名
	 * @param objectName
	 *  オブジェクト名
	 * @param localFileName
	 *  保存先ローカルファイル名
	 * @throws NiftyCloudServiceException
	 * @throws NiftyCloudClientException
	 */
	public void getObject(String bucketName, String objectName, String localFileName, HashMap<String, String> headers) throws NiftyCloudServiceException, NiftyCloudClientException
	{
		// 保存ディレクトリが存在しない場合は作成する
		File file = new File(localFileName);
		if (localFileName.endsWith("/")) {
			if (file.exists() == false) {
				file.mkdirs();
			}
		}
		if (file.isDirectory()) {
			if (localFileName.endsWith("/") == false) {
				localFileName += "/";
			}
			int idx = objectName.lastIndexOf("/");
			if (idx == -1) {
				localFileName = localFileName + objectName;
			} else {
				localFileName = localFileName + objectName.substring(idx+1);
			}
		}
		if (file.getParentFile() != null) {
			if (file.getParentFile().exists() == false) {
				file.getParentFile().mkdirs();
			}
		}

		NcsObject object = new NcsObject();
		object.setBucketName(bucketName);
		object.setObjectName(objectName);

		for (Entry<String, String> header : headers.entrySet()) {
			object.addHeader(header.getKey(), header.getValue());
		}

		client.getObject(object, localFileName);
		System.out.println(String.format("%s%s/%s -> %s", NIFS3_PREFIX, bucketName, objectName, localFileName));
	}

	/**
	 * 一覧出力（バケットまたはオブジェクト）
	 * @param bucketName
	 *  バケット名
	 * @throws NiftyCloudServiceException
	 * @throws NiftyCloudClientException
	 */
	public void listBucket(String bucketName, String[] parameters) throws NiftyCloudServiceException, NiftyCloudClientException
	{
		// パラメータ設定
		String prefix = null;
		String marker = null;
		Integer maxKeys = null;
		for (String s : parameters) {
			String[] param = s.split("=");
			if (param[0].toLowerCase().equals(PREFIX) && (param.length > 1)) {
				prefix = param[1];
			} else if (param[0].toLowerCase().equals(KEY_MARKER) && (param.length > 1)) {
				marker = param[1];
			} else if (param[0].toLowerCase().equals(MAX_KEYS) && (param.length > 1)) {
				maxKeys = new Integer(param[1]);
			}
		}

		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm");

		if (bucketName == null) {
			// バケット一覧
			List<NcsBucket> list = client.getService();
			for (NcsBucket bucket : list) {
				System.out.println(String.format("%s  %s%s", sdf.format(bucket.getCreated()), NIFS3_PREFIX, bucket.getName()));
			}
		} else {
			// オブジェクト一覧
			ObjectListing list = client.listObjects(bucketName, prefix, marker, maxKeys);
			List<NcsObjectContents> objects = list.getObjectList();
			for (NcsObjectContents object : objects) {
				System.out.println(String.format("%s    %13d  %s%s/%s", sdf.format(object.getLastModified()), object.getSize(), NIFS3_PREFIX, object.getBucketName(), object.getName()));
			}
		}
	}

	/**
	 * 全オブジェクト一覧出力
	 *
	 * @throws NiftyCloudServiceException
	 * @throws NiftyCloudClientException
	 */
	public void listAllObjects() throws NiftyCloudServiceException, NiftyCloudClientException
	{
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm");
		List<NcsBucket> bucketList = client.getService();

		for (NcsBucket bucket : bucketList) {
			ObjectListing objectListing = client.listObjects(bucket.getName(), null, null, null);
			for (NcsObjectContents object : objectListing.getObjectList()) {
				if (object.getName().endsWith("/")) {
					System.out.println(String.format("                   DIR             %s%s/%s", NIFS3_PREFIX, object.getBucketName(), object.getName()));
				} else {
					System.out.println(String.format("%s    %13d  %s%s/%s", sdf.format(object.getLastModified()), object.getSize(), NIFS3_PREFIX, object.getBucketName(), object.getName()));
				}
			}
			System.out.println();
		}
	}


	/**
	 * オブジェクトのコピー
	 *
	 * @param srcBucketName
	 *  コピー元バケット名
	 * @param srcObjectName
	 *  コピー元オブジェクト名
	 * @param destBucketName
	 *  コピー先バケット名
	 * @param destObjectName
	 *  コピー先オブジェクト名
	 * @param policy
	 *  ACL情報(privateのみ指定可能)
	 * @param headers
	 *  ヘッダー情報
	 *
	 * @throws NiftyCloudServiceException
	 * @throws NiftyCloudClientException
	 */
	public void copyObject(String srcBucketName, String srcObjectName,
			String destBucketName, String destObjectName, Policy policy, HashMap<String, String> headers)
	throws NiftyCloudServiceException, NiftyCloudClientException
	{
		NcsObject src = new NcsObject(srcBucketName, srcObjectName);
		NcsObject dest = new NcsObject(destBucketName, destObjectName);

		client.copyObject(src, dest, policy);

		System.out.println(String.format("%s%s/%s -> %s%s/%s", NIFS3_PREFIX, srcBucketName, srcObjectName,
				NIFS3_PREFIX, destBucketName, destObjectName));
	}

	/**
	 * オブジェクトのヘッダー情報一覧表示
	 *
	 * @param bucketName
	 *  バケット名
	 * @param objectName
	 *  オブジェクト名
	 */
	public void headObject(String bucketName, String objectName)
	{
		Map<String, Object> headers = client.headObject(bucketName, objectName);

		int maxKeyLength = 0;
		for (String key : headers.keySet()) {
			maxKeyLength = Math.max(maxKeyLength, key.length());
		}

		System.out.println(String.format("Headers of %s%s/%s", NIFS3_PREFIX, bucketName, objectName));
		for (Entry<String, Object> entry : headers.entrySet()) {
			if (StorageHeadersImpl.LAST_MODIFIED.equals(entry.getKey())) {
				entry.setValue(DateUtils.formatRfc822Date((Date)entry.getValue()));
			}
			System.out.println(String.format("  %s : %s",
					String.format("%-"+ maxKeyLength + "s", entry.getKey()),
					entry.getValue()));
		}
	}
}
