package online.filter;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import core.config.Factory;
import core.util.NumberUtil;
import core.util.bean.Pair;
import online.context.token.Token;
import online.filter.helper.DuplicateBodyResponse;
import online.filter.helper.DuplicateHeaderWrapper;
import online.listener.SessionMutexListener;

/**
 * RedirectRequestフィルタ
 *
 * @author Tadashi Nakayama
 */
public class RedirectRequestFilter implements Filter {

	/** クラス名 */
	private static final String CLAZZ = RedirectRequestFilter.class.getName();

	/** アクションセション最大数 */
	private int max = 3;

	/** 有効間隔 */
	private int effective = 0;

	/**
	 * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
	 */
	@Override
	public void init(final FilterConfig filterConfig) {
		var val = filterConfig.getInitParameter("max");
		if (!Objects.toString(val, "").isEmpty()) {
			this.max = NumberUtil.toInt(val, 3);
		}
		val = filterConfig.getInitParameter("effective");
		if (!Objects.toString(val, "").isEmpty()) {
			this.effective = NumberUtil.toInt(val, 0);
		}
	}

	/**
	 * @see javax.servlet.Filter#destroy()
	 */
	@Override
	public void destroy() {
		return;
	}

	/**
	 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
	 * javax.servlet.ServletResponse, javax.servlet.FilterChain)
	 */
	@Override
	public void doFilter(final ServletRequest svRequest, final ServletResponse svResponse,
			final FilterChain chain) throws IOException, ServletException {
		if (HttpServletRequest.class.isInstance(svRequest)
				&& HttpServletResponse.class.isInstance(svResponse)) {
			final var request = HttpServletRequest.class.cast(svRequest);
			final var response = HttpServletResponse.class.cast(svResponse);

			if (request.getAttribute(CLAZZ) == null) {
				request.setAttribute(CLAZZ, CLAZZ);

				if (!response.isCommitted()) {
					if (!setResponse(request, response)) {
						chain.doFilter(request, new DuplicateHeaderWrapper(response));
						setAttribute(request);
					}
					return;
				}
			}
		}

		chain.doFilter(svRequest, svResponse);
	}

	/**
	 * レスポンス設定処理
	 *
	 * @param request リクエスト
	 * @param response レスポンス
	 * @return 設定した場合 true を返す。
	 */
	private boolean setResponse(final HttpServletRequest request,
			final HttpServletResponse response) {
		final var session = request.getSession(false);
		if (session != null) {
			final var location = FilterUtil.getRequestLocation(request);
			synchronized (SessionMutexListener.getMutex(session)) {
				final List<String> list = Factory.cast(session.getAttribute(CLAZZ));
				if (FilterUtil.isGetMethod(request.getMethod())) {
					// RAP
					return setResponseForGet(response, session, location, list);
				} else if (FilterUtil.isPostMethod(request.getMethod())) {
					// ダブルクリック用
					return setResponseForPost(request, response, session, location, list);
				}
			}
		}
		return false;
	}

	/**
	 * レスポンス設定(GET用)
	 *
	 * @param response HttpServletResponse
	 * @param session HttpSession
	 * @param location ロケーション
	 * @param list 管理リスト
	 * @return 設定した場合 true を返す。
	 */
	private boolean setResponseForGet(final HttpServletResponse response,
			final HttpSession session, final String location, final List<String> list) {

		final Consumer<Pair<LocalDateTime, DuplicateBodyResponse>> consumer = pair -> {
			pair.right().copyResponse(response);
			if (0 != pair.right().getHeader().getStatus()) {
				session.removeAttribute(location);
				if (list != null) {
					list.remove(location);
				}
			}
		};

		final Optional<Pair<LocalDateTime, DuplicateBodyResponse>> opt =
				Optional.ofNullable(Factory.cast(session.getAttribute(location)));
		opt.ifPresent(consumer);
		return opt.isPresent();
	}

	/**
	 * レスポンス設定(POST用)
	 *
	 * @param request HttpServletRequest
	 * @param response HttpServletResponse
	 * @param session HttpSession
	 * @param location ロケーション
	 * @param list 管理リスト
	 * @return 設定した場合 true を返す。
	 */
	private boolean setResponseForPost(final HttpServletRequest request,
			final HttpServletResponse response, final HttpSession session,
			final String location, final List<String> list) {
		if (list != null) {
			final var ldt = LocalDateTime.now();
			for (final var loc : list) {
				final Pair<LocalDateTime, DuplicateBodyResponse> pair =
						Factory.cast(session.getAttribute(loc));
				if (pair != null && isSameRequest(pair.right(), location, request)) {
					final var until = pair.left().until(ldt, ChronoUnit.SECONDS);
					if (0 < this.effective && until <= this.effective) {
						FilterUtil.redirect(response, loc);
						return true;
					}
				}
			}
		}
		return false;
	}

	/**
	 * リクエスト確認
	 *
	 * @param res DuplicateBodyResponse
	 * @param location 位置
	 * @param request リクエスト
	 * @return 同一の場合 true を返す。
	 */
	private boolean isSameRequest(final DuplicateBodyResponse res,
			final String location, final HttpServletRequest request) {
		return location.equals(res.getRequestUrl())
				&& Token.isSameToken(res.getToken(), request);
	}

	/**
	 * セション設定
	 *
	 * @param request リクエスト
	 */
	private void setAttribute(final HttpServletRequest request) {
		final var location = FilterUtil.getRedirect(request);
		final var session = request.getSession(false);
		if (session != null && location != null) {
			synchronized (SessionMutexListener.getMutex(session)) {
				session.setAttribute(location,
						new Pair<>(LocalDateTime.now(), request.getAttribute(location)));

				final CopyOnWriteArrayList<String> list = Objects.requireNonNullElseGet(
						Factory.cast(session.getAttribute(CLAZZ)),
						CopyOnWriteArrayList::new);
				list.remove(location);
				while (this.max < list.size()) {
					session.removeAttribute(list.remove(0));
				}
				list.add(location);
				session.setAttribute(CLAZZ, list);
			}
		}
	}
}
