<?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\Entities\Worker;
use App\Framework\Queue\Interfaces\DistributedLockInterface;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\WorkerId;

/**
 * Service für intelligente Job-Verteilung an Worker
 */
final readonly class JobDistributionService
{
    private const JOB_LOCK_TTL_SECONDS = 300; // 5 Minuten
    private const DISTRIBUTION_LOCK_TTL_SECONDS = 30; // 30 Sekunden

    public function __construct(
        private WorkerRegistry $workerRegistry,
        private DistributedLockInterface $distributedLock,
        private ConnectionInterface $connection,
        private Logger $logger
    ) {
    }

    /**
     * Job an besten verfügbaren Worker verteilen
     */
    public function distributeJob(JobId $jobId, QueueName $queueName, array $jobData = []): ?WorkerId
    {
        $this->logger->info('Starting job distribution', LogContext::withData([
            'job_id' => $jobId->toString(),
            'queue_name' => $queueName->toString(),
        ]));

        // Lock für Distribution-Prozess erwerben
        $distributionLock = LockKey::forQueue($queueName)->withSuffix('distribution');
        $tempWorkerId = WorkerId::generate(); // Temporäre Worker ID für Distribution Lock

        if (! $this->distributedLock->acquireWithTimeout(
            $distributionLock,
            $tempWorkerId,
            Duration::fromSeconds(self::DISTRIBUTION_LOCK_TTL_SECONDS),
            Duration::fromSeconds(5)
        )) {
            $this->logger->warning('Failed to acquire distribution lock', LogContext::withData([
                'job_id' => $jobId->toString(),
                'queue_name' => $queueName->toString(),
            ]));

            return null;
        }

        try {
            // Job Lock erwerben
            $jobLock = LockKey::forJob($jobId);
            if (! $this->distributedLock->acquire(
                $jobLock,
                $tempWorkerId,
                Duration::fromSeconds(self::JOB_LOCK_TTL_SECONDS)
            )) {
                $this->logger->warning('Job already locked by another process', LogContext::withData([
                    'job_id' => $jobId->toString(),
                ]));

                return null;
            }

            // Besten Worker finden
            $worker = $this->findBestWorkerForJob($queueName, $jobData);
            if (! $worker) {
                $this->logger->warning('No suitable worker found', LogContext::withData([
                    'job_id' => $jobId->toString(),
                    'queue_name' => $queueName->toString(),
                ]));

                // Job Lock freigeben
                $this->distributedLock->release($jobLock, $tempWorkerId);

                return null;
            }

            // Job Lock an Worker übertragen
            if (! $this->transferJobLock($jobLock, $tempWorkerId, $worker->id)) {
                $this->logger->error('Failed to transfer job lock to worker', LogContext::withData([
                    'job_id' => $jobId->toString(),
                    'worker_id' => $worker->id->toString(),
                ]));

                // Job Lock freigeben
                $this->distributedLock->release($jobLock, $tempWorkerId);

                return null;
            }

            // Job Assignment in Datenbank speichern
            $this->recordJobAssignment($jobId, $worker->id, $queueName);

            $this->logger->info('Job successfully distributed', LogContext::withData([
                'job_id' => $jobId->toString(),
                'worker_id' => $worker->id->toString(),
                'queue_name' => $queueName->toString(),
                'worker_load' => $worker->getLoadPercentage()->getValue(),
            ]));

            return $worker->id;

        } finally {
            // Distribution Lock freigeben
            $this->distributedLock->release($distributionLock, $tempWorkerId);
        }
    }

    /**
     * Besten Worker für Job finden
     */
    public function findBestWorkerForJob(QueueName $queueName, array $jobData = []): ?Worker
    {
        // Worker für Queue finden
        $workers = $this->workerRegistry->findWorkersForQueue($queueName);

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

        // Worker nach verschiedenen Kriterien bewerten
        $scoredWorkers = [];

        foreach ($workers as $worker) {
            if (! $worker->isAvailableForJobs()) {
                continue;
            }

            $score = $this->calculateWorkerScore($worker, $jobData);
            $scoredWorkers[] = [
                'worker' => $worker,
                'score' => $score,
            ];
        }

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

        // Nach Score sortieren (höchster Score = bester Worker)
        usort($scoredWorkers, fn ($a, $b) => $b['score'] <=> $a['score']);

        return $scoredWorkers[0]['worker'];
    }

    /**
     * Worker Score berechnen (höher = besser)
     */
    private function calculateWorkerScore(Worker $worker, array $jobData): float
    {
        $score = 100.0; // Basis Score

        // Last-basierte Bewertung (weniger Last = höherer Score)
        $loadPenalty = $worker->getLoadPercentage()->getValue();
        $score -= $loadPenalty;

        // CPU-basierte Bewertung
        $cpuPenalty = $worker->cpuUsage->getValue() * 0.5;
        $score -= $cpuPenalty;

        // Memory-basierte Bewertung (vereinfacht)
        $memoryUsageGB = $worker->memoryUsage->toBytes() / (1024 * 1024 * 1024);
        if ($memoryUsageGB > 1.5) { // Über 1.5GB = Penalty
            $score -= ($memoryUsageGB - 1.5) * 10;
        }

        // Capability-basierte Bewertung
        if (isset($jobData['required_capabilities'])) {
            foreach ($jobData['required_capabilities'] as $capability) {
                if ($worker->hasCapability($capability)) {
                    $score += 20; // Bonus für jede erfüllte Capability
                } else {
                    return 0; // Worker kann Job nicht ausführen
                }
            }
        }

        // Bonus für Worker mit niedrigerer aktueller Job-Anzahl
        $availableSlots = $worker->maxJobs - $worker->currentJobs;
        $score += $availableSlots * 2;

        return max(0, $score);
    }

    /**
     * Job Lock von einem Worker an anderen übertragen
     */
    private function transferJobLock(LockKey $jobLock, WorkerId $fromWorker, WorkerId $toWorker): bool
    {
        // Neuen Lock für Ziel-Worker erstellen
        if (! $this->distributedLock->acquire(
            $jobLock,
            $toWorker,
            Duration::fromSeconds(self::JOB_LOCK_TTL_SECONDS)
        )) {
            return false;
        }

        // Alten Lock freigeben
        $this->distributedLock->release($jobLock, $fromWorker);

        return true;
    }

    /**
     * Job Assignment in Datenbank speichern
     */
    private function recordJobAssignment(JobId $jobId, WorkerId $workerId, QueueName $queueName): void
    {
        try {
            $sql = "INSERT INTO job_assignments (
                        job_id, worker_id, queue_name, assigned_at
                    ) VALUES (
                        :job_id, :worker_id, :queue_name, NOW()
                    ) ON DUPLICATE KEY UPDATE
                        worker_id = VALUES(worker_id),
                        assigned_at = VALUES(assigned_at)";

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

        } catch (\Exception $e) {
            $this->logger->error('Failed to record job assignment', LogContext::withData([
                'job_id' => $jobId->toString(),
                'worker_id' => $workerId->toString(),
                'error' => $e->getMessage(),
            ]));
            // Nicht critical - Job kann trotzdem verarbeitet werden
        }
    }

    /**
     * Job von Worker freigeben
     */
    public function releaseJob(JobId $jobId, WorkerId $workerId): bool
    {
        $this->logger->info('Releasing job from worker', LogContext::withData([
            'job_id' => $jobId->toString(),
            'worker_id' => $workerId->toString(),
        ]));

        // Job Lock freigeben
        $jobLock = LockKey::forJob($jobId);
        $released = $this->distributedLock->release($jobLock, $workerId);

        if ($released) {
            // Job Assignment aus Datenbank entfernen
            try {
                $sql = "DELETE FROM job_assignments
                       WHERE job_id = :job_id AND worker_id = :worker_id";

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

            } catch (\Exception $e) {
                $this->logger->warning('Failed to remove job assignment record', LogContext::withData([
                    'job_id' => $jobId->toString(),
                    'worker_id' => $workerId->toString(),
                    'error' => $e->getMessage(),
                ]));
            }

            $this->logger->info('Job released successfully', LogContext::withData([
                'job_id' => $jobId->toString(),
                'worker_id' => $workerId->toString(),
            ]));
        }

        return $released;
    }

    /**
     * Job Lock verlängern
     */
    public function extendJobLock(JobId $jobId, WorkerId $workerId, ?Duration $extension = null): bool
    {
        $extension = $extension ?? Duration::fromSeconds(self::JOB_LOCK_TTL_SECONDS);

        $jobLock = LockKey::forJob($jobId);

        return $this->distributedLock->extend($jobLock, $workerId, $extension);
    }

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

        // Alle Locks des Workers freigeben
        $releasedLocks = $this->distributedLock->releaseAllWorkerLocks($workerId);

        // Job Assignments aus Datenbank entfernen
        try {
            $sql = "DELETE FROM job_assignments WHERE worker_id = :worker_id";
            $releasedAssignments = $this->connection->execute(SqlQuery::create($sql, [
                'worker_id' => $workerId->toString(),
            ]));

            $this->logger->info('Released all worker jobs', LogContext::withData([
                'worker_id' => $workerId->toString(),
                'released_locks' => $releasedLocks,
                'released_assignments' => $releasedAssignments,
            ]));

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

        return $releasedLocks;
    }

    /**
     * Distribution Statistiken
     */
    public function getDistributionStatistics(): array
    {
        try {
            // Job Assignments Statistiken
            $sql = "SELECT
                       COUNT(*) as total_assignments,
                       COUNT(DISTINCT worker_id) as active_workers,
                       COUNT(DISTINCT queue_name) as active_queues,
                       AVG(TIMESTAMPDIFF(SECOND, assigned_at, NOW())) as avg_assignment_age_seconds
                   FROM job_assignments";

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

            // Worker Load Distribution
            $workerStats = [];
            $activeWorkers = $this->workerRegistry->findActiveWorkers();

            foreach ($activeWorkers as $worker) {
                $workerStats[] = [
                    'worker_id' => $worker->id->toString(),
                    'hostname' => $worker->hostname,
                    'current_jobs' => $worker->currentJobs,
                    'max_jobs' => $worker->maxJobs,
                    'load_percentage' => $worker->getLoadPercentage()->getValue(),
                    'queues' => array_map(fn ($q) => $q->toString(), $worker->queues),
                ];
            }

            // Lock Statistiken
            $lockStats = $this->distributedLock->getLockStatistics();

            return [
                'assignments' => [
                    'total' => (int) $assignmentStats['total_assignments'],
                    'active_workers' => (int) $assignmentStats['active_workers'],
                    'active_queues' => (int) $assignmentStats['active_queues'],
                    'avg_assignment_age_seconds' => round((float) $assignmentStats['avg_assignment_age_seconds'], 2),
                ],
                'workers' => $workerStats,
                'locks' => $lockStats,
                'distribution_health' => $this->calculateDistributionHealth($workerStats),
            ];

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

            throw $e;
        }
    }

    /**
     * Distribution Health Score berechnen
     */
    private function calculateDistributionHealth(array $workerStats): array
    {
        if (empty($workerStats)) {
            return [
                'score' => 0,
                'status' => 'critical',
                'issues' => ['No active workers'],
            ];
        }

        $score = 100;
        $issues = [];

        // Load Balance prüfen
        $loads = array_column($workerStats, 'load_percentage');
        $avgLoad = array_sum($loads) / count($loads);
        $maxLoad = max($loads);
        $minLoad = min($loads);

        if ($maxLoad > 90) {
            $score -= 30;
            $issues[] = 'Workers overloaded (>90%)';
        } elseif ($maxLoad > 75) {
            $score -= 15;
            $issues[] = 'High worker load (>75%)';
        }

        // Load Distribution prüfen
        $loadVariance = $maxLoad - $minLoad;
        if ($loadVariance > 50) {
            $score -= 20;
            $issues[] = 'Uneven load distribution';
        }

        // Worker Availability prüfen
        $availableWorkers = array_filter($workerStats, fn ($w) => $w['load_percentage'] < 80);
        $availabilityRatio = count($availableWorkers) / count($workerStats);

        if ($availabilityRatio < 0.3) {
            $score -= 25;
            $issues[] = 'Low worker availability (<30%)';
        }

        $status = match(true) {
            $score >= 80 => 'healthy',
            $score >= 60 => 'warning',
            $score >= 40 => 'degraded',
            default => 'critical'
        };

        return [
            'score' => max(0, $score),
            'status' => $status,
            'avg_load' => round($avgLoad, 2),
            'max_load' => $maxLoad,
            'load_variance' => $loadVariance,
            'availability_ratio' => round($availabilityRatio * 100, 2),
            'issues' => $issues,
        ];
    }
}
