/*
 * @adonisjs/lucid
 *
 * (c) Harminder Virk <virk@adonisjs.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
import knex from 'knex';
import { EventEmitter } from 'node:events';
import { patchKnex } from 'knex-dynamic-connection';
// @ts-expect-error
import { resolveClientNameWithAliases } from 'knex/lib/util/helpers.js';
import * as errors from '../errors.js';
import LibSQLClient from '../clients/libsql.cjs';
import { clientsNames } from '../dialects/index.js';
import { Logger as ConnectionLogger } from './logger.js';
/**
 * Connection class manages a given database connection. Internally it uses
 * knex to build the database connection with appropriate database
 * driver.
 */
export class Connection extends EventEmitter {
    name;
    config;
    logger;
    /**
     * Reference to knex. The instance is created once the `open`
     * method is invoked
     */
    client;
    /**
     * Read client when read/write replicas are defined in the config, otherwise
     * it is a reference to the `client`.
     */
    readClient;
    /**
     * Connection dialect name.
     * @deprecated
     * @see clientName
     */
    dialectName;
    /**
     * Connection client name.
     */
    clientName;
    /**
     * A boolean to know if connection operates on read/write
     * replicas
     */
    hasReadWriteReplicas;
    /**
     * Config for one or more read replicas. Only exists, when replicas are
     * defined
     */
    readReplicas = [];
    /**
     * The round robin counter for reading config
     */
    roundRobinCounter = 0;
    constructor(name, config, logger) {
        super();
        this.name = name;
        this.config = config;
        this.logger = logger;
        this.validateConfig();
        this.clientName = resolveClientNameWithAliases(this.config.client);
        this.dialectName = this.clientName;
        this.hasReadWriteReplicas = !!(this.config.replicas &&
            this.config.replicas.read &&
            this.config.replicas.write);
        if (!clientsNames.includes(this.clientName)) {
            throw new errors.E_UNSUPPORTED_CLIENT([this.clientName]);
        }
    }
    /**
     * Validates the config to ensure that read/write replicas are defined
     * properly.
     */
    validateConfig() {
        if (this.config.replicas) {
            if (!this.config.replicas.read || !this.config.replicas.write) {
                throw new errors.E_INCOMPLETE_REPLICAS_CONFIG();
            }
            if (!this.config.replicas.read.connection || !this.config.replicas.read.connection) {
                throw new errors.E_INVALID_REPLICAS_CONFIG();
            }
        }
    }
    /**
     * Cleans up reference for the write client and also the
     * read client when not using replicas
     */
    cleanupWriteClient() {
        if (this.client === this.readClient) {
            this.cleanupReadClient();
        }
        this.client = undefined;
    }
    /**
     * Cleans up reference for the read client
     */
    cleanupReadClient() {
        this.roundRobinCounter = 0;
        this.readClient = undefined;
        this.readReplicas = [];
    }
    /**
     * Does cleanup by removing knex reference and removing all listeners.
     * For the same of simplicity, we get rid of both read and write
     * clients, when anyone of them disconnects.
     */
    monitorPoolResources() {
        /**
         * Pool has destroyed and hence we must cleanup resources
         * as well.
         */
        this.pool.on('poolDestroySuccess', () => {
            this.logger.trace({ connection: this.name }, 'pool destroyed, cleaning up resource');
            this.cleanupWriteClient();
            this.emit('disconnect', this);
            this.removeAllListeners();
        });
        if (this.readPool !== this.pool) {
            this.readPool.on('poolDestroySuccess', () => {
                this.logger.trace({ connection: this.name }, 'pool destroyed, cleaning up resource');
                this.cleanupReadClient();
                this.emit('disconnect', this);
                this.removeAllListeners();
            });
        }
    }
    /**
     * Returns normalized config object for write replica to be
     * used by knex
     */
    getWriteConfig() {
        if (!this.config.replicas) {
            /**
             * Replacing string based libsql client with the
             * actual implementation
             */
            if (this.config.client === 'libsql') {
                return {
                    ...this.config,
                    client: LibSQLClient,
                };
            }
            return this.config;
        }
        const { replicas, ...config } = this.config;
        /**
         * Give preference to the replica write connection when and merge values from
         * the main connection object when defined.
         */
        if (typeof replicas.write.connection === 'string' || typeof config.connection === 'string') {
            config.connection = replicas.write.connection;
        }
        else {
            config.connection = Object.assign({}, config.connection, replicas.write.connection);
        }
        /**
         * Add pool to the config when pool config defined on main connection
         * or the write replica
         */
        if (config.pool || replicas.write.pool) {
            config.pool = Object.assign({}, config.pool, replicas.write.pool);
        }
        return config;
    }
    /**
     * Returns the config for read replicas.
     */
    getReadConfig() {
        if (!this.config.replicas) {
            return this.config;
        }
        const { replicas, ...config } = this.config;
        /**
         * Reading replicas and storing them as a reference, so that we
         * can pick a config from replicas as round robin.
         */
        this.readReplicas = replicas.read.connection.map((one) => {
            if (typeof one === 'string' || typeof config.connection === 'string') {
                return one;
            }
            else {
                return Object.assign({}, config.connection, one);
            }
        });
        /**
         * Add database property on the main connection, since knexjs needs it
         * internally
         */
        config.connection = {
            database: this.readReplicas[0].database,
        };
        /**
         * Add pool to the config when pool config defined on main connection
         * or the read replica
         */
        if (config.pool || replicas.read.pool) {
            config.pool = Object.assign({}, config.pool, replicas.read.pool);
        }
        return config;
    }
    /**
     * Resolves connection config for the writer connection
     */
    writeConfigResolver(originalConfig) {
        return originalConfig.connection;
    }
    /**
     * Resolves connection config for the reader connection
     */
    readConfigResolver(originalConfig) {
        if (!this.readReplicas.length) {
            return originalConfig.connection;
        }
        const index = this.roundRobinCounter++ % this.readReplicas.length;
        this.logger.trace({ connection: this.name }, `round robin using host at ${index} index`);
        return this.readReplicas[index];
    }
    /**
     * Creates the write connection.
     */
    setupWriteConnection() {
        this.client = knex.knex(Object.assign({ log: new ConnectionLogger(this.name, this.logger) }, this.getWriteConfig(), {
            debug: false,
        }));
        // @ts-ignore
        patchKnex(this.client, this.writeConfigResolver.bind(this));
    }
    /**
     * Creates the read connection. If there aren't any replicas in use, then
     * it will use the write client instead.
     */
    setupReadConnection() {
        if (!this.hasReadWriteReplicas) {
            this.readClient = this.client;
            return;
        }
        this.logger.trace({ connection: this.name }, 'setting up read/write replicas');
        this.readClient = knex.knex(Object.assign({ log: new ConnectionLogger(this.name, this.logger) }, this.getReadConfig(), {
            debug: false,
        }));
        // @ts-ignore
        patchKnex(this.readClient, this.readConfigResolver.bind(this));
    }
    /**
     * Returns the pool instance for the given connection
     */
    get pool() {
        return this.client ? this.client.client.pool : null;
    }
    /**
     * Returns the pool instance for the read connection. When replicas are
     * not in use, then read/write pools are same.
     */
    get readPool() {
        return this.readClient ? this.readClient.client.pool : null;
    }
    /**
     * Returns a boolean indicating if the connection is ready for making
     * database queries. If not, one must call `connect`.
     */
    get ready() {
        return !!(this.client || this.readClient);
    }
    /**
     * Opens the connection by creating knex instance
     */
    connect() {
        try {
            this.setupWriteConnection();
            this.setupReadConnection();
            this.monitorPoolResources();
            this.emit('connect', this);
        }
        catch (error) {
            this.emit('error', error, this);
            throw error;
        }
    }
    /**
     * Closes DB connection by destroying knex instance. The `connection`
     * object must be free for garbage collection.
     *
     * In case of error this method will emit `close:error` event followed
     * by the `close` event.
     */
    async disconnect() {
        this.logger.trace({ connection: this.name }, 'destroying connection');
        /**
         * Disconnect write client
         */
        if (this.client) {
            try {
                await this.client.destroy();
            }
            catch (error) {
                this.emit('disconnect:error', error, this);
            }
        }
        /**
         * Disconnect read client when it exists and both clients
         * aren't same
         */
        if (this.readClient && this.readClient !== this.client) {
            try {
                await this.readClient.destroy();
            }
            catch (error) {
                this.emit('disconnect:error', error, this);
            }
        }
    }
}
