<?php

declare(strict_types=1);

namespace App\Framework\EventSourcing\Projections;

use App\Framework\EventSourcing\AggregateEnvelope;
use App\Framework\EventSourcing\EventStore;
use App\Framework\Queue\Queue;
use App\Framework\Queue\ValueObjects\JobPayload;

/**
 * Projection Manager
 *
 * Orchestrates projection execution and manages projection lifecycle
 */
final readonly class ProjectionManager
{
    /**
     * @param array<ProjectionInterface> $projections
     */
    public function __construct(
        private EventStore $eventStore,
        private ProjectionRepository $repository,
        private Queue $queue,
        private array $projections = []
    ) {
    }

    /**
     * Run single projection synchronously
     */
    public function runProjection(string $projectionName): ProjectionState
    {
        $projection = $this->findProjection($projectionName);
        $state = $this->repository->getState($projectionName);

        try {
            // Get events since last processed version
            $events = $this->getEventsSince($state->lastEventVersion);

            foreach ($events as $envelope) {
                // Check if projection subscribes to this event type
                if ($this->shouldProject($projection, $envelope)) {
                    $projection->project($envelope);
                    $state = $state->withEventProcessed($envelope->version);
                    $this->repository->saveState($state);
                }
            }

            $state = $state->withCompleted();
            $this->repository->saveState($state);

            return $state;
        } catch (\Throwable $e) {
            $state = $state->withError($e->getMessage());
            $this->repository->saveState($state);

            throw $e;
        }
    }

    /**
     * Run projection asynchronously via queue
     */
    public function runProjectionAsync(string $projectionName): void
    {
        $job = new RunProjectionJob($projectionName);
        $payload = JobPayload::immediate($job);
        $this->queue->push($payload);
    }

    /**
     * Run all projections
     */
    public function runAllProjections(): array
    {
        $results = [];

        foreach ($this->projections as $projection) {
            $results[$projection->getName()] = $this->runProjection(
                $projection->getName()
            );
        }

        return $results;
    }

    /**
     * Rebuild projection from scratch
     */
    public function rebuildProjection(string $projectionName): ProjectionState
    {
        $projection = $this->findProjection($projectionName);

        // Reset projection and read model
        $projection->reset();

        // Reset state to initial
        $state = ProjectionState::initial($projectionName);
        $this->repository->saveState($state);

        // Run from beginning
        return $this->runProjection($projectionName);
    }

    /**
     * Get projection status
     */
    public function getProjectionStatus(string $projectionName): ProjectionState
    {
        return $this->repository->getState($projectionName);
    }

    /**
     * Get all projection statuses
     */
    public function getAllProjectionStatuses(): array
    {
        $statuses = [];

        foreach ($this->projections as $projection) {
            $statuses[$projection->getName()] = $this->repository->getState(
                $projection->getName()
            );
        }

        return $statuses;
    }

    private function findProjection(string $name): ProjectionInterface
    {
        foreach ($this->projections as $projection) {
            if ($projection->getName() === $name) {
                return $projection;
            }
        }

        throw new \RuntimeException("Projection '{$name}' not found");
    }

    private function getEventsSince(int $version): iterable
    {
        // In production: implement efficient event fetching since version
        // For now: return all events (will be optimized with event store query)
        // This would typically be: $this->eventStore->loadStreamSince($version)

        // Placeholder - needs EventStore enhancement
        return [];
    }

    private function shouldProject(
        ProjectionInterface $projection,
        AggregateEnvelope $envelope
    ): bool {
        $subscribedEvents = $projection->subscribedTo();
        $eventClass = $envelope->eventName();

        return in_array($eventClass, $subscribedEvents, true);
    }
}
