<?php

declare(strict_types=1);

namespace App\Framework\AsyncExamples\Database;

use App\Framework\Async\AsyncPool;
use App\Framework\Async\FiberManager;
use PDO;

/**
 * Asynchroner Database Layer
 */
final class AsyncDatabase
{
    private PDO $connection;

    public function __construct(
        string $dsn,
        string $username = '',
        string $password = '',
        /** @var array<int, mixed> */
        array $options = [],
        private readonly FiberManager $fiberManager = new FiberManager()
    ) {
        /** @var array<int, mixed> */
        $defaultOptions = [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ];

        $this->connection = new PDO($dsn, $username, $password, array_merge($defaultOptions, $options));
    }

    /**
     * Führt eine Query aus
     *
     * @param array<string, mixed> $params
     */
    public function query(string $sql, array $params = []): DatabaseResult
    {
        $startTime = microtime(true);

        $stmt = $this->connection->prepare($sql);
        $success = $stmt->execute($params);

        if (! $success) {
            throw new DatabaseException("Query failed: " . implode(', ', $stmt->errorInfo()));
        }

        return new DatabaseResult(
            statement: $stmt,
            executionTime: microtime(true) - $startTime,
            affectedRows: $stmt->rowCount()
        );
    }

    /**
     * Führt mehrere Queries parallel aus
     *
     * @param array<string, array<string, mixed>> $queries ['key' => ['sql' => '...', 'params' => []], ...]
     * @return array<string, DatabaseResult>
     */
    public function queryMultiple(array $queries): array
    {
        /** @var array<string, \Closure> */
        $operations = [];
        foreach ($queries as $key => $query) {
            $operations[$key] = fn () => $this->query($query['sql'], $query['params'] ?? []);
        }

        return $this->fiberManager->batch($operations);
    }

    /**
     * Führt Queries mit begrenzter Parallelität aus
     *
     * @param array<string, array<string, mixed>> $queries
     * @return array<string, DatabaseResult>
     */
    public function queryBatch(array $queries, int $maxConcurrency = 5): array
    {
        $pool = new AsyncPool($maxConcurrency, $this->fiberManager);

        foreach ($queries as $key => $query) {
            $pool->add(
                fn () => $this->query($query['sql'], $query['params'] ?? []),
                $key
            );
        }

        return $pool->execute();
    }

    /**
     * Führt eine SELECT-Query aus
     *
     * @param array<string, mixed> $params
     * @return array<int, array<string, mixed>>
     */
    public function select(string $sql, array $params = []): array
    {
        return $this->query($sql, $params)->fetchAll();
    }

    /**
     * Führt eine INSERT-Query aus
     *
     * @param array<string, mixed> $data
     */
    public function insert(string $table, array $data): int
    {
        /** @var array<int, string> */
        $columns = array_keys($data);
        /** @var array<int, string> */
        $placeholders = array_map(fn ($col) => ":$col", $columns);

        $sql = "INSERT INTO $table (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";

        $this->query($sql, $data);

        return (int)$this->connection->lastInsertId();
    }

    /**
     * Führt mehrere INSERTs parallel aus
     *
     * @param array<int, array<string, mixed>> $records
     * @return array<string, int>
     */
    public function insertMultiple(string $table, array $records): array
    {
        /** @var array<int|string, \Closure> */
        $operations = [];
        foreach ($records as $key => $data) {
            $operations[$key] = fn () => $this->insert($table, $data);
        }

        return $this->fiberManager->batch($operations);
    }

    /**
     * Führt eine UPDATE-Query aus
     *
     * @param array<string, mixed> $data
     * @param array<string, mixed> $where
     */
    public function update(string $table, array $data, array $where): int
    {
        /** @var array<int, string> */
        $setParts = array_map(fn ($col) => "$col = :set_$col", array_keys($data));
        /** @var array<int, string> */
        $whereParts = array_map(fn ($col) => "$col = :where_$col", array_keys($where));

        $sql = "UPDATE $table SET " . implode(', ', $setParts) . " WHERE " . implode(' AND ', $whereParts);

        /** @var array<string, mixed> */
        $params = [];
        foreach ($data as $key => $value) {
            $params["set_$key"] = $value;
        }
        foreach ($where as $key => $value) {
            $params["where_$key"] = $value;
        }

        return $this->query($sql, $params)->getAffectedRows();
    }

    /**
     * Führt eine DELETE-Query aus
     *
     * @param array<string, mixed> $where
     */
    public function delete(string $table, array $where): int
    {
        /** @var array<int, string> */
        $whereParts = array_map(fn ($col) => "$col = :$col", array_keys($where));
        $sql = "DELETE FROM $table WHERE " . implode(' AND ', $whereParts);

        return $this->query($sql, $where)->getAffectedRows();
    }

    /**
     * Startet eine Transaktion
     */
    public function beginTransaction(): void
    {
        $this->connection->beginTransaction();
    }

    /**
     * Bestätigt eine Transaktion
     */
    public function commit(): void
    {
        $this->connection->commit();
    }

    /**
     * Bricht eine Transaktion ab
     */
    public function rollback(): void
    {
        $this->connection->rollBack();
    }

    /**
     * Führt eine Funktion in einer Transaktion aus
     */
    public function transaction(callable $callback): mixed
    {
        $this->beginTransaction();

        try {
            $result = $callback($this);
            $this->commit();

            return $result;
        } catch (\Throwable $e) {
            $this->rollback();

            throw $e;
        }
    }

    /**
     * Gibt die rohe PDO-Verbindung zurück
     */
    public function getConnection(): PDO
    {
        return $this->connection;
    }
}
