package project.svc.generic.db;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;

import org.apache.logging.log4j.LogManager;

import common.db.JdbcSource;
import common.db.dao.Dao.JdbcWork;
import common.db.dao.DaoConstraintException;
import common.db.dao.DaoLockException;
import common.db.dao.DaoUtil;
import core.config.Factory;
import core.util.NumberUtil;
import core.util.bean.CamelCase;
import online.model.ModelUtil;
import online.model.UniModel;
import project.base.UpdateAbstract;

/**
 * 汎用レコード操作
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class RecordOperate extends UpdateAbstract {

	/** テーブル名 */
	private EntityInfo mm;
	/** 論理削除フラグ */
	private Boolean invisible = null;

	/**
	 * テーブル名設定
	 *
	 * @param val テーブル名
	 */
	public void setTable(final String val) {
		this.mm = new EntityInfo(val);
		setCreatorName(this.mm.getCreatorName());
		setCreatedName(this.mm.getCreatedName());
		setUpdaterName(this.mm.getUpdaterName());
		setUpdatedName(this.mm.getUpdatedName());
		setVersionName(this.mm.getVersionName());
	}

	/**
	 * 管理項目判断
	 * @param name 項目名
	 * @return 管理項目の場合 true を返す。
	 */
	public boolean isMaintenance(final String name) {
		return this.mm.getVersionName().equals(name)
			|| this.mm.getCreatorName().equals(name)
			|| this.mm.getCreatedName().equals(name)
			|| this.mm.getUpdaterName().equals(name)
			|| this.mm.getUpdatedName().equals(name);
	}

	/**
	 * 論理削除設定
	 * @param val 論理削除フラグ
	 */
	public void setLogicalDelete(final boolean val) {
		this.invisible = val;
	}

	/**
	 * 論理削除判断
	 * @return 論理削除の場合 true を返す。
	 */
	private boolean isLogicalDelete() {
		return (this.invisible == null && this.mm.isLogicalDelete())
				|| Boolean.TRUE.equals(this.invisible);
	}

	/**
	 * 検索処理
	 *
	 * @param id ID
	 * @param model 汎用モデル
	 * @return レコードが不在の場合 false を返す。
	 */
	public boolean find(final Number id, final UniModel model) {
		if (id != null) {
			final var obj = find(this.mm.getDaoClass(), NumberUtil.toLong(id));
			if (obj != null && (!isLogicalDelete() || !isDeleted(obj))) {
				ModelUtil.setBeanValue(model, obj);
				return true;
			}
		}
		ModelUtil.noValueFields(model, this.mm.getDaoClass());
		return false;
	}

	/**
	 * 検索処理
	 *
	 * @param id ID
	 * @param ver バージョン
	 * @return 他で更新されていた場合 null を返す。
	 */
	private Serializable findWithLoc(final Number id, final Number ver) {
		try {
			final var obj = findWithLock(
					this.mm.getDaoClass(), NumberUtil.toLong(id));
			if (obj != null && (!isLogicalDelete() || !isDeleted(obj))) {
				if (isUpdatable(obj, ver)) {
					return obj;
				}
			}
		} catch (final DaoLockException ex) {
			if (!ex.isNoWait()) {
				throw ex;
			}
		}
		return null;
	}

	/**
	 * 作成処理
	 *
	 * @param model 汎用モデル
	 * @return 他で更新されていた場合 false を返す。
	 */
	public boolean insert(final UniModel model) {
		final var obj = Factory.create(this.mm.getDaoClass());
		ModelUtil.setModelValue(obj, model);
		if (isLogicalDelete()) {
			removeDeleted(obj);
		}

		try {
			super.insert(obj);
			return true;
		} catch (final DaoConstraintException ex) {
			if (!ex.isNoWait()) {
				throw ex;
			}
			return false;
		}
	}

	/**
	 * 更新処理
	 *
	 * @param id ID
	 * @param ver バージョン
	 * @param model 汎用モデル
	 * @return 他で更新されていた場合 false を返す。
	 */
	public boolean update(final Number id, final Number ver, final UniModel model) {
		final var obj = findWithLoc(id, ver);
		if (obj != null) {
			ModelUtil.setModelValue(obj, model);
			try {
				return update(obj);
			} catch (final DaoConstraintException ex) {
				if (!ex.isNoWait()) {
					throw ex;
				}
				LogManager.getLogger().warn(ex.getMessage());
			}
		}
		return false;
	}

	/**
	 * 削除処理
	 *
	 * @param id ID
	 * @param ver バージョン
	 * @return 他で更新されていた場合 false を返す。
	 */
	public boolean delete(final Number id, final Number ver) {
		final var obj = findWithLoc(id, ver);
		if (obj != null) {
			try {
				// 論理削除
				if (isLogicalDelete()) {
					return invisible(obj);
				}
				return delete(obj);
			} catch (final DaoConstraintException ex) {
				if (!ex.isNoWait()) {
					throw ex;
				}
				LogManager.getLogger().warn(ex.getMessage());
			}
		}
		return false;
	}

	/**
	 * 削除判断
	 *
	 * @param obj オブジェクト
	 * @return 削除済みの場合 true を返す。
	 */
	private boolean isDeleted(final Object obj) {
		return Integer.valueOf(0).equals(DaoUtil.getValue(obj, this.mm.getVersionName()));
	}

	/**
	 * 論理削除物理削除処理
	 * @param obj 削除値保持オブジェクト
	 */
	private void removeDeleted(final Serializable obj) {
		try (var dao = JdbcSource.getDao(getSchema())) {
			final var iw = new IndexWork(this.mm.getTable());
			dao.doWork(iw);
			final var keys = iw.getUniqueIndex();
			if (0 != keys.length
					&& (1 < keys.length || !"Id".equalsIgnoreCase(keys[0]))) {
				dao.execute("DELETE FROM " + this.mm.getTable()
						+ " WHERE " + getWhere(keys), getParams(obj, keys));
			}
		}
	}

	/**
	 * 削除用条件句取得
	 * @param keys キー文字列配列
	 * @return 削除用条件句
	 */
	private String getWhere(final String[] keys) {
		final var sb = new StringJoiner(" AND ");
		var i = 1;
		for (final var key : keys) {
			sb.add(key + " = :" + i++);
		}
		sb.add(this.mm.getVersionName() + " = 0 ");
		return sb.toString();
	}

	/**
	 * 削除用パラメタ取得
	 * @param obj 削除値保持オブジェクト
	 * @param keys キー文字列配列
	 * @return 削除用パラメタ
	 */
	private Object[] getParams(final Serializable obj, final String[] keys) {
		final var ret = new ArrayList<>();
		for (final var key : keys) {
			ret.add(DaoUtil.getValue(obj, CamelCase.convert(key)));
		}
		return ret.toArray(new Object[ret.size()]);
	}

	/**
	 * ユニークインデックス取得
	 * @author Tadashi Nakayama
	 */
	private static final class IndexWork implements JdbcWork {
		/** テーブル名 */
		private final String table;
		/** インデックス */
		private String[] index;

		/**
		 * コンストラクタ
		 * @param val テーブル名
		 */
		IndexWork(final String val) {
			this.table = val;
		}

		/**
		 * ユニークインデックス取得
		 * @return ユニークインデックス
		 */
		public String[] getUniqueIndex() {
			return (this.index != null) ? this.index.clone() : new String[0];
		}

		/**
		 * @see common.db.dao.Dao.JdbcWork#execute(java.sql.Connection)
		 */
		@Override
		public void execute(final Connection conn) throws SQLException {
			final var list = getUniqueIndexList(conn);
			if (list.size() == 1) {
				this.index = list.get(0).split(",");
			} else {
				for (final var val : list) {
					if (!"id".equalsIgnoreCase(val)) {
						this.index = val.split(",");
						return;
					}
				}
				this.index = new String[0];
			}
		}

		/**
		 * ユニークインデックス取得
		 * @param conn コネクション
		 * @return ユニークインデックスリスト
		 * @throws SQLException SQL例外
		 */
		private List<String> getUniqueIndexList(final Connection conn) throws SQLException {
			final var list = new ArrayList<String>();
			final var dmd = conn.getMetaData();
			try (var rs = dmd.getIndexInfo(null, null, this.table, true, false)) {
				var sb = new StringBuilder();
				String name = null;
				while (rs.next()) {
					// ResultSetMetaData rsmd = rs.getMetaData();
					//	for (int i = 0; i < rsmd.getColumnCount(); i++) {
					// 	System.out.println(
					//	rsmd.getColumnName(i + 1) + ":" + rs.getObject(i + 1));
					// }
					if (name != null && !name.equalsIgnoreCase(rs.getString("INDEX_NAME"))) {
						list.add(sb.toString());
						sb = new StringBuilder();
					}
					if (0 < sb.length()) {
						sb.append(",");
					}
					sb.append(rs.getString("COLUMN_NAME"));
					name = rs.getString("INDEX_NAME");
				}
				if (0 < sb.length()) {
					list.add(sb.toString());
				}
			}
			return list;
		}
	}
}
