package ru.yandex.qe.dispenser.datasources;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.qe.dispenser.domain.util.FunctionalUtils;
import ru.yandex.monlib.metrics.labels.Labels;
import ru.yandex.monlib.metrics.registry.MetricRegistry;

public class ConnectionPoolSensors {

    private static final Logger LOG = LoggerFactory.getLogger(ConnectionPoolSensors.class);

    private static final String DB_POOL_BUSY_CONNECTIONS = "db.pool.busy_connections";
    private static final String DB_POOL_IDLE_CONNECTIONS = "db.pool.idle_connections";
    private static final String DB_POOL_CONNECTIONS = "db.pool.connections";
    private static final String DB_POOL_UNCLOSED_ORPHANED_CONNECTIONS = "db.pool.unclosed_orphaned_connections";
    private static final String DB_POOL_CACHED_STATEMENTS = "db.pool.stmt_cache.cached";
    private static final String DB_POOL_CHECKED_OUT_STATEMENTS = "db.pool.stmt_cache.checked_out";
    private static final String DB_POOL_CONNECTIONS_WITH_STATEMENTS = "db.pool.stmt_cache.connections";
    private static final String DB_POOL_STATEMENTS_DEFERRED_CLOSE = "db.pool.stmt_cache.deferred_close";
    private static final String DB_POOL_CONNECTIONS_USED_BY_STATEMENT_DESTROYER = "db.pool.stmt_destroyer.conn_used";
    private static final String DB_POOL_CONNECTIONS_WITH_DEFERRED_DESTROY = "db.pool.stmt_destroyer.conn_deferred_destroy";
    private static final String DB_POOL_DEFERRED_DESTROY_STATEMENTS = "db.pool.stmt_destroyer.deferred_destroy_stmt";
    private static final String DB_POOL_POOL_SIZE = "db.pool.pool.size";
    private static final String DB_POOL_POOL_ACTIVE = "db.pool.pool.active";
    private static final String DB_POOL_POOL_IDLE = "db.pool.pool.idle";
    private static final String DB_POOL_POOL_PENDING = "db.pool.pool.pending";
    private static final String DB_POOL_STATEMENT_DESTROYER_THREADS = "db.pool.stmt_destroyer.threads";
    private static final String DB_POOL_STATEMENT_DESTROYER_ACTIVE = "db.pool.stmt_destroyer.active";
    private static final String DB_POOL_STATEMENT_DESTROYER_IDLE = "db.pool.stmt_destroyer.idle";
    private static final String DB_POOL_STATEMENT_DESTROYER_PENDING = "db.pool.stmt_destroyer.pending";
    private static final String DB_POOL_USER_POOLS = "db.pool.user_pools";
    private static final String DB_POOL_HELPER_THREADS = "db.pool.helper_threads";
    private static final String DB_POOL_AWAITING_CHECKOUT_DEFAULT_USER = "db.pool.awaiting_checkout";
    private static final String DB_POOL_EFFECTIVE_PROPERTY_CYCLE_DEFAULT_USER = "db.pool.eff_property_cycle";
    private static final String DB_DATA_SOURCE = "db_data_source";
    private static final long INITIAL_DELAY_SECONDS = 0L;
    private static final long DELAY_SECONDS = 1L;

    @NotNull
    private final ComboPooledDataSource dataSource;
    @NotNull
    private final ScheduledExecutorService schedulerExecutorService;

    private volatile ScheduledFuture<?> scheduledFuture;
    private volatile int busyConnections = 0;
    private volatile int idleConnections = 0;
    private volatile int connections = 0;
    private volatile int unclosedOrphanedConnections = 0;
    private volatile int cachedStatements = 0;
    private volatile int checkedOutCachedStatements = 0;
    private volatile int connectionsWithCachedStatements = 0;
    private volatile int cachedStatementsDeferredCloseThreads = 0;
    private volatile int connectionsInUseByStatementDestroyer = 0;
    private volatile int connectionsWithDeferredDestroyStatements = 0;
    private volatile int deferredDestroyStatements = 0;
    private volatile int threadPoolSize = 0;
    private volatile int activeThreads = 0;
    private volatile int idleThreads = 0;
    private volatile int tasksPending = 0;
    private volatile int statementDestroyerThreads = 0;
    private volatile int statementDestroyerActiveThreads = 0;
    private volatile int statementDestroyerIdleThreads = 0;
    private volatile int statementDestroyerTasksPending = 0;
    private volatile int userPools = 0;
    private volatile int helperThreads = 0;
    private volatile int threadAwaitingCheckoutForDefaultUser = 0;
    private volatile float effectivePropertyCycleForDefaultUser = 0.0f;

    public ConnectionPoolSensors(@NotNull final MetricRegistry registry, @NotNull final String dataSourceKey,
                                 @NotNull final ComboPooledDataSource dataSource) {
        this.dataSource = dataSource;
        final ThreadFactory threadFactory = new ThreadFactoryBuilder()
                .setDaemon(true)
                .setNameFormat("db-connection-pool-sensors-pool-%d")
                .setUncaughtExceptionHandler((t, e) -> LOG.error("Uncaught exception in thread " + t, e))
                .build();
        final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1, threadFactory);
        scheduledThreadPoolExecutor.setRemoveOnCancelPolicy(true);
        this.schedulerExecutorService = scheduledThreadPoolExecutor;
        registry.lazyGaugeInt64(DB_POOL_BUSY_CONNECTIONS, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> busyConnections);
        registry.lazyGaugeInt64(DB_POOL_IDLE_CONNECTIONS, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> idleConnections);
        registry.lazyGaugeInt64(DB_POOL_CONNECTIONS, Labels.of(DB_DATA_SOURCE, dataSourceKey), () -> connections);
        registry.lazyGaugeInt64(DB_POOL_UNCLOSED_ORPHANED_CONNECTIONS, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> unclosedOrphanedConnections);
        registry.lazyGaugeInt64(DB_POOL_CACHED_STATEMENTS, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> cachedStatements);
        registry.lazyGaugeInt64(DB_POOL_CHECKED_OUT_STATEMENTS, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> checkedOutCachedStatements);
        registry.lazyGaugeInt64(DB_POOL_CONNECTIONS_WITH_STATEMENTS, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> connectionsWithCachedStatements);
        registry.lazyGaugeInt64(DB_POOL_STATEMENTS_DEFERRED_CLOSE, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> cachedStatementsDeferredCloseThreads);
        registry.lazyGaugeInt64(DB_POOL_CONNECTIONS_USED_BY_STATEMENT_DESTROYER, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> connectionsInUseByStatementDestroyer);
        registry.lazyGaugeInt64(DB_POOL_CONNECTIONS_WITH_DEFERRED_DESTROY, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> connectionsWithDeferredDestroyStatements);
        registry.lazyGaugeInt64(DB_POOL_DEFERRED_DESTROY_STATEMENTS, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> deferredDestroyStatements);
        registry.lazyGaugeInt64(DB_POOL_POOL_SIZE, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> threadPoolSize);
        registry.lazyGaugeInt64(DB_POOL_POOL_ACTIVE, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> activeThreads);
        registry.lazyGaugeInt64(DB_POOL_POOL_IDLE, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> idleThreads);
        registry.lazyGaugeInt64(DB_POOL_POOL_PENDING, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> tasksPending);
        registry.lazyGaugeInt64(DB_POOL_STATEMENT_DESTROYER_THREADS, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> statementDestroyerThreads);
        registry.lazyGaugeInt64(DB_POOL_STATEMENT_DESTROYER_ACTIVE, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> statementDestroyerActiveThreads);
        registry.lazyGaugeInt64(DB_POOL_STATEMENT_DESTROYER_IDLE, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> statementDestroyerIdleThreads);
        registry.lazyGaugeInt64(DB_POOL_STATEMENT_DESTROYER_PENDING, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> statementDestroyerTasksPending);
        registry.lazyGaugeInt64(DB_POOL_USER_POOLS, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> userPools);
        registry.lazyGaugeInt64(DB_POOL_HELPER_THREADS, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> helperThreads);
        registry.lazyGaugeInt64(DB_POOL_AWAITING_CHECKOUT_DEFAULT_USER, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> threadAwaitingCheckoutForDefaultUser);
        registry.lazyGaugeDouble(DB_POOL_EFFECTIVE_PROPERTY_CYCLE_DEFAULT_USER, Labels.of(DB_DATA_SOURCE, dataSourceKey),
                () -> effectivePropertyCycleForDefaultUser);
    }

    @PostConstruct
    public void postConstruct() {
        LOG.info("Starting db connection pool sensors update scheduler...");
        scheduledFuture = schedulerExecutorService.scheduleWithFixedDelay(() -> {
            try {
                busyConnections = dataSource.getNumBusyConnectionsAllUsers();
                idleConnections = dataSource.getNumIdleConnectionsAllUsers();
                connections = dataSource.getNumConnectionsAllUsers();
                unclosedOrphanedConnections = dataSource.getNumUnclosedOrphanedConnectionsAllUsers();
                cachedStatements = dataSource.getStatementCacheNumStatementsAllUsers();
                checkedOutCachedStatements = dataSource.getStatementCacheNumCheckedOutStatementsAllUsers();
                connectionsWithCachedStatements = dataSource.getStatementCacheNumConnectionsWithCachedStatementsAllUsers();
                cachedStatementsDeferredCloseThreads = dataSource.getStatementCacheNumDeferredCloseThreads();
                connectionsInUseByStatementDestroyer = dataSource.getStatementDestroyerNumConnectionsInUseAllUsers();
                connectionsWithDeferredDestroyStatements = dataSource.getStatementDestroyerNumConnectionsWithDeferredDestroyStatementsAllUsers();
                deferredDestroyStatements = dataSource.getStatementDestroyerNumDeferredDestroyStatementsAllUsers();
                threadPoolSize = dataSource.getThreadPoolSize();
                activeThreads = dataSource.getThreadPoolNumActiveThreads();
                idleThreads = dataSource.getThreadPoolNumIdleThreads();
                tasksPending = dataSource.getThreadPoolNumTasksPending();
                statementDestroyerThreads = dataSource.getStatementDestroyerNumThreads();
                statementDestroyerActiveThreads = dataSource.getStatementDestroyerNumActiveThreads();
                statementDestroyerIdleThreads = dataSource.getStatementDestroyerNumIdleThreads();
                statementDestroyerTasksPending = dataSource.getStatementDestroyerNumTasksPending();
                userPools = dataSource.getNumUserPools();
                helperThreads = dataSource.getNumHelperThreads();
                threadAwaitingCheckoutForDefaultUser = dataSource.getNumThreadsAwaitingCheckoutDefaultUser();
                effectivePropertyCycleForDefaultUser = dataSource.getEffectivePropertyCycleDefaultUser();
            } catch (final Throwable e) {
                FunctionalUtils.throwIfUnrecoverable(e);
                LOG.error("Db connection pool sensors update failure", e);
            }
        }, INITIAL_DELAY_SECONDS, DELAY_SECONDS, TimeUnit.SECONDS);
        LOG.info("Db connection pool sensors update scheduler started successfully");
    }

    @PreDestroy
    public void preDestroy() {
        LOG.info("Stopping db connection pool sensors update scheduler...");
        if (scheduledFuture != null) {
            scheduledFuture.cancel(true);
            scheduledFuture = null;
        }
        schedulerExecutorService.shutdown();
        try {
            schedulerExecutorService.awaitTermination(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        schedulerExecutorService.shutdownNow();
        LOG.info("Db connection pool sensors update scheduler stopped successfully");
    }

}
