<?php

declare(strict_types=1);

namespace App\Framework\Queue;

use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\EntityManager;
use App\Framework\Queue\Entities\JobHistoryEntry;
use App\Framework\Queue\Entities\JobIndexEntry;
use App\Framework\Queue\Exceptions\JobNotFoundException;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\JobState;
use App\Framework\Queue\ValueObjects\JobStatus;
use App\Framework\Queue\ValueObjects\QueueType;
use App\Framework\StateManagement\StateManager;

/**
 * Production Job Persistence Layer
 *
 * High-performance job persistence with database indexing and caching
 */
final readonly class ProductionJobPersistenceLayer
{
    public function __construct(
        private StateManager $stateManager,      // Hot data (active jobs)
        private EntityManager $entityManager,   // Persistent storage + queries
        private Cache $cache                     // Query results cache
    ) {
    }

    /**
     * Store a new job
     */
    public function storeJob(
        JobId $jobId,
        QueueType $queueType,
        mixed $jobData,
        int $maxAttempts = 3,
        array $metadata = []
    ): JobState {
        $jobState = JobState::create(
            jobId: $jobId,
            queueType: $queueType,
            priority: $queueType->getDefaultPriority(),
            maxAttempts: $maxAttempts,
            metadata: array_merge($metadata, ['job_data' => $jobData])
        );

        // 1. Store in StateManager (hot storage for workers)
        $this->stateManager->setState(
            key: $this->getJobKey($jobId),
            state: $jobState,
            ttl: Duration::fromDays(1) // Shorter TTL for hot storage
        );

        // 2. Store in database (persistent indexing)
        $this->entityManager->beginTransaction();

        try {
            $indexEntry = JobIndexEntry::fromJobState($jobState);
            $this->entityManager->persist($indexEntry);

            // Create history entry for job creation
            $historyEntry = JobHistoryEntry::forStatusChange(
                jobId: $jobId,
                oldStatus: null,
                newStatus: JobStatus::PENDING,
                metadata: ['queue_type' => $queueType->value, 'priority' => $jobState->priority->value]
            );
            $this->entityManager->persist($historyEntry);

            $this->entityManager->flush();
            $this->entityManager->commit();
        } catch (\Exception $e) {
            $this->entityManager->rollback();

            throw $e;
        }

        // 3. Invalidate relevant caches
        $this->invalidateStatusCache($jobState->status);

        return $jobState;
    }

    /**
     * Get job state
     */
    public function getJobState(JobId $jobId): ?JobState
    {
        // Try StateManager first (hot storage)
        $jobState = $this->stateManager->getState($this->getJobKey($jobId));

        if ($jobState !== null) {
            return $jobState;
        }

        // Fallback to database if not in hot storage
        $indexEntry = $this->entityManager->find(JobIndexEntry::class, $jobId->toString());

        if ($indexEntry === null) {
            return null;
        }

        // Reconstruct JobState from index entry (simplified - metadata would need to be stored separately)
        return JobState::create(
            jobId: $indexEntry->jobId,
            queueType: $indexEntry->queueType,
            priority: $indexEntry->priority,
            maxAttempts: $indexEntry->maxAttempts,
            metadata: [] // TODO: Store metadata separately if needed
        );
    }

    /**
     * Update job state atomically
     */
    public function updateJobState(JobId $jobId, callable $updater): JobState
    {
        $currentState = $this->getJobState($jobId);

        if ($currentState === null) {
            throw JobNotFoundException::byId($jobId);
        }

        $oldStatus = $currentState->status;
        $newState = $updater($currentState);

        // Update both StateManager and database
        $this->stateManager->setState(
            key: $this->getJobKey($jobId),
            state: $newState,
            ttl: Duration::fromDays(1)
        );

        $this->entityManager->beginTransaction();

        try {
            // Update index entry
            $indexEntry = $this->entityManager->find(JobIndexEntry::class, $jobId->toString());
            if ($indexEntry !== null) {
                $updatedEntry = $indexEntry->updateFromJobState($newState);
                $this->entityManager->persist($updatedEntry);
            }

            // Create history entry for status change
            if ($oldStatus !== $newState->status) {
                $historyEntry = JobHistoryEntry::forStatusChange(
                    jobId: $jobId,
                    oldStatus: $oldStatus,
                    newStatus: $newState->status,
                    errorMessage: $newState->errorMessage
                );
                $this->entityManager->persist($historyEntry);
            }

            $this->entityManager->flush();
            $this->entityManager->commit();
        } catch (\Exception $e) {
            $this->entityManager->rollback();

            throw $e;
        }

        // Invalidate caches
        if ($oldStatus !== $newState->status) {
            $this->invalidateStatusCache($oldStatus);
            $this->invalidateStatusCache($newState->status);
        }

        return $newState;
    }

    /**
     * Mark job as processing
     */
    public function markAsProcessing(JobId $jobId): JobState
    {
        return $this->updateJobState($jobId, function (?JobState $state) use ($jobId): JobState {
            if ($state === null) {
                throw JobNotFoundException::byId($jobId);
            }

            return $state->markAsProcessing();
        });
    }

    /**
     * Mark job as completed
     */
    public function markAsCompleted(JobId $jobId, array $result = []): JobState
    {
        return $this->updateJobState($jobId, function (?JobState $state) use ($jobId, $result): JobState {
            if ($state === null) {
                throw JobNotFoundException::byId($jobId);
            }

            $completedState = $state->markAsCompleted();

            if (! empty($result)) {
                $completedState = $completedState->withMetadata(['result' => $result]);
            }

            return $completedState;
        });
    }

    /**
     * Mark job as failed
     */
    public function markAsFailed(JobId $jobId, string $errorMessage, ?\Throwable $exception = null): JobState
    {
        return $this->updateJobState($jobId, function (?JobState $state) use ($jobId, $errorMessage, $exception): JobState {
            if ($state === null) {
                throw JobNotFoundException::byId($jobId);
            }

            $failedState = $state->markAsFailed($errorMessage);

            if ($exception !== null) {
                $failedState = $failedState->withMetadata([
                    'exception_class' => get_class($exception),
                    'exception_trace' => $exception->getTraceAsString(),
                ]);
            }

            return $failedState;
        });
    }

    /**
     * Mark job for retry
     */
    public function markForRetry(JobId $jobId, string $errorMessage): JobState
    {
        return $this->updateJobState($jobId, function (?JobState $state) use ($jobId, $errorMessage): JobState {
            if ($state === null) {
                throw JobNotFoundException::byId($jobId);
            }

            return $state->markForRetry($errorMessage);
        });
    }

    /**
     * Get jobs by status
     *
     * @return JobState[]
     */
    public function getJobsByStatus(JobStatus $status, int $limit = 100): array
    {
        $cacheKey = CacheKey::fromString("jobs_by_status:{$status->value}:{$limit}");

        // Try cache first
        $cacheResult = $this->cache->get($cacheKey);
        $jobIds = $cacheResult->getHits()[$cacheKey->toString()] ?? null;

        if ($jobIds === null) {
            // Query database for job IDs
            $repository = $this->entityManager->getRepository(JobIndexEntry::class);
            $queryBuilder = $this->entityManager->createQueryBuilder();

            $indexEntries = $queryBuilder
                ->select('j')
                ->from(JobIndexEntry::class, 'j')
                ->where('j.status = :status')
                ->orderBy('j.priority', 'DESC')
                ->addOrderBy('j.createdAt', 'ASC')
                ->setParameter('status', $status->value)
                ->setMaxResults($limit)
                ->getQuery()
                ->getResult();

            $jobIds = array_map(fn ($entry) => $entry->jobId->toString(), $indexEntries);

            // Cache for 30 seconds
            $cacheItem = CacheItem::forSetting(
                key: $cacheKey,
                value: $jobIds,
                ttl: Duration::fromSeconds(30)
            );
            $this->cache->set($cacheItem);
        }

        // Load actual job states from StateManager (hot) or fallback to database
        $jobs = [];
        foreach ($jobIds as $jobIdString) {
            $jobId = JobId::fromString($jobIdString);
            $jobState = $this->getJobState($jobId);
            if ($jobState && $jobState->status === $status) {
                $jobs[] = $jobState;
            }
        }

        return $jobs;
    }

    /**
     * Get failed jobs that can be retried
     *
     * @return JobState[]
     */
    public function getRetryableJobs(int $limit = 50): array
    {
        $repository = $this->entityManager->getRepository(JobIndexEntry::class);
        $queryBuilder = $this->entityManager->createQueryBuilder();

        $indexEntries = $queryBuilder
            ->select('j')
            ->from(JobIndexEntry::class, 'j')
            ->where('j.status IN (:retryable_statuses)')
            ->andWhere('j.attempts < j.maxAttempts')
            ->orderBy('j.priority', 'DESC')
            ->addOrderBy('j.createdAt', 'ASC')
            ->setParameter('retryable_statuses', [
                JobStatus::FAILED->value,
                JobStatus::RETRYING->value,
            ])
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();

        $jobs = [];
        foreach ($indexEntries as $entry) {
            // Check if job needs retry based on backoff
            if ($entry->needsRetry()) {
                $jobState = $this->getJobState($entry->jobId);
                if ($jobState) {
                    $jobs[] = $jobState;
                }
            }
        }

        return $jobs;
    }

    /**
     * Clean up old completed jobs
     */
    public function cleanup(?Duration $olderThan = null): int
    {
        $olderThan ??= Duration::fromDays(30);
        $cutoffTimestamp = time() - $olderThan->toSeconds();

        $queryBuilder = $this->entityManager->createQueryBuilder();

        $deletedCount = $queryBuilder
            ->delete(JobIndexEntry::class, 'j')
            ->where('j.status IN (:final_statuses)')
            ->andWhere('j.completedAt < :cutoff')
            ->setParameter('final_statuses', [
                JobStatus::COMPLETED->value,
                JobStatus::CANCELLED->value,
                JobStatus::EXPIRED->value,
            ])
            ->setParameter('cutoff', $cutoffTimestamp)
            ->getQuery()
            ->execute();

        // Also clean up history entries
        $queryBuilder = $this->entityManager->createQueryBuilder();
        $queryBuilder
            ->delete(JobHistoryEntry::class, 'h')
            ->where('h.changedAt < :cutoff')
            ->setParameter('cutoff', $cutoffTimestamp)
            ->getQuery()
            ->execute();

        return $deletedCount;
    }

    /**
     * Get job statistics
     */
    public function getStats(): array
    {
        $cacheKey = CacheKey::fromString('job_stats');
        $cacheResult = $this->cache->get($cacheKey);
        $stats = $cacheResult->getHits()[$cacheKey->toString()] ?? null;

        if ($stats === null) {
            $queryBuilder = $this->entityManager->createQueryBuilder();

            $result = $queryBuilder
                ->select('j.status, COUNT(j.jobId) as count')
                ->from(JobIndexEntry::class, 'j')
                ->groupBy('j.status')
                ->getQuery()
                ->getArrayResult();

            $stats = array_fill_keys([
                'pending', 'processing', 'completed', 'failed', 'retrying', 'cancelled', 'expired',
            ], 0);

            foreach ($result as $row) {
                $stats[$row['status']] = (int) $row['count'];
            }

            $stats['total_jobs'] = array_sum($stats);

            // Cache for 1 minute
            $cacheItem = CacheItem::forSetting(
                key: $cacheKey,
                value: $stats,
                ttl: Duration::fromMinutes(1)
            );
            $this->cache->set($cacheItem);
        }

        return $stats;
    }

    /**
     * Check if job exists
     */
    public function hasJob(JobId $jobId): bool
    {
        // Check StateManager first
        if ($this->stateManager->hasState($this->getJobKey($jobId))) {
            return true;
        }

        // Check database
        $indexEntry = $this->entityManager->find(JobIndexEntry::class, $jobId->toString());

        return $indexEntry !== null;
    }

    /**
     * Remove job from persistence
     */
    public function removeJob(JobId $jobId): void
    {
        // Remove from StateManager
        $this->stateManager->removeState($this->getJobKey($jobId));

        // Remove from database
        $this->entityManager->beginTransaction();

        try {
            $indexEntry = $this->entityManager->find(JobIndexEntry::class, $jobId->toString());
            if ($indexEntry !== null) {
                $this->entityManager->remove($indexEntry);
            }

            $this->entityManager->flush();
            $this->entityManager->commit();
        } catch (\Exception $e) {
            $this->entityManager->rollback();

            throw $e;
        }
    }

    /**
     * Generate state key for job
     */
    private function getJobKey(JobId $jobId): string
    {
        return "job:{$jobId->toString()}";
    }

    /**
     * Invalidate status-related caches
     */
    private function invalidateStatusCache(JobStatus $status): void
    {
        $cacheKeys = [
            CacheKey::fromString("jobs_by_status:{$status->value}:50"),
            CacheKey::fromString("jobs_by_status:{$status->value}:100"),
            CacheKey::fromString('job_stats'),
        ];

        $this->cache->forget(...$cacheKeys);
    }
}
