package project.svc.auth;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Objects;

import common.db.JdbcSource;
import common.db.jdbc.Jdbc;
import common.sql.QueryUtil;
import core.exception.PhysicalException;
import core.exception.ThrowableUtil;
import core.util.DateUtil;
import project.common.CodecUtil;
import project.common.StringUtil;

/**
 * 認証実装
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class AuthenticationImpl implements Authentication {

	/** カラム名（ログオンセッション） */
	private static final String SK_COL_LOGON_SESS = "LOGON_SESS_PRM";
	/** カラム名（最終処理時間） */
	private static final String SK_COL_SAISHU_DTM = "SAISHU_SHORI_DTM";
	/** カラム名（チケットパラメタ） */
	private static final String SK_COL_TICKET = "TICKET_PRM";
	/** カラム名（ログオン時間） */
	private static final String SK_COL_LOGON_DTM = "LOGON_DTM";
	/** カラム名（ログオフ時間） */
	private static final String SK_COL_LOGOFF_DTM = "LOGOFF_DTM";

	/**
	 * ログオンチェック
	 *
	 * @param uid ユーザID
	 * @param sid セションID
	 * @return 正常なら true
	 */
	@Override
	public boolean checkLogon(final String uid, final String sid) {
		// セション情報取得
		final var m = getSessionKanriInfo(uid);
		return m != null && m.getShoriDateTime() != null
				&& Objects.equals(m.getLogonSession(), sid);
	}

	/**
	 * ログオン処理
	 *
	 * @param uid ユーザID
	 * @param pwd パスワード
	 * @return 成功なら true
	 */
	@Override
	public boolean logon(final String uid, final String pwd) {
		return logonProc(uid, pwd) && clearTicket(uid);
	}

	/**
	 * ユーザのステータスを更新/追加
	 *
	 * @param uid ユーザID
	 * @param now 現在日時
	 * @param sid セションID
	 * @param interval 最大無効間隔
	 * @return アクションセションID
	 */
	@Override
	public SessionInfo updateStatus(final String uid, final Date now,
			final String sid, final int interval) {

		final var datetime = DateUtil.toDateTime(now);
		try (var conn = JdbcSource.getConnection()) {
			final var param = new HashMap<String, Object>();
			param.put("UserId", uid);
			param.put("LogonSessionParam", sid);
			param.put("ShoriDtm", datetime);
			param.put("LogonDtm", datetime);

			var m = getSessionKanriInfoWithLock(uid, conn);
			if (m == null) {
				final var query = QueryUtil.getSqlFromFile(
						"InsertSessionKanri", this.getClass());
				try (var upsmt = QueryUtil.createStatement(
						query, param, Jdbc.wrap(conn)::prepareStatement)) {
					upsmt.executeUpdate();
				}
				m = new SessionInfo();
			} else {
				final var sess = m.getLogonSession();
				if (Objects.equals(sess, sid)) {
					return null;
				}
				m.setInterval(interval);
				m.setDateTime(datetime);

				param.put("LogoffDtm", m.getLogoffDateTime());
				param.put("LogoffSbt", m.getLogoffType().value());

				final var query = QueryUtil.getSqlFromFile(
						"UpdateSessionKanri", this.getClass());
				try (var upsmt = QueryUtil.createStatement(
						query, param, Jdbc.wrap(conn)::prepareStatement)) {
					upsmt.executeUpdate();
				}
			}
			conn.commit();
			return m;

		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * ログオフ
	 *
	 * @param uid ユーザID
	 * @param sid セションID
	 * @param time 日時
	 */
	@Override
	public void logoff(final String uid, final String sid, final Timestamp time) {
		// セション情報取得
		if (sid != null) {
			try (var conn = JdbcSource.getConnection()) {
				final var m = getSessionKanriInfoWithLock(uid, conn);
				if (m != null && sid.equals(m.getLogonSession())) {
					final var param = new HashMap<String, Object>();
					param.put("UserId", uid);
					param.put("LogonSessionParam", sid);
					param.put("ShoriDtm", m.getShoriDateTime());
					param.put("LogonDtm", m.getLogonDateTime());
					param.put("LogoffDtm", time);
					param.put("LogoffSbt", m.getLogoffType().value());

					final var query = QueryUtil.getSqlFromFile(
							"UpdateSessionKanri", this.getClass());
					try (var psmt = QueryUtil.createStatement(
							query, param, Jdbc.wrap(conn)::prepareStatement)) {
						psmt.executeUpdate();
					}
					conn.commit();
				}
			} catch (final SQLException ex) {
				ThrowableUtil.error(ex);
				throw new PhysicalException(ex);
			}
		}
	}

	/**
	 * アクション時間を設定する。
	 *
	 * @param uid ユーザID
	 * @param now 現在日時
	 * @param sid セションID
	 */
	@Override
	public void setActionTime(final String uid, final Date now, final String sid) {
		// セション情報取得
		try (var conn = JdbcSource.getConnection()) {
			if (getSessionKanriInfoWithLock(uid, conn) != null) {
				final var param = new HashMap<String, Object>();
				param.put("UserId", uid);
				param.put("LogonSessionParam", sid);
				param.put("ShoriDtm", DateUtil.toDateTime(now));

				final var query = QueryUtil.getSqlFromFile("UpdateShoriDtm", this.getClass());
				try (var psmt = QueryUtil.createStatement(
						query, param, Jdbc.wrap(conn)::prepareStatement)) {
					// 更新
					psmt.executeUpdate();
				}
				conn.commit();
			}
		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * チケット追加
	 *
	 * @param uid ユーザID
	 * @return チケット
	 */
	@Override
	public String addTicket(final String uid) {
		try (Connection conn = JdbcSource.getConnection()) {
			// セション情報取得
			final var m = getSessionKanriInfoWithLock(uid, conn);
			if (m == null) {
				return null;
			}

			// チケット作成
			final var ticket = StringUtil.randomization(10);
			var prm = m.getTicket();
			if (!Objects.toString(prm, "").isEmpty()) {
				prm = prm + "," + ticket;
			} else {
				prm = ticket;
			}

			// 更新
			updateTicket(uid, prm, conn);

			return ticket;
		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * チケット削除処理
	 *
	 * @param uid ユーザID
	 * @param val チケット
	 * @return 存在していて削除された場合 true 存在しない場合等はfalse
	 */
	@Override
	public boolean removeTicket(final String uid, final String val) {
		try (Connection conn = JdbcSource.getConnection()) {
			// セション情報取得
			final var m = getSessionKanriInfoWithLock(uid, conn);
			if (m != null && !Objects.toString(m.getTicket(), "").isEmpty()) {
				var prm = m.getTicket();
				var loc = prm.indexOf(val);
				if (0 <= loc) {
					var len = val.length();
					if (0 < loc && prm.codePointBefore(loc) == ',') {
						loc--;
						len++;
					} else if (loc + len < prm.length()) {
						len++;
					}
					prm = prm.substring(0, loc) + prm.substring(loc + len);
					// 更新
					updateTicket(uid, prm, conn);
					return true;
				}
			}
			return false;
		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * パスワードチェック
	 *
	 * @param uid ユーザID
	 * @param pwd パスワード
	 * @return 成功なら true
	 */
	private boolean logonProc(final String uid, final String pwd) {
		// サブミット値取得
		if (Objects.toString(uid, "").isEmpty() || Objects.toString(pwd, "").isEmpty()) {
			return false;
		}

		try (var conn = JdbcSource.getConnection()) {
			// ユーザ存在確認
			final var query = QueryUtil.getSqlFromFile("SelectUserInfo", this.getClass());
			try (var psmt = QueryUtil.createStatement(
					query, Collections.singletonMap("UserId", uid),
					Jdbc.wrap(conn)::readonlyStatement)) {
				try (var rs = psmt.executeQuery()) {
					if (!rs.next()) {
						return false;
					}
				}
			}

			final var param = new HashMap<String, Object>();
			param.put("UserId", uid);
			param.put("PassWd", CodecUtil.toDigestString(pwd));

			// パスワード確認
			try (var psmt = QueryUtil.createStatement(
					query, param, Jdbc.wrap(conn)::prepareStatement)) {
				try (var rs = psmt.executeQuery()) {
					return rs.next();
				}
			}

		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * チケットクリア
	 *
	 * @param uid ユーザID
	 * @return クリアした場合 true
	 */
	private boolean clearTicket(final String uid) {
		try (Connection conn = JdbcSource.getConnection()) {
			if (getSessionKanriInfoWithLock(uid, conn) != null) {
				return updateTicket(uid, "", conn);
			}
			return false;
		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * チケット更新
	 *
	 * @param uid ユーザID
	 * @param val チケット
	 * @param conn コネクション
	 * @return 更新した場合 true
	 */
	private boolean updateTicket(final String uid, final String val, final Connection conn) {
		// セション情報取得
		final var param = new HashMap<String, Object>();
		param.put("UserId", uid);
		param.put("Ticket", val);

		final var query = QueryUtil.getSqlFromFile("UpdateTicket", this.getClass());
		try (var psmt = QueryUtil.createStatement(
				query, param, Jdbc.wrap(conn)::prepareStatement)) {
			final var cnt = psmt.executeUpdate();
			if (cnt == 1) {
				conn.commit();
				return true;
			}
		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
		return false;
	}

	/**
	 * セション管理情報取得
	 *
	 * @param uid ユーザID
	 * @return セション情報
	 */
	private SessionInfo getSessionKanriInfo(final String uid) {
		if (!Objects.toString(uid, "").isEmpty()) {
			try (Connection conn = JdbcSource.getConnection()) {
				final var query = QueryUtil.getSqlFromFile(
						"SelectSessionKanri", this.getClass());
				try (var psmt = QueryUtil.createStatement(
						query, Collections.singletonMap("UserId", uid),
						Jdbc.wrap(conn)::readonlyStatement)) {
					try (var rs = psmt.executeQuery()) {
						if (rs.next()) {
							return toSessionInfo(rs);
						}
					}
				}
			} catch (final SQLException ex) {
				ThrowableUtil.error(ex);
				throw new PhysicalException(ex);
			}
		}
		return null;
	}

	/**
	 * セション管理情報取得
	 *
	 * @param uid ユーザID
	 * @param conn コネクション
	 * @return セション情報
	 */
	private SessionInfo getSessionKanriInfoWithLock(final String uid, final Connection conn) {
		if (!Objects.toString(uid, "").isEmpty()) {
			final var query = QueryUtil.getSqlFromFile(
					"SelectSessionKanriForUpdate", this.getClass());
			try (var psmt = QueryUtil.createStatement(
					query, Collections.singletonMap("UserId", uid),
					Jdbc.wrap(conn)::readonlyStatement)) {
				try (var rs = psmt.executeQuery()) {
					if (rs.next()) {
						toSessionInfo(rs);
					}
				}
			} catch (final SQLException ex) {
				ThrowableUtil.error(ex);
				throw new PhysicalException(ex);
			}
		}
		return null;
	}

	/**
	 * PreparedStatement 取得
	 * @param rs ResultSet
	 * @return SessionInfo
	 * @throws SQLException SQL例外
	 */
	private SessionInfo toSessionInfo(final ResultSet rs) throws SQLException {
		return new SessionInfo(rs.getString(SK_COL_LOGON_SESS),
				rs.getTimestamp(SK_COL_SAISHU_DTM),
				rs.getString(SK_COL_TICKET),
				rs.getTimestamp(SK_COL_LOGON_DTM),
				rs.getTimestamp(SK_COL_LOGOFF_DTM));
	}
}
