package common.db.jdbc;

import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.apache.logging.log4j.LogManager;

import core.config.Env;
import core.config.Factory;
import core.exception.PhysicalException;
import core.exception.ThrowableUtil;

/**
 * DB接続管理実装
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class JdbcSessionImpl implements JdbcSession {

	/** デフォルト接続先名 */
	private static final String ENV_JDBC_DEFAULT = "DB";
	/** JDBC Property: DataSource Name */
	private static final String ENV_JDBC_DATASOURCE = ".Source";
	/** JDBC Property: URL */
	private static final String ENV_JDBC_URL = ".Url";
	/** JDBC Property: User */
	private static final String ENV_JDBC_USER = ".User";
	/** JDBC Property: Password */
	private static final String ENV_JDBC_PASSWD = ".Password";
	/** JDBC Property: Charset */
	private static final String ENV_JDBC_CHARSET = ".Charset";
	/** JDBC Property: Driver */
	private static final String ENV_JDBC_DRIVER = ".Driver";

	/** JDBC Property: initial context factory */
	private static final String ENV_JNDI_INITIAL_CONTEXT_FACTORY = "Jndi.InitialContextFactory";

	/** 自身のインスタンス */
	private static final AtomicReference<JdbcSessionImpl> INSTANCE = new AtomicReference<>();

	/** データソースマップ */
	private final ConcurrentMap<String, DataSource> datasource = new ConcurrentHashMap<>();

	static {
		for (final var e : Env.entrySet()) {
			if (e.getKey().endsWith(ENV_JDBC_DRIVER)) {
				// to register a Driver to DriverManager
				Factory.create(Factory.loadClass(e.getValue()).asSubclass(Driver.class));
			}
		}
	}

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

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

	/**
	 * DBエンコーディング取得
	 *
	 * @param name プロパティ項目名
	 * @return エンコーディング文字列
	 */
	@Override
	public Charset getCharset(final String name) {
		final var charset = Env.getEnv(toValidName(name) + ENV_JDBC_CHARSET);
		return Objects.toString(charset, "").isEmpty() ? null : Charset.forName(charset);
	}

	/**
	 * データソース取得処理
	 *
	 * @param jndi JNDI名
	 * @return DataSource
	 */
	private DataSource getDataSource(final String jndi) {
		final Supplier<DataSource> supplier = () -> {
			Context ctx = null;
			try {
				ctx = getContext();
				final var src = DataSource.class.cast(ctx.lookup(jndi));
				final var ds = this.datasource.putIfAbsent(jndi, src);
				return (ds != null) ? ds : src;
			} catch (final NamingException ex) {
				LogManager.getLogger().error(ex.getMessage(), ex);
				throw new PhysicalException(ex);
			} finally {
				if (ctx != null) {
					try {
						ctx.close();
					} catch (final NamingException ex) {
						LogManager.getLogger().warn(ex.getMessage(), ex);
					}
				}
			}
		};
		return Objects.requireNonNullElseGet(this.datasource.get(jndi), supplier);
	}

	/**
	 * イニシャルコンテキスト取得
	 *
	 * @return イニシャルコンテキスト
	 * @throws NamingException Naming例外
	 */
	private Context getContext() throws NamingException {
		// イニシャルコンテキスト取得
		final var initial = Env.getEnv(ENV_JNDI_INITIAL_CONTEXT_FACTORY);
		if (!Objects.toString(initial, "").trim().isEmpty()) {
			return new InitialContext(new Hashtable<>(
					Collections.singletonMap(Context.INITIAL_CONTEXT_FACTORY, initial)));
		}
		return new InitialContext();
	}

	/**
	 * コネクション取得
	 *
	 * @param name コネクション名
	 * @return コネクション
	 */
	@Override
	public Connection getConnection(final String name) {
		final var source = Env.getEnv(toValidName(name) + ENV_JDBC_DATASOURCE);
		if (Objects.toString(source, "").trim().isEmpty()) {
			final var ret = getConnectionFromProvider(name);
			return (ret != null) ? ret : newConnection(name);
		}
		return getConnectionFromDataSource(source);
	}

	/**
	 * 適正接続名化
	 *
	 * @param name 接続名
	 * @return 適正接続名
	 */
	private String toValidName(final String name) {
		return Objects.toString(name, "").isEmpty() ? ENV_JDBC_DEFAULT : name;
	}

	/**
	 * コネクションプロバイダから取得
	 *
	 * @param name 接続名
	 * @return コネクション
	 */
	private Connection getConnectionFromProvider(final String name) {
		try {
			final var dsp = Factory.create(JdbcDataSourceProvider.class);
			if (dsp != null) {
				final var conn = dsp.getDataSource(toValidName(name)).getConnection();
				conn.setAutoCommit(false);
				return conn;
			}
			return null;
		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * データソースからコネクション取得
	 *
	 * @param source ソース名
	 * @return コネクション
	 */
	private Connection getConnectionFromDataSource(final String source) {
		try {
			final var ds = getDataSource(source);
			if (ds != null) {
				final var conn = ds.getConnection();
				conn.setAutoCommit(false);
				return conn;
			}
			return null;

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

	/**
	 * コネクション取得
	 *
	 * @param name 接続名
	 * @return コネクション
	 */
	@Override
	public Connection newConnection(final String name) {
		try {
			final var conn = DriverManager.getConnection(
					getUrl(name), getUser(name), getPassword(name));
			conn.clearWarnings();
			conn.setReadOnly(false);
			conn.setAutoCommit(false);
			return conn;
		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * ユーザ取得
	 *
	 * @param name 接続名
	 * @return ユーザ
	 */
	@Override
	public String getUser(final String name) {
		return Env.getEnv(toValidName(name) + ENV_JDBC_USER);
	}

	/**
	 * パスワード取得
	 *
	 * @param name 接続名
	 * @return パスワード
	 */
	@Override
	public String getPassword(final String name) {
		return Env.getEnv(toValidName(name) + ENV_JDBC_PASSWD);
	}

	/**
	 * 接続Url取得
	 *
	 * @param name 接続名
	 * @return 接続Url
	 */
	@Override
	public String getUrl(final String name) {
		return Env.getEnv(toValidName(name) + ENV_JDBC_URL);
	}
}
