<?php

declare(strict_types=1);

namespace App\Framework\Database;

use App\Framework\Database\Exception\ConnectionFailedException;
use App\Framework\Database\Exception\ConstraintViolationException;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\Database\Exception\DeadlockException;
use App\Framework\Database\Exception\QueryExecutionException;
use App\Framework\Database\Exception\QuerySyntaxException;
use App\Framework\Database\Services\SqlStateErrorMapper;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Database\ValueObjects\SqlState;

final class PdoConnection implements ConnectionInterface
{
    private \PDO $pdo;

    public function __construct(
        \PDO $pdo,
        private ?SqlStateErrorMapper $errorMapper = null
    ) {
        $this->pdo = $pdo;
        $this->errorMapper ??= new SqlStateErrorMapper();

        # SOllte bereits aus den Options kommen!
        #$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
        #$this->pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
    }

    public function execute(SqlQuery $query): int
    {
        try {
            $statement = $this->pdo->prepare($query->sql);
            $statement->execute($query->parameters->toPdoArray());

            return $statement->rowCount();
        } catch (\PDOException $e) {
            throw $this->handlePdoException($e, $query->sql);
        }
    }

    public function query(SqlQuery $query): ResultInterface
    {
        try {
            $statement = $this->pdo->prepare($query->sql);
            $statement->execute($query->parameters->toPdoArray());

            return new PdoResult($statement);
        } catch (\PDOException $e) {
            throw $this->handlePdoException($e, $query->sql);
        }
    }

    public function queryOne(SqlQuery $query): ?array
    {
        $result = $this->query($query);

        return $result->fetch();
    }

    public function queryColumn(SqlQuery $query): array
    {
        $result = $this->query($query);

        return $result->fetchColumn();
    }

    public function queryScalar(SqlQuery $query): mixed
    {
        $result = $this->query($query);

        return $result->fetchScalar();
    }

    public function beginTransaction(): void
    {
        try {
            $this->pdo->beginTransaction();
        } catch (\PDOException $e) {
            throw $this->handlePdoException($e);
        }
    }

    public function commit(): void
    {
        try {
            $this->pdo->commit();
        } catch (\PDOException $e) {
            throw $this->handlePdoException($e);
        }
    }

    public function rollback(): void
    {
        try {
            $this->pdo->rollBack();
        } catch (\PDOException $e) {
            throw $this->handlePdoException($e);
        }
    }

    public function inTransaction(): bool
    {
        return $this->pdo->inTransaction();
    }

    public function lastInsertId(): string
    {
        return $this->pdo->lastInsertId();
    }

    public function getPdo(): \PDO
    {
        return $this->pdo;
    }

    /**
     * Handle PDOException and convert to appropriate framework exception
     *
     * @param \PDOException $e The PDO exception
     * @param string|null $query Optional SQL query that failed
     * @throws DatabaseException
     */
    private function handlePdoException(\PDOException $e, ?string $query = null): never
    {
        // Extract SQLSTATE from PDOException
        $sqlStateCode = $e->getCode();

        // PDO sometimes returns error info array as code
        if (is_array($sqlStateCode)) {
            $sqlStateCode = $sqlStateCode[0] ?? 'HY000';
        }

        // Fallback to error info if code is not a valid SQLSTATE
        if (!is_string($sqlStateCode) || strlen($sqlStateCode) !== 5) {
            $errorInfo = $this->pdo->errorInfo();
            $sqlStateCode = $errorInfo[0] ?? 'HY000';
        }

        try {
            $sqlState = new SqlState($sqlStateCode);
        } catch (\InvalidArgumentException) {
            // Fallback to generic database exception if SQLSTATE is invalid
            throw DatabaseException::simple(
                "Database error: {$e->getMessage()}",
                $e
            );
        }

        // Use SqlStateErrorMapper to determine exception type
        $errorCode = $this->errorMapper->mapToErrorCode($sqlState);

        // Throw appropriate exception based on SQLSTATE
        throw match (true) {
            $sqlState->isConnectionError() => $this->handleConnectionError($sqlState, $e, $query),
            $sqlState->isConstraintViolation() => $this->handleConstraintViolation($sqlState, $e, $query),
            $sqlState->isDeadlock() => $this->handleDeadlock($sqlState, $e, $query),
            $sqlState->isSyntaxError() => $this->handleSyntaxError($sqlState, $e, $query),
            default => $this->handleQueryExecutionError($sqlState, $e, $query),
        };
    }

    /**
     * Handle connection errors (SQLSTATE class 08)
     */
    private function handleConnectionError(
        SqlState $sqlState,
        \PDOException $e,
        ?string $query = null
    ): ConnectionFailedException {
        // Extract connection details from PDO DSN if available
        $host = 'unknown';
        $database = 'unknown';

        // Attempt to parse DSN from exception or PDO attributes
        try {
            $dsn = $this->pdo->getAttribute(\PDO::ATTR_CONNECTION_STATUS) ?? '';
            if (preg_match('/host=([^;]+)/', $dsn, $matches)) {
                $host = $matches[1];
            }
            if (preg_match('/dbname=([^;]+)/', $dsn, $matches)) {
                $database = $matches[1];
            }
        } catch (\PDOException) {
            // Ignore if we can't get connection details
        }

        return match ($sqlState->code) {
            '08003' => ConnectionFailedException::connectionDoesNotExist($sqlState),
            '08004' => ConnectionFailedException::serverRejectedConnection($host, 'unknown', $sqlState, $e->getMessage()),
            '08006' => ConnectionFailedException::connectionFailedDuringTransaction($sqlState),
            default => ConnectionFailedException::cannotConnect($host, $database, $sqlState, $e->getMessage(), $e),
        };
    }

    /**
     * Handle constraint violations (SQLSTATE class 23)
     */
    private function handleConstraintViolation(
        SqlState $sqlState,
        \PDOException $e,
        ?string $query = null
    ): ConstraintViolationException {
        // Try to extract table and column information from error message
        $errorMessage = $e->getMessage();
        $table = 'unknown';
        $column = 'unknown';

        // Parse error message for constraint details (database-specific patterns)
        if (preg_match('/table[:\s]+["`]?(\w+)["`]?/i', $errorMessage, $matches)) {
            $table = $matches[1];
        }
        if (preg_match('/column[:\s]+["`]?(\w+)["`]?/i', $errorMessage, $matches)) {
            $column = $matches[1];
        }

        return match (true) {
            $sqlState->isUniqueViolation() => ConstraintViolationException::uniqueViolation(
                $table,
                $column,
                'unknown',
                $sqlState
            ),
            $sqlState->isForeignKeyViolation() => ConstraintViolationException::foreignKeyViolation(
                $table,
                $column,
                'unknown',
                'unknown',
                $sqlState
            ),
            $sqlState->isNotNullViolation() => ConstraintViolationException::notNullViolation(
                $table,
                $column,
                $sqlState
            ),
            $sqlState->isCheckViolation() => ConstraintViolationException::checkViolation(
                $table,
                'unknown',
                $sqlState
            ),
            default => ConstraintViolationException::constraintViolated(
                'unknown',
                $sqlState,
                $errorMessage,
                $e
            ),
        };
    }

    /**
     * Handle deadlocks (SQLSTATE 40001)
     */
    private function handleDeadlock(
        SqlState $sqlState,
        \PDOException $e,
        ?string $query = null
    ): DeadlockException {
        return DeadlockException::detected($sqlState, $query, $e);
    }

    /**
     * Handle syntax errors (SQLSTATE class 42)
     */
    private function handleSyntaxError(
        SqlState $sqlState,
        \PDOException $e,
        ?string $query = null
    ): QuerySyntaxException {
        return QuerySyntaxException::forQuery($query ?? 'unknown', $sqlState, $e->getMessage(), $e);
    }

    /**
     * Handle query execution errors (SQLSTATE class 22)
     */
    private function handleQueryExecutionError(
        SqlState $sqlState,
        \PDOException $e,
        ?string $query = null
    ): QueryExecutionException {
        return QueryExecutionException::forQuery($query ?? 'unknown', $sqlState, $e->getMessage(), $e);
    }
}
