package project.web.processor;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.commons.chain2.Processing;
import org.apache.struts.action.ActionForward;
import org.apache.struts.chain.contexts.ServletActionContext;

import common.db.ExclusiveException;
import common.transaction.XATransaction;
import core.config.Factory;
import core.exception.ThrowableUtil;
import core.util.BooleanUtil;
import online.filter.FilterUtil;
import online.listener.SessionMutexListener;
import online.struts.action.UniForm;
import online.struts.chain.command.RequestCommand;
import online.struts.mapping.RequestMapping;
import project.master.MsgUtil;
import project.svc.generic.QueryService;
import project.svc.generic.UpdateService;
import project.web.InstanceFactory;

/**
 * 検索パタンプロセッサ
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class PatternSearchProcessor implements RequestCommand {
	/** クラス名 */
	private static final String CLAZZ = PatternSearchProcessor.class.getName();

	/** 空パターン情報 */
	private static final PatternInfo EMPTY = new PatternInfo(
					null,
					Collections.emptyMap(),
					Collections.emptyList(),
					Collections.emptyList());

	/** パタン登録アクションID */
	private String aid = "RegisterPattern";
	/** パタンID項目名 */
	private String patternId = "PatternId";
	/** パタン名項目名 */
	private String patternName = "PatternName";
	/** デフォルトパタン項目名 */
	private String defaultPattern = "DefaultPattern";
	/** パタンクエリ */
	private String patternQuery = "PatternQuery";
	/** 登録最大数（画面毎） */
	private int max = 10;

	/**
	 * @return the aid
	 */
	public String getAid() {
		return this.aid;
	}

	/**
	 * @param val the aid to set
	 */
	public void setAid(final String val) {
		this.aid = val;
	}

	/**
	 * @return the patternId
	 */
	public String getPatternId() {
		return this.patternId;
	}

	/**
	 * @param val the patternId to set
	 */
	public void setPatternId(final String val) {
		this.patternId = val;
	}

	/**
	 * @return the patternName
	 */
	public String getPatternName() {
		return this.patternName;
	}

	/**
	 * @param val the patternName to set
	 */
	public void setPatternName(final String val) {
		this.patternName = val;
	}

	/**
	 * @return the defaultPattern
	 */
	public String getDefaultPattern() {
		return this.defaultPattern;
	}

	/**
	 * @param val the defaultPattern to set
	 */
	public void setDefaultPattern(final String val) {
		this.defaultPattern = val;
	}

	/**
	 * @return the patternQuery
	 */
	public String getPatternQuery() {
		return this.patternQuery;
	}

	/**
	 * @param val the patternQuery to set
	 */
	public void setPatternQuery(final String val) {
		this.patternQuery = val;
	}

	/**
	 * @return the max
	 */
	public int getMax() {
		return this.max;
	}

	/**
	 * @param val the max to set
	 */
	public void setMax(final int val) {
		this.max = val;
	}

	/**
	 * @see online.struts.chain.command.RequestCommand
	 * #command(org.apache.struts.chain.contexts.ServletActionContext,
	 * online.struts.mapping.RequestMapping, online.struts.action.UniForm)
	 */
	@Override
	public Processing command(final ServletActionContext sac,
			final RequestMapping mapping, final UniForm uf) {
		if (RequestCommand.isException(sac) || sac.getResponse().isCommitted()
				|| sac.getForwardConfig() != null) {
			return Processing.CONTINUE;
		}

		final var pi = getPatternInfo(mapping.getGid(), sac, uf);
		sac.getRequest().setAttribute(PatternInfo.class.getName(), pi);

		if (RequestCommand.isFirst(sac, this.getClass())) {
			if (FilterUtil.isGetMethod(sac.getRequest().getMethod())) {
				if (!doGet(sac, pi)) {
					sac.getRequest().removeAttribute(PatternInfo.class.getName());
				}
			} else if (FilterUtil.isPostMethod(sac.getRequest().getMethod())) {
				if (this.aid.equalsIgnoreCase(uf.getActionParameter().getAid())) {
					final var af = getForwardPost(sac);
					sac.setForwardConfig(af);
					if (register(mapping.getGid(), sac.getRequest(), uf)) {
						removePatternInfo(mapping.getGid(), sac);
						sac.getRequest().setAttribute(PatternInfo.class.getName(),
								getPatternInfo(mapping.getGid(), sac, uf));
					} else {
						MsgUtil.putTopMessage(uf, "ZZ000000021");
						uf.getActionParameter().rollback(null);
						af.setRedirect(false);
					}
				} else {
					doPost(sac, pi, uf);
				}
			}
		}

		return Processing.CONTINUE;
	}

	/**
	 * 表示処理
	 *
	 * @param sac コンテキスト
	 * @param pi パタン情報
	 * @return 継続する場合 true　を返す。
	 */
	private boolean doGet(final ServletActionContext sac, final PatternInfo pi) {
		final var query = FilterUtil.getRequestQuery(sac.getRequest());
		if (query == null) {
			final var pat = pi.getDefaultId();
			if (!Objects.toString(pat, "").isEmpty()) {
				sac.setForwardConfig(new ActionForward(
						"/" + FilterUtil.getRequestPath(sac.getRequest())
						+ "?" + this.patternId + "=" + pat, true));
				return false;
			}
		} else {
			final var id = sac.getRequest().getParameter(this.patternId);
			if (id != null && query.contains(this.patternId + "=")) {
				sac.setForwardConfig(getForwardGet(sac, id, pi));
				return false;
			}
		}
		return true;
	}

	/**
	 * POST時処理
	 *
	 * @param sac コンテキスト
	 * @param pi パタン情報
	 * @param model 汎用フォーム
	 */
	private void doPost(final ServletActionContext sac, final PatternInfo pi, final UniForm model) {
		final var query = FilterUtil.getRequestQuery(sac.getRequest());
		if (query != null && query.contains(this.patternId + "=")) {
			final var id = sac.getRequest().getParameter(this.patternId);
			if (id != null) {
				final var map = FilterUtil.toParameterMap(
						pi.getPatternQuery(id), sac.getRequest().getCharacterEncoding());
				map.forEach(model.getActionParameter().getParameter()::setValue);
			}
		}
	}

	/**
	 * パタン登録処理
	 *
	 * @param gid 画面ID
	 * @param request リクエスト
	 * @param model 汎用フォーム
	 * @return 登録した場合 true を返す。
	 */
	private boolean register(final String gid,
			final HttpServletRequest request, final UniForm model) {
		final var name = request.getParameter(this.patternName);
		final var pat = request.getParameter(this.patternQuery);
		if (!Objects.toString(name, "").isEmpty() && !Objects.toString(pat, "").isEmpty()) {
			model.setValue("PatternName", name);
			model.setValue("PatternQuery", pat);
			model.setValue("Gid", gid);
			final var xa = Factory.create(XATransaction.class);
			try {
				xa.beginTransaction();

				if (canRegister(model)) {
					if (request.getParameter(this.defaultPattern) != null) {
						updateDefault(model);
						model.setValue("Default", BooleanUtil.toFlag(true));
					}
					deletePattern(model);
					insertPattern(model);
					return xa.commit();
				}
			} finally {
				xa.endTransaction();
				model.remove("Default");
				model.remove("PatternName");
				model.remove("PatternQuery");
				model.remove("Gid");
			}
		}
		return false;
	}

	/**
	 * 登録可能判断
	 *
	 * @param model 汎用モデル
	 * @return 登録可能な場合 true を返す。
	 */
	private boolean canRegister(final UniForm model) {
		select("project.svc.generic.pattern.CountPattern", model);
		return model.getNumber("Cnt").intValue() < this.max;
	}

	/**
	 * フォワード先取得
	 *
	 * @param id パタンID
	 * @param sac コンテキスト
	 * @param pi パタン情報
	 * @return ActionForward
	 */
	private ActionForward getForwardGet(final ServletActionContext sac,
			final String id, final PatternInfo pi) {
		final var query = pi.getPatternQuery(id);
		if (!Objects.toString(query, "").isEmpty()) {
			return new ActionForward(
					"/" + FilterUtil.getResponsePath(sac.getRequest()) + "?" + query);
		}
		return new ActionForward("/" + FilterUtil.getRequestPath(sac.getRequest()), true);
	}

	/**
	 * フォワード先取得
	 *
	 * @param sac コンテキスト
	 * @return ActionForward
	 */
	private ActionForward getForwardPost(final ServletActionContext sac) {
		final var path = "/" + FilterUtil.getRequestPath(sac.getRequest());
		final var query = FilterUtil.getRequestQuery(sac.getRequest());
		if (!Objects.toString(query, "").isEmpty()) {
			return new ActionForward(path + "?" + query, true);
		}
		return new ActionForward(path, true);
	}

	/**
	 * デフォルト更新
	 *
	 * @param model 汎用モデル
	 */
	private void updateDefault(final UniForm model) {
		update("project.svc.generic.pattern.UpdateDefault", model);
	}

	/**
	 * パタン削除
	 *
	 * @param model 汎用モデル
	 */
	private void deletePattern(final UniForm model) {
		update("project.svc.generic.pattern.DeletePattern", model);
	}

	/**
	 * パタン作成
	 *
	 * @param model 汎用モデル
	 */
	private void insertPattern(final UniForm model) {
		update("project.svc.generic.pattern.InsertPattern", model);
	}

	/**
	 * デフォルトパタンID取得
	 *
	 * @param ids パタンID配列
	 * @param vals 優先配列
	 * @return デフォルトパタンID
	 */
	private String getDefaultPatternId(final String[] ids, final String[] vals) {
		for (var i = 0; i < vals.length; i++) {
			if (BooleanUtil.toBool(vals[i])) {
				return ids[i];
			}
		}
		return "";
	}

	/**
	 * パタン一覧取得
	 *
	 * @param model 汎用モデル
	 */
	private void getPatternList(final UniForm model) {
		select("project.svc.generic.pattern.PatternList", model);
	}

	/**
	 * 選択処理
	 *
	 * @param query クエリ
	 * @param model 汎用モデル
	 */
	private void select(final String query, final UniForm model) {
		final var ql = InstanceFactory.create(QueryService.class, model);
		ql.setQueryFile(query);
		ql.search();
		model.putAll(ql.getResultModel());
	}

	/**
	 * 更新処理
	 *
	 * @param query クエリ
	 * @param model 汎用モデル
	 * @return 処理件数
	 */
	private int update(final String query, final UniForm model) {
		final var ql = InstanceFactory.create(UpdateService.class, model);
		ql.setQueryFile(query);
		try {
			return ql.update(model);
		} catch (final ExclusiveException ex) {
			ThrowableUtil.warn(ex);
			return 0;
		}
	}

	/**
	 * パタン情報削除
	 *
	 * @param gid 画面ID
	 * @param sac コンテキスト
	 */
	private void removePatternInfo(final String gid, final ServletActionContext sac) {
		final var session = sac.getRequest().getSession(false);
		if (session != null) {
			final ConcurrentMap<String, PatternInfo> map = getCacheMap(session);
			map.remove(gid);
			session.setAttribute(CLAZZ, map);
		}
	}

	/**
	 * パタン情報取得
	 *
	 * @param gid 画面ID
	 * @param sac コンテキスト
	 * @param model 汎用モデル
	 * @return パタン情報
	 */
	private PatternInfo getPatternInfo(final String gid,
			final ServletActionContext sac, final UniForm model) {
		final var session = sac.getRequest().getSession(false);
		if (session == null) {
			return EMPTY;
		}

		synchronized (SessionMutexListener.getMutex(session)) {
			final var map = getCacheMap(session);
			final Supplier<PatternInfo> supplier = () -> {
				final var info = getNewPatternInfo(gid, model);
				final var pt = map.putIfAbsent(gid, info);
				if (pt != null) {
					return pt;
				}
				session.setAttribute(CLAZZ, map);
				return info;
			};
			return Objects.requireNonNullElseGet(map.get(gid), supplier);
		}
	}

	/**
	 * パタン情報作成
	 *
	 * @param gid 画面ID
	 * @param model 汎用モデル
	 * @return パタン情報
	 */
	private PatternInfo getNewPatternInfo(final String gid, final UniForm model) {
		try {
			model.setValue("Gid", gid);

			getPatternList(model);
			final var vals = model.getStringArray("PatternValue");
			if (0 == vals.length) {
				return EMPTY;
			}

			return new PatternInfo(getDefaultPatternId(vals, model.getStringArray("Preference")),
					toMap(vals, model.getStringArray("PatternQueryString")),
					Arrays.asList(model.getStringArray("PatternLabel")), Arrays.asList(vals));
		} finally {
			model.remove("PatternQueryString");
			model.remove("PatternLabel");
			model.remove("PatternValue");
			model.remove("Preference");
			model.remove("Gid");
		}
	}

	/**
	 * キー、バリュー配列マップ化
	 *
	 * @param <K> ジェネリクス
	 * @param <V> ジェネリクス
	 * @param key キー配列
	 * @param val 値配列
	 * @return マップ
	 */
	private static <K, V> Map<K, V> toMap(final K[] key, final V[] val) {
		final var ret = new HashMap<K, V>();
		if (key != null && val != null) {
			for (var i = 0; i < key.length; i++) {
				ret.put(key[i], val[i]);
			}
		}
		return ret;
	}

	/**
	 * キャッシュマップ取得
	 *
	 * @param session セション
	 * @return キャッシュマップ
	 */
	private ConcurrentMap<String, PatternInfo> getCacheMap(final HttpSession session) {
		return Objects.requireNonNullElseGet(
				Factory.cast(session.getAttribute(CLAZZ)),
				ConcurrentHashMap::new);
	}

	/**
	 * パタンリスト設定
	 *
	 * @param sac コンテキスト
	 * @param model 汎用モデル
	 */
	static void setPatternList(final ServletActionContext sac, final UniForm model) {
		final var pi = PatternInfo.class.cast(
				sac.getRequest().getAttribute(PatternInfo.class.getName()));
		if (pi != null) {
			model.setValue("PatternLabel", pi.getPatternLabel());
			model.setValue("PatternValue", pi.getPatternValue());
		}
	}

	/**
	 * パタン情報
	 * TODO:マップ化
	 *
	 * @author Tadashi Nakayama
	 */
	static final class PatternInfo implements Serializable {
		/** serialVersionUID */
		private static final long serialVersionUID = 1L;

		/** デフォルトID */
		private final String defaultId;
		/** パタンクエリマップ */
		private final Map<String, String> patternQuery;
		/** パタンラベルリスト */
		private final List<String> patternLabel;
		/** パタン値リスト */
		private final List<String> patternValue;

		/**
		 * コンストラクタ
		 *
		 * @param id デフォルトID
		 * @param query クエリマップ
		 * @param label ラベル配列
		 * @param value 値配列
		 */
		PatternInfo(final String id, final Map<String, String> query,
				final List<String> label, final List<String> value) {
			this.defaultId = id;
			this.patternQuery = new HashMap<>(query);
			this.patternLabel = new ArrayList<>(label);
			this.patternValue = new ArrayList<>(value);
		}

		/**
		 * デフォルトID取得
		 *
		 * @return デフォルトID
		 */
		public String getDefaultId() {
			return this.defaultId;
		}

		/**
		 * パタンクエリ取得
		 *
		 * @param id パタンID
		 * @return パタンクエリ
		 */
		public String getPatternQuery(final String id) {
			return this.patternQuery.get(id);
		}

		/**
		 * パタンラベルリスト取得
		 *
		 * @return パタンラベルリスト
		 */
		public String[] getPatternLabel() {
			return this.patternLabel.toArray(new String[this.patternLabel.size()]);
		}

		/**
		 * パタン値リスト取得
		 *
		 * @return パタン値リスト
		 */
		public String[] getPatternValue() {
			return this.patternValue.toArray(new String[this.patternValue.size()]);
		}
	}
}
