package online.context;

import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;

import javax.servlet.http.HttpServletRequest;

import core.config.Factory;
import core.util.DateUtil;
import online.context.session.SessionAttributeUtil;
import online.context.token.Token;
import online.filter.FilterUtil;
import online.model.ModelUtil;
import online.model.UniModel;
import online.model.UniModelImpl;

/**
 * アクションパラメタ
 *
 * @author Tadashi Nakayama
 */
public class ActionParameter implements Serializable {

	/** serialVersionUID */
	private static final long serialVersionUID = 1L;

	/** 初期時フラグ */
	private final boolean first;
	/** リモートアドレス */
	private final String remote;
	/** 画面ID */
	private final String gid;
	/** アクションID */
	private final String aid;
	/** クエリ */
	private final String query;
	/** 汎用データモデル */
	private final UniModel um;
	/** パラメタマップ */
	private final Map<String, String[]> param;
	/** 共通項目 */
	private final Map<String, String> comm;

	/** 保存アクションセション */
	private final SessionStore store;

	/** トークン */
	private String token;
	/** ロールバックフラグ */
	private boolean rollbacked = false;
	/** アクションセション設定済フラグ */
	private boolean actionSession = false;
	/** 保存済消滅 */
	private boolean expiration = false;

	/**
	 * コンストラクタ
	 *
	 * @param request サーブレットリクエスト
	 * @param gamen 画面ID
	 * @param common 共通項目
	 * @param map パラメタマップ
	 * @param model 汎用モデル
	 */
	public ActionParameter(final HttpServletRequest request, final String gamen,
			final Map<String, String> common, final Map<String, String[]> map,
			final UniModel model) {
		this.first = model.getPresent() == null;
		this.gid = gamen;
		this.param = map;
		this.remote = request.getRemoteAddr();
		this.store = populate(request);
		this.token = this.store.getToken();
		this.query = SessionAttributeUtil.stripSid(FilterUtil.getQueryString(request));
		this.comm = common;
		this.um = model;
		if (this.first) {
			this.um.setPresent(DateUtil.getDateTime());
			this.aid = getAidFromParam(map, request.getMethod());
		} else {
			this.aid = "get";
		}
	}

	/**
	 * 汎用モデル化
	 *
	 * @param ap ActionParameter
	 * @return 汎用モデル
	 */
	public static UniModel getUniModelToSet(final ActionParameter ap) {
		return Optional.ofNullable(ap).
				map(p -> new UniModelImpl(p.um, true)).orElse(new UniModelImpl(true));
	}

	/**
	 * 保存アクションセション取得
	 *
	 * @param request サーブレットリクエスト
	 * @return 保存アクションセション
	 */
	private SessionStore populate(final HttpServletRequest request) {
		final SessionStore ret = SessionAttributeUtil.getSessionAttribute(request);
		return Optional.ofNullable(ret).
				orElse(new SessionStore(null, null, false, null, null, null));
	}

	/**
	 * 日時取得
	 *
	 * @return 日時
	 */
	public Timestamp getDateTime() {
		return this.um.getPresent();
	}

	/**
	 * IPアドレス取得
	 *
	 * @return IPアドレス
	 */
	public String getIp() {
		return this.remote;
	}

	/**
	 * 画面ID取得
	 *
	 * @return 画面ID
	 */
	public String getGid() {
		return this.gid;
	}

	/**
	 * アクションID取得
	 *
	 * @return アクションID
	 */
	public String getAid() {
		return this.aid;
	}

	/**
	 * 表示処理判断
	 *
	 * @return 表示処理時 true を返す。
	 */
	public boolean isGet() {
		return "get".equalsIgnoreCase(this.aid);
	}

	/**
	 * 初期処理確認
	 *
	 * @return 初期処理時 true を返す。
	 */
	public boolean isFirst() {
		return this.first;
	}

	/**
	 * ロールバック判断
	 *
	 * @return ロールバックした場合 true を返す。
	 */
	public boolean isRollbacked() {
		return this.rollbacked;
	}

	/**
	 * ロールバック設定
	 *
	 */
	public void setRollback() {
		this.rollbacked = true;
	}

	/**
	 * 共通項目設定
	 *
	 * @return 共通項目
	 */
	public Map<String, String> getProperties() {
		return Collections.unmodifiableMap(this.comm);
	}

	/**
	 * パラメータ取得
	 *
	 * @return パラメータ
	 */
	public RequestParameter getParameter() {
		return new RequestParameter(this.param);
	}

	/**
	 * Token設定
	 *
	 * @return トークン文字列
	 */
	public String getToken() {
		if (this.token == null) {
			final var tk = Factory.create(Token.class);
			this.token = tk.toHashString(String.valueOf(ThreadLocalRandom.current().nextDouble())
					+ getDateTime().getTime() + this.remote);
		}
		return this.token;
	}

	/**
	 * トークン削除
	 */
	public void removeToken() {
		this.token = null;
	}

	/**
	 * セション存在確認
	 *
	 * @param set 存在確認項目名集合
	 * @return 存在する場合 true を返す。
	 */
	public boolean hasItem(final Set<String> set) {
		// 項目存在チェック
		final var model = this.store.getUniModel();
		return model != null && model.getReservedKeySet().containsAll(set);
	}

	/**
	 * クエリ文字列存在確認
	 *
	 * @return 存在した場合 true を返す。
	 */
	public boolean hasQueryString() {
		return !Objects.toString(getQueryString(), "").isEmpty();
	}

	/**
	 * クエリ文字列取得
	 *
	 * @return 初期時、セション期限切れ時の場合、現在のクエリ文字列を返す。
	 * それ以外の場合、セション開始時のクエリ文字列を返す。
	 */
	private String getQueryString() {
		if (this.expiration || this.store.getQueryString() == null) {
			return this.query;
		}
		return this.store.getQueryString();
	}

	/**
	 * 保存値設定
	 */
	public void copyReserved() {
		if (this.store.getUniModel() != null) {
			for (final var key : this.store.getUniModel().getReservedKeySet()) {
				if (ModelUtil.canSet(key) && !this.um.containsKey(key)) {
					this.store.getUniModel().copyValueTo(this.um, key);
				}
			}
		}
		this.actionSession = true;
	}

	/**
	 * 保存済消滅設定
	 */
	public void expireReserved() {
		this.expiration = true;
	}

	/**
	 * 退避処理
	 *
	 * @param request サーブレットリクエスト
	 * @return 退避した場合 true を返す。
	 */
	public boolean evacuate(final HttpServletRequest request) {
		// 初回時
		if (SessionAttributeUtil.hasSessionAttribute(request) || !this.rollbacked) {
			final var ss = new SessionStore(this.um, this.param,
					this.rollbacked, getQueryString(), this.gid, this.token);
			if (SessionAttributeUtil.setSessionAttribute(request, ss)) {
				if (!this.expiration) {
					rereserve();
				}
				return true;
			}
		}
		return false;
	}

	/**
	 * 前回継続
	 *
	 * @param val メッセージ削除フラグ
	 */
	public void last(final boolean val) {
		setPreviousParameterMap();
		if (this.store.getUniModel() != null) {
			this.um.clear();
			final var bool = permitted(val);
			for (final var key : this.store.getUniModel().keySet()) {
				if (bool || !ModelUtil.isMsgKey(key)) {
					this.store.getUniModel().putValueTo(this.um, key);
				}
			}
		}
		rereserve();

		this.rollbacked = this.store.isRollback();
	}

	/**
	 * 許可確認
	 *
	 * @param val メッセージ削除フラグ
	 * @return 許可されてる場合 true を返す。
	 */
	private boolean permitted(final boolean val) {
		final var owner = this.store.getGid();
		return !val || Objects.toString(owner, "").isEmpty() || owner.equals(this.gid);
	}

	/**
	 * リトライ
	 */
	public void retry() {
		this.um.clear();

		copyReserved();
	}

	/**
	 * ロールバック
	 *
	 * @param item ロールバックモデル項目名
	 */
	public void rollback(final String item) {
		if (!this.rollbacked) {
			if (isGet()) {
				setPreviousParameterMap();
			}
			setPreviousModel(item);

			setRollback();
		}
	}

	/**
	 * 保存アクションパラメタを現在のパラメタに設定
	 */
	private void setPreviousParameterMap() {
		if (this.store.getParameterMap() != null) {
			this.param.clear();
			this.param.putAll(this.store.getParameterMap());
		}
	}

	/**
	 * 画面表示用に元へ戻す。
	 *
	 * @param item ロールバックモデル項目名
	 */
	private void setPreviousModel(final String item) {
		// 現モデル保存項目削除
		final var set = this.um.getReservedKeySet();
		if (set != null) {
			set.clear();
		}

		if (this.store.getUniModel() == null || !copyModelValue(
				this.store.getUniModel().getModel(item), this.um.getModel(item))) {
			copyModelValue(this.store.getUniModel(), this.um);
		}
	}

	/**
	 * モデル項目複写
	 *
	 * @param from 元モデル
	 * @param to 先モデル
	 * @return ロールバックした場合 true を返す。
	 */
	private boolean copyModelValue(final UniModel from, final UniModel to) {
		// モデルクリア
		Optional.ofNullable(to).ifPresent(
			t -> t.keySet().stream().filter(ModelUtil::canSet).forEach(to::remove)
		);

		Optional.ofNullable(from).ifPresent(
			f -> f.keySet().stream().filter(ModelUtil::canSet).forEach(k -> {
				setOptionParameter(k);
				from.copyValueTo(to, k);
			})
		);

		return from != null && to != null;
	}

	/**
	 * チェックボックス・ラジオボタン設定
	 *
	 * @param key キー
	 */
	private void setOptionParameter(final String key) {
		if (key.startsWith("chk") || key.startsWith("rdo")) {
			if (!this.param.containsKey(key)) {
				this.param.put(key, new String[0]);
			}
		}
	}

	/**
	 * アクションID設定
	 *
	 * @param map パラメタマップ
	 * @param method メソッド名
	 * @return アクションID
	 */
	private String getAidFromParam(final Map<String, String[]> map, final String method) {
		if (!FilterUtil.isGetMethod(method)) {
			// リクエストパラメタ項目名取得
			final var val = map.get("AID");
			if (val != null && 0 < val.length && !Objects.toString(val[0], "").isEmpty()) {
				return val[0].trim();
			}
		}
		return method.toLowerCase(Locale.ENGLISH);
	}

	/**
	 * 保存値再保存処理
	 */
	private void rereserve() {
		if (this.store.getUniModel() != null) {
			for (final var key : this.store.getUniModel().getReservedKeySet()) {
				if (ModelUtil.canSet(key)) {
					if (this.um.containsKey(key)) {
						this.um.reserve(key);
					} else if (!this.actionSession) {
						this.store.getUniModel().copyValueTo(this.um, key);
						this.um.reserve(key);
					}
				}
			}
		}
	}

	/**
	 * セション保存
	 *
	 * @author Tadashi Nakayama
	 */
	private static final class SessionStore implements Serializable {
		/** serialVersionUID */
		private static final long serialVersionUID = 1L;

		/** 汎用モデル */
		private final UniModel model;
		/** パラメタマップ */
		private final Map<String, String[]> map;
		/** ロールバックフラグ */
		private final boolean rb;
		/** クエリ */
		private final String qry;
		/** 画面ID */
		private final String gid;
		/** トークン */
		private final String token;

		/**
		 * コンストラクタ
		 *
		 * @param um 汎用モデル
		 * @param param パラメタマップ
		 * @param rollback ロールバックフラグ
		 * @param qs クエリ文字列
		 * @param gamen 画面ID
		 * @param tk トークン
		 */
		SessionStore(final UniModel um,
				final Map<String, String[]> param, final boolean rollback,
				final String qs, final String gamen, final String tk) {
			this.model = um;
			this.rb = rollback;
			this.qry = qs;
			this.gid = gamen;
			this.token = tk;
			this.map = Optional.ofNullable(param).map(HashMap::new).orElse(null);
		}

		/**
		 * 汎用モデル取得
		 *
		 * @return 汎用モデル
		 */
		public UniModel getUniModel() {
			return this.model;
		}

		/**
		 * パラメタマップ取得
		 *
		 * @return パラメタマップ
		 */
		public Map<String, String[]> getParameterMap() {
			return this.map;
		}

		/**
		 * ロールバックフラグ取得
		 *
		 * @return ロールバックフラグ
		 */
		public boolean isRollback() {
			return this.rb;
		}

		/**
		 * クエリ取得
		 *
		 * @return クエリ
		 */
		public String getQueryString() {
			return this.qry;
		}

		/**
		 * 画面ID取得
		 *
		 * @return 画面ID
		 */
		public String getGid() {
			return this.gid;
		}

		/**
		 * トークン取得
		 *
		 * @return トークン
		 */
		public String getToken() {
			return this.token;
		}
	}
}
