<?php

/**
 * @file
 * Definition of Drupal\autoslave\Database\Driver\autoslave\Connection.
 */

namespace Drupal\Core\Database\Driver\autoslave;

use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection as DatabaseConnection;
use Drupal\Core\Logger\RfcLogLevel;

/**
 * @file
 * Database interface code for automatic slave selection depending on type of
 * query.
 *
 * @todo Ensure default method arguments will work if they differ from what's
 *       defined in class Database.
 *
 * ============================================================================
 * ARCHITECTURAL CHANGE WARNING - BREAKING CHANGES
 * ============================================================================
 *
 * This file has undergone significant architectural refactoring to comply with
 * Drupal 10 coding standards and PHP 8.1+ compatibility requirements.
 *
 * BREAKING CHANGES IMPLEMENTED:
 * - All property names changed from underscore-prefixed to lowerCamel format
 *   (e.g., $__master → $masterConnectionId)
 * - Method names updated to lowerCamel convention
 *   (e.g., rand_weighted() → randWeighted())
 * - Constants updated to lowercase (TRUE/FALSE/NULL → true/false/null)
 * - Deprecated functions replaced (drupal_is_cli() → PHP_SAPI === 'cli')
 *
 * MIGRATION REQUIRED:
 * If you have custom code that extends or directly accesses properties/methods
 * from this class, you MUST update your code. See docs/BREAKING_CHANGES.md
 * for a complete migration guide.
 *
 * VERSION IMPACT:
 * These changes require a major version bump (8.x-2.0 or 10.x-1.0).
 *
 * ROLLBACK:
 * A backup branch was created: backup-before-refactoring-YYYYMMDD-HHMMSS
 * Use: git checkout backup-before-refactoring-YYYYMMDD-HHMMSS to restore
 *
 * For more information, see:
 * - docs/BREAKING_CHANGES.md - Complete migration guide
 * - docs/ARCHITECTURAL_UPGRADE_PLAN.md - Detailed upgrade strategy
 * ============================================================================
 */

include_once 'defines.inc';

/**
 * Base class for tracking affected tables in autoslave connections.
 */
class AutoslaveAffectedTables {
  protected $connection = NULL;

  public function __construct($connection) {
    $this->connection = $connection;
  }

}


/**
 * @addtogroup database
 * @{
 */
/**
 * Specific auto slave implementation of DatabaseConnection.
 */
class Connection extends DatabaseConnection {

  /**
   * List of tables that should always use "master" as target.
   *
   * @var array
   */
  protected $masterTables = [];

  /**
   * List of tables that should use "master" as target in the current request.
   *
   * @var array
   */
  protected $affectedTables = [];

  /**
   * Affected tables data array.
   *
   * @var array
   */
  public $affectedTablesData = [];

  /**
   * Chosen master connection ID.
   *
   * @var string|null
   */
  protected $masterConnectionId;

  /**
   * Chosen slave connection ID.
   *
   * @var string|null
   */
  protected $slaveConnectionId;

  /**
   * System connection ID.
   *
   * @var string|null
   */
  protected $systemConnectionId;

  /**
   * Maximum expiration time for affected tables.
   *
   * @var int
   */
  public $maxExpires = 0;

  /**
   * Force queries to master counter.
   *
   * @var int
   */
  private $forceMasterCounter = 0;

  /**
   * Session setup completion flag.
   *
   * @var bool
   */
  private $sessionSetupComplete = FALSE;

  /**
   * Global setup completion flag.
   *
   * @var bool
   */
  private $globalSetupComplete = FALSE;

  /**
   * Automatic id assignment counter.
   *
   * @var int
   */
  private static $autoslaveIdCounter = 1;

  /**
   * Pool of connections.
   *
   * @var array
   */
  private $connectionPool = [];

  /**
   * Watchdog messages to log.
   *
   * @var array
   */
  private $watchdogMessages = [];

  /**
   * System is in read-only mode.
   *
   * @var bool
   */
  protected $readOnlyMode = FALSE;

  /**
   * Exception data storage.
   *
   * @var mixed
   */
  protected $exceptionData = FALSE;

  /**
   * Affected tables backend instance.
   *
   * @var object|null
   */
  private $affectedTablesBackend = NULL;

  /**
   * Static registry of affected tables backend classes.
   *
   * @var array
   */
  private static $affectedTablesBackendClasses = [];

  /**
   * Constructor.
   */
  public function __construct(?\PDO $connection = NULL, array $connection_options = []) {
    // Sanitize connection options.
    $connection_options['master'] = !empty($connection_options['master']) ? $connection_options['master'] : ['master'];
    if (!is_array($connection_options['master'])) {
      $connection_options['master'] = [$connection_options['master']];
    }

    $connection_options['slave'] = !empty($connection_options['slave']) ? $connection_options['slave'] : ['autoslave'];
    if (!is_array($connection_options['slave'])) {
      $connection_options['slave'] = [$connection_options['slave']];
    }
    $connection_options['watchdog on shutdown'] = $connection_options['watchdog on shutdown'] ?? AUTOSLAVE_WATCHDOG_ON_SHUTDOWN;
    $connection_options['replication lag'] = $connection_options['replication lag'] ?? AUTOSLAVE_ASSUMED_REPLICATION_LAG;
    $connection_options['global replication lag'] = $connection_options['global replication lag'] ?? AUTOSLAVE_GLOBAL_REPLICATION_LAG;
    $connection_options['invalidation path'] = $connection_options['invalidation path'] ?? NULL;
    $connection_options['use autoslave schema'] = $connection_options['use autoslave schema'] ?? TRUE;
    $connection_options['affected tables backend'] = $connection_options['affected tables backend'] ?? 'autoslave.affected_tables.db-accurate.inc';
    $connection_options['init_commands'] = $connection_options['init_commands'] ?? [];
    $connection_options['use system connection'] = $connection_options['use system connection'] ?? FALSE;
    $connection_options['bypass page cache'] = $connection_options['bypass page cache'] ?? 0;
    $connection_options['preconnect'] = $connection_options['preconnect'] ?? FALSE;
    $connection_options['flag all tables'] = $connection_options['flag all tables'] ?? FALSE;
    $connection_options['resync on cache miss'] = $connection_options['resync on cache miss'] ?? TRUE;
    $connection_options['transactionalize writes'] = $connection_options['transactionalize writes'] ?? TRUE;
    $connection_options['debug'] = $connection_options['debug'] ?? FALSE;

    $this->affectedTables = !empty($connection_options['tables']) ? $connection_options['tables'] : ['sessions', 'sempahore'];
    $this->affectedTables[] = 'autoslave_affected_tables';

    $this->connectionOptions = $connection_options;

    // Load affected tables backend.
    $class = NULL;
    require_once $connection_options['affected tables backend'];
    if ($class) {
      self::$__affected_tables_backend_classes[$connection_options['affected tables backend']] = $class;
    }
    else {
      $class = self::$__affected_tables_backend_classes[$connection_options['affected tables backend']];
    }
    $this->affectedTablesData_backend = new $class($this);

    // Initialize and prepare the connection prefix.
    $this->setPrefix($this->connectionOptions['prefix'] ?? '');

    // Initialize force master tables.
    if (!empty($this->affectedTables)) {
      $this->masterConnectionId_tables = $this->affectedTables = array_combine($this->affectedTables, $this->affectedTables);
    }

    // Has master been forced before Database bootstrapping?
    if (!empty($_GLOBALS['autoslave_pre_bootstrap_force_master'])) {
      $this->forceMaster(1);
    }

    // Register shutdown function - using traditional method for database layer.
    register_shutdown_function([$this, 'logWatchdogMessages']);
  }

  /**
   * Set key is called immediatly after the constructor, so
   * now we can set up the connections.
   */
  public function setKey($key) {
    if (!isset($this->key)) {
      $this->key = $key;
      $this->setupConnections();
      // Check if we're in a testing environment.
      if (defined('DRUPAL_TEST_IN_CHILD_SITE') || (isset($_SERVER['HTTP_USER_AGENT']) && stripos($_SERVER['HTTP_USER_AGENT'], 'simpletest') !== FALSE)) {
        $this->connectionOptions['replication lag'] = 0;
        $this->connectionOptions['global replication lag'] = FALSE;
        return;
      }
    }
  }

  /**
   * Dispatch all methods defined in class PDO to the appropiate backend.
   */
  public function beginTransaction() {
    return $this->getMasterConnection()->beginTransaction();
  }

  /**
   * Gets the SQLSTATE error code.
   */
  public function errorCode() {
    return $this->getMasterConnection()->errorCode();
  }

  /**
   * Gets extended error information.
   */
  public function errorInfo() {
    return $this->getMasterConnection()->errorInfo();
  }

  /**
   * Executes an SQL statement.
   */
  public function exec($statement) {
    return $this->getMasterConnection()->exec($statement);
  }

  /**
   * Gets a database connection attribute.
   */
  public function getAttribute($attribute) {
    return $this->getMasterConnection()->getAttribute($attribute);
  }

  /**
   * Gets available PDO drivers.
   */
  public static function getAvailableDrivers() {
    $drivers = $this->getMasterConnection()->getAvailableDrivers();
    $drivers[] = 'autoslave';
    return $drivers;
  }

  /**
   * Gets the last inserted ID.
   */
  public function lastInsertId($name = NULL): string {
    return $this->getMasterConnection()->lastInsertId($name);
  }

  /**
   * Quotes a string for use in a query.
   */
  public function quote($string, $paramtype = NULL) {
    return $this->getMasterConnection()->quote($string, $paramtype);
  }

  /**
   * Sets a database connection attribute.
   */
  public function setAttribute($attribute, $value) {
    return $this->getMasterConnection()->setAttribute($attribute, $value);
  }

  /**
   * Dispatch all methods defined in class Database to the appropiate backend.
   */

  /**
   * "master" functions.
   */
  public function inTransaction() {
    return $this->getMasterConnection()->inTransaction();
  }

  /**
   * Gets the current transaction depth.
   */
  public function transactionDepth() {
    return $this->getMasterConnection()->transactionDepth();
  }

  /**
   * Rolls back a transaction.
   */
  public function rollback($savepoint_name = 'drupal_transaction') {
    return $this->getMasterConnection()->rollback($savepoint_name);
  }

  /**
   * Pushes a new transaction onto the stack.
   */
  public function pushTransaction($name) {
    return $this->getMasterConnection()->pushTransaction($name);
  }

  /**
   * Pops a transaction from the stack.
   */
  public function popTransaction($name) {
    return $this->getMasterConnection()->popTransaction($name);
  }

  /**
   * Pops all committable transactions from the stack.
   */
  protected function popCommittableTransactions() {
    return $this->getMasterConnection()->popCommittableTransactions();
  }

  /**
   * Generates a temporary table name.
   */
  protected function generateTemporaryTableName() {
    return $this->getMasterConnection()->generateTemporaryTableName();
  }

  /**
   * Checks if the database supports transactions.
   */
  public function supportsTransactions() {
    return $this->getMasterConnection()->supportsTransactions();
  }

  /**
   * Checks if the database supports transactional DDL.
   */
  public function supportsTransactionalDdl() {
    return $this->getMasterConnection()->supportsTransactionalDdl();
  }

  /**
   * Commits the current transaction.
   */
  public function commit() {
    return $this->getMasterConnection()->commit();
  }

  /**
   * "slave" functions.
   */
  protected function defaultOptions($full = TRUE) {
    $options = $full ? $this->getSlaveConnection()->defaultOptions() : [];
    if ($this->readOnlyMode) {
      $options['throw_exception'] = FALSE;
    }
    return $options;
  }

  /**
   * Gets the connection options.
   */
  public function getConnectionOptions() {
    return $this->getSlaveConnection()->getConnectionOptions();
  }

  /**
   * Adds table prefixes to SQL.
   */
  public function prefixTables($sql) {
    return $this->getSlaveConnection()->prefixTables($sql);
  }

  /**
   * Gets the table prefix for a specific table.
   */
  public function tablePrefix($table = 'default') {
    return $this->getSlaveConnection()->tablePrefix($table);
  }

  /**
   * Prepares a query for execution.
   */
  public function prepareQuery($query, $quote_identifiers = TRUE) {
    return $this->getSlaveConnection()->prepareQuery($query);
  }

  /**
   * Creates a sequence name for a table field.
   */
  public function makeSequenceName($table, $field) {
    return $this->getSlaveConnection()->makeSequenceName($table, $field);
  }

  /**
   * Generates SQL comments from an array.
   */
  public function makeComment($comments) {
    return $this->getSlaveConnection()->makeComment($comments);
  }

  /**
   * Filters a SQL comment to prevent SQL injection.
   */
  protected function filterComment($comment = '') {
    return $this->getSlaveConnection()->filterComment($comment);
  }

  /**
   * Expands query arguments.
   */
  protected function expandArguments(&$query, &$args) {
    return $this->getSlaveConnection()->expandArguments($query, $args);
  }

  /**
   * Checks if writes should be transactionalized.
   */
  public function transactionalizeWrite($option_only = FALSE) {
    if ($option_only) {
      return $this->connectionOptions['transactionalize writes'];
    }
    else {
      return $this->connectionOptions['transactionalize writes'] && !$this->__transaction;
    }
  }

  /**
   * The following methods are absolutely necessary to overload manually (at least for MySQL)
   */
  public function version() {
    return $this->getSlaveConnection()->version();
  }

  /**
   * Gets the database schema.
   */
  public function schema() {
    return $this->getMasterConnection()->schema();
  }

  /**
   * Gets the driver name.
   */
  public function driver() {
    return 'autoslave';
  }

  /**
   * Gets the database type.
   */
  public function databaseType() {
    return $this->getSlaveConnection()->databaseType();
  }

  /**
   * Executes a query against the appropriate connection.
   */
  public function query($query, array $args = [], $options = []) {
    if (!$this->prepareAutoslaveTarget($options)) {
      $options['target'] = $this->deriveTargetFromQuery($query);
    }
    $target = $options['target'];
    unset($options['target']);
    return $this->getSafeConnection($target)->query($query, $args, $options);
  }

  /**
   * Executes a range query against the appropriate connection.
   */
  public function queryRange($query, $from, $count, array $args = [], array $options = []) {
    if (!$this->prepareAutoslaveTarget($options)) {
      $options['target'] = $this->deriveTargetFromQuery($query);
    }
    $target = $options['target'];
    unset($options['target']);
    return $this->getSafeConnection($target)->queryRange($query, $from, $count, $args, $options);
  }

  /**
   * Creates a temporary table from a query.
   */
  public function queryTemporary($query, array $args = [], array $options = []) {
    if (!$this->prepareAutoslaveTarget($options)) {
      $options['target'] = $this->determineMasterTarget();
    }
    $target = $options['target'];
    unset($options['target']);
    $table = $this->getSafeConnection($target)->queryTemporary($query, $args, $options);
    if ($table) {
      $this->addAffectedTable($table, FALSE);
    }
    return $table;
  }

  /**
   * Maps a condition operator to a database-specific operator.
   */
  public function mapConditionOperator($operator) {
    return $this->getMasterConnection()->mapConditionOperator($operator);
  }

  /**
   * Gets the next ID in a sequence.
   */
  public function nextId($existing_id = 0) {
    return $this->getMasterConnection()->nextId($existing_id);
  }

  /**
   * Creates a SELECT query against the appropriate connection.
   */
  public function select($table, $alias = NULL, array $options = []) {
    if (!$this->prepareAutoslaveTarget($options)) {
      $options['target'] = $this->getTargetForTable($table);
    }
    $target = $options['target'];
    unset($options['target']);
    $query = $this->getSafeConnection($target)->select($table, $alias, $options);
    $query->addMetaData('autoslave_connection', [$this->getTarget(), $this->getKey()]);

    /**
     * This function doesn't exist in D8 and not sure what to replace it with or if it's needed here.
     */
    // drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES);
    include_once 'injector.inc';
    return $query->addTag('autoslave');
  }

  /**
   * Creates an INSERT query against the master connection.
   */
  public function insert($table, array $options = []) {
    if (!$this->prepareAutoslaveTarget($options)) {
      $options['target'] = $this->determineMasterTarget();
      $this->addAffectedTable($table);
    }
    $target = $options['target'];
    unset($options['target']);
    return $this->getSafeConnection($target)->insert($table, $options);
  }

  /**
   * Creates a MERGE query against the master connection.
   */
  public function merge($table, array $options = []) {
    if (!$this->prepareAutoslaveTarget($options)) {
      $options['target'] = $this->determineMasterTarget();
      $this->addAffectedTable($table);
    }
    $target = $options['target'];
    unset($options['target']);
    return $this->getSafeConnection($target)->merge($table, $options);
  }

  /**
   * Creates an UPDATE query against the master connection.
   */
  public function update($table, array $options = []) {
    if (!$this->prepareAutoslaveTarget($options)) {
      $options['target'] = $this->determineMasterTarget();
      $this->addAffectedTable($table);
    }
    $target = $options['target'];
    unset($options['target']);
    return $this->getSafeConnection($target)->update($table, $options);
  }

  /**
   * Creates a DELETE query against the master connection.
   */
  public function delete($table, array $options = []) {
    if (!$this->prepareAutoslaveTarget($options)) {
      $options['target'] = $this->determineMasterTarget();
      $this->addAffectedTable($table);
    }
    $target = $options['target'];
    unset($options['target']);
    return $this->getSafeConnection($target)->delete($table, $options);
  }

  /**
   * Creates a TRUNCATE query against the master connection.
   */
  public function truncate($table, array $options = []) {
    if (!$this->prepareAutoslaveTarget($options)) {
      $options['target'] = $this->determineMasterTarget();
      $this->addAffectedTable($table);
    }
    $target = $options['target'];
    unset($options['target']);
    return $this->getSafeConnection($target)->truncate($table, $options);
  }

  /**
   * Magic methods. Supports propeties/functions/methods not defined by the Database class.
   */
  public static function __callStatic($method, $args) {
    return call_user_func_array([$this->getMasterConnection()->get_class_name(), $method], $args);
  }

  /**
   * Calls methods not defined in this class on the master connection.
   */
  public function __call($method, $args) {
    return call_user_func_array([$this->getMasterConnection(), $method], $args);
  }

  /**
   * Gets properties from the master connection.
   */
  public function __get($key) {
    return $this->getMasterConnection()->$key;
  }

  /**
   * Sets properties on the master connection.
   */
  public function __set($key, $value) {
    $this->getMasterConnection()->$key = $value;
  }

  /**
   * Internal autoslave functions.
   */

  /**
   * Get the current pool of available targets.
   */
  public function getPool() {
    return $this->connectionPool;
  }

  /**
   * Get the assumed maximum replication lag.
   */
  public function getReplicationLag() {
    return intval($this->connectionOptions['replication lag']);
  }

  /**
   * Get the assumed maximum replication lag.
   */
  public function getGlobalReplicationLag() {
    return $this->connectionOptions['global replication lag'];
  }

  /**
   * Check if a connection is available.
   *
   * @param int $id
   *   Autoslave ID.
   *
   * @return mixed
   *   Exception object if error.
   *   TRUE if available.
   *   FALSE if available but flagged as down.
   *   NULL if connection does not exist.
   */
  public function checkConnection(int $id) {
    $status = NULL;
    if (isset($this->connectionPool['all'][$id])) {
      $conninfo = &$this->connectionPool['all'][$id];
      if (!isset($conninfo['status']) || $conninfo['status'] === FALSE) {
        // Try it out ...
        $key = $conninfo['key'];
        $c = &DatabaseInternals::getConnections();
        $d = &DatabaseInternals::getDatabaseInfo();
        try {
          $d[$key]['autoslave_check'] = $conninfo;
          Database::getConnection('autoslave_check', $key);
          $databases = $this->loadInvalidationFile($key);
          $status = isset($databases[$key][$conninfo['target']][$conninfo['idx']]['status']) ? FALSE : TRUE;
        }
        catch (\Exception $e) {
          $status = $e;
        }
        unset($d[$key]['autoslave_check']);
        unset($c[$key]['autoslave_check']);
      }
      else {
        $status = TRUE;
      }
    }
    return $status;
  }

  /**
   * Return the Autoslave ID for a given target.
   *
   * @param string $target
   *   Name of target.
   */
  public function getAutoslaveId(string $target) {
    $d = DatabaseInternals::getDatabaseInfo();
    return $d[$this->getKey()][$target]['autoslave_id'] ?? NULL;
  }

  /**
   * Loads database invalidation status from a file.
   */
  public function loadInvalidationFile(string $key) {
    $databases = [];
    if (isset($this->connectionOptions['invalidation path'])) {
      $file = $this->connectionOptions['invalidation path'] . "/autoslave-invalidation-$key.inc";
      if (file_exists($file)) {
        include $file;
      }
    }
    return $databases;
  }

  /**
   * Update invalidation file.
   */
  public function updateInvalidationFile(string $key, string $target, int $idx, bool $status) {
    $databases = $this->loadInvalidationFile($key);
    if ($databases) {
      $file = $this->connectionOptions['invalidation path'] . "/autoslave-invalidation-$key.inc";
      if (!isset($databases[$key][$target][$idx]['status'])) {
        if (!$status) {
          $databases[$key][$target][$idx]['status'] = FALSE;
        }
        else {
          return;
        }
      }
      else {
        if ($databases[$key][$target][$idx]['status'] === $status) {
          return;
        }
      }

      if ($status) {
        unset($databases[$key][$target][$idx]['status']);
      }
      else {
        $databases[$key][$target][$idx]['status'] = FALSE;
      }
      $output = '<' . '?php' . "\n";
      if (!is_numeric($idx)) {
        \Drupal::logger('autoslave')->error("[$key][$target][$idx] is not a valid connection!");
        return;
      }
      if (!empty($databases[$key])) {
        foreach ($databases[$key] as $target => $conninfos) {
          foreach ($conninfos as $idx => $conninfo) {
            if (isset($databases[$key][$target][$idx]['status'])) {
              $output .= '$databases["' . $key . '"]["' . $target . '"][' . $idx . ']["status"] = FALSE;' . "\n";
            }
          }
        }
      }
      // Use Drupal's file system service for proper temp file handling.
      $file_system = \Drupal::service('file_system');
      $temp_dir = $file_system->getTempDirectory();
      $temp_name = $file_system->tempnam($temp_dir, 'autoslave_invalidation');
      \Drupal::service('file_system')->saveData($output, $temp_name, FILE_EXISTS_REPLACE);
      $file_system->move($temp_name, $file);
    }
  }

  /**
   * Assign id's to connections, sanitize slave probabilities
   * and populate pools.
   */
  public function setupConnections() {

    $key = $this->getKey();

    $databases = Database::getAllConnectionInfo();

    $this->connectionPool = [
      'master' => [],
      'slave' => [],
      'all' => [],
    ];

    $backends = [
      'master' => $this->connectionOptions['master'],
      'slave' => $this->connectionOptions['slave'],
    ];

    foreach ($backends as $backend => $targets) {
      foreach ($targets as $target) {
        if (empty($databases[$key][$target])) {
          $conninfos = [];
        }
        elseif (empty($databases[$key][$target]['driver'])) {
          $conninfos = &$databases[$key][$target];
        }
        else {
          $databases[$key][$target] = [$databases[$key][$target]];
          $conninfos = &$databases[$key][$target];
        }

        foreach ($conninfos as $idx => &$conninfo) {

          if (empty($conninfo['autoslave_id'])) {
            $conninfo['target'] = $target;
            $conninfo['key'] = $key;
            $conninfo['idx'] = $idx;
            $conninfo['autoslave_id'] = self::$autoslave_id++;
            $conninfo['weight'] = isset($conninfo['weight']) ? intval($conninfo['weight']) : 100;

            // Parse the prefix information.
            if (!isset($conninfo['prefix'])) {
              // Default to an empty prefix.
              $conninfo['prefix'] = [
                'default' => '',
              ];
            }
            elseif (!is_array($conninfo['prefix'])) {
              // Transform the flat form into an array form.
              $conninfo['prefix'] = [
                'default' => $conninfo['prefix'],
              ];
            }

            $this->connectionPool['all'][$conninfo['autoslave_id']] = &$conninfo;
          }
          $this->connectionPool[$backend][$target][$conninfo['autoslave_id']] = &$this->connectionPool['all'][$conninfo['autoslave_id']];
        }
      }
    }
    $this->connectionPool['registered'] = $this->connectionPool['all'];

    if (isset($this->connectionOptions['invalidation path'])) {
      $target = $this->getTarget();
      $file = $this->connectionOptions['invalidation path'] . "/autoslave-invalidation-$key.inc";
      if (file_exists($file)) {
        include $file;
      }
    }

    // At this level, using Exceptions may result in endless loops ... so we die!
    if (empty($this->connectionPool['master'])) {
      die('There are no masters defined for AutoSlave. Please configure settings.php');
    }
    if (empty($this->connectionPool['slave'])) {
      die('There are no slaves defined for AutoSlave. Please configure settings.php');
    }

    $this->determineMaster();
    $this->determineSlave();

    return;
  }

  /**
   * Determine the slave to be used, and includes the master in the selection
   * if necessary.
   */
  public function determineConnection($backend) {
    $targets = $this->connectionPool[$backend];
    foreach ($targets as $target => $conninfos) {
      // Gather weights.
      $values = [];
      $weights = [];
      foreach ($conninfos as $conninfo) {
        // If we stumble upon an already connected connection, then use that one.
        if (isset($conninfo['connected'])) {
          if ($conninfo['connected'] === TRUE) {
            return $conninfo;
          }
          continue;
        }

        // Don't try an already failed one.
        if (isset($conninfo['status']) && $conninfo['status'] !== TRUE) {
          continue;
        }

        $values[] = $conninfo;
        $weights[] = $conninfo['weight'];
      }

      // If no connection infos for this target, then try the next.
      if (!$values) {
        continue;
      }

      // Weighted random selection!
      $conninfo = $this->randWeighted($values, $weights);

      $d = &DatabaseInternals::getDatabaseInfo();
      $d[$conninfo['key']][$conninfo['target']] = $conninfo;
      return $conninfo;
    }

    if ($backend == 'master') {
      $this->goReadOnly();
    }

    $this->fatalThrow(new \Exception("There are no connections available in the pool: $backend"));
  }

  /**
   * Throw an exception and disable watchdog on shutdown if necessary.
   */
  public function fatalThrow($exception) {
    if (!$this->exceptionData) {
      // We remove any watchdog hooks, as they may cause a double-fault upon logging.
      if (!$this->connectionOptions['watchdog on shutdown']) {
        // In Drupal 10, use the module handler service instead.
        $module_handler = \Drupal::moduleHandler();
        $module_handler->resetImplementations();
      }

      $this->exceptionData = $exception;
    }
    throw $exception;
  }

  /**
   * Put system into read only mode.
   */
  public function goReadOnly() {
    $this->readOnlyMode = TRUE;
    \Drupal::messenger()->addMessage(t('The system is currently in read-only mode. Any changes you make will not be saved!'), 'error');
  }

  /**
   * Determine the master to be use.
   */
  public function determineMaster($reload = FALSE) {
    if ($reload || !isset($this->masterConnectionId)) {
      $this->masterConnectionId = NULL;
      $conninfo = $this->determineConnection('master');
      $this->masterConnectionId = $conninfo['autoslave_id'];
      if (!empty($conninfo['readonly'])) {
        $this->goReadOnly();
      }
      $this->determineSystemTarget($reload);
    }
    return $this->masterConnectionId;
  }

  /**
   * Determine the master target.
   */
  public function determineMasterTarget() {
    if (!$this->masterConnectionId) {
      $this->fatalThrow(new \Exception("No master connection has been chosen"));
    }
    $conninfo = $this->connectionPool['all'][$this->masterConnectionId];
    return $conninfo['target'];
  }

  /**
   * Determine the slave.
   */
  public function determineSlave($reload = FALSE) {
    if ($reload || !isset($this->slaveConnectionId)) {
      $this->slaveConnectionId = NULL;
      $conninfo = $this->determineConnection('slave');
      $this->slaveConnectionId = $conninfo['autoslave_id'];
    }
    return $this->slaveConnectionId;
  }

  /**
   * Determine the slave to be used, and includes the master in the selection
   * if necessary.
   */
  public function determineSlaveTarget() {
    if (!$this->slaveConnectionId) {
      $this->fatalThrow(new \Exception("No slave connection has been chosen"));
    }
    $conninfo = $this->connectionPool['all'][$this->slaveConnectionId];
    return $conninfo['target'];
  }

  /**
   * Determine the target to be used for system maintenance (affected tables).
   */
  public function determineSystemTarget($reload = FALSE) {
    if (!$this->connectionOptions['use system connection']) {
      return $this->determineMasterTarget();
    }
    elseif ($reload || !isset($this->systemConnectionId)) {
      $conninfo = $this->connectionPool['all'][$this->determineMaster()];
      $d = &DatabaseInternals::getDatabaseInfo();
      $this->systemConnectionId = self::$autoslave_id++;
      $target = $this->getTarget() . '_autoslave_system';
      $conninfo['autoslave_id'] = $this->systemConnectionId;
      $conninfo['target'] = $target;
      $conninfo['idx'] = 0;
      $conninfo['init_commands'] = $this->connectionOptions['init_commands'];
      $d[$conninfo['key']][$target] = $conninfo;
      $this->connectionPool['all'][$this->systemConnectionId] = $conninfo;
    }
    return $this->connectionPool['all'][$this->systemConnectionId]['target'];
  }

  /**
   * Store watchdog message for later ... watchdogging!?
   */
  public function watchdog($name, $msg, $args, $level) {
    $this->watchdogMessages[] = [$name, $msg, $args, $level];
  }

  /**
   * Log all registered messages to logger.
   */
  public function logWatchdogMessages() {
    foreach ($this->watchdogMessages as $log) {
      [$name, $message, $variables, $severity] = $log;
      \Drupal::logger($name)->log($severity, $message, $variables);
    }
  }

  /**
   * Invalidate a connection, so that a failover connection may be attempted.
   */
  public function invalidateConnection($id) {
    $conninfo = $this->connectionPool['all'][$id];
    $key = $conninfo['key'];
    $target = $conninfo['target'];
    $idx = $conninfo['idx'];

    $database_info = &DatabaseInternals::getDatabaseInfo();
    $conninfo = $database_info[$key][$target];
    $databases = Database::getAllConnectionInfo();

    unset($databases[$key][$target][$idx]);
    if (empty($databases[$key][$target])) {
      // No more slaves, remove completely.
      unset($database_info[$key][$target]);
      unset($databases[$key][$target]);
    }
    else {
      // Reindex target array for random select purposes.
      $targets = array_values($databases[$key][$target]);
      $database_info[$key][$target] = $targets[mt_rand(0, count($targets) - 1)];
    }

    $this->updateInvalidationFile($key, $target, $idx, FALSE);

    $this->watchdog('autoslave', "Invalidated connection [@key][@target]@idx", [
      '@key' => $key,
      '@target' => $target,
      '@idx' => isset($idx) ? "[$idx]" : '',
    ], RfcLogLevel::ALERT);

    if ($id == $this->slaveConnectionId) {
      $this->determineSlave(TRUE);
      $target = $this->determineSlaveTarget();
    }
    if ($id == $this->masterConnectionId) {
      $this->determineMaster(TRUE);
      $target = $this->determineMasterTarget();
    }
    return $target;
  }

  /**
   * Get a connection for the given target.
   * Invalidates and redetermines if necessary.
   */
  public function getSafeConnection($target) {
    $key = $this->getKey();
    $id = $this->getAutoslaveId($target);
    try {
      $ignoreTargets = &DatabaseInternals::getIgnoreTargets();
      unset($ignoreTargets[$key][$target]);
      $result = Database::getConnection($target, $key);
      if ($id) {
        $this->connectionPool['all'][$id]['connected'] = TRUE;
        $this->connectionPool['all'][$id]['status'] = TRUE;
      }
      return $result;
    }
    catch (\Exception $e) {
      if ($id) {
        $this->connectionPool['all'][$id]['connected'] = FALSE;
        $this->connectionPool['all'][$id]['status'] = $e;
        $target = $this->invalidateConnection($id);
        try {
          return $this->getSafeConnection($target, $key);
        }
        catch (\Exception $e) {
          throw $e;
        }
      }
      throw $e;
    }
  }

  /**
   * Get the master connection.
   *
   * @return \Drupal\Core\Database\DatabaseConnection
   */
  public function getMasterConnection() {
    return $this->getSafeConnection($this->determineMasterTarget());
  }

  /**
   * Get the slave connection.
   *
   * @return \Drupal\Core\Database\DatabaseConnection
   */
  public function getSlaveConnection() {
    if ($this->forceMaster()) {
      return $this->getMasterConnection();
    }
    else {
      return $this->getSafeConnection($this->determineSlaveTarget());
    }
  }

  /**
   * Check if a database target is available.
   *
   * @param string $target
   *   Database target.
   *
   * @return bool
   *   TRUE if target is available.
   */
  public function isTargetAvailable(string $target) {
    $d = DatabaseInternals::getDatabaseInfo();
    $result = isset($d[$this->getKey()][$target]);
    return $result;
  }

  /**
   * Prepare options array for autoslave determination.
   *
   * @param array $options
   *   Connection options for a query.
   */
  public function prepareAutoslaveTarget(array &$options) {
    $options += $this->defaultOptions(FALSE);
    if (empty($options['target'])) {
      $options['target'] = $this->getTarget();
      return FALSE;
    }
    if ($options['target'] == 'slave') {
      // Direct to the autoslave if 'slave' is explicitely chosen.
      $options['target'] = $this->determineSlaveTarget();
    }
    if (!$this->isTargetAvailable($options['target'])) {
      $options['target'] = $this->getTarget();
      return FALSE;
    }
    if ($options['target'] == $this->getTarget()) {
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Get the target for a given query.
   */
  public function deriveTargetFromQuery(string $query) {
    switch ($this->deriveBackendFromQuery($query)) {
      case 'master':
        return $this->determineMasterTarget();

      case 'slave':
        return $this->determineSlaveTarget();

      default:
        $this->fatalThrow(new \Exception("Unexpected error! No target found for query! This exception should never be thrown!"));
    }
  }

  /**
   * Determine the backend to use based on a query.
   *
   * @param string $query
   *   The query to examine.
   */
  private function deriveBackendFromQuery(string $query) {
    $this->ensureAffectedTables();

    $is_write_query = preg_match('/^\s*(' .
      'UPDATE|INSERT|REPLACE|DELETE|' .
      'ALTER|CREATE|DROP|TRUNCATE|RENAME|' .
      'BEGIN|START\s+TRANSACTION|COMMIT|ROLLBACK|' .
      'RELEASE|SAVEPOINT|' .
      '(.*FOR UPDATE$)|(.*LOCK IN SHARE MODE$)' .
    ')\b/i', $query);

    // Find all tables used in the query.
    preg_match_all('@\{(\w+)\}@', $query, $matches);
    $tables = $matches[1];

    if ($is_write_query) {
      // Even if forceMaster() is true, we still need to tag tables that have been written to,
      // in case we go back to forceMaster() false later on.
      $this->addAffectedTables($tables);
      return 'master';
    }
    elseif ($this->forceMaster()) {
      return 'master';
    }
    else {
      return array_intersect($tables, $this->affectedTables) ? 'master' : 'slave';
    }
  }

  /**
   * Get the target for a table.
   *
   * @param string $table
   *   Table to get target for.
   *
   * @return string
   *   Target.
   */
  private function getTargetForTable(string $table) {
    $this->ensureAffectedTables();
    return $this->forceMaster() || isset($this->affectedTables[$table]) ? $this->determineMasterTarget() : $this->determineSlaveTarget();
  }

  /**
   * Get/set force master counter.
   *
   * @param int|null $force_master
   *   Number to increase force master counter with, or NULL to return current
   *   value.
   */
  public function forceMaster(?int $force_master = NULL) {
    if (isset($force_master)) {
      $this->forceMasterCounter += $force_master;
    }
    return $this->forceMasterCounter;
  }

  /**
   * Redeclare the master target as the default target.
   */
  public function hardSwitch() {
    // @todo Revisit this again, since it probably doesn't work anymore ...
    $connection_info = $this->connectionPool['all'];
    Database::renameConnection('default', 'autoslave_original_default');
    Database::addConnectionInfo('default', 'default', $connection_info[$this->masterConnectionId]);
  }

  /**
   * Get list of affected tables.
   */
  public function getAffectedTables(int $expires = 0) {
    $tables = $this->affectedTablesData_backend->get($expires - $this->getReplicationLag());
    foreach ($tables as &$row) {
      $row['expires'] += $this->getReplicationLag();
    }
    return $tables;
  }

  /**
   * Updates the affected tables in the backend.
   */
  public function updateAffectedTables(string $target, string $key, array $tables, int $expires) {
    return $this->affectedTablesData_backend->update($target, $key, $tables, $expires);
  }

  /**
   * Buffers an affected table for later flushing.
   */
  public function bufferAffectedTable(string $target, string $key, string $table, int $expires) {
    if (!isset($this->affectedTablesData[$key][$target][$table]) || abs($this->affectedTablesData[$key][$target][$table]) < $expires) {
      $this->affectedTablesData[$key][$target][$table] = -$expires;
    }
  }

  /**
   * Flushes buffered affected tables to the backend or session.
   */
  public function flushAffectedTables(bool $commit = TRUE) {

    $flushed = [];
    foreach ($this->affectedTablesData as $key => $targets) {
      foreach ($targets as $target => $tables) {
        foreach ($tables as $table => $expires) {

          if ($expires < 0) {
            $expires = $this->affectedTablesData[$key][$target][$table] = -$expires;
            if (!$commit) {

              // Adjust expiration, in case a new bufferAffectedTable() is called.
              $this->affectedTablesData[$key][$target][$table]--;
            }
            else {
              $this->maxExpires = $this->maxExpires < $expires ? $expires : $this->maxExpires;

              if ($this->getGlobalReplicationLag()) {
                $flushed[$key][$target][$expires][] = $table;
              }
              else {
                $_SESSION['autoslave_affected_tables'][$key][$target][$table] = $expires;
                $_SESSION['autoslave_affected_tables_version'] = AUTOSLAVE_VERSION;
              }
            }
          }
        }
      }
    }

    // Update multiple rows in one statement if possible.
    if ($flushed) {
      foreach ($flushed as $key => $targets) {
        foreach ($targets as $target => $expire) {
          foreach ($expire as $expires => $tables) {
            try {

              $this->updateAffectedTables($target, $key, $tables, $expires);
            }
            catch (\Exception $e) {
              // Ignore duplicate key errors on race conditions...
            }
          }
        }
      }
    }

    if ($commit) {
      if ($this->connectionOptions['bypass page cache'] > 0) {
        $_SESSION['bypass_page_cache'] = time() + $this->connectionOptions['bypass page cache'];
      }
    }
    if ($this->transactionalizeWrite(TRUE)) {
      $this->affectedTablesData = [];
    }
  }

  /**
   * Add tables to list of affected tables.
   *
   * @param array $tables
   *   Array of tables affected by write.
   * @param bool $update_session
   *   Update session with new expiration for replication lag mitigation.
   */
  public function addAffectedTables(array $tables, bool $update_session = TRUE) {
    // Only session-track tables that are not already on "always-master".
    $tables = array_diff($tables, $this->masterConnectionId_tables);
    $this->affectedTables = array_unique(array_merge($this->affectedTables, $tables));

    $time = microtime(TRUE);
    if ($tables && $update_session) {
      $key = $this->getKey();
      $target = $this->getTarget();
      foreach ($tables as $table) {
        // Reflag tables with timestamp later, if we're inside a transaction.
        $expires = ceil($time);
        $this->bufferAffectedTable($target, $key, $table, $expires);
      }
      if ($this->transactionDepth() == 0) {
        $this->flushAffectedTables(TRUE);
      }
    }
    return $this->affectedTables;
  }

  /**
   * Add one affected table.
   *
   * @param string $table
   *   Table affected by write.
   * @param bool $update_session
   *   Update session with new expiration for replication lag mitigation.
   */
  public function addAffectedTable(string $table, bool $update_session = TRUE) {
    return $this->addAffectedTables([$table => $table], $update_session);
  }

  /**
   * Ensure that tables affected by write from previous requests are flagged,
   * so that queries for these tables will go to the master.
   */
  public function ensureAffectedTables() {
    $key = $this->getKey();

    // Load globally affected tables.
    if (!$this->globalSetupComplete) {
      $this->globalSetupComplete = TRUE;

      if ($this->getGlobalReplicationLag()) {
        $tables = [];
        try {
          $tables = $this->getAffectedTables(time());
        }
        catch (\Exception $e) {
          // Just ignore error for now.
        }
        foreach ($tables as $table) {
          $connection = Database::getConnection($table['db_target'], $table['db_key']);
          $connection->addAffectedTable($table['affected_table'], FALSE);
        }
      }
    }

    // Use connection to master for affected tables within the given assumed lag time interval.
    if (!$this->sessionSetupComplete &&
      \Drupal::hasRequest() &&
      \Drupal::request()->hasSession() &&
      \Drupal::request()->getSession()->isStarted()
    ) {
      $this->sessionSetupComplete = TRUE;

      if ($this->getGlobalReplicationLag()) {
        $_SESSION['autoslave_affected_tables'] = NULL;
        $_SESSION['autoslave_affected_tables_version'] = NULL;
        unset($_SESSION['autoslave_affected_tables']);
        unset($_SESSION['autoslave_affected_tables_version']);
      }
      elseif (!empty($_SESSION['autoslave_affected_tables'])) {
        // Ensure BC for running sessions.
        $version = $_SESSION['autoslave_affected_tables_version'] ?? '1.3';
        if (version_compare($version, '1.3', '<=')) {
          $_SESSION['autoslave_affected_tables'] = [
            'default' => $_SESSION['autoslave_affected_tables'],
          ];
          $_SESSION['autoslave_affected_tables_version'] = AUTOSLAVE_VERSION;
        }

        if (isset($_SESSION['autoslave_affected_tables'][$key])) {
          // We use server request time instead of time() for the sake of db's with isolation level snapshots.
          foreach ($_SESSION['autoslave_affected_tables'][$key] as $target => $tables) {
            $connection = Database::getConnection($target, $key);
            if ($connection->driver() != 'autoslave') {
              continue;
            }

            foreach ($tables as $table => $expires) {
              if ($_SERVER['REQUEST_TIME'] <= $expires) {
                $connection->addAffectedTable($table, FALSE);
              }
              else {
                unset($_SESSION['autoslave_affected_tables'][$key][$target][$table]);
              }
            }
            // If no affected tables, remove the variable from session.
            if (empty($_SESSION['autoslave_affected_tables'][$key][$target])) {
              unset($_SESSION['autoslave_affected_tables'][$key][$target]);
            }
          }
          // If no affected tables, remove the variable from session.
          if (empty($_SESSION['autoslave_affected_tables'][$key])) {
            unset($_SESSION['autoslave_affected_tables'][$key]);
          }
        }

        // If no affected tables, remove the variable from session.
        if (empty($_SESSION['autoslave_affected_tables'])) {
          unset($_SESSION['autoslave_affected_tables']);
          unset($_SESSION['autoslave_affected_tables_version']);
        }
      }
    }
  }

  /**
   * Weighted_random_simple() from http://w-shadow.com/blog/2008/12/10/fast-weighted-random-choice-in-php/
   * modified by Thomas Gielfeldt to presort by weights.
   * Pick a random item based on weights.
   *
   * @param array $values
   *   Array of elements to choose from.
   * @param array $weights
   *   An array of weights. Weight must be a positive number.
   *
   * @return mixed Selected element.
   */
  public function randWeighted(array $values, array $weights) {
    asort($weights);
    $num = mt_rand(1, array_sum($weights));
    $n = 0;
    foreach ($weights as $i => $weight) {
      $n += $weights[$i];
      if ($n >= $num) {
        return $values[$i];
      }
    }
    return NULL;
  }

  /**
   * Recursively collect used tables in a SelectQuery.
   *
   * @param SelectQueryInterface $query
   *
   * @return array
   *   Tables present in query.
   */
  public function findTablesInQuery(string $query) {
    $tables = [];
    foreach ($query->getTables() as $tableinfo) {
      if ($tableinfo['table'] instanceof SelectQueryInterface) {
        $tables = array_merge($tables, $this->findTablesInQuery($tableinfo['table']));
      }
      else {
        $tables[] = $tableinfo['table'];
      }
    }
    return $tables;
  }

  /**
   * Creates a new database (stub implementation).
   */
  public function createDatabase($database) {}

}



/**
 * Get access to Database internal properties.
 */
class DatabaseInternals extends Database {

  /**
   * Gets the database information array.
   */
  public static function &getDatabaseInfo() {
    return self::$databaseInfo;
  }

  /**
   * Gets the connections array.
   */
  public static function &getConnections() {
    return self::$connections;
  }

  /**
   * Gets the ignore targets array.
   */
  public static function &getIgnoreTargets() {
    return self::$ignoreTargets;
  }

}

/**
 * @} End of "addtogroup database".
 */