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

import java.util.Date;
import java.util.List;
import java.util.Map;
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.json.ShopSyncWatermarkJSON;
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 String shopSyncWatermarkLastChangedAt;
	private boolean performedFullSyncByWatermark;
	
	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(); // ログ送信

					boolean needsFullSync = shouldRunFullSyncByShopSyncWatermark();
					performedFullSyncByWatermark = needsFullSync;
					if (needsFullSync) {
						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();
					} else {
						Logger.d(TAG, "skip full DB sync by shopSyncWatermark.");
					}
					
					if (interrupt) { // この時点で停止要求が来た場合先には進まない。（ServiceOption/Group/Categoryの更新は１セットで行う（トランザクションはそれぞれ別））
						Logger.d(TAG, "stop refresh worker before content update.");
						setFail();
						updateRefreshContentListState(-1L, null);
						return;
					}
					if (needsFullSync) {
						isFinishedContentCheck = retrieveServerContent(localContents); // ContentVersionAPIを呼出し新規と更新の場合ContentInfoをDLする
					}
					saveShopSyncWatermarkLastFetchDateWithRefresh(performedFullSyncByWatermark);
				}
				
				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リストを取得（登録・更新の判定のため）
				List<Integer> localSppDeviceIdList = sppDeviceDao.getSppDeviceIdList();
				for (SppDeviceDto sppDeviceDto : sppDeviceDtoList) {
					if (CollectionUtil.isNotEmpty(localSppDeviceIdList) && localSppDeviceIdList.contains(sppDeviceDto.sppDeviceId)) {
						SppDeviceDto localSppDevice = sppDeviceDao.getSppDeviceById(sppDeviceDto.sppDeviceId);
						// ペアリング情報だけローカルから取得して、更新する
						sppDeviceDto.pairingDeviceName = localSppDevice.pairingDeviceName;
						sppDeviceDto.pairingDeviceAddress = localSppDevice.pairingDeviceAddress;
						sppDeviceDao.updateSppDevice(sppDeviceDto);

						// 削除のため、更新したら該当するIDを削除する
						localSppDeviceIdList.remove(localSppDeviceIdList.indexOf(sppDeviceDto.sppDeviceId));
					} else {
						// 取得したSPP通信端末のデータベースへ登録
						sppDeviceDao.insert(sppDeviceDto);
					}
				}

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

            List<ContentDto> serverContents = json.contentVersions;
			// DTO Info:contentId, metaVersion, resourceVersion, contentNameKana, readerShareFlg

			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) {
					int localContentIndex = localContents.indexOf(serverContentDto);
					if (localContentIndex >= 0) {
						ContentDto localDto = localContents.get(localContentIndex);
						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); 
						}

						localContents.remove(localDto); //	既に存在するコンテンツはローカルのリストからはずしておく。
					} else {
						//	新規 ContentInfo.zipをダウンロードする。
						contentDownloader.downloadContentInfo(serverContentDto.contentId);
						if (serverContentDto.isLinkType() && serverContentDto.isDownloadable(true)) {
							contentDownloader.addAutoDownload(serverContentDto.contentId); 
						}
					}
				}
			}
            return true;
        }

		private boolean shouldRunFullSyncByShopSyncWatermark() {
			try {
				AcmsDao dao = AbstractDao.getDao(AcmsDao.class);
				String lastFetchDate = dao.selectShopSyncWatermarkLastFetchDate();
				FetchDateParameters param = new FetchDateParameters(cache.getMemberInfo().sid, lastFetchDate);
				ShopSyncWatermarkJSON json = AcmsClient.getInstance(cache.getUrlPath(), networkAdapter).checkShopSyncWatermark(param);
				shopSyncWatermarkLastChangedAt = json.lastChangedAt;
				return json.needsFullSync;
			} catch (Exception e) {
				// フェイルセーフ: 判定できない場合は従来通りフル同期を実施
				Logger.w(TAG, "shopSyncWatermark check failed. run full sync.", e);
				shopSyncWatermarkLastChangedAt = null;
				return true;
			}
		}

		private void saveShopSyncWatermarkLastFetchDateWithRefresh(boolean shouldRefreshFromServer) {
			String lastChangedAt = shopSyncWatermarkLastChangedAt;
			try {
				if (shouldRefreshFromServer) {
					FetchDateParameters latestParam = new FetchDateParameters(cache.getMemberInfo().sid, null);
					ShopSyncWatermarkJSON latestJson = AcmsClient.getInstance(cache.getUrlPath(), networkAdapter).checkShopSyncWatermark(latestParam);
					if (latestJson != null && latestJson.lastChangedAt != null && latestJson.lastChangedAt.length() > 0) {
						lastChangedAt = latestJson.lastChangedAt;
					}
				}
				if (lastChangedAt == null || lastChangedAt.length() == 0) {
					return;
				}
				AcmsDao dao = AbstractDao.getDao(AcmsDao.class);
				dao.updateShopSyncWatermarkLastFetchDate(lastChangedAt);
			} catch (Exception e) {
				Logger.w(TAG, "failed to save shopSyncWatermark lastFetchDate.", e);
			}
		}
	}
	
	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();
			AcmsClient acms = AcmsClient.getInstance(cache.getUrlPath(), ABVEnvironment.getInstance().networkAdapter);
			
			if (list != null && list.size() > 0) {
				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); // 例外は上にあげない。失敗したら次送られるはず（要確認）
		}
	}
}
