package com.mzh.library.util;

import com.mzh.library.exception.DaoException;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

public final class JdbcUtil {
    private static final Logger LOGGER = Logger.getLogger(JdbcUtil.class.getName());
    private static final String CONFIG_FILE = "db.properties";
    private static final String DEFAULT_DRIVER = "com.mysql.cj.jdbc.Driver";
    private static final String DRIVER_KEY = "db.driver";
    private static final String URL_KEY = "db.url";
    private static final String USERNAME_KEY = "db.username";
    private static final String PASSWORD_KEY = "db.password";

    @FunctionalInterface
    public interface TransactionCallback<T> {
        T execute(Connection connection) throws SQLException;
    }

    private JdbcUtil() {
    }

    public static Connection getConnection() {
        Properties properties = loadProperties();
        String driver = properties.getProperty(DRIVER_KEY, DEFAULT_DRIVER);
        String url = required(properties, URL_KEY);
        String username = required(properties, USERNAME_KEY);
        String password = required(properties, PASSWORD_KEY);

        LOGGER.info("Database connection configuration resolved"
            + " file=" + CONFIG_FILE
            + " driverKey=" + DRIVER_KEY
            + " driver=" + safeLogValue(driver)
            + " jdbcUrl=" + redactJdbcUrl(url)
            + " usernameKey=" + USERNAME_KEY
            + " usernameConfigured=" + !username.isEmpty()
            + " password=<redacted>");
        LOGGER.info("Database connection attempt"
            + " driverKey=" + DRIVER_KEY
            + " driver=" + safeLogValue(driver)
            + " jdbcUrl=" + redactJdbcUrl(url)
            + " usernameKey=" + USERNAME_KEY);

        try {
            Class.forName(driver);
            Connection connection = DriverManager.getConnection(url, username, password);
            LOGGER.info("Database connection established jdbcUrl=" + redactJdbcUrl(url)
                + " usernameKey=" + USERNAME_KEY);
            return connection;
        } catch (ClassNotFoundException ex) {
            LOGGER.log(Level.SEVERE, "JDBC driver unavailable driver=" + safeLogValue(driver)
                + " jdbcUrl=" + redactJdbcUrl(url)
                + " usernameKey=" + USERNAME_KEY, ex);
            throw new DaoException("Unable to open database connection", ex);
        } catch (SQLException ex) {
            SQLException safeException = safeSqlException(ex);
            LOGGER.log(Level.SEVERE, "Database connection failed driver=" + safeLogValue(driver)
                + " jdbcUrl=" + redactJdbcUrl(url)
                + " usernameKey=" + USERNAME_KEY, safeException);
            throw new DaoException("Unable to open database connection", safeException);
        }
    }

    public static <T> T executeInTransaction(TransactionCallback<T> callback) {
        try (Connection connection = getConnection()) {
            connection.setAutoCommit(false);
            try {
                T result = callback.execute(connection);
                connection.commit();
                return result;
            } catch (SQLException ex) {
                rollbackQuietly(connection);
                throw new DaoException("Unable to complete database transaction", ex);
            } catch (RuntimeException ex) {
                rollbackQuietly(connection);
                throw ex;
            }
        } catch (SQLException ex) {
            throw new DaoException("Unable to complete database transaction", ex);
        }
    }

    private static void rollbackQuietly(Connection connection) {
        try {
            connection.rollback();
        } catch (SQLException ignored) {
            // Preserve the original transaction failure for callers and logs.
        }
    }

    private static Properties loadProperties() {
        try (InputStream inputStream = Thread.currentThread()
            .getContextClassLoader()
            .getResourceAsStream(CONFIG_FILE)) {
            if (inputStream == null) {
                LOGGER.severe("Database configuration file not found file=" + CONFIG_FILE);
                throw new DaoException("Missing database configuration file: " + CONFIG_FILE, null);
            }

            Properties properties = new Properties();
            properties.load(inputStream);
            LOGGER.info("Database configuration loaded file=" + CONFIG_FILE
                + " driverConfigured=" + hasText(properties, DRIVER_KEY)
                + " urlConfigured=" + hasText(properties, URL_KEY)
                + " usernameConfigured=" + hasText(properties, USERNAME_KEY)
                + " passwordConfigured=" + hasText(properties, PASSWORD_KEY));
            return properties;
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, "Unable to read database configuration file=" + CONFIG_FILE, ex);
            throw new DaoException("Unable to read database configuration", ex);
        }
    }

    private static String required(Properties properties, String key) {
        String value = properties.getProperty(key);
        if (value == null || value.trim().isEmpty()) {
            LOGGER.severe("Missing database configuration value key=" + key);
            throw new DaoException("Missing database configuration value: " + key, null);
        }
        return value.trim();
    }

    private static String redactJdbcUrl(String value) {
        if (value == null) {
            return "";
        }
        return safeLogValue(redactSensitive(value));
    }

    private static SQLException safeSqlException(SQLException ex) {
        SQLException safeException = new SQLException(
            safeLogValue(redactSensitive(ex.getMessage())),
            ex.getSQLState(),
            ex.getErrorCode()
        );
        safeException.setStackTrace(ex.getStackTrace());
        return safeException;
    }

    private static String redactSensitive(String value) {
        if (value == null) {
            return "";
        }
        return value.replaceAll("(?i)(password|pwd|pass|secret|token)(\\s*[=:]\\s*)([^;&\\s]*)", "$1$2<redacted>");
    }

    private static boolean hasText(Properties properties, String key) {
        String value = properties.getProperty(key);
        return value != null && !value.trim().isEmpty();
    }

    private static String safeLogValue(String value) {
        if (value == null) {
            return "";
        }

        StringBuilder builder = new StringBuilder();
        int limit = Math.min(value.length(), 240);
        for (int i = 0; i < limit; i++) {
            char current = value.charAt(i);
            builder.append(Character.isISOControl(current) ? '?' : current);
        }
        if (value.length() > limit) {
            builder.append("...");
        }
        return builder.toString();
    }
}
