"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.clickHouseService = exports.ClickHouseService = void 0;
const client_1 = require("@clickhouse/client");
const logger_1 = __importDefault(require("../../logger"));
class ClickHouseService {
    constructor() {
        this.client = null;
        this.config = null;
        this.credentials = null;
        this.isInitialized = false;
        this.initPromise = null;
        // Initialize retry configuration from environment variables
        this.retryConfig = {
            maxRetries: Number.parseInt(process.env.CLICKHOUSE_MAX_RETRIES || '3'),
            initialDelayMs: Number.parseInt(process.env.CLICKHOUSE_RETRY_INITIAL_DELAY_MS || '100'),
            maxDelayMs: Number.parseInt(process.env.CLICKHOUSE_RETRY_MAX_DELAY_MS || '5000'),
            backoffMultiplier: Number.parseFloat(process.env.CLICKHOUSE_RETRY_BACKOFF_MULTIPLIER || '2'),
        };
    }
    /**
     * Check if an error is retryable (503, 502, 504, network errors, timeouts)
     */
    isRetryableError(error) {
        if (!error) {
            return false;
        }
        const errorMessage = String(error.message || error).toLowerCase();
        const errorCode = error.code || error.status || error.statusCode;
        // HTTP status codes that are retryable
        const retryableStatusCodes = [502, 503, 504, 408, 429];
        if (errorCode && retryableStatusCodes.includes(Number(errorCode))) {
            return true;
        }
        // Network and connection errors
        const retryableErrorPatterns = [
            'service unavailable',
            'bad gateway',
            'gateway timeout',
            'timeout',
            'econnrefused',
            'econnreset',
            'enotfound',
            'eai_again',
            'socket hang up',
            'connection closed',
            'connection reset',
            'network error',
            'temporary failure',
            'too many connections',
            'connection pool',
        ];
        return retryableErrorPatterns.some((pattern) => errorMessage.includes(pattern));
    }
    /**
     * Sleep for a given number of milliseconds
     */
    sleep(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
    }
    /**
     * Execute a function with retry logic and exponential backoff
     */
    async executeWithRetry(operation, operationName) {
        let lastError;
        let delay = this.retryConfig.initialDelayMs;
        for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
            try {
                return await operation();
            }
            catch (error) {
                lastError = error;
                // Don't retry on the last attempt
                if (attempt === this.retryConfig.maxRetries) {
                    break;
                }
                // Only retry if the error is retryable
                if (!this.isRetryableError(error)) {
                    logger_1.default.debug(`ClickHouse ${operationName} failed with non-retryable error: ${error}`);
                    throw error;
                }
                // Log retry attempt
                logger_1.default.warn(`ClickHouse ${operationName} failed (attempt ${attempt + 1}/${this.retryConfig.maxRetries + 1}): ${error}. Retrying in ${delay}ms...`);
                // Wait before retrying with exponential backoff
                await this.sleep(delay);
                // Calculate next delay with exponential backoff, capped at maxDelayMs
                delay = Math.min(delay * this.retryConfig.backoffMultiplier, this.retryConfig.maxDelayMs);
                // If connection-related error, try to reset the pool on the second attempt
                if (attempt === 1 && this.isRetryableError(error)) {
                    try {
                        logger_1.default.info('Attempting to reset ClickHouse connection pool due to persistent errors');
                        await this.resetPool();
                    }
                    catch (resetError) {
                        logger_1.default.warn(`Failed to reset connection pool: ${resetError}`);
                    }
                }
            }
        }
        // All retries exhausted
        logger_1.default.error(`ClickHouse ${operationName} failed after ${this.retryConfig.maxRetries + 1} attempts: ${lastError}`);
        throw lastError;
    }
    /**
     * Initialize the ClickHouse service with credentials and connection pooling
     * This function fetches credentials and sets up the connection pool
     * Uses a promise lock to prevent race conditions during initialization
     */
    async init() {
        // If already initialized, return immediately
        if (this.isInitialized && this.client) {
            logger_1.default.debug('ClickHouse service already initialized, reusing existing connection pool');
            return;
        }
        // If initialization is in progress, wait for it
        if (this.initPromise) {
            logger_1.default.debug('ClickHouse initialization already in progress, waiting...');
            return this.initPromise;
        }
        // Start initialization and store the promise
        this.initPromise = this._doInit();
        try {
            await this.initPromise;
        }
        finally {
            // Clear the promise after initialization completes (success or failure)
            this.initPromise = null;
        }
    }
    /**
     * Internal initialization method
     */
    async _doInit() {
        try {
            // Mock credentials fetch - replace with actual implementation
            this.credentials = await this.fetchCredentials();
            // Initialize ClickHouse configuration with connection pooling
            this.config = {
                host: process.env.CLICKHOUSE_DB_HOST || 'localhost',
                // port: Number.parseInt(process.env.CLICKHOUSE_PORT || '8123'),
                username: process.env.CLICKHOUSE_DB_USERNAME || 'default',
                password: process.env.CLICKHOUSE_DB_PASSWORD || '',
                database: process.env.CLICKHOUSE_DB_NAME || 'default',
                // Connection pool configuration
                max_open_connections: Number.parseInt(process.env.CLICKHOUSE_MAX_CONNECTIONS || '10'),
                request_timeout: Number.parseInt(process.env.CLICKHOUSE_REQUEST_TIMEOUT || '30000'),
                // Keep-alive configuration for connection reuse
                keep_alive: {
                    enabled: true,
                    idle_socket_ttl: Number.parseInt(process.env.CLICKHOUSE_IDLE_SOCKET_TTL || '2500'),
                },
            };
            // Create ClickHouse client with connection pooling
            this.client = (0, client_1.createClient)({
                url: `https://${this.config.username}:${this.config.password}@${this.config.host}/${this.config.database}`,
                // username: this.config.username,
                // password: this.config.password,
                // database: this.config.database,
                // Connection pooling settings
                max_open_connections: this.config.max_open_connections,
                request_timeout: this.config.request_timeout,
                // Keep-alive configuration for efficient connection reuse
                keep_alive: this.config.keep_alive,
                // Application identifier for connection tracking
                application: 'trusys-clickhouse-service',
                // Add compression for better performance over network
                compression: {
                    response: true,
                    request: false, // Enable if dealing with large insert payloads
                },
                // Log level for debugging connection issues
                log: {
                    level: (process.env.NODE_ENV === 'development' ? 'DEBUG' : 'ERROR'),
                },
            });
            this.isInitialized = true;
            logger_1.default.info('ClickHouse service initialized successfully with connection pool');
        }
        catch (error) {
            this.isInitialized = false;
            logger_1.default.error(`Failed to initialize ClickHouse service: ${error}`);
            throw error;
        }
    }
    /**
     * Mock function to fetch credentials
     * Replace this with actual secrets management implementation
     */
    async fetchCredentials() {
        // Mock implementation - replace with actual secrets fetch
        // This could integrate with AWS Secrets Manager, HashiCorp Vault, etc.
        logger_1.default.info('Fetching ClickHouse credentials (mocked)');
        return {
            apiKey: process.env.CLICKHOUSE_API_KEY || 'mock-api-key',
            token: process.env.CLICKHOUSE_TOKEN || 'mock-token',
        };
    }
    /**
     * Ensure the service is initialized before performing operations
     */
    async ensureInitialized() {
        if (!this.isInitialized || !this.client) {
            await this.init();
        }
    }
    /**
     * Execute a query and return results
     * Utilizes the connection pool for efficient connection management
     * Includes retry logic with exponential backoff for transient failures
     */
    async query(sql, params) {
        return this.executeWithRetry(async () => {
            await this.ensureInitialized();
            if (!this.client) {
                throw new Error('ClickHouse client not initialized');
            }
            // logger.debug(`Executing ClickHouse query: ${sql}`);
            const result = await this.client.query({
                query: sql,
                query_params: params,
                format: 'JSONEachRow',
            });
            const data = await result.json();
            // logger.debug(
            //   `Query executed successfully, returned ${Array.isArray(data) ? data.length : 1} rows`,
            // );
            return Array.isArray(data) ? data : [data];
        }, 'query');
    }
    /**
     * Execute an insert operation
     * Uses the connection pool for efficient bulk inserts
     * Includes retry logic with exponential backoff for transient failures
     */
    async insert(table, data) {
        return this.executeWithRetry(async () => {
            await this.ensureInitialized();
            if (!this.client) {
                throw new Error('ClickHouse client not initialized');
            }
            logger_1.default.debug(`Inserting ${data.length} rows into table: ${table}`);
            await this.client.insert({
                table,
                values: data,
                format: 'JSONEachRow',
            });
            logger_1.default.debug('Insert operation completed successfully');
        }, 'insert');
    }
    /**
     * Execute a command (DDL operations, etc.)
     * Reuses pooled connections for efficient command execution
     * Includes retry logic with exponential backoff for transient failures
     */
    async command(sql) {
        return this.executeWithRetry(async () => {
            await this.ensureInitialized();
            if (!this.client) {
                throw new Error('ClickHouse client not initialized');
            }
            logger_1.default.debug(`Executing ClickHouse command: ${sql}`);
            await this.client.command({
                query: sql,
            });
            logger_1.default.debug('Command executed successfully');
        }, 'command');
    }
    /**
     * Execute a delete operation
     * Uses the connection pool for efficient delete operations
     * Includes retry logic with exponential backoff for transient failures
     */
    async delete(table, whereClause, params) {
        return this.executeWithRetry(async () => {
            await this.ensureInitialized();
            if (!this.client) {
                throw new Error('ClickHouse client not initialized');
            }
            const sql = `ALTER TABLE ${table} DELETE WHERE ${whereClause}`;
            logger_1.default.debug(`Executing ClickHouse delete: ${sql}`);
            await this.client.command({
                query: sql,
                query_params: params,
            });
            logger_1.default.debug('Delete operation completed successfully');
        }, 'delete');
    }
    /**
     * Check if the connection is healthy
     * Uses the built-in ping method which leverages the connection pool
     */
    async ping() {
        try {
            await this.ensureInitialized();
            if (!this.client) {
                return false;
            }
            const result = await this.client.ping();
            logger_1.default.debug('ClickHouse ping successful');
            return result.success;
        }
        catch (error) {
            logger_1.default.error(`ClickHouse ping failed: ${error}`);
            return false;
        }
    }
    /**
     * Get connection pool statistics (if available)
     * Note: The ClickHouse client doesn't expose detailed pool stats,
     * but this method can be extended in the future
     */
    getPoolStats() {
        return {
            initialized: this.isInitialized,
            max_connections: this.config?.max_open_connections || 0,
            keep_alive_enabled: this.config?.keep_alive?.enabled || false,
            idle_socket_ttl: this.config?.keep_alive?.idle_socket_ttl || 0,
            retry_config: this.retryConfig,
        };
    }
    /**
     * Close the connection pool and cleanup resources
     * This closes all pooled connections gracefully
     */
    async close() {
        if (this.client) {
            try {
                await this.client.close();
                this.client = null;
                this.isInitialized = false;
                logger_1.default.info('ClickHouse connection pool closed successfully');
            }
            catch (error) {
                logger_1.default.error(`Error closing ClickHouse connection pool: ${error}`);
                throw error;
            }
        }
    }
    /**
     * Get client instance (for advanced operations)
     * Note: Direct client access should be used carefully to maintain connection pool efficiency
     */
    getClient() {
        if (!this.isInitialized) {
            logger_1.default.warn('Attempting to get client before initialization. Call init() first.');
            return null;
        }
        return this.client;
    }
    /**
     * Reset the connection pool
     * Useful for handling connection issues or configuration changes
     */
    async resetPool() {
        logger_1.default.info('Resetting ClickHouse connection pool');
        // Clear the init promise to allow re-initialization
        this.initPromise = null;
        await this.close();
        this.isInitialized = false;
        await this.init();
    }
}
exports.ClickHouseService = ClickHouseService;
// Export a singleton instance
exports.clickHouseService = new ClickHouseService();
//# sourceMappingURL=clickhouseService.js.map