/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed 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 org.opengion.plugin.daemon;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.opengion.fukurou.db.DBUtil;
import org.opengion.fukurou.db.Transaction;
import org.opengion.fukurou.db.TransactionReal;
import org.opengion.fukurou.transfer.TransferConfig;
import org.opengion.fukurou.transfer.TransferExec;
import org.opengion.fukurou.util.ApplicationInfo;
import org.opengion.fukurou.util.LogWriter;
import org.opengion.fukurou.util.StringUtil;
import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
import static org.opengion.fukurou.util.HybsConst.CR ;				// 6.1.0.0 (2014/12/26)
import static org.opengion.fukurou.util.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

/**
 * 【伝送システム】旧伝送DB(CB01)を監視して、実行方法に応じた処理プログラムを呼び出します。
 *
 * このデーモンは、伝送定義マスタの読取方法が、旧伝送DB読取(CB01)の定義を対象として実行されます。
 * 読取対象は、旧伝送DB(CB01)で、データコード、送り先、テキスト種別、状況='1'を条件に読み込まれます。
 * 伝送定義マスタ上では、読取対象にて、以下の形式で定義する必要があります。
 *   (データコード) (送り先) (テキスト種別)   例):"3 D9 B119"
 * 処理実行後は、読み取ったヘッダーデータの状況を'2'に更新します。
 * 但し、読取パラメーターに"NOUPDATE"を指定した場合、処理後の更新は行われません。
 * また、エラーが発生した場合はヘッダーデータの状況を'9'に更新します。
 *
 * トランザクションは、読取対象の単位になります。
 * 同じ読取対象で、異なる実行方法、実行対象を定義した場合、同じデータに対して複数回処理が行われます。
 * しかし、この場合においても、トランザクションは読取対象の単位で生成されるため、複数回の処理の内、
 * 1回でもエラーが発生した場合は、同じ読取対象でそれまでに処理した分についてもrollbackされます。
 *
 * また、この伝送デーモン(読取方法)は、旧伝送DB(CB01)に対するクエリ回数を減らすため、旧伝送DB(CB01)と
 * 伝送定義マスタ(GE62)をJOINして一括でデータを取得しています。
 * このため、他の伝送デーモン(読取方法)とは読取部分の実装方法が異なっています。
 * 具体的には、{@link org.opengion.fukurou.transfer.TransferRead}インターフェースを利用せずに、
 * このデーモン自体に読取及びステータス更新の処理を実装しています。
 *
 * ※処理中に何らかのエラーが1度でも発生した場合、このデーモンは停止します。
 *
 * このクラスは、HybsTimerTask を継承した タイマータスククラスです。
 * startDaemon() がタイマータスクによって、呼び出されます。
 *
 * @og.rev 5.4.1.0 (2011/11/01) 伝送システム対応
 * @og.group デーモン
 *
 * @version  5.0
 * @author   Hiroki Nakamura
 * @since    JDK6.0,
 */
public class Daemon_Transfer_CB01 extends Daemon_Transfer {
	//* このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "5.4.1.0 (2011/11/01)" ;

	// 実行方法に対応する実装クラスの基準名
	private static final String EXEC_CLS_BASE = "org.opengion.fukurou.transfer.TransferExec_" ;

	// 伝送データ取得用SQL(クエリ回数を減らすため旧伝送DBと伝送定義マスタをJOINして検索)
	private static final String GE62CB01_SELECT =
		"SELECT B.KBREAD,B.READOBJ,B.READPRM,B.KBEXEC,B.EXECDBID,B.EXECOBJ,B.EXECPRM,B.ERROR_SENDTO,A.HTCNO" +
		" FROM CB01 A,GE62 B" +
		" WHERE A.HCDD = SUBSTR(B.READOBJ,1,INSTR(B.READOBJ,' ',1,1)-1)" + // "3 D9 B119"の"3"
		" AND A.HTO    = RPAD(SUBSTR(B.READOBJ,INSTR(B.READOBJ,' ',1,1)+1,INSTR(B.READOBJ,' ',1,2)-INSTR(B.READOBJ,' ',1,1)-1),8)" + // "3 D9 B119"の"D9"
		" AND A.HSYU   = RPAD(SUBSTR(B.READOBJ,INSTR(B.READOBJ,' ',1,2)+1),4)" +  // "3 D9 B119"の"B119"
		" AND A.HCDJ = '1'" +
		" AND B.FGJ = '1'";

	// コネクションにアプリケーション情報を追記するかどうか指定
	private static final boolean USE_DB_APPLICATION_INFO  = HybsSystem.sysBool( "USE_DB_APPLICATION_INFO" ) ;

	// HTTP接続時のプロキシホスト
	private static final String HTTP_PROXY_HOST = HybsSystem.sys( "HTTP_PROXY_HOST" );

	// HTTP接続時のプロキシポート
	private static final int HTTP_PROXY_PORT = HybsSystem.sysInt( "HTTP_PROXY_PORT" );

	// 呼び出し元ホストコード
	private static final String HFROM = HybsSystem.sys( "TRANSFER_HOST_CODE" );

	// ループカウンタを24回に設定
	private static final int LOOP_COUNTER = 24;

	private boolean running = true;
	private int loopCnt		;

	private String ge62Cb01Select	;
	private String dmnName			;

	private ApplicationInfo appInfo	;
	private boolean debug			;

	/**
	 * このタイマータスクによって初期化されるアクションです。
	 * パラメータを使用した初期化を行います。
	 *
	 */
	@Override
	public void initDaemon() {
		debug = StringUtil.nval( getValue( "DEBUG" ),debug );

		dmnName = getName();

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
		buf.append( GE62CB01_SELECT );

		// システムIDは必須指定
		final String systemId = getValue( "SYSTEM_ID" );
		if( StringUtil.isNull( systemId ) ) {
			final String errMsg = "システムID方法は必須指定です。" ;
			throw new HybsSystemException( errMsg );
		}
		else {
			buf.append( " AND B.SYSTEM_ID='" ).append( systemId ).append( '\'' );
		}

		// 読取方法は必須指定
		final String kbRead = getValue( "KBREAD" );
		if( StringUtil.isNull( kbRead ) ) {
			final String errMsg = "読取方法は必須指定です。" ;
			throw new HybsSystemException( errMsg );
		}
		else {
			buf.append( " AND B.KBREAD='" ).append( kbRead ).append( '\'' );
		}

		// デーモングループは必須指定
		final String dmnGroup = getValue( "DMN_GRP" );
		if( StringUtil.isNull( dmnGroup ) ) {
			final String errMsg = "デーモングループは必須指定です。" ;
			throw new HybsSystemException( errMsg );
		}
		else {
			buf.append( " AND B.DMN_GRP='" ).append( dmnGroup ).append( '\'' );
		}

		buf.append( " ORDER BY A.HTC" );

		ge62Cb01Select = buf.toString() ;

		if( debug ) {
			System.out.println( "DMN_NAME=[" + dmnName + "]" );
			System.out.println( "QUERY=[" + ge62Cb01Select + "]" );
		}

		if( USE_DB_APPLICATION_INFO ) {
			appInfo = new ApplicationInfo();
			// ユーザーID,IPアドレス,ホスト名
			appInfo.setClientInfo( systemId,HybsSystem.HOST_ADRS,HybsSystem.HOST_NAME );
			// 画面ID,操作,プログラムID
			appInfo.setModuleInfo( "TransferDaemon",dmnName,dmnName );
		}
		else {
			appInfo = null;
		}
	}

	/**
	 * タイマータスクのデーモン処理の開始ポイントです。
	 *
	 */
	@Override
	protected void startDaemon() {
		if( loopCnt % LOOP_COUNTER == 0 ) {
			loopCnt = 1;
			System.out.println();
			System.out.print( toString() + " " + new Date()  + " " );
		}
		else {
			System.out.print( "." );
			loopCnt++ ;
		}

		// 伝送DB読取
		final GE62CB01Data ge62Cb01Data = new GE62CB01Data();
		try {
			// 6.0.2.5 (2014/10/31) refactoring:無効な代入です。
			final String[][] vals = DBUtil.dbExecute( ge62Cb01Select,null,appInfo );
			if( vals != null && vals.length > 0 ) {
				for( int row=0; running && row<vals.length; row++ ) {
					ge62Cb01Data.addData( vals[row] );
				}
			}
		}
		catch( Throwable ex ) {
			final StringBuilder errMsg = new StringBuilder( BUFFER_MIDDLE )
				.append( "伝送読取エラー：DMN_NAME=[" ).append(  dmnName )
				.append(  "] , DMN_HOST=[" ).append( HybsSystem.HOST_NAME )
				.append(  "] , QUERY=["  ).append( ge62Cb01Select ).append( ']' );
			final String header = errMsg.toString();

			errMsg.append( CR ).append( StringUtil.stringStackTrace( ex ) ) ;
			System.out.println( errMsg );
			LogWriter.log( errMsg.toString() );
			final String errorSendto = HybsSystem.sys( "ERROR_MAIL_TO_USERS" );
			sendMail( header, errMsg.toString(), errorSendto );
		}

		// 処理実行
		// 読取対象の単位でトランザクションを生成します。
		for( final String tranKey : ge62Cb01Data.getTranSet() ) {
			Transaction tran = null;
			TransferConfig conf = null;
			String[] htcnoArr = null;
			boolean isUpdate = true;
			try {
				tran = new TransactionReal( appInfo );

				// 読取対象+実行方法+実行対象の単位で処理を行います。
				for( final String confKey : ge62Cb01Data.getExecKeySet( tranKey ) ) {
					conf = ge62Cb01Data.getConfig( confKey );
					htcnoArr = ge62Cb01Data.getHtcno( confKey );

					// デバッグ情報を出力します。
					if( debug ) {
						System.out.println();
						System.out.print( " START = " + new Date() );
						System.out.print( "[" + dmnName + "]:[" + StringUtil.array2csv( htcnoArr ) + "]:[" + conf.toString() + "]" );
					}

					// 伝送データを読み出します。
					final String[] val = read( htcnoArr, tran );
					// 実行方法のオブジェクトを生成します。
					final TransferExec exec = (TransferExec)StringUtil.newInstance( EXEC_CLS_BASE + conf.getKbExec() );
					// 処理を実行します。
					exec.execute( val, conf, tran );

					// デバッグ情報を出力します。
					if( debug ) {
						System.out.println();
						System.out.print( " END = " + new Date() );
						System.out.print( "[" + dmnName + "]:[" + StringUtil.array2csv( htcnoArr ) + "]:[" + conf.toString() + "]" );
					}

					// 対象となるマスタの内、読取パラメーターに１つでも"NOUPDATE"が指定されている場合は、CB01の状況を更新しない
					if( "NOUPDATE".equalsIgnoreCase( conf.getReadPrm() ) ) {
						isUpdate = false;
					}
				}

				// 対象となるマスタの内、読取パラメーターに１つでも"NOUPDATE"が指定されている場合は、CB01の状況を更新しない
				if( isUpdate ) {
					complete( htcnoArr, tran );
				}
			}
			catch( Throwable ex ) {
				// エラーが発生した場合はデーモンを停止します。
				cancel();

				if( tran != null ) {
					tran.rollback();
					tran.close();
					tran = null; // エラー発生時は、接続を終了します。(次の状況更新でデッドロックになるため)
				}

				if( htcnoArr != null && htcnoArr.length > 0 ) {
					error( htcnoArr ); // エラー発生時はCB01>状態を9:エラーに更新
				}

				final StringBuilder errMsg = new StringBuilder( BUFFER_MIDDLE )
					.append( "伝送エラー：DMN_NAME=[" ).append( dmnName ).append( "] , DMN_HOST=[" ).append( HybsSystem.HOST_NAME ).append( "]" );
				if( htcnoArr != null && htcnoArr.length > 0 ) {
					errMsg.append( " , HTCNO=[" ).append( StringUtil.array2csv( htcnoArr ) ).append( ']' );
				}
				String errorSendto = null;
				if( conf != null ) {
					errMsg.append( " , CONFIG=[" ).append( conf.toString() ).append( ']' );
					errorSendto = conf.getErrorSendto();
				}
				final String header = errMsg.toString();

				errMsg.append( CR ).append( StringUtil.stringStackTrace( ex ) ) ;
				System.out.println( errMsg );
				LogWriter.log( errMsg.toString() );
				sendMail( header, errMsg.toString(), errorSendto );
			}
			finally {
				if( tran != null ) { tran.close(); }
			}
		}
	}

	/**
	 * このタイマータスクのcancel() メソッドをオーバーライドします。
	 * HybsTimerTaskManager#cancelTask( int ) を実行します。
	 *
	 * @return	スケジュールされている 1 回以上実行されない場合に true
	 * @see java.util.TimerTask#cancel()
	 */
	@Override
	public boolean cancel() {
		running = false;
		return super.cancel();
	}

	/**
	 * CB01を読み込みデータを配列で返します。
	 *
	 * @param htcnoArr 読取対象の通番NO(配列)
	 * @param tran トランザクション
	 *
	 * @return データ(配列)
	 * @og.rtnNotNull
	 */
	private String[] read( final String[] htcnoArr, final Transaction tran ) {
		if( htcnoArr == null || htcnoArr.length == 0 ) { return new String[0]; }

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
			.append( "SELECT A.HTEXT" )
			.append( " FROM CB01 A" )
			.append( " WHERE A.HCDJ = '5'" )
			.append( " AND A.HTCNO IN (" )
			.append( StringUtil.array2csv( htcnoArr ) )
			.append( ") ORDER BY A.HTC, A.HTCNO" );

		final String[][] vals = DBUtil.dbExecute( buf.toString(),null,tran );
		String[] rtn = new String[vals.length];
		for( int i=0; i<vals.length; i++ ) {
			rtn[i] = vals[i][0];
		}
		return rtn;
	}

	/**
	 * CB01のヘッダーデータの状況を2:抜出済みに更新します。
	 *
	 * @param htcnoArr 更新対象の通番NO(配列)
	 * @param tran トランザクション
	 */
	private void complete( final String[] htcnoArr, final Transaction tran ) {
		if( htcnoArr == null || htcnoArr.length == 0 ) { return; }

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
			.append( "UPDATE CB01 A" )
			.append( " SET A.HCDJ = '2'" )
			.append( " WHERE A.HCDJ = '1'" )
			.append( " AND A.HTCNO IN (" )
			.append( StringUtil.array2csv( htcnoArr ) )
			.append( ')' );							// 6.0.2.5 (2014/10/31) char を append する。

		DBUtil.dbExecute( buf.toString(),null,tran );
	}

	/**
	 * CB01のヘッダーデータの状況を9:エラーに更新します。
	 *
	 * @param htcnoArr 更新対象の通番NO配列(可変長引数)
	 */
	private void error( final String... htcnoArr ) {
		// 6.1.1.0 (2015/01/17) 可変長引数でもnullは来る。
		if( htcnoArr != null && htcnoArr.length > 0 ) {
			final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
				.append( "UPDATE CB01 A" )
				.append( " SET A.HCDJ = '9'" )
				.append( " WHERE A.HCDJ in ('1','2')" )		// 既に実行PGで抜出済みに更新されている可能性がある
				.append( " AND A.HTCNO IN (" )
				.append( StringUtil.array2csv( htcnoArr ) )
				.append( ')' );								// 6.0.2.5 (2014/10/31) char を append する。

			DBUtil.dbExecute( buf.toString(),null,appInfo ); // エラー更新はトランザクションを分けて処理する
		}
	}

	/**
	 * 伝送定義マスタ及び旧伝送DBから読み出したデータを管理します。
	 */
	private static class GE62CB01Data {

		// トランザクションを生成するキーのセット(読取対象単位)
		private final Set<String> tranSet = new LinkedHashSet<String>();
		// トランザクションキー(読取対象)に対する、処理キー(読取対象+実行方法+実行対象)のセット
		private final Map<String,Set<String>> tran2ExecKeySet = new LinkedHashMap<String,Set<String>>();
		// 処理キー(読取対象+実行方法+実行対象)に対する設定オブジェクトのマッピング
		private final Map<String,TransferConfig> execKey2Conf = new HashMap<String,TransferConfig>();
		// 処理キー(読取対象+実行方法+実行対象)に対する通番NO(配列)のマッピング
		private final Map<String,List<String>> execKey2HtcnoArr = new LinkedHashMap<String,List<String>>();

		/**
		 * GE62及びCB01読取データを追加します。
		 *
		 * @param data GE62及びCB01読取データ配列(可変長引数)
		 */
		private void addData( final String... data ) {
			final String kbRead		= data[0];
			final String readObj	= data[1];
			final String readPrm	= data[2];
			final String kbExec		= data[3];
			final String execDbid	= data[4];
			final String execObj	= data[5];
			final String execPrm	= data[6];
			final String errorSendto= data[7];
			final String htcno		= data[8];

			final String tranKey = readObj;
			tranSet.add( tranKey );

			// 読取対象 + 実行方法 + 実行対象 単位に処理対象通番NOを集約する
			final String execKey = readObj + kbExec + execObj;
			Set<String> execKeySet = tran2ExecKeySet.get( tranKey );
			if( execKeySet == null ) {
				execKeySet = new LinkedHashSet<String>();
			}
			execKeySet.add( execKey );
			tran2ExecKeySet.put( tranKey, execKeySet );

			// 伝送設定オブジェクトのマップ
			TransferConfig conf = execKey2Conf.get( execKey );
			if( conf == null ) {
				conf = new TransferConfig(
								kbRead, readObj, readPrm
								, kbExec, execDbid, execObj, execPrm
								, errorSendto, HFROM, HTTP_PROXY_HOST, HTTP_PROXY_PORT );
				execKey2Conf.put( execKey, conf );
			}

			// 通番NOのマップ
			List<String> htcnoArr = execKey2HtcnoArr.get( execKey );
			if( htcnoArr == null ) {
				htcnoArr = new ArrayList<String>();
			}
			htcnoArr.add( htcno );
			execKey2HtcnoArr.put( execKey, htcnoArr );
		}

		/**
		 * トランザクション生成キー(読取対象)のセットを返します。
		 *
		 * @return トランザクション生成キー(読取対象)のセット
		 */
		private Set<String> getTranSet() {
			return tranSet;
		}

		/**
		 * トランザクション生成キー(読取対象)に対する処理キー(読取対象+実行方法+実行対象)のセットを返します。
		 *
		 * @param tranKey トランザクション生成キー(読取対象)
		 * @return トランザクション生成キー(読取対象)に対する処理キー(読取対象+実行方法+実行対象)のセット
		 */
		private Set<String> getExecKeySet( final String tranKey ) {
			return tran2ExecKeySet.get( tranKey );
		}

		/**
		 * 処理キー(読取対象+実行方法+実行対象)に対する設定オブジェクトを返します。
		 *
		 * @param execKey 処理キー(読取対象+実行方法+実行対象)
		 * @return 設定オブジェクト
		 */
		private TransferConfig getConfig( final String execKey ) {
			return execKey2Conf.get( execKey );
		}

		/**
		 * 処理キー(読取対象+実行方法+実行対象)に対する通番NO(配列)を返します。
		 *
		 * @param execKey 処理キー(読取対象+実行方法+実行対象)
		 * @return 通番NO(配列)
		 * @og.rtnNotNull
		 */
		private String[] getHtcno( final String execKey ) {
			final List<String> lst = execKey2HtcnoArr.get( execKey );
			if( lst == null ) { return new String[0]; }
			else { return lst.toArray( new String[lst.size()] ); }
		}
	}
}
