<?php

declare(strict_types=1);

namespace App\Shared\Infrastructure\Persistence\Doctrine;

use App\Shared\Infrastructure\Tenant\TenantDatabaseSwitcher;

use function array_merge;

use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Connection\StaticServerVersionProvider;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Tools\DsnParser;

use function is_array;
use function is_string;

use RuntimeException;
use SensitiveParameter;

/**
 * @phpstan-import-type Params from DriverManager
 */
final class TenantAwareConnection extends Connection implements TenantDatabaseSwitcher
{
    private const array URL_SCHEME_MAP = [
        'db2' => 'ibm_db2',
        'mssql' => 'pdo_sqlsrv',
        'mysql' => 'pdo_mysql',
        'mysql2' => 'pdo_mysql',
        'postgres' => 'pdo_pgsql',
        'postgresql' => 'pdo_pgsql',
        'pgsql' => 'pdo_pgsql',
        'sqlite' => 'pdo_sqlite',
        'sqlite3' => 'pdo_sqlite',
    ];

    /** @phpstan-var Params */
    private array $defaultConnectionParams;

    /** @phpstan-var Params */
    private array $currentConnectionParams;

    private ?string $currentDatabaseUrl = null;

    private ?AbstractPlatform $currentPlatform = null;

    private readonly DsnParser $dsnParser;

    /**
     * @phpstan-param Params $params
     */
    public function __construct(
        #[SensitiveParameter]
        array $params,
        Driver $driver,
        ?Configuration $config = null,
    ) {
        parent::__construct($params, $driver, $config);

        $this->defaultConnectionParams = $params;
        $this->currentConnectionParams = $params;
        $this->dsnParser = new DsnParser(self::URL_SCHEME_MAP);
    }

    public function useTenantDatabase(string $databaseUrl): void
    {
        /** @phpstan-var Params $connectionParams */
        $connectionParams = array_merge($this->defaultConnectionParams, $this->dsnParser->parse($databaseUrl));

        // Preserve dbname_suffix from Doctrine config (e.g. '_test' in test env)
        // so that tenant connections target the same database as the default one.
        $suffix = $this->defaultConnectionParams['dbname_suffix'] ?? null;
        if (is_string($suffix) && isset($connectionParams['dbname']) && is_string($connectionParams['dbname'])) {
            $connectionParams['dbname'] .= $suffix;
        }

        $this->applyConnectionParams($connectionParams, $databaseUrl);
    }

    public function useDefaultDatabase(): void
    {
        $this->applyConnectionParams($this->defaultConnectionParams, null);
    }

    public function currentDatabaseUrl(): ?string
    {
        return $this->currentDatabaseUrl;
    }

    /**
     * @phpstan-return Params
     */
    public function getParams(): array
    {
        return $this->currentConnectionParams;
    }

    public function getDatabasePlatform(): AbstractPlatform
    {
        if ($this->currentPlatform === null) {
            $versionProvider = $this;

            $serverVersion = $this->currentConnectionParams['serverVersion'] ?? null;
            if (is_string($serverVersion)) {
                $versionProvider = new StaticServerVersionProvider($serverVersion);
            } else {
                $primaryConnection = $this->currentConnectionParams['primary'] ?? null;
                if (is_array($primaryConnection)) {
                    $primaryServerVersion = $primaryConnection['serverVersion'] ?? null;
                    if (is_string($primaryServerVersion)) {
                        $versionProvider = new StaticServerVersionProvider($primaryServerVersion);
                    }
                }
            }

            $this->currentPlatform = $this->getDriver()->getDatabasePlatform($versionProvider);
        }

        return $this->currentPlatform;
    }

    public function close(): void
    {
        parent::close();
        $this->currentPlatform = null;
    }

    protected function connect(): DriverConnection
    {
        if ($this->_conn !== null) {
            return $this->_conn;
        }

        try {
            $connection = $this->_conn = $this->getDriver()->connect($this->currentConnectionParams);
        } catch (Driver\Exception $e) {
            throw $this->convertException($e);
        }

        if (!$this->isAutoCommit()) {
            $this->beginTransaction();
        }

        return $connection;
    }

    /**
     * @phpstan-param Params $params
     */
    private function applyConnectionParams(array $params, ?string $databaseUrl): void
    {
        if ($this->currentConnectionParams === $params && $this->currentDatabaseUrl === $databaseUrl) {
            return;
        }

        if ($this->isConnected()) {
            if ($this->isTransactionActive()) {
                throw new RuntimeException('Cannot switch database while a transaction is active.');
            }

            $this->close();
        }

        $this->currentConnectionParams = $params;
        $this->currentDatabaseUrl = $databaseUrl;
        $this->currentPlatform = null;
    }
}
