<?php

declare(strict_types=1);

class Database
{
    private static ?PDO $pdo = null;

    public static function connect(string $host, string $name, string $user, string $pass): PDO
    {
        return new PDO(
            "mysql:host={$host};dbname={$name};charset=utf8mb4",
            $user,
            $pass,
            [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES   => false,
            ]
        );
    }

    public static function get(): PDO
    {
        if (self::$pdo === null) {
            self::$pdo = getDbConnection();
        }
        return self::$pdo;
    }

    public static function set(PDO $pdo): void
    {
        self::$pdo = $pdo;
    }

    public static function getName(): string
    {
        $row = self::fetchOne("SELECT DATABASE() AS db");
        return $row['db'] ?? '';
    }

    public static function reconnect(): void
    {
        self::$pdo = null;
        self::$pdo = getDbConnection(true);
    }

    public static function query(string $sql, array $params = []): \PDOStatement
    {
        try {
            $stmt = self::get()->prepare($sql);
            $stmt->execute($params);
            return $stmt;
        } catch (\PDOException $e) {
             
             
            if (!self::get()->inTransaction()) {
                 
                if (self::isConnectionError($e)) {
                    self::reconnect();
                    $stmt = self::get()->prepare($sql);
                    $stmt->execute($params);
                    return $stmt;
                }
                 
                 
                if (self::isDeadlock($e)) {
                    for ($try = 1; $try <= 3; $try++) {
                        usleep(40000 * $try);  
                        try {
                            $stmt = self::get()->prepare($sql);
                            $stmt->execute($params);
                            return $stmt;
                        } catch (\PDOException $e2) {
                            if (!self::isDeadlock($e2)) { throw $e2; }
                            $e = $e2;
                        }
                    }
                }
            }
            throw $e;
        }
    }

    private static function isDeadlock(\PDOException $e): bool
    {
        $code = $e->errorInfo[1] ?? null;
        return $code === 1213   
            || $code === 1205;  
    }

    private static function isConnectionError(\PDOException $e): bool
    {
        $msg = $e->getMessage();
         
         
         
         
        $driverCode = $e->errorInfo[1] ?? null;
        return $driverCode === 2006
            || $driverCode === 2013
            || str_contains($msg, 'server has gone away')
            || str_contains($msg, 'Lost connection')
            || str_contains($msg, 'Connection refused');
    }

    public static function fetchOne(string $sql, array $params = []): ?array
    {
        $result = self::query($sql, $params)->fetch();
        return $result ?: null;
    }

    public static function fetchAll(string $sql, array $params = []): array
    {
        return self::query($sql, $params)->fetchAll();
    }

    public static function insert(string $table, array $data): int
    {
        $columns = implode(', ', array_keys($data));
        $placeholders = implode(', ', array_fill(0, count($data), '?'));
        $sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
        self::query($sql, array_values($data));
        return (int) self::get()->lastInsertId();
    }

    public static function update(string $table, array $data, string $where, array $whereParams = []): int
    {
        $set = implode(', ', array_map(fn($col) => "{$col} = ?", array_keys($data)));
        $sql = "UPDATE {$table} SET {$set} WHERE {$where}";
        $stmt = self::query($sql, array_merge(array_values($data), $whereParams));
        return $stmt->rowCount();
    }

    public static function delete(string $table, string $where, array $params = []): int
    {
        $stmt = self::query("DELETE FROM {$table} WHERE {$where}", $params);
        return $stmt->rowCount();
    }

    public static function count(string $table, string $where = '1', array $params = []): int
    {
        $row = self::fetchOne("SELECT COUNT(*) as cnt FROM {$table} WHERE {$where}", $params);
        return (int) ($row['cnt'] ?? 0);
    }

    public static function beginTransaction(): void
    {
        self::get()->beginTransaction();
    }

    public static function commit(): void
    {
        self::get()->commit();
    }

    public static function rollback(): void
    {
        if (self::get()->inTransaction()) {
            self::get()->rollBack();
        }
    }

    public static function transaction(callable $callback): mixed
    {
        for ($try = 1; ; $try++) {
            self::beginTransaction();
            try {
                $result = $callback();
                self::commit();
                return $result;
            } catch (\Throwable $e) {
                self::rollback();
                 
                 
                if ($try < 3 && $e instanceof \PDOException && self::isDeadlock($e)) {
                    usleep(40000 * $try);
                    continue;
                }
                throw $e;
            }
        }
    }
}
