<?php

declare(strict_types=1);

namespace App\Framework\Database\Exception;

use App\Framework\Database\ValueObjects\SqlState;
use App\Framework\Exception\Core\DatabaseErrorCode;
use App\Framework\Exception\ExceptionContext;

/**
 * Connection Failed Exception
 *
 * Thrown when database connection cannot be established or is lost.
 *
 * This exception represents SQLSTATE class 08 (Connection Exception) errors:
 * - 08001: Cannot connect to database
 * - 08003: Connection does not exist
 * - 08004: Server rejected connection
 * - 08006: Connection failure during transaction
 * - 08007: Transaction resolution unknown
 */
final class ConnectionFailedException extends DatabaseException
{
    /**
     * Create exception for general connection failure
     *
     * @param string $host Database host
     * @param string $database Database name
     * @param SqlState $sqlState The SQLSTATE error code
     * @param string|null $errorMessage Optional database error message
     * @param \Throwable|null $previous Previous exception
     */
    public static function cannotConnect(
        string $host,
        string $database,
        SqlState $sqlState,
        ?string $errorMessage = null,
        ?\Throwable $previous = null
    ): self {
        $message = "Failed to connect to database '{$database}' on '{$host}': {$errorMessage}";

        $context = ExceptionContext::forOperation('connection.establish', 'DatabaseConnection')
            ->withData([
                'host' => $host,
                'database' => $database,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Connection Exception',
            ])
            ->withDebug([
                'sqlstate_class' => $sqlState->getClass(),
                'sqlstate_subclass' => $sqlState->getSubclass(),
            ]);

        return self::fromContext($message, $context, DatabaseErrorCode::CONNECTION_FAILED, $previous, 500, $sqlState);
    }

    /**
     * Create exception when connection does not exist
     *
     * @param SqlState $sqlState The SQLSTATE error code (typically 08003)
     * @param string|null $connectionId Optional connection identifier
     */
    public static function connectionDoesNotExist(
        SqlState $sqlState,
        ?string $connectionId = null
    ): self {
        $message = $connectionId
            ? "Database connection '{$connectionId}' does not exist or has been closed"
            : 'Database connection does not exist or has been closed';

        $context = ExceptionContext::forOperation('connection.use', 'DatabaseConnection')
            ->withData([
                'sqlstate' => $sqlState->code,
                'error_category' => 'Connection Exception',
            ]);

        if ($connectionId !== null) {
            $context = $context->withData(['connection_id' => $connectionId]);
        }

        return self::fromContext($message, $context, DatabaseErrorCode::CONNECTION_FAILED, null, 500, $sqlState);
    }

    /**
     * Create exception when server rejects connection
     *
     * @param string $host Database host
     * @param string $username Database username
     * @param SqlState $sqlState The SQLSTATE error code (typically 08004)
     * @param string|null $rejectionReason Optional rejection reason
     */
    public static function serverRejectedConnection(
        string $host,
        string $username,
        SqlState $sqlState,
        ?string $rejectionReason = null
    ): self {
        $message = "Database server at '{$host}' rejected connection for user '{$username}'";

        if ($rejectionReason !== null) {
            $message .= ": {$rejectionReason}";
        }

        $context = ExceptionContext::forOperation('connection.authenticate', 'DatabaseConnection')
            ->withData([
                'host' => $host,
                'username' => $username,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Connection Exception',
            ]);

        if ($rejectionReason !== null) {
            $context = $context->withDebug(['rejection_reason' => $rejectionReason]);
        }

        return self::fromContext($message, $context, DatabaseErrorCode::CONNECTION_FAILED, null, 500, $sqlState);
    }

    /**
     * Create exception for connection failure during transaction
     *
     * @param SqlState $sqlState The SQLSTATE error code (typically 08006)
     * @param string|null $transactionId Optional transaction identifier
     */
    public static function connectionFailedDuringTransaction(
        SqlState $sqlState,
        ?string $transactionId = null
    ): self {
        $message = 'Database connection failed during transaction - transaction has been rolled back';

        $context = ExceptionContext::forOperation('connection.transaction', 'DatabaseConnection')
            ->withData([
                'sqlstate' => $sqlState->code,
                'error_category' => 'Connection Exception',
                'transaction_rolled_back' => true,
            ]);

        if ($transactionId !== null) {
            $context = $context->withData(['transaction_id' => $transactionId]);
        }

        return self::fromContext($message, $context, DatabaseErrorCode::CONNECTION_FAILED, null, 500, $sqlState);
    }

    /**
     * Create exception for connection timeout
     *
     * @param string $host Database host
     * @param int $timeoutSeconds Timeout duration in seconds
     * @param SqlState $sqlState The SQLSTATE error code
     */
    public static function connectionTimeout(
        string $host,
        int $timeoutSeconds,
        SqlState $sqlState
    ): self {
        $message = "Connection to database at '{$host}' timed out after {$timeoutSeconds} seconds";

        $context = ExceptionContext::forOperation('connection.establish', 'DatabaseConnection')
            ->withData([
                'host' => $host,
                'timeout_seconds' => $timeoutSeconds,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Connection Exception',
            ]);

        return self::fromContext($message, $context, DatabaseErrorCode::CONNECTION_FAILED, null, 500, $sqlState);
    }

    /**
     * Create exception for connection lost
     *
     * @param SqlState $sqlState The SQLSTATE error code
     * @param string|null $lastQuery Optional last executed query
     */
    public static function connectionLost(
        SqlState $sqlState,
        ?string $lastQuery = null
    ): self {
        $message = 'Database connection lost unexpectedly';

        $context = ExceptionContext::forOperation('connection.lost', 'DatabaseConnection')
            ->withData([
                'sqlstate' => $sqlState->code,
                'error_category' => 'Connection Exception',
            ]);

        if ($lastQuery !== null) {
            $context = $context->withDebug([
                'last_query' => self::truncateQuery($lastQuery),
            ]);
        }

        return self::fromContext($message, $context, DatabaseErrorCode::CONNECTION_FAILED, null, 500, $sqlState);
    }

    /**
     * Create exception when too many connections
     *
     * @param string $host Database host
     * @param int $maxConnections Maximum allowed connections
     * @param SqlState $sqlState The SQLSTATE error code
     */
    public static function tooManyConnections(
        string $host,
        int $maxConnections,
        SqlState $sqlState
    ): self {
        $message = "Too many connections to database at '{$host}' - maximum {$maxConnections} connections allowed";

        $context = ExceptionContext::forOperation('connection.establish', 'DatabaseConnection')
            ->withData([
                'host' => $host,
                'max_connections' => $maxConnections,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Connection Exception',
            ]);

        return self::fromContext($message, $context, DatabaseErrorCode::POOL_EXHAUSTED, null, 500, $sqlState);
    }

    /**
     * Create exception for SSL/TLS connection failure
     *
     * @param string $host Database host
     * @param SqlState $sqlState The SQLSTATE error code
     * @param string|null $sslError Optional SSL error message
     */
    public static function sslConnectionFailed(
        string $host,
        SqlState $sqlState,
        ?string $sslError = null
    ): self {
        $message = "SSL/TLS connection to database at '{$host}' failed";

        if ($sslError !== null) {
            $message .= ": {$sslError}";
        }

        $context = ExceptionContext::forOperation('connection.ssl', 'DatabaseConnection')
            ->withData([
                'host' => $host,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Connection Exception',
            ]);

        if ($sslError !== null) {
            $context = $context->withDebug(['ssl_error' => $sslError]);
        }

        return self::fromContext($message, $context, DatabaseErrorCode::CONNECTION_FAILED, null, 500, $sqlState);
    }

    /**
     * Truncate query for safe logging
     *
     * @param string $query The SQL query
     * @param int $maxLength Maximum length
     * @return string Truncated query
     */
    private static function truncateQuery(string $query, int $maxLength = 200): string
    {
        if (strlen($query) <= $maxLength) {
            return $query;
        }

        return substr($query, 0, $maxLength) . '... (truncated)';
    }
}
