package jp.agentec.abook.abv.bl.download;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import jp.agentec.abook.abv.bl.acms.client.AcmsClient;
import jp.agentec.abook.abv.bl.acms.client.json.ContentVersionsJSON;
import jp.agentec.abook.abv.bl.acms.client.parameters.ContentDownloadLogParameters;
import jp.agentec.abook.abv.bl.acms.client.parameters.FetchDateParameters;
import jp.agentec.abook.abv.bl.common.ABVEnvironment;
import jp.agentec.abook.abv.bl.common.exception.ABVException;
import jp.agentec.abook.abv.bl.common.exception.ABVExceptionCode;
import jp.agentec.abook.abv.bl.common.exception.NetworkDisconnectedException;
import jp.agentec.abook.abv.bl.common.log.Logger;
import jp.agentec.abook.abv.bl.common.nw.NetworkAdapter;
import jp.agentec.abook.abv.bl.data.ABVDataCache;
import jp.agentec.abook.abv.bl.data.dao.AbstractDao;
import jp.agentec.abook.abv.bl.data.dao.AcmsDao;
import jp.agentec.abook.abv.bl.data.dao.ContentDao;
import jp.agentec.abook.abv.bl.data.dao.SppDeviceDao;
import jp.agentec.abook.abv.bl.dto.ContentDto;
import jp.agentec.abook.abv.bl.dto.EnqueteDto;
import jp.agentec.abook.abv.bl.dto.SppDeviceDto;
import jp.agentec.abook.abv.bl.logic.AbstractLogic;
import jp.agentec.abook.abv.bl.logic.CategoryLogic;
import jp.agentec.abook.abv.bl.logic.ContentLogic;
import jp.agentec.abook.abv.bl.logic.ContentReadingLogLogic;
import jp.agentec.abook.abv.bl.logic.ContractLogic;
import jp.agentec.abook.abv.bl.logic.EnqueteLogic;
import jp.agentec.abook.abv.bl.logic.GroupLogic;
import jp.agentec.abook.abv.bl.logic.MasterDataLogic;
import jp.agentec.abook.abv.bl.logic.OperationGroupMasterLogic;
import jp.agentec.abook.abv.bl.logic.OperationLogic;
import jp.agentec.abook.abv.bl.logic.PushMessageLogic;
import jp.agentec.adf.util.CollectionUtil;
import jp.agentec.adf.util.DateTimeUtil;

/**
 * 新着更新を扱うクラス
 * 
 * FIXME: DBのトランザクションができていない
 *
 */
	public class ContentRefresher {
	private static final String TAG = "ContentRefresher";
	private static ContentRefresher instance; 
	
	private ContractLogic contractLogic = AbstractLogic.getLogic(ContractLogic.class);
	private GroupLogic groupLogic = AbstractLogic.getLogic(GroupLogic.class);
	private CategoryLogic categoryLogic = AbstractLogic.getLogic(CategoryLogic.class);
	private EnqueteLogic enqueteLogic = AbstractLogic.getLogic(EnqueteLogic.class);
	private ContentLogic contentLogic = AbstractLogic.getLogic(ContentLogic.class);
	private OperationLogic operationLogic = AbstractLogic.getLogic(OperationLogic.class);
	private PushMessageLogic pushMessageLogic = AbstractLogic.getLogic(PushMessageLogic.class);

	private ContentDao contentDao = AbstractDao.getDao(ContentDao.class);
	private SppDeviceDao sppDeviceDao = AbstractDao.getDao(SppDeviceDao.class);
	private OperationGroupMasterLogic operationGroupMasterLogic = AbstractLogic.getLogic(OperationGroupMasterLogic.class);

	private ABVDataCache cache = ABVDataCache.getInstance();
	private ContentDownloader contentDownloader = ContentDownloader.getInstance();
	private NetworkAdapter networkAdapter = ABVEnvironment.getInstance().networkAdapter;

	private ContentDownloadListener contentDownloadListener;
	private RefreshContentWorker refreshWorker = null;
	
	private Map<Long, Boolean> refreshingContentMap = new ConcurrentHashMap<Long, Boolean>(); // TODO: 値は使っていないのでSetに変更する
	private boolean initializingRefreshing = false;
	private static final int refreshSleepInterval = 500;
	private static final int maxRefreshWorker = 2;

	// masterDataを取得するため登録
	private MasterDataLogic masterDataLogic = AbstractLogic.getLogic(MasterDataLogic.class);
	
	public static ContentRefresher getInstance() {
		if (instance == null) {
			synchronized (ContentRefresher.class) {
				if (instance == null) {
					instance = new ContentRefresher();
				}
			}
		}
		
		return instance;
	}
	
	private ContentRefresher() {
	}

	/**
	 * コンテンツ情報をACMSから更新します。
	 * @throws ABVException　キャッシュにユーザ情報がありません。再度ログインする必要があります。
	 * @throws Exception その他、例外です。
	 * @since 1.0.0
	 */
	public void refreshContentList(ContentDownloadListener listener) throws Exception {
		if (contentDownloader.getActiveCount() > 0) { //	コンテンツのダウンロード中は、自動更新を行わない。
			contentDownloader.kickTask(); // waitingで止まらないように実行を促す
			throw new ABVException(ABVExceptionCode.C_I_CONTENT_0002);
		}

		networkAdapter = ABVEnvironment.getInstance().networkAdapter;
		if (networkAdapter != null && !networkAdapter.isNetworkConnected()) { // NWのチェック
			throw new NetworkDisconnectedException();
		}
		if (!AcmsClient.getInstance(cache.getUrlPath(), networkAdapter).checkSid(cache.getMemberInfo().sid)) { // SIDのチェックをここで行う
			throw new ABVException(ABVExceptionCode.S_E_ACMS_1403);
		}
		if (isRefreshing()) { // すでに実行中か
			throw new ABVException(ABVExceptionCode.C_I_CONTENT_0001);
		}
		
		if (listener != null) {
			contentDownloadListener = listener;
		}
		
		initializingRefreshing = true;
		contentDownloader.pauseAll();
		refreshWorker = new RefreshContentWorker();
		refreshWorker.setPriority(Thread.MIN_PRIORITY);
		refreshWorker.start();
	}

	public void addRefreshingContentId(long contentId) {
		refreshingContentMap.put(contentId, false);
	}
	
	public void removeRefreshingContentId(long contentId) {
		refreshingContentMap.remove(contentId);
	}
	
	public boolean isBusyRefreshingContent() {
		return refreshingContentMap.size() > maxRefreshWorker;
	}
	
	public boolean isRefreshing() {
		return (initializingRefreshing || (refreshingContentMap.size() > 0));
	}
	
	public void setFail() {
		initializingRefreshing = false;
		refreshingContentMap.clear();
	}
	
	private class RefreshContentWorker extends Thread {
		private boolean interrupt = false;
		
		public void stopWorker() {
			interrupt = true;
		}
		
		@Override
		public void run() {
			Logger.i(TAG, "refresh main thread start.-----------------------------------------------");
			
			interrupt = false;
			try {
                boolean isFinishedContentCheck = false;
				List<ContentDto> localContents = contentDao.getAllContents();
				if (networkAdapter.isNetworkConnected()) { // オンライン時の処理
					resendLog(); // ログ送信
					contractLogic.initializeContractServiceOption(); // サービスオプション関連処理
					groupLogic.initializeGroups(); // グループ設定(グループ変更の場合、FetchDateをクリアする)
					categoryLogic.initializeCategories(); // カテゴリ設定

					// マスタデータの最新更新された時のFetchDateを一時に保存する。
					Logger.d(TAG, "before fetchDate : " + ABVDataCache.getInstance().tempMasterDataFetchDate);

					// CMSでメンテナンスされるHACCPマスタデータをアプリから取得できるようにJSONファイルを生成する。
					String fetchDate = null;
					//失敗時、3回リトライ処理
					for (int i = 0; i < 4; i++) {
						try {
							fetchDate = masterDataLogic.initializeMasterData(ABVDataCache.getInstance().tempMasterDataFetchDate, i);
							break;
						} catch (Exception e) {
							Logger.e(TAG, "initializeMasterData Exception", e);
							if (i == 3) { //3回目のリトライ失敗時、例外処理投げる。
								throw new Exception("initializeMasterData");
							} else {
								Thread.sleep(500); //0.5秒間隔でリトライ処理を実行
							}
						}
					}

					Logger.d(TAG, "after fetchDate : " + fetchDate);
					if (fetchDate != null) {
						// マスタデータの最新更新された時のFetchDateを一時に保存する。
						ABVDataCache.getInstance().tempMasterDataFetchDate = fetchDate;
					}

					// 作業種別情報を取得
					operationGroupMasterLogic.setOperationGroupMaster();
					
					if (interrupt) { // この時点で停止要求が来た場合先には進まない。（ServiceOption/Group/Categoryの更新は１セットで行う（トランザクションはそれぞれ別））
						Logger.d(TAG, "stop refresh worker before content update.");
						setFail();
						updateRefreshContentListState(-1L, null);
						return;
					}
                    isFinishedContentCheck = retrieveServerContent(localContents); // ContentVersionAPIを呼出し新規と更新の場合ContentInfoをDLする
				}
				
				deleteLocalContent(localContents, isFinishedContentCheck); // コンテンツ削除処理
				initializingRefreshing = false;
				
				if (!isRefreshing()) {
					updateRefreshContentListState(-1L, null);
				}
				
			} catch (Exception e) {
				Logger.e("refreshContent failed.", e);
				updateRefreshContentListState(-1L, e);
			}
			contentDownloader.kickTask();
			Logger.i(TAG, "refresh main thread end.-----------------------------------------------");
		}

        /**
         * ローカルコンテンツの公開期限や権限喪失のチェックを行う
         * @param localContents List<ContentDto> 新着や更新対象のチェックが終わったコンテンツリスト
         * @param isFinishedContentCheck boolean localContentsとServerコンテンツの比較が終わったか否か
         *                               falseは変更なし(304)の場合なので、公開期限切れのチェックのみを行う
         * @return boolean
         */
		private boolean deleteLocalContent(List<ContentDto> localContents, boolean isFinishedContentCheck) {
			if (CollectionUtil.isEmpty(localContents)) {
				return false;
			}

			boolean isOnline = networkAdapter.isNetworkConnected();
			//	公開期間が過ぎたコンテンツを削除するため、サーバ時間を格納する変数。オフラインの場合、ローカルタイムを基準にしてコンテンツを削除するために、ローカルタイムで初期化する。
            Date serverTime = ABVEnvironment.getInstance().getCurrentTime(isOnline);

			boolean hasDeleted = false;
			for (ContentDto localDto : localContents) {
				if (interrupt) {
					Logger.d(TAG, "stop refresh worker before doing with local content [%s]", localDto.contentName);
					break;
				}

				// 権限喪失,非公開,削除時のDL済コンテンツ強制削除 Option
				if (isOnline && cache.serviceOption.isNoAuthDelete() && isFinishedContentCheck) { // 権限喪失削除：　onで、公開期限削除がoffのときこれだと問題（公開一覧には含まれなくなる！？）
					hasDeleted = true;
                    Logger.d(TAG, "[deleteLocalContent]:Delete For NoAuth Content");
                    contentLogic.deleteContent(localDto, true);
				} else {
                    // 公開終了時のDL済コンテンツ強制削除 Option
					if (cache.serviceOption.isUndelivarableDelete()) {
						//	公開終了コンテンツ削除がtrueの場合、公開期間をチェックして、期間が過ぎたら削除
						if (localDto.deliveryEndDate != null && serverTime.getTime() >= localDto.deliveryEndDate.getTime()) {
							hasDeleted = true;
                            Logger.d(TAG, "[deleteLocalContent]:Id=" + localDto.contentId + ":deliveryEndDate:" + localDto.deliveryEndDate);
                            contentLogic.deleteContent(localDto, true);
						}

						//	cache.serviceOption.getUndelivarableDelete()がtrue、localDto.getDeliveryEndDateがnullの場合
						//	コンテンツはダウンロード済みだとする。
						//	実際にはサーバでこのコンテンツの終了期間を設定したとしても、サーバからのコンテンツ一覧にこのコンテンツがでないため、削除しようがない。
						//  ⇒仕様
					}

					//	権限喪失時削除オプションがfalse
					//	公開期間が過ぎてないのに、サーバからのコンテンツリストに存在しないコンテンツ、つまり権限喪失コンテンツである
					//	権限喪失時削除オプションがfalseなので、ダウンロードしていないコンテンツのみ削除
					if (isOnline && !localDto.downloadedFlg && isFinishedContentCheck) {
						hasDeleted = true;
                        Logger.d(TAG, "[deleteContent]:権限喪失:contentId=" + localDto.contentId);
                        contentLogic.deleteContent(localDto, true);
					}
				}
				
				if (hasDeleted) {
					ContentDownloader.getInstance().cancel(localDto.contentId);
				}
				
			}
			return hasDeleted;
		}

        /**
         * API「contentVersion」から取得したサーバ公開のコンテンツリストをローカルコンテンツを比較して、
         * 新規や更新の場合はダウンロードを行う
         * また、変更日付（FetchDate）による更新有無を判断して３０４の場合は処理を行わないようにする
         * ただし、３０４の場合でもサービスオプション設定に従って公開期限切れのチェックを行う
         * @param localContents List<ContentDto> ローカルに保存されたコンテンツのリスト
         * @return 変更あり:true、変更なし：false
         * @throws Exception
         */
		private boolean retrieveServerContent(List<ContentDto> localContents) throws Exception {
            // コンテンツバージョン用lastFetchDateの取得
            AcmsDao dao = AbstractDao.getDao(AcmsDao.class);
            String lastFetchDate = dao.selectContentVersionLastFetchDate();

            FetchDateParameters param = new FetchDateParameters(cache.getMemberInfo().sid, lastFetchDate);
			ContentVersionsJSON json = AcmsClient.getInstance(cache.getUrlPath(), networkAdapter).contentVersion(param);
            if (json == null) {
                // 変更なし(304)の場合、FetchDateを更新する必要がないため、tempはクリアする
                ABVEnvironment.getInstance().tempContentVersionLastFetchDate = "";
                return false;
            }

			// リソースパターンの値を取得する
			ABVEnvironment.getInstance().resourcePatternType = json.resourcePatternType;

            // 一時値を保存。ダウンロード処理が完了したあとにlastFetchDateを更新
            String fetchDate = json.fetchDate;
            ABVEnvironment.getInstance().tempContentVersionLastFetchDate = fetchDate;

            // sppDeviceDtoを登録
            List<SppDeviceDto> sppDeviceDtoList = json.sppDeviceDtoList;
            if (CollectionUtil.isNotEmpty(sppDeviceDtoList)) {
            	// ローカルに保存されてるID（contains/remove を O(1) に）
				List<Integer> localSppDeviceIdList = sppDeviceDao.getSppDeviceIdList();
				Set<Integer> localSppDeviceIds = new HashSet<Integer>();
				if (CollectionUtil.isNotEmpty(localSppDeviceIdList)) {
					localSppDeviceIds.addAll(localSppDeviceIdList);
				}
				for (SppDeviceDto sppDeviceDto : sppDeviceDtoList) {
					if (localSppDeviceIds.contains(sppDeviceDto.sppDeviceId)) {
						SppDeviceDto localSppDevice = sppDeviceDao.getSppDeviceById(sppDeviceDto.sppDeviceId);
						// ペアリング情報だけローカルから取得して、更新する
						sppDeviceDto.pairingDeviceName = localSppDevice.pairingDeviceName;
						sppDeviceDto.pairingDeviceAddress = localSppDevice.pairingDeviceAddress;
						sppDeviceDao.updateSppDevice(sppDeviceDto);

						localSppDeviceIds.remove(sppDeviceDto.sppDeviceId);
					} else {
						// 取得したSPP通信端末のデータベースへ登録
						sppDeviceDao.insert(sppDeviceDto);
					}
				}

				// ローカル情報削除（サーバーと端末データの差分）
				for (Integer deleteSppDeviceId : localSppDeviceIds) {
					sppDeviceDao.deleteById(deleteSppDeviceId);
				}
			}

            List<ContentDto> serverContents = json.contentVersions;
			// DTO Info:contentId, metaVersion, resourceVersion, contentNameKana, readerShareFlg
			// AbstractDto#equals は getKeyValues()（ContentDto は contentId のみ）のため、従来の indexOf(serverDto) は contentId 一致と同値。
			// indexOf は先頭一致を採用するため、同一 contentId が複数ある場合も先頭のみマップへ入れる。残りリストの順序は元リストの部分列として再現する。
			ArrayList<ContentDto> localContentsInitialOrder = null;
			HashMap<Long, ContentDto> localByContentId = new HashMap<Long, ContentDto>();
			if (localContents != null) {
				localContentsInitialOrder = new ArrayList<ContentDto>(localContents);
				for (ContentDto c : localContentsInitialOrder) {
					if (!localByContentId.containsKey(c.contentId)) {
						localByContentId.put(c.contentId, c);
					}
				}
			}

			for (ContentDto serverContentDto : serverContents) {
				while (isBusyRefreshingContent()) {
					if (interrupt) {
						Logger.d(TAG, "stop refresh worker while waiting to do with content [%s]", serverContentDto.contentName);
						break;
					}
					Logger.d(TAG, "refreshWorker is busy.");
					Thread.sleep(refreshSleepInterval);								
				}

				if (interrupt) {
					Logger.d(TAG, "stop refresh worker before doing with content [%s]", serverContentDto.contentName);
					break;
				}

				if (localContents != null) {
					ContentDto localDto = localByContentId.remove(serverContentDto.contentId);
					if (localDto != null) {
						if (!serverContentDto.equalsVersion(localDto)) { //	更新 ContentInfo.zipをダウンロードする。
							contentDownloader.downloadContentInfo(serverContentDto.contentId);
							if (serverContentDto.isLinkType() && localDto.isDownloadable(true)) {
								contentDownloader.addAutoDownload(serverContentDto.contentId); 
							}
						}
						else if (!localDto.downloadedFlg && serverContentDto.isLinkType() && localDto.isDownloadable(true)) { // ダウンロード未完了も追加
							contentDownloader.addAutoDownload(serverContentDto.contentId); 
						}
					} else {
						//	新規 ContentInfo.zipをダウンロードする。
						contentDownloader.downloadContentInfo(serverContentDto.contentId);
						if (serverContentDto.isLinkType() && serverContentDto.isDownloadable(true)) {
							contentDownloader.addAutoDownload(serverContentDto.contentId); 
						}
					}
				}
			}
			if (localContents != null && localContentsInitialOrder != null) {
				localContents.clear();
				for (ContentDto c : localContentsInitialOrder) {
					Long id = c.contentId;
					if (localByContentId.containsKey(id)) {
						ContentDto still = localByContentId.get(id);
						if (still == c) {
							localContents.add(c);
							localByContentId.remove(id);
						}
					}
				}
			}
            return true;
        }
	}
	
	public void updateRefreshContentListState(long contentId, Exception e) {
		Logger.d(TAG, "[updateRefreshContentListState]: contentId=%s", contentId);
		//noinspection VariableNotUsedInsideIf
		if (e != null) {
			setFail();
		}

		if (contentDownloadListener != null) {
			if (!isRefreshing() && e == null) {
				// 新着処理が終わったら以下の処理が実行
				try {
					// サーバー通信でプロジェクト取得
					operationLogic.initializeOperations();
					//プッシュメッセージ情報を取得
					pushMessageLogic.initializePushMessage();
				} catch (Exception e1) {
					Logger.e(TAG, e1);
					e = e1;
				}
			}
			contentDownloadListener.onRefreshedContent(e == null, contentId, e);
		}
	}
	
	public void stopRefresh() {
		if (isRefreshing() && refreshWorker != null) {
			refreshWorker.stopWorker();
		}
	}
	
	/**
	 * ログ等を送信する
	 */
	private void resendLog() {
		// 失敗したアンケート結果を再送する
		resendEnquete();
		// 配信ログを再送する
		batchSendDownloadLog();
		// 閲覧ログを再送する
		AbstractLogic.getLogic(ContentReadingLogLogic.class).batchSendReadingLog();
	}

	/**
	 * アンケート回答を送信する
	 */
	private void resendEnquete() {
		List<EnqueteDto> dtoList = enqueteLogic.getReplyEnquete();

		if (dtoList != null && dtoList.size() > 0) {
			for (EnqueteDto dto : dtoList) {
				long contentId = dto.contentId;
				long abObjectId = dto.abObjectId;
				Date ts = dto.replyDateStr;
				String param = dto.param;
				boolean result = false;
				try {
					result = enqueteLogic.enqueteReply(contentId, abObjectId, ts, param);
				} catch (Exception e) {
					Logger.e(TAG, "enqueteReply failed.", e); // ignore
				}

				if (result) {
					// 再送成功したアンケートを回答済みとしてアップデートする
					dto.replyFlg = true;
					boolean upResult = enqueteLogic.updateEnquete(dto);
					Logger.d(TAG, "アップデート結果 : %s", upResult);
				}
			}
		}
	}

	/**
	 * ダウンロードログを送信する
	 */
	public void batchSendDownloadLog() {
		if (ABVEnvironment.getInstance().disableLogSend) {
			return;
		}
		try {
			List<ContentDto> list = contentDao.getSendLog();
			if (list == null || list.size() == 0) {
				return;
			}
			AcmsClient acms = AcmsClient.getInstance(cache.getUrlPath(), ABVEnvironment.getInstance().networkAdapter);
			for (ContentDto dto : list) { // FIXME: Readerがここに来るかどうか
				ContentDownloadLogParameters downloadParam = new ContentDownloadLogParameters(cache.getMemberInfo().sid
						, dto.contentId, DateTimeUtil.dateToTimestamp(dto.downloadStartDate)
						, dto.resourceVersion, ABVEnvironment.DeviceTypeId, dto.getDownloadStatus());
				downloadParam.setDownloadSize(dto.downloadedBytes);
				downloadParam.setDownloadEndtime(DateTimeUtil.dateToTimestamp(dto.downloadEndDate));
				
				if (acms.contentDownloadLog(downloadParam)) {
					dto.logSendedFlg = true;
					contentDao.updateLogSendFlg(dto);
				}
			}
		} catch (Exception e) {
			Logger.e("batchSendDownloadLog failed.", e); // 例外は上にあげない。失敗したら次送られるはず（要確認）
		}
	}
}
