<?php

declare(strict_types=1);

namespace App\Framework\Queue\Services;

use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Percentage;
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\Entities\Worker;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\WorkerId;

/**
 * Worker Registry für Distributed Job Processing
 */
final readonly class WorkerRegistry
{
    private const TABLE_NAME = 'queue_workers';

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

    /**
     * Worker registrieren
     */
    public function register(Worker $worker): void
    {
        $this->logger->info('Registering worker', LogContext::withData([
            'worker_id' => $worker->id->toString(),
            'hostname' => $worker->hostname,
            'process_id' => $worker->processId,
            'queues' => $worker->queues,
            'max_jobs' => $worker->maxJobs,
        ]));

        try {
            $sql = "INSERT INTO " . self::TABLE_NAME . " (
                id, hostname, process_id, queues, max_jobs, current_jobs,
                is_active, cpu_usage, memory_usage_bytes, registered_at,
                last_heartbeat, capabilities, version
            ) VALUES (
                :id, :hostname, :process_id, :queues, :max_jobs, :current_jobs,
                :is_active, :cpu_usage, :memory_usage_bytes, :registered_at,
                :last_heartbeat, :capabilities, :version
            ) ON CONFLICT (id) DO UPDATE SET
                is_active = EXCLUDED.is_active,
                last_heartbeat = EXCLUDED.last_heartbeat,
                cpu_usage = EXCLUDED.cpu_usage,
                memory_usage_bytes = EXCLUDED.memory_usage_bytes,
                current_jobs = EXCLUDED.current_jobs,
                capabilities = EXCLUDED.capabilities,
                version = EXCLUDED.version";

            $data = $worker->toArray();
            // PostgreSQL expects boolean values directly
            $data['is_active'] = $data['is_active'] ? true : false;

            $this->connection->execute(SqlQuery::create($sql, $data));

            $this->logger->debug('Worker registered successfully', LogContext::withData([
                'worker_id' => $worker->id->toString(),
            ]));

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

            throw $e;
        }
    }

    /**
     * Worker deregistrieren
     */
    public function deregister(WorkerId $workerId): void
    {
        $this->logger->info('Deregistering worker', LogContext::withData([
            'worker_id' => $workerId->toString(),
        ]));

        try {
            $sql = "UPDATE " . self::TABLE_NAME . "
                   SET is_active = false, last_heartbeat = NOW()
                   WHERE id = :id";

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

            $this->logger->debug('Worker deregistered successfully', LogContext::withData([
                'worker_id' => $workerId->toString(),
            ]));

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

            throw $e;
        }
    }

    /**
     * Worker Heartbeat aktualisieren
     */
    public function updateHeartbeat(
        WorkerId $workerId,
        Percentage $cpuUsage,
        Byte $memoryUsage,
        int $currentJobs
    ): void {
        try {
            $sql = "UPDATE " . self::TABLE_NAME . "
                   SET last_heartbeat = NOW(),
                       cpu_usage = :cpu_usage,
                       memory_usage_bytes = :memory_usage_bytes,
                       current_jobs = :current_jobs,
                       is_active = true
                   WHERE id = :id";

            $affectedRows = $this->connection->execute(SqlQuery::create($sql, [
                'id' => $workerId->toString(),
                'cpu_usage' => $cpuUsage->getValue(),
                'memory_usage_bytes' => $memoryUsage->toBytes(),
                'current_jobs' => $currentJobs,
            ]));

            if ($affectedRows === 0) {
                $this->logger->warning('Heartbeat update failed - worker not found', LogContext::withData([
                    'worker_id' => $workerId->toString(),
                ]));
            }

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

            throw $e;
        }
    }

    /**
     * Worker by ID finden
     */
    public function findById(WorkerId $workerId): ?Worker
    {
        try {
            $sql = "SELECT * FROM " . self::TABLE_NAME . " WHERE id = :id";
            $data = $this->connection->queryOne(SqlQuery::create($sql, ['id' => $workerId->toString()]));
            if (! $data) {
                return null;
            }

            return Worker::fromArray($data);

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

            throw $e;
        }
    }

    /**
     * Alle aktiven Worker finden
     */
    public function findActiveWorkers(): array
    {
        try {
            $sql = "SELECT * FROM " . self::TABLE_NAME . "
                   WHERE is_active = true
                   AND last_heartbeat > NOW() - INTERVAL '2 minutes'
                   ORDER BY hostname, process_id";

            $result = $this->connection->query(SqlQuery::create($sql));
            $workers = [];
            foreach ($result->fetchAll() as $data) {
                $workers[] = Worker::fromArray($data);
            }

            return $workers;

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

            throw $e;
        }
    }

    /**
     * Worker für bestimmte Queue finden
     */
    public function findWorkersForQueue(QueueName $queueName): array
    {
        try {
            $sql = "SELECT * FROM " . self::TABLE_NAME . "
                   WHERE is_active = true
                   AND last_heartbeat > NOW() - INTERVAL '2 minutes'
                   AND queues::jsonb @> to_jsonb(:queue_name::text)
                   ORDER BY current_jobs ASC, cpu_usage ASC";

            $result = $this->connection->query(SqlQuery::create($sql, ['queue_name' => $queueName->toString()]));
            $workers = [];
            foreach ($result->fetchAll() as $data) {
                $worker = Worker::fromArray($data);
                if ($worker->handlesQueue($queueName) && $worker->isAvailableForJobs()) {
                    $workers[] = $worker;
                }
            }

            return $workers;

        } catch (\Exception $e) {
            $this->logger->error('Failed to find workers for queue', LogContext::withData([
                'queue_name' => $queueName->toString(),
                'error' => $e->getMessage(),
            ]));

            throw $e;
        }
    }

    /**
     * Beste verfügbare Worker für Queue finden
     */
    public function findBestWorkerForQueue(QueueName $queueName): ?Worker
    {
        $workers = $this->findWorkersForQueue($queueName);

        if (empty($workers)) {
            return null;
        }

        // Sortiere nach Load (niedrigste zuerst)
        usort($workers, function (Worker $a, Worker $b) {
            return $a->getLoadPercentage()->getValue() <=> $b->getLoadPercentage()->getValue();
        });

        return $workers[0];
    }

    /**
     * Inaktive Worker cleanup
     */
    public function cleanupInactiveWorkers(int $inactiveMinutes = 5): int
    {
        $this->logger->info('Starting cleanup of inactive workers', LogContext::withData([
            'inactive_minutes' => $inactiveMinutes,
        ]));

        try {
            $sql = "UPDATE " . self::TABLE_NAME . "
                   SET is_active = false
                   WHERE is_active = true
                   AND (last_heartbeat IS NULL OR last_heartbeat < NOW() - INTERVAL '1 minute' * :minutes)";

            $count = $this->connection->execute(SqlQuery::create($sql, ['minutes' => $inactiveMinutes]));

            $this->logger->info('Inactive workers cleanup completed', LogContext::withData([
                'deactivated_count' => $count,
                'inactive_minutes' => $inactiveMinutes,
            ]));

            return $count;

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

            throw $e;
        }
    }

    /**
     * Worker Statistiken
     */
    public function getWorkerStatistics(): array
    {
        try {
            $sql = "SELECT
                       COUNT(*) as total_workers,
                       SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) as active_workers,
                       SUM(CASE WHEN is_active = true AND last_heartbeat > NOW() - INTERVAL '2 minutes' THEN 1 ELSE 0 END) as healthy_workers,
                       SUM(max_jobs) as total_capacity,
                       SUM(current_jobs) as current_load,
                       AVG(cpu_usage) as avg_cpu_usage,
                       AVG(memory_usage_bytes) as avg_memory_usage,
                       COUNT(DISTINCT hostname) as unique_hosts
                   FROM " . self::TABLE_NAME;

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

            // Queue-spezifische Statistiken
            $queueStats = $this->getQueueStatistics();

            return [
                'total_workers' => (int) $stats['total_workers'],
                'active_workers' => (int) $stats['active_workers'],
                'healthy_workers' => (int) $stats['healthy_workers'],
                'unique_hosts' => (int) $stats['unique_hosts'],
                'total_capacity' => (int) $stats['total_capacity'],
                'current_load' => (int) $stats['current_load'],
                'capacity_utilization' => $stats['total_capacity'] > 0
                    ? round(($stats['current_load'] / $stats['total_capacity']) * 100, 2)
                    : 0,
                'avg_cpu_usage' => round((float) $stats['avg_cpu_usage'], 2),
                'avg_memory_usage_mb' => round(((float) $stats['avg_memory_usage']) / 1024 / 1024, 2),
                'queue_distribution' => $queueStats,
            ];

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

            throw $e;
        }
    }

    /**
     * Queue-spezifische Statistiken
     */
    private function getQueueStatistics(): array
    {
        try {
            $sql = "SELECT queues FROM " . self::TABLE_NAME . " WHERE is_active = true";
            $result = $this->connection->query(SqlQuery::create($sql));

            $queueCounts = [];
            foreach ($result->fetchAll() as $row) {
                $queues = json_decode($row['queues'], true);
                foreach ($queues as $queue) {
                    $queueCounts[$queue] = ($queueCounts[$queue] ?? 0) + 1;
                }
            }

            arsort($queueCounts);

            return $queueCounts;

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

            return [];
        }
    }

    /**
     * Worker mit bestimmter Capability finden
     */
    public function findWorkersWithCapability(string $capability): array
    {
        try {
            $sql = "SELECT * FROM " . self::TABLE_NAME . "
                   WHERE is_active = true
                   AND last_heartbeat > NOW() - INTERVAL '2 minutes'
                   AND capabilities::jsonb @> to_jsonb(:capability::text)
                   ORDER BY current_jobs ASC";

            $result = $this->connection->query(SqlQuery::create($sql, ['capability' => $capability]));
            $workers = [];
            foreach ($result->fetchAll() as $data) {
                $worker = Worker::fromArray($data);
                if ($worker->hasCapability($capability) && $worker->isAvailableForJobs()) {
                    $workers[] = $worker;
                }
            }

            return $workers;

        } catch (\Exception $e) {
            $this->logger->error('Failed to find workers with capability', LogContext::withData([
                'capability' => $capability,
                'error' => $e->getMessage(),
            ]));

            throw $e;
        }
    }
}
