<?php

declare(strict_types=1);

namespace App\Framework\Queue\Services;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Queue\Interfaces\DistributedLockInterface;
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\WorkerId;

/**
 * Database-basierte Distributed Lock Implementation
 */
final readonly class DatabaseDistributedLock implements DistributedLockInterface
{
    private const TABLE_NAME = 'distributed_locks';
    private const MAX_TIMEOUT_ATTEMPTS = 100;
    private const TIMEOUT_SLEEP_MS = 100;

    public function __construct(
        private ConnectionInterface $connection,
        private Logger $logger
    ) {
    }

    /**
     * Lock erwerben
     */
    public function acquire(LockKey $key, WorkerId $workerId, Duration $ttl): bool
    {
        $this->logger->debug('Attempting to acquire lock', LogContext::withData([
            'lock_key' => $key->toString(),
            'worker_id' => $workerId->toString(),
            'ttl_seconds' => $ttl->toSeconds(),
        ]));

        try {
            $expiresAt = new \DateTimeImmutable('+' . $ttl->toSeconds() . ' seconds');

            // Versuche Lock zu erstellen
            $sql = "INSERT INTO " . self::TABLE_NAME . " (
                        lock_key, worker_id, acquired_at, expires_at
                    ) VALUES (
                        :lock_key, :worker_id, NOW(), :expires_at
                    )";

            $affectedRows = $this->connection->execute(SqlQuery::create($sql, [
                'lock_key' => $key->toString(),
                'worker_id' => $workerId->toString(),
                'expires_at' => $expiresAt->format('Y-m-d H:i:s'),
            ]));

            if ($affectedRows > 0) {
                $this->logger->info('Lock acquired successfully', LogContext::withData([
                    'lock_key' => $key->toString(),
                    'worker_id' => $workerId->toString(),
                ]));

                return true;
            }

            return false;

        } catch (\Exception $e) {
            // Duplicate key error = Lock bereits vorhanden
            if ($this->isDuplicateKeyError($e)) {
                $this->logger->debug('Lock already exists', LogContext::withData([
                    'lock_key' => $key->toString(),
                    'worker_id' => $workerId->toString(),
                ]));

                return false;
            }

            $this->logger->error('Failed to acquire lock', LogContext::withData([
                'lock_key' => $key->toString(),
                'worker_id' => $workerId->toString(),
                'error' => $e->getMessage(),
            ]));

            throw $e;
        }
    }

    /**
     * Lock verlängern
     */
    public function extend(LockKey $key, WorkerId $workerId, Duration $ttl): bool
    {
        $this->logger->debug('Attempting to extend lock', LogContext::withData([
            'lock_key' => $key->toString(),
            'worker_id' => $workerId->toString(),
            'ttl_seconds' => $ttl->toSeconds(),
        ]));

        try {
            $expiresAt = new \DateTimeImmutable('+' . $ttl->toSeconds() . ' seconds');

            $sql = "UPDATE " . self::TABLE_NAME . "
                   SET expires_at = :expires_at
                   WHERE lock_key = :lock_key
                   AND worker_id = :worker_id
                   AND expires_at > NOW()";

            $affectedRows = $this->connection->execute(SqlQuery::create($sql, [
                'lock_key' => $key->toString(),
                'worker_id' => $workerId->toString(),
                'expires_at' => $expiresAt->format('Y-m-d H:i:s'),
            ]));

            $success = $affectedRows > 0;

            if ($success) {
                $this->logger->debug('Lock extended successfully', LogContext::withData([
                    'lock_key' => $key->toString(),
                    'worker_id' => $workerId->toString(),
                ]));
            } else {
                $this->logger->warning('Failed to extend lock - not owned or expired', LogContext::withData([
                    'lock_key' => $key->toString(),
                    'worker_id' => $workerId->toString(),
                ]));
            }

            return $success;

        } catch (\Exception $e) {
            $this->logger->error('Failed to extend lock', LogContext::withData([
                'lock_key' => $key->toString(),
                'worker_id' => $workerId->toString(),
                'error' => $e->getMessage(),
            ]));

            throw $e;
        }
    }

    /**
     * Lock freigeben
     */
    public function release(LockKey $key, WorkerId $workerId): bool
    {
        $this->logger->debug('Attempting to release lock', LogContext::withData([
            'lock_key' => $key->toString(),
            'worker_id' => $workerId->toString(),
        ]));

        try {
            $sql = "DELETE FROM " . self::TABLE_NAME . "
                   WHERE lock_key = :lock_key
                   AND worker_id = :worker_id";

            $affectedRows = $this->connection->execute(SqlQuery::create($sql, [
                'lock_key' => $key->toString(),
                'worker_id' => $workerId->toString(),
            ]));

            $success = $affectedRows > 0;

            if ($success) {
                $this->logger->info('Lock released successfully', LogContext::withData([
                    'lock_key' => $key->toString(),
                    'worker_id' => $workerId->toString(),
                ]));
            } else {
                $this->logger->warning('Lock not found or not owned by worker', LogContext::withData([
                    'lock_key' => $key->toString(),
                    'worker_id' => $workerId->toString(),
                ]));
            }

            return $success;

        } catch (\Exception $e) {
            $this->logger->error('Failed to release lock', LogContext::withData([
                'lock_key' => $key->toString(),
                'worker_id' => $workerId->toString(),
                'error' => $e->getMessage(),
            ]));

            throw $e;
        }
    }

    /**
     * Prüfe ob Lock existiert
     */
    public function exists(LockKey $key): bool
    {
        try {
            $sql = "SELECT COUNT(*) as count FROM " . self::TABLE_NAME . "
                   WHERE lock_key = :lock_key
                   AND expires_at > NOW()";

            $result = $this->connection->queryOne(SqlQuery::create($sql, ['lock_key' => $key->toString()]));

            return (int) $result['count'] > 0;

        } catch (\Exception $e) {
            $this->logger->error('Failed to check lock existence', LogContext::withData([
                'lock_key' => $key->toString(),
                'error' => $e->getMessage(),
            ]));

            throw $e;
        }
    }

    /**
     * Lock Informationen abrufen
     */
    public function getLockInfo(LockKey $key): ?array
    {
        try {
            $sql = "SELECT * FROM " . self::TABLE_NAME . "
                   WHERE lock_key = :lock_key
                   AND expires_at > NOW()";

            $data = $this->connection->queryOne(SqlQuery::create($sql, ['lock_key' => $key->toString()]));
            if (! $data) {
                return null;
            }

            return [
                'lock_key' => $data['lock_key'],
                'worker_id' => $data['worker_id'],
                'acquired_at' => $data['acquired_at'],
                'expires_at' => $data['expires_at'],
                'ttl_seconds' => strtotime($data['expires_at']) - time(),
            ];

        } catch (\Exception $e) {
            $this->logger->error('Failed to get lock info', LogContext::withData([
                'lock_key' => $key->toString(),
                'error' => $e->getMessage(),
            ]));

            throw $e;
        }
    }

    /**
     * Lock mit Timeout versuchen
     */
    public function acquireWithTimeout(LockKey $key, WorkerId $workerId, Duration $ttl, Duration $timeout): bool
    {
        $this->logger->debug('Attempting to acquire lock with timeout', LogContext::withData([
            'lock_key' => $key->toString(),
            'worker_id' => $workerId->toString(),
            'ttl_seconds' => $ttl->toSeconds(),
            'timeout_seconds' => $timeout->toSeconds(),
        ]));

        $startTime = microtime(true);
        $timeoutSeconds = $timeout->toSeconds();
        $attempts = 0;
        $maxAttempts = min(self::MAX_TIMEOUT_ATTEMPTS, (int) ($timeoutSeconds * 1000 / self::TIMEOUT_SLEEP_MS));

        while ($attempts < $maxAttempts) {
            // Prüfe Timeout
            if ((microtime(true) - $startTime) >= $timeoutSeconds) {
                $this->logger->debug('Lock acquisition timeout reached', LogContext::withData([
                    'lock_key' => $key->toString(),
                    'worker_id' => $workerId->toString(),
                    'attempts' => $attempts,
                    'elapsed_seconds' => microtime(true) - $startTime,
                ]));

                break;
            }

            // Versuche Lock zu erwerben
            if ($this->acquire($key, $workerId, $ttl)) {
                $this->logger->info('Lock acquired with timeout after attempts', LogContext::withData([
                    'lock_key' => $key->toString(),
                    'worker_id' => $workerId->toString(),
                    'attempts' => $attempts + 1,
                    'elapsed_seconds' => microtime(true) - $startTime,
                ]));

                return true;
            }

            $attempts++;

            // Kurz warten vor nächstem Versuch
            usleep(self::TIMEOUT_SLEEP_MS * 1000);
        }

        $this->logger->info('Failed to acquire lock within timeout', LogContext::withData([
            'lock_key' => $key->toString(),
            'worker_id' => $workerId->toString(),
            'attempts' => $attempts,
            'timeout_seconds' => $timeoutSeconds,
        ]));

        return false;
    }

    /**
     * Alle Locks eines Workers freigeben
     */
    public function releaseAllWorkerLocks(WorkerId $workerId): int
    {
        $this->logger->info('Releasing all locks for worker', LogContext::withData([
            'worker_id' => $workerId->toString(),
        ]));

        try {
            $sql = "DELETE FROM " . self::TABLE_NAME . "
                   WHERE worker_id = :worker_id";

            $count = $this->connection->execute(SqlQuery::create($sql, ['worker_id' => $workerId->toString()]));

            $this->logger->info('Released all worker locks', LogContext::withData([
                'worker_id' => $workerId->toString(),
                'released_count' => $count,
            ]));

            return $count;

        } catch (\Exception $e) {
            $this->logger->error('Failed to release worker locks', LogContext::withData([
                'worker_id' => $workerId->toString(),
                'error' => $e->getMessage(),
            ]));

            throw $e;
        }
    }

    /**
     * Abgelaufene Locks aufräumen
     */
    public function cleanupExpiredLocks(): int
    {
        $this->logger->debug('Starting cleanup of expired locks');

        try {
            $sql = "DELETE FROM " . self::TABLE_NAME . "
                   WHERE expires_at <= NOW()";

            $count = $this->connection->execute(SqlQuery::create($sql));

            $this->logger->info('Cleaned up expired locks', LogContext::withData([
                'cleaned_count' => $count,
            ]));

            return $count;

        } catch (\Exception $e) {
            $this->logger->error('Failed to cleanup expired locks', LogContext::withData([
                'error' => $e->getMessage(),
            ]));

            throw $e;
        }
    }

    /**
     * Lock Statistiken
     */
    public function getLockStatistics(): array
    {
        try {
            $sql = "SELECT
                       COUNT(*) as total_locks,
                       COUNT(CASE WHEN expires_at > NOW() THEN 1 END) as active_locks,
                       COUNT(CASE WHEN expires_at <= NOW() THEN 1 END) as expired_locks,
                       COUNT(DISTINCT worker_id) as unique_workers,
                       AVG(TIMESTAMPDIFF(SECOND, acquired_at, expires_at)) as avg_ttl_seconds,
                       MIN(acquired_at) as oldest_lock,
                       MAX(acquired_at) as newest_lock
                   FROM " . self::TABLE_NAME;

            $stats = $this->connection->queryOne(SqlQuery::create($sql));

            // Top Lock Keys
            $topKeysSql = "SELECT lock_key, COUNT(*) as count
                          FROM " . self::TABLE_NAME . "
                          WHERE expires_at > NOW()
                          GROUP BY lock_key
                          ORDER BY count DESC
                          LIMIT 10";

            $result = $this->connection->query(SqlQuery::create($topKeysSql));
            $topKeys = $result->fetchAll();

            return [
                'total_locks' => (int) $stats['total_locks'],
                'active_locks' => (int) $stats['active_locks'],
                'expired_locks' => (int) $stats['expired_locks'],
                'unique_workers' => (int) $stats['unique_workers'],
                'avg_ttl_seconds' => round((float) $stats['avg_ttl_seconds'], 2),
                'oldest_lock' => $stats['oldest_lock'],
                'newest_lock' => $stats['newest_lock'],
                'top_lock_keys' => $topKeys,
            ];

        } catch (\Exception $e) {
            $this->logger->error('Failed to get lock statistics', LogContext::withData([
                'error' => $e->getMessage(),
            ]));

            throw $e;
        }
    }

    /**
     * Prüfe ob Exception ein Duplicate Key Error ist
     */
    private function isDuplicateKeyError(\Exception $e): bool
    {
        $message = strtolower($e->getMessage());

        return strpos($message, 'duplicate') !== false
            || strpos($message, 'unique') !== false
            || $e->getCode() === 23000; // SQLSTATE für Constraint Violation
    }
}
