<?php

declare(strict_types=1);

namespace App\Framework\Database\Locks;

use PDO;

/**
 * PostgreSQL Advisory Locks Service
 *
 * Advisory Locks sind application-managed Locks für verteilte Koordination:
 * - Session-scoped Locks (manuelles unlock erforderlich)
 * - Transaction-scoped Locks (auto-unlock bei COMMIT/ROLLBACK)
 *
 * Use Cases:
 * - Job Queue Processing (verhindert doppelte Verarbeitung)
 * - Singleton Operations (nur ein Prozess gleichzeitig)
 * - Distributed Resource Coordination
 */
final readonly class AdvisoryLockService
{
    public function __construct(
        private PDO $connection
    ) {}

    /**
     * Versucht, ein Lock zu erwerben (blockierend)
     *
     * Wartet, bis das Lock verfügbar ist.
     * Lock bleibt bis zum manuellen unlock() oder Session-Ende bestehen.
     *
     * @return bool True wenn Lock erworben wurde
     */
    public function lock(LockKey $key): bool
    {
        $stmt = $this->connection->prepare('SELECT pg_advisory_lock(:key)');
        $stmt->execute(['key' => $key->toInt()]);

        return true; // pg_advisory_lock gibt void zurück, kehrt erst zurück wenn Lock erworben
    }

    /**
     * Versucht, ein Lock zu erwerben (nicht-blockierend)
     *
     * Gibt sofort zurück, ob Lock erworben wurde oder bereits gehalten wird.
     *
     * @return bool True wenn Lock erworben wurde, False wenn Lock bereits gehalten wird
     */
    public function tryLock(LockKey $key): bool
    {
        $stmt = $this->connection->prepare('SELECT pg_try_advisory_lock(:key) as acquired');
        $stmt->execute(['key' => $key->toInt()]);

        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        return (bool) $result['acquired'];
    }

    /**
     * Gibt ein Session-Lock frei
     *
     * @return bool True wenn Lock freigegeben wurde, False wenn Lock nicht gehalten wurde
     */
    public function unlock(LockKey $key): bool
    {
        $stmt = $this->connection->prepare('SELECT pg_advisory_unlock(:key) as released');
        $stmt->execute(['key' => $key->toInt()]);

        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        return (bool) $result['released'];
    }

    /**
     * Gibt alle Session-Locks frei
     */
    public function unlockAll(): void
    {
        $this->connection->exec('SELECT pg_advisory_unlock_all()');
    }

    /**
     * Versucht, ein Transaction-scoped Lock zu erwerben (blockierend)
     *
     * Lock wird automatisch bei COMMIT/ROLLBACK freigegeben.
     * Kein manuelles unlock() erforderlich.
     */
    public function lockTransaction(LockKey $key): bool
    {
        $stmt = $this->connection->prepare('SELECT pg_advisory_xact_lock(:key)');
        $stmt->execute(['key' => $key->toInt()]);

        return true;
    }

    /**
     * Versucht, ein Transaction-scoped Lock zu erwerben (nicht-blockierend)
     *
     * Lock wird automatisch bei COMMIT/ROLLBACK freigegeben.
     *
     * @return bool True wenn Lock erworben wurde, False wenn Lock bereits gehalten wird
     */
    public function tryLockTransaction(LockKey $key): bool
    {
        $stmt = $this->connection->prepare('SELECT pg_try_advisory_xact_lock(:key) as acquired');
        $stmt->execute(['key' => $key->toInt()]);

        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        return (bool) $result['acquired'];
    }

    /**
     * Führt eine Operation mit einem Lock aus
     *
     * Lock wird automatisch freigegeben nach Operation.
     *
     * @template T
     * @param LockKey $key
     * @param callable(): T $callback
     * @return T
     */
    public function withLock(LockKey $key, callable $callback): mixed
    {
        $this->lock($key);

        try {
            return $callback();
        } finally {
            $this->unlock($key);
        }
    }

    /**
     * Versucht, eine Operation mit einem Lock auszuführen (nicht-blockierend)
     *
     * @template T
     * @param LockKey $key
     * @param callable(): T $callback
     * @return T|null Ergebnis der Operation oder null wenn Lock nicht erworben werden konnte
     */
    public function tryWithLock(LockKey $key, callable $callback): mixed
    {
        if (!$this->tryLock($key)) {
            return null;
        }

        try {
            return $callback();
        } finally {
            $this->unlock($key);
        }
    }

    /**
     * Führt eine Operation innerhalb einer Transaction mit Lock aus
     *
     * Lock wird automatisch bei COMMIT/ROLLBACK freigegeben.
     *
     * @template T
     * @param LockKey $key
     * @param callable(): T $callback
     * @return T
     */
    public function withTransactionLock(LockKey $key, callable $callback): mixed
    {
        $this->connection->beginTransaction();

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

            return $result;
        } catch (\Throwable $e) {
            $this->connection->rollBack();
            throw $e;
        }
    }
}
