package project.common.master;

import java.lang.ref.SoftReference;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;

import common.db.JdbcSource;
import common.db.jdbc.Jdbc;
import common.master.IntervalTimer.IntervalCache;
import common.sql.QueryUtil;
import core.config.Env;
import core.config.Factory;
import core.exception.PhysicalException;
import core.exception.ThrowableUtil;
import core.util.MapUtil;

/**
 * メッセージ保持実装
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class MsgImpl implements Msg, IntervalCache {

	/** プロパティ名 */
	private static final String PROP_NAME = "message";

	/** メッセージコード */
	private static final String NO_CODE = "E99999";
	/** メッセージステータス */
	private static final String NO_STS = "E";
	/** メッセージ */
	private static final String NO_MSG = "メッセージ {0} は登録されていません。";

	/** メッセージファイルアクセスキー ステータス */
	private static final String KEY_STS = "Status.";
	/** メッセージファイルアクセスキー メッセージ */
	private static final String KEY_MSG = "Message.";

	/** 自身保持用 */
	private static final AtomicReference<MsgImpl> INSTANCE = new AtomicReference<>();

	/** フォーマッタ */
	private volatile MsgFormatter format = Factory.create(MsgFormatter.class);

	/** メッセージ格納用ハッシュ (ファイルとDB先読）*/
	private final ConcurrentMap<String, String> propMsg = new ConcurrentHashMap<>();
	/** メッセージ格納用ハッシュ (DB)*/
	private final AtomicReference<SoftReference<ConcurrentMap<String, String>>> message =
					new AtomicReference<>(new SoftReference<>(null));

	/**
	 * コンストラクタ
	 */
	private MsgImpl() {
		if (INSTANCE.get() != null) {
			throw new AssertionError();
		}
	}

	/**
	 * インスタンス取得
	 *
	 * @return インスタンス
	 */
	public static MsgImpl getInstance() {
		if (INSTANCE.get() == null) {
			INSTANCE.compareAndSet(null, new MsgImpl());
		}
		return INSTANCE.get();
	}

	/**
	 * メッセージフォーマッタ設定
	 *
	 * @param fmt メッセージフォーマッタ
	 */
	@Override
	public void setMsgFormatter(final MsgFormatter fmt) {
		if (fmt != null) {
			this.format = fmt;
		}
	}

	/**
	 * メッセージ存在確認
	 *
	 * @param code メッセージコード
	 * @return 存在している場合 true を返す。
	 */
	@Override
	public boolean contains(final String code) {
		final var key = KEY_MSG + code;
		final var map = MapUtil.getCacheMap(this.message);
		return this.propMsg.containsKey(key) || map.containsKey(key);
	}

	/**
	 * ソフト参照メッセージ設定
	 *
	 * @param code メッセージID
	 * @param msg メッセージ
	 * @param sts ステータス
	 */
	@Override
	public void setSoftMessage(final String code, final String msg, final String sts) {
		final var map = MapUtil.getCacheMap(this.message);
		map.putIfAbsent(KEY_MSG + code, msg);
		map.putIfAbsent(KEY_STS + code, sts);
	}

	/**
	 * 強参照メッセージ設定
	 *
	 * @param code メッセージID
	 * @param msg メッセージ
	 * @param sts ステータス
	 */
	@Override
	public void setHardMessage(final String code, final String msg, final String sts) {
		this.propMsg.putIfAbsent(KEY_MSG + code, msg);
		this.propMsg.putIfAbsent(KEY_STS + code, sts);
	}

	/**
	 * 初期処理を行う。
	 *
	 */
	@Override
	public void initialize() {
		try (var conn = JdbcSource.getConnection()) {
			initialize(conn);
		}
	}

	/**
	 * 初期処理を行う。
	 *
	 * @param conn コネクション
	 */
	public void init(final Connection conn) {
		initialize(conn);
	}

	/**
	 * 初期化処理
	 *
	 * @param conn コネクション
	 */
	private synchronized void initialize(final Connection conn) {
		// Message.propertiesからメッセージ取得
		final var map = MapUtil.getCacheMap(this.message);
		map.clear();
		this.propMsg.clear();
		setPropMessage();
		if (conn != null) {
			this.propMsg.putAll(getMessage(conn));
		}
	}

	/**
	 * プロパティメッセージ設定
	 */
	@Override
	public void setPropMessage() {
		Env.setProperties(PROP_NAME, this.propMsg);
	}

	/**
	 * メッセージを返す。
	 *
	 * @param code メッセージコード
	 * @param vals 可変項目
	 * @return String メッセージ
	 */
	@Override
	public String getMessage(final String code, final String... vals) {
		return getMessage(null, code, vals);
	}

	/**
	 * メッセージを返す。
	 *
	 * @param code メッセージコード
	 * @param vals 可変項目
	 * @param conn コネクション
	 * @return String メッセージ
	 */
	public String getMessage(final Connection conn,
			final String code, final String... vals) {
		final var key = KEY_MSG + code;
		if (this.propMsg.containsKey(key)) {
			final var msg = this.propMsg.get(key);
			return this.format.format(code, msg, vals);
		}

		final var msg = getMsgSts(key, code, conn);
		if (!Objects.toString(msg, "").trim().isEmpty()) {
			return this.format.format(code, msg, vals);
		}

		return getNoMessage(code);
	}

	/**
	 * ステータスを返す。
	 *
	 * @param code メッセージコード
	 * @return String ステータス
	 */
	@Override
	public String getStatus(final String code) {
		return getStatus(null, code);
	}

	/**
	 * ステータスを返す。
	 *
	 * @param conn コネクション
	 * @param code メッセージコード
	 * @return String ステータス
	 */
	public String getStatus(final Connection conn, final String code) {
		final var key = KEY_STS + code;
		if (this.propMsg.containsKey(key)) {
			return this.propMsg.get(key);
		}

		final var sts = getMsgSts(key, code, conn);
		if (!Objects.toString(sts, "").trim().isEmpty()) {
			return sts;
		}
		return getNoStatus();
	}

	/**
	 * メッセージが存在しないメッセージを返す。
	 *
	 * @param code 非存在メッセージコード
	 * @return メッセージ
	 */
	public String getNoMessage(final String code) {
		return this.format.format(NO_CODE, NO_MSG, code);
	}

	/**
	 * メッセージが存在しないステータスを返す。
	 *
	 * @return ステータス
	 */
	public String getNoStatus() {
		return NO_STS;
	}

	/**
	 * メッセージステータス取得
	 *
	 * @param key キー
	 * @param code メッセージコード
	 * @param conn コネクション
	 * @return メッセージステータス
	 */
	private String getMsgSts(final String key, final String code, final Connection conn) {

		final var cache = MapUtil.getCacheMap(this.message);
		final var ret = cache.get(key);
		if (ret == null) {
			// DBエラーからロールバックする間でも取得できるように
			try (var con = JdbcSource.newConnection()) {
				final var map = getMessage(Objects.requireNonNullElse(con, conn), code);
				if (map.isEmpty()) {
					cache.putIfAbsent(key, "");
				} else {
					for (final var me : map.entrySet()) {
						cache.putIfAbsent(me.getKey(), me.getValue());
					}
				}
				return map.get(key);
			}
		}

		return ret;
	}

	/**
	 * メッセージ取得
	 *
	 * @param conn コネクション
	 * @param code コード
	 * @return マップ
	 */
	private Map<String, String> getMessage(final Connection conn, final String code) {
		final var query = QueryUtil.getSqlFromFile("SelectMessage", this.getClass());
		try (var psmt = QueryUtil.createStatement(query, Collections.singletonMap("MsgId", code),
				Jdbc.wrap(conn)::readonlyStatement)) {
			return getMessage(psmt);
		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * メッセージ取得
	 *
	 * @param conn コネクション
	 * @return マップ
	 */
	private Map<String, String> getMessage(final Connection conn) {
		final var query = QueryUtil.getSqlFromFile("SelectPreloadMessage", this.getClass());
		try (var psmt = Jdbc.wrap(conn).readonlyStatement(query)) {
			psmt.setFetchSize(1000);
			return getMessage(psmt);
		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * メッセージ取得
	 *
	 * @param psmt ステートメント
	 * @return マップ
	 * @throws SQLException SQL例外
	 */
	private Map<String, String> getMessage(final PreparedStatement psmt) throws SQLException {
		final var id = getColumnName("MSG_ID");
		final var msg = getColumnName("MSG_CONTENT");
		final var sts = getColumnName("MSG_TYPE");
		final var ret = new HashMap<String, String>();
		try (var rs = psmt.executeQuery()) {
			while (rs.next()) {
				final String code = rs.getString(id);
				ret.put(KEY_MSG + code, rs.getString(msg));
				ret.put(KEY_STS + code, rs.getString(sts));
			}
		}
		return ret;
	}

	/**
	 * カラム名取得
	 *
	 * @param val 項目名
	 * @return カラム名
	 */
	private String getColumnName(final String val) {
		final var quote = "\"";
		var ret = val;
		if (ret != null) {
			if (!ret.isEmpty() && ret.startsWith(quote)) {
				ret = ret.substring(quote.length());
			}
			if (!ret.isEmpty() && ret.endsWith(quote)) {
				ret = ret.substring(0, ret.length() - quote.length());
			}
		}
		return ret;
	}

	/**
	 * デフォルトフォーマッタ
	 *
	 * @author Tadashi Nakayama
	 * @version 1.0.0
	 */
	public static class MsgFormatterImpl implements Msg.MsgFormatter {
		/** メッセージセパレータ */
		private static final String MSG_SEPARATOR1 = "[";
		/** メッセージセパレータ */
		private static final String MSG_SEPARATOR2 = "]";

		/**
		 * フォーマット処理
		 *
		 * @param code メッセージコード
		 * @param msg メッセージ
		 * @param vals 可変項目
		 * @return フォーマット後文字列
		 */
		@Override
		public String format(final String code, final String msg, final String... vals) {
			var m = msg.replace("\\n", "\n");
			if (vals != null && 0 < vals.length) {
				m = new MessageFormat(m).format(vals);
			}
			return m + MSG_SEPARATOR1 + code.trim() + MSG_SEPARATOR2;
		}
	}
}
