package common.db.dao.hibernate;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.apache.logging.log4j.LogManager;
import org.hibernate.HibernateException;
import org.hibernate.JDBCException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.PropertyValueException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.exception.ConstraintViolationException;
import org.hibernate.exception.DataException;
import org.hibernate.exception.LockAcquisitionException;
import org.hibernate.jdbc.Work;
import org.hibernate.query.Query;
import org.hibernate.transform.Transformers;

import common.db.dao.Dao;
import common.db.dao.DaoConstraintException;
import common.db.dao.DaoLockException;
import common.db.dao.DaoPropertyException;
import common.db.dao.DaoUtil;
import common.db.jdbc.Jdbc;
import common.sql.QueryUtil;
import core.config.Factory;
import core.exception.PhysicalException;
import core.exception.ThrowableUtil;

/**
 * DAOベース
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public abstract class BaseDao implements Dao {

	/** NO WAIT排他 */
	private static final LockOptions NO_WAIT_UPGRADE;

	static {
		NO_WAIT_UPGRADE = new LockOptions(LockMode.UPGRADE_NOWAIT);
		NO_WAIT_UPGRADE.setTimeOut(LockOptions.NO_WAIT);
	}

	/** コンフィグ */
	private final Config config;

	/** 排他フラグ */
	private boolean noWait = false;

	/**
	 * コンストラクタ
	 * @param cfg コンフィグ
	 */
	protected BaseDao(final Config cfg) {
		this.config = cfg;
	}

	/**
	 * セションファクトリ取得
	 *
	 * @return セションファクトリ
	 */
	protected SessionFactory getSessionFactory() {
		return this.config.getSessionFactory();
	}

	/**
	 * 排他待ち設定
	 *
	 * @param val 排他待ちフラグ
	 */
	@Override
	public void setNoWait(final boolean val) {
		this.noWait = val;
	}

	/**
	 * 排他待ち取得
	 *
	 * @return 排他待ちフラグ
	 */
	public boolean isNoWait() {
		return this.noWait;
	}

	/**
	 * プライマリキーによる検索
	 *
	 * @param <T> ジェネリックス
	 * @param cls 読込モデルのクラス
	 * @param id プライマリキーオブジェクト
	 * @return 検索結果オブジェクト
	 */
	@Override
	public <T> T findByIdWithLock(final Class<T> cls, final Serializable id) {
		final var lo = this.noWait ? NO_WAIT_UPGRADE : LockOptions.UPGRADE;

		if (!DaoUtil.isEntity(cls)) {
			final var param = new ArrayList<>();
			final var query = toSelectQuery(cls, id, param, lo);
			return selectWithLock(cls, query, param.toArray(new Object[param.size()]));
		}
		return find(cls, id, lo);
	}

	/**
	 * プライマリキーによる検索
	 *
	 * @param <T> ジェネリックス
	 * @param cls 読込モデルのクラス
	 * @param id プライマリキーオブジェクト
	 * @return 検索結果オブジェクト
	 */
	@Override
	public <T> T findById(final Class<T> cls, final Serializable id) {
		if (!DaoUtil.isEntity(cls)) {
			final var param = new ArrayList<>();
			final var query = toSelectQuery(cls, id, param, LockOptions.READ);
			return select(cls, query, param.toArray(new Object[param.size()]));
		}
		return find(cls, id, LockOptions.READ);
	}

	/**
	 * 選択クエリ取得
	 *
	 * @param <T> ジェネリックス
	 * @param cls 読込モデルのクラス
	 * @param id プライマリキーオブジェクト
	 * @param param パラメタ設定リスト
	 * @param lo ロックオブジェクト
	 * @return 選択クエリ
	 */
	private <T> String toSelectQuery(final Class<T> cls, final Serializable id,
			final List<Object> param, final LockOptions lo) {
		var ret = "SELECT * FROM " + DaoUtil.getTableName(cls)
				+ EntityUtil.toWhereString(id, param, DaoUtil.getIdMethod(cls).orElse(null));
		if (NO_WAIT_UPGRADE == lo) {
			ret = ret + " " + this.config.getDialect().getForUpdateNowaitString();
		} else if (LockOptions.UPGRADE == lo) {
			ret = ret + " " + this.config.getDialect().getForUpdateString();
		}
		return ret;
	}

	/**
	 * プライマリキーによる検索
	 *
	 * @param <T> ジェネリックス
	 * @param cls 読込モデルのクラス
	 * @param id プライマリキーオブジェクト
	 * @param lo ロックオプション
	 * @return 検索結果オブジェクト
	 */
	private <T> T find(final Class<T> cls, final Serializable id, final LockOptions lo) {
		try {
			// トランザクション開始
			beginTransaction();
			// 読込
			final var obj = getSession().load(cls, id, lo);
			getSession().evict(obj);
			return cls.cast(obj);
		} catch (final ObjectNotFoundException ex) {
			LogManager.getLogger().info(ex.getMessage());
			return null;
		} catch (final HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * レコード挿入
	 *
	 * @param item 挿入対象モデル
	 */
	@Override
	public void insert(final Serializable item) {
		// NULL可項目NULL化
		toNull(item);
		try {
			if (DaoUtil.isEntity(item.getClass())) {
				evict(item);
				// トランザクション開始
				beginTransaction();
				// 保存
				getSession().save(item);
				flushSession(item);
			} else {
				setId(item);
				final var param = new ArrayList<>();
				final var sql = EntityUtil.toInsertSql(item, param);
				execute(sql, param.toArray(new Object[param.size()]));
			}
		} catch (final LockAcquisitionException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoLockException(ex, this.noWait);
		} catch (final ConstraintViolationException ex) {
			LogManager.getLogger().warn("Item={}, Message={}", item, ex.getMessage());
			throw new DaoConstraintException(ex, this.noWait);
		} catch (final PropertyValueException ex) {
			// not null例外
			LogManager.getLogger().warn("Item={}, Message={}", item, ex.getMessage());
			throw new DaoPropertyException(ex);
		} catch (final HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * Id設定
	 *
	 * @param item 挿入対象モデル
	 */
	private void setId(final Serializable item) {
		if (!DaoUtil.isEmbeddedId(item.getClass())) {
			final var name = DaoUtil.getSequenceName(item.getClass());
			if (!Objects.toString(name, "").isEmpty()) {
				final var mt = Factory.getMethod(item.getClass(), "setId", Long.class);
				Factory.invoke(item, mt, sequence(name));
			}
		}
	}

	/**
	 * レコード追加または更新
	 *
	 * @param item 更新対象モデル
	 */
	@Override
	public void merge(final Serializable item) {
		if (!DaoUtil.isEntity(item.getClass())) {
			throw new IllegalArgumentException(item.getClass().getName());
		}

		// NULL可項目NULL化
		toNull(item);
		try {
			evict(item);
			// トランザクション開始
			beginTransaction();
			// 更新
			getSession().merge(item);
			flushSession(item);
		} catch (final LockAcquisitionException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoLockException(ex, this.noWait);
		} catch (final ConstraintViolationException ex) {
			LogManager.getLogger().warn("Item={}, Message={}", item, ex.getMessage());
			throw new DaoConstraintException(ex, this.noWait);
		} catch (final PropertyValueException ex) {
			// not null例外
			LogManager.getLogger().warn("Item={}, Message={}", item, ex.getMessage());
			throw new DaoPropertyException(ex);
		} catch (final HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * レコード更新
	 *
	 * @param item 更新対象モデル
	 * @return 更新された場合 true を返す。対象なしの場合 false を返す。
	 */
	@Override
	public boolean update(final Serializable item) {
		// NULL可項目NULL化
		toNull(item);
		try {
			if (DaoUtil.isEntity(item.getClass())) {
				evict(item);
				// トランザクション開始
				beginTransaction();
				// 更新
				getSession().update(item);
				flushSession(item);
			} else {
				final var param = new ArrayList<>();
				final var sql = EntityUtil.toUpdateSql(item, param);
				execute(sql, param.toArray(new Object[param.size()]));
			}
			return true;
		} catch (final LockAcquisitionException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoLockException(ex, this.noWait);
		} catch (final ConstraintViolationException ex) {
			LogManager.getLogger().warn("Item={}, Message={}", item, ex.getMessage());
			throw new DaoConstraintException(ex, this.noWait);
		} catch (final PropertyValueException ex) {
			// not null例外
			LogManager.getLogger().warn("Item={}, Message={}", item, ex.getMessage());
			throw new DaoPropertyException(ex);
		} catch (final JDBCException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		} catch (final HibernateException ex) {
			if (!isUpdateFailed(ex)) {
				ThrowableUtil.error(ex);
				throw new PhysicalException(ex);
			}
			LogManager.getLogger().info(ex.getMessage());
			return false;
		}
	}

	/**
	 * レコード削除
	 *
	 * @param item 削除対象モデル
	 * @return 削除された場合 true を返す。
	 */
	@Override
	public boolean delete(final Serializable item) {
		try {
			if (DaoUtil.isEntity(item.getClass())) {
				evict(item);
				// トランザクション開始
				beginTransaction();
				// 削除処理
				getSession().delete(item);
				flushSession(item);
			} else {
				final var param = new ArrayList<>();
				final var sql = EntityUtil.toDeleteSql(item, param);
				execute(sql, param.toArray(new Object[param.size()]));
			}
			return true;
		} catch (final LockAcquisitionException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoLockException(ex, this.noWait);
		} catch (final JDBCException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		} catch (final HibernateException ex) {
			if (!isUpdateFailed(ex)) {
				ThrowableUtil.error(ex);
				throw new PhysicalException(ex);
			}
			LogManager.getLogger().info(ex.getMessage());
			return false;
		}
	}

	/**
	 * 分離
	 *
	 * @param item 対象モデル
	 */
	private void evict(final Serializable item) {
		if (getSession().contains(item) && getSession().isReadOnly(item)) {
			getSession().evict(item);
		}
	}

	/**
	 * ネイティブSQLクエリ実行
	 *
	 * @param <T> ジェネリックス
	 * @param cls 返却クラスタイプ
	 * @param query SQL
	 * @param param パラメタ値マップ
	 * @return モデルオブジェクト
	 */
	@Override
	public <T> T select(final Class<T> cls, final String query, final Map<String, ?> param) {
		try {
			// トランザクション開始
			beginTransaction();
			final var s = createQuery(getSession(), cls, query, param).
					setReadOnly(true).setMaxResults(1).stream();
			return s.findFirst().map(r -> {
				if (DaoUtil.isEntity(cls)) {
					getSession().evict(r);
				} else if (cls != null && !isJavaClass(cls)) {
					return toEntity(Factory.cast(r), cls);
				}
				return r;
			}).orElse(null);

		} catch (final ObjectNotFoundException | DataException ex) {
			LogManager.getLogger().info(ex.getMessage());
			return null;
		} catch (final HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * ネイティブSQLクエリ実行（排他）
	 *
	 * @param <T> ジェネリックス
	 * @param cls 返却クラスタイプ
	 * @param query SQL
	 * @param param パラメタ値マップ
	 * @return モデルオブジェクト
	 */
	@Override
	public <T> T selectWithLock(final Class<T> cls,
			final String query, final Map<String, ?> param) {
		if (isJavaClass(cls)) {
			throw new IllegalArgumentException(cls.getName());
		}

		final Supplier<String> addFor = () ->
			query + " " + (this.noWait ? this.config.getDialect().getForUpdateNowaitString()
							: this.config.getDialect().getForUpdateString());

		try {
			// トランザクション開始
			beginTransaction();
			final var it = createQuery(getSession(), cls, addFor.get(), param).stream().iterator();

			T o = null;
			if (it.hasNext()) {
				o = it.next();
				if (it.hasNext()) {
					throw new IllegalStateException("More than one record is selected.");
				}

				if (DaoUtil.isEntity(cls)) {
					getSession().evict(o);
				} else if (cls != null) {
					return toEntity(Factory.cast(o), cls);
				}
			}
			return o;

		} catch (final ObjectNotFoundException | DataException ex) {
			LogManager.getLogger().info(ex.getMessage());
			return null;
		} catch (final LockAcquisitionException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoLockException(ex, this.noWait);
		} catch (final HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * エンティティ化
	 *
	 * @param <T> ジェネリクス
	 * @param data データ
	 * @param cls クラス
	 * @return エンティティ
	 */
	private static <T> T toEntity(final Map<String, Object> data, final Class<T> cls) {
		return EntityUtil.toInstance(data, cls);
	}

	/**
	 * ネイティブSQLクエリ実行
	 *
	 * @param <T> ジェネリックス
	 * @param cls 返却クラスタイプ
	 * @param query SQL
	 * @param param パラメタ値マップ
	 * @return モデルリスト
	 */
	@Override
	public <T> List<T> selectAll(final Class<T> cls,
			final String query, final Map<String, ?> param) {
		try {
			// トランザクション開始
			beginTransaction();

			// String sql = query.replaceAll(" [Ff][Oo][Rr] .*", "");
			final var s = createQuery(getSession(), cls, query, param).setReadOnly(true).list();
			if (cls != null && !DaoUtil.isEntity(cls) && !isJavaClass(cls)) {
				return toEntityList(Factory.cast(s), cls);
			}
			return s;
		} catch (final ObjectNotFoundException | DataException ex) {
			LogManager.getLogger().info(ex.getMessage());
			return null;
		} catch (final HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * エンティティ変換
	 *
	 * @param <T> ジェネリクス
	 * @param data データ
	 * @param cls クラス
	 * @return エンティティリスト
	 */
	private static <T> List<T> toEntityList(
			final List<Map<String, Object>> data, final Class<T> cls) {
		return data.stream().map(m -> EntityUtil.toInstance(m, cls)).collect(Collectors.toList());
	}

	/**
	 * SQL更新実行
	 *
	 * @param query クエリ
	 * @param param パラメタ値マップ
	 * @return 処理件数
	 */
	@Override
	public int execute(final String query, final Map<String, ?> param) {
		try {
			// トランザクション開始
			beginTransaction();
			getSession().flush();
			final var sql = createQuery(getSession(), null, query, param);
			return sql.executeUpdate();
		} catch (final LockAcquisitionException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoLockException(ex, this.noWait);
		} catch (final ConstraintViolationException ex) {
			LogManager.getLogger().info(ex.getMessage());
			throw new DaoConstraintException(ex, this.noWait);
		} catch (final HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * JDBC実行処理
	 *
	 * @param work 実行クラス
	 */
	@Override
	public void doWork(final JdbcWork work) {
		if (HibernateJdbcWork.class.isInstance(work)) {
			HibernateJdbcWork.class.cast(work).setDialect(this.config.getDialect());
		}
		try {
			beginTransaction();
			getSession().doWork(work::execute);
		} catch (final HibernateException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * シーケンス取得クエリ取得
	 *
	 * @param name シーケンス名
	 * @return シーケンス取得クエリ
	 */
	protected String getSequenceNextValString(final String name) {
		return this.config.getDialect().getSequenceNextValString(name);
	}

	/**
	 * 文字列項目が空文字の場合nullにする。
	 *
	 * @param obj エンティティBean
	 */
	private void toNull(final Serializable obj) {
		for (final Method m : obj.getClass().getMethods()) {
			if (Factory.isGetter(m) && String.class.equals(m.getReturnType())
					&& DaoUtil.isNullable(m)) {
				final String val = Factory.invoke(obj, m);
				if (val != null && val.trim().isEmpty()) {
					final var mt = Factory.getMethod(obj.getClass(),
							"set" + Factory.toItemName(m), String.class);
					Factory.invoke(obj, mt, (String)null);
				}
			}
		}
	}

	/**
	 * Queryオブジェクト作成
	 *
	 * @param <T> ジェネリクス
	 * @param session セション
	 * @param cls 返却クラスタイプ
	 * @param query SQL条件文
	 * @param param パラメタ値マップ
	 * @return Queryオブジェクト
	 */
	private <T> Query<T> createQuery(final Session session, final Class<T> cls,
			final String query, final Map<String, ?> param) {

		final var qlist = new ArrayList<>();
		final var sql = QueryUtil.toPrepareQuery(query, param, qlist);
		final var qry = session.createNativeQuery(sql, cls);

		qry.setCacheable(false);
		if (DaoUtil.isEntity(cls)) {
			qry.addEntity(cls);
		} else if (List.class.equals(cls)) {
			qry.setResultTransformer(Transformers.TO_LIST);
		} else if (Map.class.equals(cls) || !isJavaClass(cls)) {
			qry.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
		}

		for (int i = 0; i < qlist.size(); i++) {
			qry.setParameter(i + 1, qlist.get(i));
		}
		return qry;
	}

	/**
	 * javaクラス確認
	 *
	 * @param cls クラス
	 * @return javaクラスの場合 true を返す。
	 */
	private boolean isJavaClass(final Class<?> cls) {
		return cls != null && cls.getName().startsWith("java");
	}

	/**
	 * セション取得
	 *
	 * @return セション
	 */
	protected abstract Session getSession();

	/**
	 * セション取得
	 *
	 * @return トランザクション
	 */
	protected abstract Transaction beginTransaction();

	/**
	 * 同期化
	 *
	 * @param item 挿入対象モデル
	 */
	protected abstract void flushSession(Serializable item);

	/**
	 * 更新用処理確認
	 *
	 * @param ex 例外
	 * @return 未更新時 true
	 */
	protected static boolean isUpdateFailed(final HibernateException ex) {
		return ex != null && ex.getMessage() != null
				&& ex.getMessage().startsWith("Batch update row count wrong: 0");
	}

	/**
	 * シーケンス取得
	 *
	 * @author Tadashi Nakayama
	 * @version 1.0.0
	 */
	protected static class SequenceWork implements Work {

		/** クエリ */
		private final String query;
		/** ステートメント */
		private volatile PreparedStatement psmt;
		/** シーケンス */
		private volatile long sequence;

		/**
		 * コンストラクタ
		 *
		 * @param sql クエリ
		 */
		public SequenceWork(final String sql) {
			this.query = sql;
		}

		/**
		 * シーケンス取得
		 *
		 * @return シーケンス
		 */
		public long getSequence() {
			return this.sequence;
		}

		/**
		 * @see org.hibernate.jdbc.Work#execute(java.sql.Connection)
		 */
		@Override
		public void execute(final Connection conn) throws SQLException {
			this.sequence = 0;

			if (this.psmt == null || this.psmt.isClosed()) {
				this.psmt = Jdbc.wrap(conn).readonlyStatement(this.query);
			}

			try (ResultSet rs = this.psmt.executeQuery()) {
				if (!rs.next()) {
					throw new IllegalStateException();
				}
				this.sequence = rs.getLong(1);
			}
		}

		/**
		 * クローズ処理
		 */
		public void close() {
			if (this.psmt != null) {
				try {
					this.psmt.close();
				} catch (final SQLException ex) {
					ThrowableUtil.warn(ex);
				} finally {
					this.psmt = null;
				}
			}
		}
	}
}
