/*********************************************************************
* All Rights reserved,Copyright (c) K-Opticom
**********************************************************************
*＜プログラム内容＞
*	システム名		：eo顧客基幹システム
*	モジュール名	：JKKCtrlNetflixImpl
*	ソースファイル名：JKKCtrlNetflixImpl.java
*	作成者			：FJ）中原
*	日付			：2020年10月20日
*＜機能概要＞
*	Netflix社 外部連携（本番環境用）コマンド発行部品です。
*＜修正履歴＞
*	バージョン	修正日		修正者		修正内容
*	v51.00.00	2020/10/20  FJ)大島		【ANK-3949-00-00】Netflix導入対応（STEP1）
*	v52.00.00	2021/01/15  FJ)澤田		 ANK-3987-00-00_Netflix導入対応(STEP2)
*
**********************************************************************/
package eo.common.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fujitsu.futurity.common.JCMLogCommon;
import com.fujitsu.futurity.common.JCMsvlLogOutputCommon;
import com.fujitsu.futurity.common.JSYLogBase;
import com.fujitsu.futurity.model.common.JCMAPLConstMgr;


/**
 *  トビラシステムズ連携独自処理部品（本番環境用）です。<p>
 * <br>
 * @author 富士通
 */
public class JKKCtrlNetflixImpl extends JKKCtrlNetflix
{
	
	/** 【HTTPヘッダ】  */
	/** CONTENT_TYPE  */
	private static final String CONTENT_TYPE = "Content-Type";

	/** CONTENT_TYPE_設定値  */
	private static final String CONTENT_TYPE_VALUE = "application/json";

	/** Netflix文字コード */
	private static final String NETFLIX_ENCODING = "UTF-8";

	/** パートナーアカウントID */
	private static final String PAI = "X-Netflix-PartnerCustomerIdentifier";

	/** 承認時間 */
	private static final String AUTHORIZATIONTIME = "X-Netflix-AuthorizationTime";
	
	/** ヘッダー認証 */
	private static final String AUTHORIZATIONHEADER = "X-Netflix-Header-Authorization";
	/** ボディー部認証 */
	private static final String AUTHORIZATIONPAYLOAD = "X-Netflix-Payload-Authorization";

	private static final String AUTHORIZATION_STATIC_CREDENTIAL= "nflxv1 Credential=";
	private static final String AUTHORIZATION_STATIC_SIGNATURE= ",Signature=";
	
	// レスポンスパラメータ定数定義
	/** エラー */
	private static final String ERROR = "Error";
	/** HTTPステータスコード */
	private static final String HTTP_STATUS_CODE = "HttpStatusCode";

	/**
	 * NetflixAPIプロパティ値 証明値
	 */
	protected static final String KK_NETFLIX_CREDENTIAL = "KK_NETFLIX_CREDENTIAL";

	/**
	 * NetflixAPIプロパティ値固定値
	 */
	protected static final String KK_NETFLIX_PROP_CONST = "KK_NETFLIX_";
	
	/**
	 * NetflixAPIリトライ回数
	 */
	protected static final String KK_NETFLIX_RETRYCOUNT = "_RETRYCOUNT";
	
	/**
	 * NetflixAPI連携接続タイムアウト値
	 */
	protected static final String KK_NETFLIX_CONNECT_TIMEOUT = "_CONNECT_TIMEOUT";
	
	/**
	 * NetflixAPI連携読取タイムアウト値
	 */
	protected static final String KK_NETFLIX_READ_TIMEOUT = "_READ_TIMEOUT";
	
	/**
	 * NetflixAPI連携リトライインターバル値
	 */
	protected static final String KK_NETFLIX_RETRYINTERVAL = "_RETRYINTERVAL";
	
	/**
	 * HMACキー
	 */
	protected static final String KK_NETFLIX_HMAC_KEY = "KK_NETFLIX_HMAC_KEY";
	
	/**
	 * 接続先URL
	 */
	protected static final String KK_NETFLIX_URL = "_URL";

	/** 
	 * Netflix契約依頼(Enroll)
	 */
	protected static final String KK_NETFLIX_API_ENROLL = "ENROLL";
	
	/** 
	 * Netflix利用開始依頼(Token)
	 */
	protected static final String KK_NETFLIX_API_TOKEN = "TOKEN";
	
	/** 
	 * Netflix解約依頼(Cancel)
	 */
	protected static final String KK_NETFLIX_API_CANCEL = "CANCEL";
	
	/** 
	 *Netflix契約変更状況照会(ReadChangeEvent)
	 */
	protected static final String KK_NETFLIX_API_READCHANGEEVENT = "READCHANGEEVENT";
	
	/** 
	 * (Netflix契約変更状況照会(ReadChangeEvent)
	 */
	protected static final String KK_NETFLIX_API_SENDCHANGERESULT = "SENDCHANGERESULT";
	
	// ANK-3987-00-00 ADD START
	/** 
	 * (Netflix契約開始照会(ActivationData)
	 */
	protected static final String KK_NETFLIX_API_ACTIVATION = "ACTIVATIONDATA";
	// ANK-3987-00-00 ADD END
	
	/**
	 *  Netflix連携処理を行います。
	 *  <br>
	 * @param serviceMap Netflix連携のインターフェイス情報
	 * @return 処理結果コード値　0：正常　1:異常
	 */
	public Map<String, Object> call_netflix(Map<String, Object> serviceMap) throws Exception
	{
		// 電文送信処理の実行
		HashMap<String, Object> result = execute(serviceMap);
		
		return result;
	}
	
	/**
	 * JSON電文送信処理を実行する。
	 * 
	 * @param serviceMap NetflixAPIのインターフェイス情報
	 * @return 処理結果コード値　0：正常　1:異常
	 * @throws Exception 
	 */
	private HashMap<String, Object>  execute(Map<String, Object>serviceMap) throws Exception 
	{
		// レスポンス
		HashMap<String, Object> result = new HashMap<String, Object>();
		// HTTPStatus
		int httpStatusCode = 500;
		IOException ex = null;
		SocketTimeoutException timeout = null;
		String reqLog = "";
		String resLog = "";
		String reqHeader = "";
		String apiSbtCd = "";
		String ptnracntId = "";
		String strOpeDateTime = "";
		Map<String, Object> requestMap = new HashMap<String, Object>();
		
		int retryCount = 0;
		Integer retryInterval = 0;
		Integer tobilaConectTimeout = 0;
		Integer tobilaReadTimeout = 0;
		String urlString = "";
		
		// パラメータの取得
		// API種別
		if (serviceMap.containsKey("apiSbtCd"))
		{
			apiSbtCd = (String) serviceMap.get("apiSbtCd");
		}
		// パートナーアカウントID
		if (serviceMap.containsKey("pai"))
		{
			ptnracntId = (String) serviceMap.get("pai");
		}
		// 運用日
		if (serviceMap.containsKey("opeDateTime"))
		{
			strOpeDateTime = (String) serviceMap.get("opeDateTime");
		}
		// リクエストボディー部
		if (serviceMap.containsKey("requestValue"))
		{
			requestMap = castHashMap(serviceMap.get("requestValue"));
		}
		
		// NetflixAPIのプロパティ値取得
		StringBuilder sb = new StringBuilder();
		// リトライ回数
		sb = sb.append(KK_NETFLIX_PROP_CONST).append(apiSbtCd).append(KK_NETFLIX_RETRYCOUNT);
		retryCount = Integer.parseInt(JCMAPLConstMgr.getString(sb.toString()));
		retryCount += 1;
		// リトライインターバル値
		sb.setLength(0);
		sb = sb.append(KK_NETFLIX_PROP_CONST).append(apiSbtCd).append(KK_NETFLIX_RETRYINTERVAL);
		retryInterval =(Integer.parseInt(JCMAPLConstMgr.getString(sb.toString())));
		// 連携接続タイムアウト値
		sb.setLength(0);
		sb = sb.append(KK_NETFLIX_PROP_CONST).append(apiSbtCd).append(KK_NETFLIX_CONNECT_TIMEOUT);
		tobilaConectTimeout =(Integer.parseInt(JCMAPLConstMgr.getString(sb.toString())));
		// 連携接続タイムアウト値
		sb.setLength(0);
		sb = sb.append(KK_NETFLIX_PROP_CONST).append(apiSbtCd).append(KK_NETFLIX_READ_TIMEOUT);
		tobilaReadTimeout =(Integer.parseInt(JCMAPLConstMgr.getString(sb.toString())));
		// 連携接続タイムアウト値
		sb.setLength(0);
		sb = sb.append(KK_NETFLIX_PROP_CONST).append(apiSbtCd).append(KK_NETFLIX_URL);
		urlString = JCMAPLConstMgr.getString(sb.toString());

		// urlが指定されていない場合は終了
		if (urlString == null || urlString.length() == 0)
		{
			return result;
		}

		for(int i = 0 ; i < retryCount ; i++)
		{
			HttpURLConnection connection = null;
			URL url = null;
			// レスポンス
			StringBuffer response = new StringBuffer();
			try
			{
				// ANK-3987-00-00 ADD START
				if (!KK_NETFLIX_API_ACTIVATION.equals(apiSbtCd))
				{
				// ANK-3987-00-00 ADD END
					url = new URL(urlString);
					// 電文を送信
					// SSLの場合、HttpsURLConnectionへキャスト
					connection = (HttpURLConnection) url.openConnection();
					connection.setDoInput(true);
					connection.setDoOutput(true);
					
					// Netflix社との通信におけるタイムアウト値
					connection.setConnectTimeout(tobilaConectTimeout);
					connection.setReadTimeout(tobilaReadTimeout);
					// HTTPリクエストコード
					// READCHANGEEVENTのリクエストメソッドはGET
					if (KK_NETFLIX_API_READCHANGEEVENT.equals(apiSbtCd))
					{
						connection.setRequestMethod("GET");
					} else {
						connection.setRequestMethod("POST");
					}
					// HTTPヘッダの設定
					Map<String, String> headerMap = new HashMap<String, String>();
					// コンテンツタイプ
					connection.setRequestProperty(CONTENT_TYPE, CONTENT_TYPE_VALUE);
					// 承認時間
					String iso8601OpeDate = parseToISO8601(strOpeDateTime);
					connection.setRequestProperty(AUTHORIZATIONTIME, iso8601OpeDate);
					headerMap.put(AUTHORIZATIONTIME, iso8601OpeDate);
					// パートナーアカウント識別子
					if (ptnracntId != null && ptnracntId.length() > 0)
					{
						connection.setRequestProperty(PAI, ptnracntId);
						headerMap.put(PAI, ptnracntId);
					}
					// ヘッダー項目をヘッダー名でソート
					Object[] mapKeyList = headerMap.keySet().toArray();
					Arrays.sort(mapKeyList);
					StringBuilder sbHeader = new StringBuilder();
					int idx = 0;
					for (Object mapKey : mapKeyList)
					{
						idx += 1;
						String nKey = (String)mapKey;
						// 認証キー用に、前後の空白除去と小文字変換を行う。
						sbHeader.append(nKey.trim().toLowerCase()).append("=").append(headerMap.get(nKey).trim());
						if (mapKeyList.length > idx)
						{
							sbHeader.append(",");
						}
					}
					// ヘッダー認証作成
					String authorizationHeader = getHMACGenerationMethod(sbHeader.toString());
					String credential = JCMAPLConstMgr.getString(KK_NETFLIX_CREDENTIAL);
					connection.setRequestProperty(AUTHORIZATIONHEADER, AUTHORIZATION_STATIC_CREDENTIAL + credential + AUTHORIZATION_STATIC_SIGNATURE + authorizationHeader);
					// ヘッダーを文字列で取得
					reqHeader = connection.getRequestProperties().toString();
					
					// HTTPメッセージボディの設定
					// JSON形式の文字列を作成する。
					String body = requestParamMake(requestMap);
					// JSON内の余分な空白を除去する。
					String json = removeSpase(body); 
					reqLog = json.replaceAll("\\s+", "");
					// ボディー部がある場合のみボディー部作成
					if (requestMap.size() > 0)
					{
						// ボディー部認証作成
						String authorizationPayload = getHMACGenerationMethod(reqLog);
						connection.setRequestProperty(AUTHORIZATIONPAYLOAD, AUTHORIZATION_STATIC_CREDENTIAL + credential + AUTHORIZATION_STATIC_SIGNATURE + authorizationPayload);
						// ヘッダーを文字列で取得
						reqHeader = connection.getRequestProperties().toString();
	
						OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream(), NETFLIX_ENCODING);
						out.write(reqLog);
						out.flush();
					}
				// ANK-3987-00-00 ADD START
				}
				else
				{
					
					urlString = urlString + "?" + requestMap.get("urlParam");
					
					url = new URL(urlString);
					result.put("url", url.toString());
					// 電文を送信
					// SSLの場合、HttpsURLConnectionへキャスト
					connection = (HttpURLConnection) url.openConnection();
					connection.setDoInput(true);
					connection.setDoOutput(true);
					
					// Netflix社との通信におけるタイムアウト値
					connection.setConnectTimeout(tobilaConectTimeout);
					connection.setReadTimeout(tobilaReadTimeout);
					// HTTPリクエストコード
					//ACTIVATIONのリクエストメソッドはGET
					connection.setRequestMethod("GET");

					// HTTPヘッダの設定
					Map<String, String> headerMap = new HashMap<String, String>();
					// コンテンツタイプ
					connection.setRequestProperty(CONTENT_TYPE, CONTENT_TYPE_VALUE);
					// 承認時間
					String iso8601OpeDate = parseToISO8601(strOpeDateTime);
					connection.setRequestProperty(AUTHORIZATIONTIME, iso8601OpeDate);
					headerMap.put(AUTHORIZATIONTIME, iso8601OpeDate);
					// パートナーアカウント識別子
					if (ptnracntId != null && ptnracntId.length() > 0)
					{
						connection.setRequestProperty(PAI, ptnracntId);
						headerMap.put(PAI, ptnracntId);
					}
					// ヘッダー項目をヘッダー名でソート
					Object[] mapKeyList = headerMap.keySet().toArray();
					Arrays.sort(mapKeyList);
					StringBuilder sbHeader = new StringBuilder();
					int idx = 0;
					for (Object mapKey : mapKeyList)
					{
						idx += 1;
						String nKey = (String)mapKey;
						// 認証キー用に、前後の空白除去と小文字変換を行う。
						sbHeader.append(nKey.trim().toLowerCase()).append("=").append(headerMap.get(nKey).trim());
						if (mapKeyList.length > idx)
						{
							sbHeader.append(",");
						}
					}
					// ヘッダー認証作成
					String authorizationHeader = getHMACGenerationMethod(sbHeader.toString());
					String credential = JCMAPLConstMgr.getString(KK_NETFLIX_CREDENTIAL);
					connection.setRequestProperty(AUTHORIZATIONHEADER, AUTHORIZATION_STATIC_CREDENTIAL + credential + AUTHORIZATION_STATIC_SIGNATURE + authorizationHeader);
					// ヘッダーを文字列で取得
					reqHeader = connection.getRequestProperties().toString();
					
					// HTTPメッセージボディの設定
					// JSON形式の文字列を作成する。
					String body = requestParamMake(requestMap);
					// JSON内の余分な空白を除去する。
					String json = removeSpase(body); 
					reqLog = json.replaceAll("\\s+", "");
				}
				// ANK-3987-00-00 ADD END
				
				// 接続
				connection.connect();

				InputStream in = null ;
				int httpStatus = connection.getResponseCode();
				if (httpStatus >= 200 && httpStatus < 300)
				{
					in = connection.getInputStream();
				} else {
					in = connection.getErrorStream();
				}
				if (in != null && httpStatus < 500)
				{
					String encoding = connection.getContentEncoding();
					if (null == encoding)
					{
						encoding = NETFLIX_ENCODING;
					}
					final InputStreamReader inReader = new InputStreamReader(in, encoding);
					final BufferedReader bufReader = new BufferedReader(inReader);
					String line = null;
	
					while ((line = bufReader.readLine()) != null)
					{
						response.append(line);
					}
					resLog = response.toString().replaceAll("\\s+", " ");
					bufReader.close();
					inReader.close();
					in.close();
					if ( response.length() > 0 )
					{
						ObjectMapper mapper = new ObjectMapper();
						JsonNode root = mapper.readTree(response.toString());
						// 【処理結果】を取得
						if (root.findValue(ERROR) != null)
						{
							JsonNode error = mapper.readTree(root.get(ERROR).toString());
							if (error.findValue(HTTP_STATUS_CODE) != null)
							{
								httpStatusCode = error.get(HTTP_STATUS_CODE).intValue();
							}
						} else {
							if (root.findValue(HTTP_STATUS_CODE) != null)
							{
								httpStatusCode = root.get(HTTP_STATUS_CODE).intValue();
							}
						}
					}
				}
			}
			catch (SocketTimeoutException se)
			{
				se.printStackTrace();
				timeout = se;
				// ANK-3987-00-00 ADD START
				// エラー情報をレスポンスに出力する。
				StringWriter sw = new StringWriter();
				timeout.printStackTrace(new PrintWriter(sw));
				resLog = sw.toString();
				// ANK-3987-00-00 ADD END
			}
			catch (IOException e)
			{
				e.printStackTrace();
				ex = e;
				// ANK-3987-00-00 ADD START
				// エラー情報をレスポンスに出力する。
				StringWriter sw = new StringWriter();
				ex.printStackTrace(new PrintWriter(sw));
				resLog = sw.toString();
				// ANK-3987-00-00 ADD END
			}
			catch (Exception exc)
			{
				exc.printStackTrace();
				// ANK-3987-00-00 ADD START
				// エラー情報をレスポンスに出力する。
				StringWriter sw = new StringWriter();
				exc.printStackTrace(new PrintWriter(sw));
				resLog = sw.toString();
				// ANK-3987-00-00 ADD END
			}
			finally
			{
				result.put("reqHeader", reqHeader);
				result.put("request", reqLog);
				result.put("response", resLog);
				// 規定回以上実行した場合はURLを返却する。
				if (retryCount > 1)
				{
					if (((ex != null) || (timeout != null) || (!(httpStatusCode >= 200 && httpStatusCode < 300) && httpStatusCode >= 500)) && (i + 1 >= retryCount))
					{
						result.put("url", url.toString());
					}
				}
				if (connection != null)
				{
					//切断処理
					connection.disconnect();
					connection = null;
				}
			}
			// 失敗時はリトライ回数の上限までリトライ
			if (((ex != null) || (timeout != null) || (!(httpStatusCode >= 200 && httpStatusCode < 300) && httpStatusCode >= 500)) && (i < retryCount))
			{
				try
				{
					Thread.sleep(retryInterval);
				}
				catch (InterruptedException e)
				{
					// 割り込み要求による例外は無視
				}
				continue;
			}
			else
			{
				break;
			}
		}
		
		return result;
	}
	/**
	 * Netflix リクエストパラメータを作成する。
	 * @param Map<String, Object> 入力パラメータ
	 * @return JSON形式のリクエストパラメータを返却する。
	 * @throws IOException JOSN形式へ変換する際のI/O例外
	 */
	private static String requestParamMake(Map<String, Object> requestMap) throws IOException
	{
		// 変換後文字列
		String json = "";
		ObjectMapper mapper = new ObjectMapper();
		mapper.enable(SerializationFeature.INDENT_OUTPUT);
		try
		{
			// JOSN形式へ変換
			json = mapper.writeValueAsString(requestMap);

		} catch (IOException e)
		{
			e.printStackTrace();
			throw e;
		}
		return json;
	}
	/**
	 * 運用年月日時分秒を、ISO8601（標準型）※yyyyMMddTHHmmssZへ変換する
	 * param: strOpeDateTime:運用日付(yyyyMMddHHmmss)
	 * @throws ParseException 
	 * 
	 */
	private String parseToISO8601(String strOpeDateTime) throws ParseException
	{
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
		Date opeDate = dateFormat.parse(strOpeDateTime);
		SimpleDateFormat isoFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmssZ");
    	return isoFormat.format(opeDate);
	}
	/**
	 * 
	 * @param message
	 * @return
	 * @throws IOException 
	 * @throws NoSuchAlgorithmException
	 * @throws InvalidKeyException
	 * @throws IOException 
	 * @throws NoSuchAlgorithmException 
	 * @throws InvalidKeyException 
	 */
	private String getHMACGenerationMethod(String message) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
		 
		String Netflix_HMAC_KEY = JCMAPLConstMgr.getString(KK_NETFLIX_HMAC_KEY);
		String HMAC_SHA256 = "HmacSHA256";
		
		BASE64Encoder en = new BASE64Encoder(); 
		BASE64Decoder de = new BASE64Decoder();
		//Netflixが提供する暗号化されたベース64の秘密キーでデコードする
		byte[] secretKeyByte = de.decodeBuffer(Netflix_HMAC_KEY);
		  
		//秘密キーが付いているヘッダー値のHMACを生成する
		Mac hmac = Mac.getInstance(HMAC_SHA256);
		SecretKeySpec secretKey = new SecretKeySpec(secretKeyByte, HMAC_SHA256);
		hmac.init(secretKey);
		  
		//ベース64の署名をエンコードする
		return en.encode(hmac.doFinal(message.getBytes()));
	}
	/**
	 * Object型をMap<String, Object>型に変換します。
	 * @param mapObj
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private Map<String, Object> castHashMap(Object mapObj)
	{
		Map<String, Object> resultMap = new HashMap<String, Object>();
		resultMap = (Map<String, Object>) mapObj;
		return resultMap;
	}
	/**
	 * ダブルクォーテーションでエスケープされてる文字以外の空白を除去
	 * @param json
	 * @return
	 */
	private static String removeSpase(String json)
	{
		boolean quoted = false;
		boolean escaped = false;
		String out = "";
		
		for(Character c : json.toCharArray())
		{
			if(escaped)
			{
				out += c;
				escaped = false;
				continue;
			}
			
			if(c == '"')
			{
				quoted = !quoted;
			} else if (c == '\\') {
				escaped = true;
			}
			
			if(c == ' ' & !quoted)
			{
				continue;
			}
			out += c;
		}
		return out;
	}
}
