<?php
declare(strict_types=1);

namespace Celena\Core\Database;

use Celena\Core\Database\Driver\DriverInterface;
use Celena\Core\Database\Driver\MysqlDriver;
use Celena\Core\Database\Driver\PgsqlDriver;
use Closure;
use PDO;
use PDOException;
use RuntimeException;

/**
 * Обёртка над PDO с фабрикой драйверов и хелперами.
 * Всегда: исключения, реальные prepared statements, FETCH_ASSOC.
 */
final class Connection
{
    private ?PDO $pdo = null;
    private DriverInterface $driver;
    private readonly string $prefix;

    public function __construct(private readonly array $config)
    {
        $name = strtolower((string) ($config['driver'] ?? 'pgsql'));
        $this->driver = match ($name) {
            'mysql', 'mariadb' => new MysqlDriver(),
            'pgsql', 'postgres', 'postgresql' => new PgsqlDriver(),
            default => throw new RuntimeException("Unsupported DB driver: $name"),
        };
        $this->prefix = (string) ($config['prefix'] ?? '');
    }

    public function pdo(): PDO
    {
        if ($this->pdo !== null) {
            return $this->pdo;
        }
        try {
            $this->pdo = new PDO(
                $this->driver->dsn($this->config),
                (string) ($this->config['user'] ?? ''),
                (string) ($this->config['pass'] ?? ''),
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES => false,
                ]
            );
            if ($this->driver->name() === 'mysql') {
                $this->pdo->exec("SET NAMES " . ($this->config['charset'] ?? 'utf8mb4'));
            }
        } catch (PDOException $e) {
            throw new RuntimeException('Database connection failed: ' . $e->getMessage(), 0, $e);
        }
        return $this->pdo;
    }

    public function driver(): DriverInterface { return $this->driver; }
    public function prefix(): string          { return $this->prefix; }

    public function quoteId(string $id): string
    {
        return $this->driver->quoteId($id);
    }

    /** Применить префикс к имени таблицы. */
    public function table(string $name): string
    {
        return $this->prefix . $name;
    }

    public function table_q(string $name): string
    {
        return $this->driver->quoteId($this->table($name));
    }

    public function builder(string $table): QueryBuilder
    {
        return new QueryBuilder($this, $table);
    }

    /** Выполнить произвольный SQL без параметров. */
    public function exec(string $sql): int
    {
        return (int) $this->pdo()->exec($sql);
    }

    /**
     * Подготовить и выполнить запрос с параметрами.
     * @param array<int|string, mixed> $params
     */
    public function run(string $sql, array $params = []): \PDOStatement
    {
        // Приводим bool → 0/1 (PG не принимает '' как boolean, а PDO при execute() делает (string) false = '').
        foreach ($params as $k => $v) {
            if (is_bool($v)) {
                $params[$k] = $v ? 1 : 0;
            }
        }
        $stmt = $this->pdo()->prepare($sql);
        $stmt->execute($params);
        return $stmt;
    }

    public function transaction(Closure $callback): mixed
    {
        $pdo = $this->pdo();
        $pdo->beginTransaction();
        try {
            $result = $callback($this);
            // MySQL делает неявный COMMIT на DDL (CREATE/ALTER TABLE), поэтому после
            // миграции транзакция может быть уже закрыта. Коммитим только если она ещё
            // активна (в PostgreSQL DDL транзакционный — транзакция остаётся открытой).
            if ($pdo->inTransaction()) {
                $pdo->commit();
            }
            return $result;
        } catch (\Throwable $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            throw $e;
        }
    }

    public function lastInsertId(string $table = '', string $column = 'id'): string
    {
        $seq = $this->driver->lastInsertSeq($this->table($table), $column);
        return $seq !== null ? (string) $this->pdo()->lastInsertId($seq) : (string) $this->pdo()->lastInsertId();
    }
}
