<?php

namespace Openguru\OpenCartFramework\QueryBuilder\Connections;

use Openguru\OpenCartFramework\Support\Utils;
use PDO;
use RuntimeException;

class MySqlConnection implements ConnectionInterface
{
    private $pdo;
    private ?array $lastError = null;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;

        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        $this->pdo->exec("SET NAMES 'utf8'");
        $this->pdo->exec("SET CHARACTER SET utf8");
        $this->pdo->exec("SET CHARACTER_SET_CONNECTION=utf8");
        $this->pdo->exec("SET SQL_MODE = ''");
    }

    public function select(string $sql, array $bindings = []): array
    {
        $statement = $this->pdo->prepare($sql);
        $statement->execute($bindings);

        return $statement->fetchAll(PDO::FETCH_ASSOC);
    }

    public function escape($value): string
    {
        switch (true) {
            case $value === null:
                return 'null';

            case is_int($value) || is_float($value):
                return (string) $value;

            case is_bool($value):
                return $value ? '1' : '0';

            case is_array($value):
                throw new RuntimeException('The database connection does not support escaping arrays.');

            default:
                if (Utils::strContains($value, "\00")) {
                    throw new RuntimeException(
                        'Strings with null bytes cannot be escaped. Use the binary escape option.'
                    );
                }

                if (preg_match('//u', $value) === false) {
                    throw new RuntimeException('Strings with invalid UTF-8 byte sequences cannot be escaped.');
                }

                return $this->pdo->quote($value);
        }
    }

    public function createTableFromSql(string $table, string $sql, array $bindings = []): bool
    {
        $createSql = "CREATE TABLE IF NOT EXISTS `$table` AS ($sql)";

        return $this->pdo->prepare($createSql)->execute($bindings);
    }

    public function dropTable(string $table): bool
    {
        return $this->pdo->prepare("DROP TABLE `$table`")->execute();
    }

    public function dropTableIfExists(string $table): bool
    {
        return $this->pdo->prepare("DROP TABLE IF EXISTS `$table`")->execute();
    }

    public function tableExists(string $table): bool
    {
        $statement = $this->pdo->prepare("SHOW TABLES LIKE ?;");
        $statement->execute([$table]);

        return $statement->rowCount() > 0;
    }

    public function statement(string $sql, array $bindings = []): bool
    {
        $statement = $this->pdo->prepare($sql);

        if (! $statement) {
            $this->lastError = $this->pdo->errorInfo();
            return false;
        }

        $success = $statement->execute($bindings);

        if (! $success) {
            $this->lastError = $statement->errorInfo();
            return false;
        }

        return $success;
    }

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

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

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

    public function insertIntoTableFromSql(string $table, string $sql, array $bindings = []): bool
    {
        $createSql = "INSERT INTO `{$table}` {$sql}";

        return $this->pdo->prepare($createSql)->execute($bindings);
    }

    public function truncateTable(string $table): bool
    {
        return $this->pdo->prepare("TRUNCATE TABLE {$table}")->execute();
    }

    public function lastInsertId($name = null): int
    {
        return $this->pdo->lastInsertId($name);
    }

    public function getVersion(): string
    {
        return $this->pdo
            ->query('SELECT VERSION()')
            ->fetch(PDO::FETCH_COLUMN);
    }

    public function insert(string $table, array $data): bool
    {
        $placeholders = implode(',', array_fill(0, count($data), '?'));
        $columns = implode(',', array_map(static fn ($key) => "`${key}`", array_keys($data)));
        $sql = sprintf('INSERT INTO `%s` (%s) VALUES (%s)', $table, $columns, $placeholders);

        return $this->statement($sql, array_values($data));
    }

    public function transaction(callable $callback): void
    {
        try {
            $this->beginTransaction();
            $callback();
            $this->commitTransaction();
        } catch (\Exception $exception) {
            $this->rollBackTransaction();
            throw $exception;
        }
    }

    public function getLastError(): ?array
    {
        return $this->lastError;
    }
}
