/*********************************************************************
* All Rights reserved,Copyright (c) K-Opticom
**********************************************************************
*＜プログラム内容＞
*	システム名		：eo顧客基幹システム
*	モジュール名	：JKKCtrlNetflixStubImpl
*	ソースファイル名：JKKCtrlNetflixStubImpl.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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
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.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
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.model.common.JCMAPLConstMgr;

/**
 *  Netflix社連携独自処理部品（本番環境用）です。<p>
 * <br>
 * @author 富士通
 */
public class JKKCtrlNetflixStubImpl 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";
	/** エラーコード */
	private static final String HTTP_STATUS_CODE = "HttpStatusCode";

	/**
	 * 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";
	
	/**
	 * NetflixAPIプロパティ値 証明値
	 */
	protected static final String KK_NETFLIX_CREDENTIAL = "KK_NETFLIX_CREDENTIAL";

	/**
	 * HMACキー
	 */
	protected static final String KK_NETFLIX_HMAC_KEY = "KK_NETFLIX_HMAC_KEY";
	
	/**
	 * 接続先URL
	 */
	protected static final String KK_NETFLIX_URL = "_URL";

	/**
	 * スタブ用レスポンス値格納場所
	 */
	protected static final String KK_NETFLIX_STB_RES = "_STB_RES";

	/** 
	 * 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
					ex = null;
					timeout = null;
					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);
					
					// 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();
				// ANK-3987-00-00 ADD START
				}
				else
				{
				// ANK-3987-00-00 ADD END
					ex = null;
					timeout = null;
					
					urlString = urlString + "?" + requestMap.get("urlParam");
					
					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);
					
					// HTTPメッセージボディの設定
					// JSON形式の文字列を作成する。
					String body = requestParamMake(requestMap);
					// JSON内の余分な空白を除去する。
					String json = removeSpase(body);
					reqLog = json.replaceAll("\\s+", "");
					// ヘッダーを文字列で取得
					reqHeader = connection.getRequestProperties().toString();
				}
				// ANK-3987-00-00 ADD END
				resLog = responseParamMake(apiSbtCd, ptnracntId);
				if ( resLog.length() > 0 )
				{
					ObjectMapper mapper = new ObjectMapper();
					JsonNode root = mapper.readTree(resLog);
					// 【処理結果】を取得
					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;
			}
			catch (IOException e)
			{
				e.printStackTrace();
				ex = e;
			}
			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: opeDate:運用日付(yyyyMMddHHmmss)
	 * @throws ParseException 
	 * 
	 */
	private String parseToIso8601(String strOpeDate) throws ParseException
	{
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
		Date opeDate = dateFormat.parse(strOpeDate);
		SimpleDateFormat isoFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmssZ");
    	return isoFormat.format(opeDate);
	}
	/**
	 * 
	 * @param message
	 * @return
	 * @throws NoSuchAlgorithmException
	 * @throws InvalidKeyException
	 * @throws IOException 
	 */
	private String getHMACGenerationMethod(String message) throws NoSuchAlgorithmException, InvalidKeyException, IOException{
		 
		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 String エラーコード
	 * @return JSON形式のレスポンスパラメータを返却する。
	 * @throws IOException JOSN形式へ変換する際のI/O例外
	 */
	private static String responseParamMake(String apiSbtCd, String ptnracntId) throws IOException
	{
		// レスポンス値
		// レスポンスファイル
		String path = JCMAPLConstMgr.getString(KK_NETFLIX_PROP_CONST + apiSbtCd + KK_NETFLIX_STB_RES);
		
		List<String> recordList = new ArrayList<String>();

		// 指定ファイルパスチェック
		if (path == null || "".equals(path.trim()))
		{
			// エラーの場合空文字を返す。
			return null;
		}
		BufferedReader br = null;
		try
		{
			// レスポンスファイル読込み
			br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(JCRUtilCommon.inspection(path))), NETFLIX_ENCODING));
			// ファイル一行読込み
			String lineData = br.readLine();
			while (lineData != null)
			{
				if (!("".equals(lineData.trim())))
				{
					// リストに格納された一行分のファイルデータを格納
					recordList.add(lineData);
				}
				// ファイル一行読込み
				lineData = br.readLine();
			}
		}
		catch(IOException ioe)
		{
			// 例外処理
			return null;
		}
		finally
		{
			if (br != null)
			{
				try
				{
					br.close();
				}
				catch (IOException ioe)
				{
					;
				}
			}
		}
		if (recordList.size() > 0)
		{
			return recordList.get(0);
		}
		return null;
	}
	/**
	 * ダブルクォーテーションでエスケープされてる文字以外の空白を除去
	 * @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;
	}
}
