<?php

declare(strict_types=1);

namespace App\Framework\EventSourcing\Replay;

use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\EventSourcing\AggregateId;
use App\Framework\EventSourcing\AggregateRoot;
use App\Framework\EventSourcing\EventStore;

/**
 * Time Travel Debugger
 *
 * Reconstruct aggregate state at any point in time for debugging
 */
final readonly class TimeTravelDebugger
{
    public function __construct(
        private EventStore $eventStore
    ) {
    }

    /**
     * Get aggregate state at specific point in time
     */
    public function getStateAtTime(
        AggregateId $aggregateId,
        Timestamp $targetTime,
        callable $aggregateFactory
    ): ?AggregateRoot {
        $allEvents = $this->eventStore->loadStream($aggregateId);

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

        // Filter events up to target time
        $eventsUntilTarget = array_filter(
            $allEvents,
            fn ($envelope) => $envelope->timestamp->isBefore($targetTime)
                || $envelope->timestamp->equals($targetTime)
        );

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

        // Reconstruct aggregate from events
        $events = array_map(fn ($envelope) => $envelope->event, $eventsUntilTarget);

        return $aggregateFactory($aggregateId, $events);
    }

    /**
     * Get aggregate state at specific version
     */
    public function getStateAtVersion(
        AggregateId $aggregateId,
        int $targetVersion,
        callable $aggregateFactory
    ): ?AggregateRoot {
        $allEvents = $this->eventStore->loadStream($aggregateId);

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

        // Filter events up to target version
        $eventsUntilVersion = array_filter(
            $allEvents,
            fn ($envelope) => $envelope->version <= $targetVersion
        );

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

        // Reconstruct aggregate from events
        $events = array_map(fn ($envelope) => $envelope->event, $eventsUntilVersion);

        return $aggregateFactory($aggregateId, $events);
    }

    /**
     * Get state changes between two points in time
     *
     * @return array{before: ?AggregateRoot, after: ?AggregateRoot, events: array}
     */
    public function getStateChanges(
        AggregateId $aggregateId,
        Timestamp $startTime,
        Timestamp $endTime,
        callable $aggregateFactory
    ): array {
        if ($startTime->isAfter($endTime)) {
            throw new \InvalidArgumentException('Start time must be before end time');
        }

        $allEvents = $this->eventStore->loadStream($aggregateId);

        // Get state before start time
        $eventsBeforeStart = array_filter(
            $allEvents,
            fn ($envelope) => $envelope->timestamp->isBefore($startTime)
        );

        $stateBefore = null;
        if (! empty($eventsBeforeStart)) {
            $events = array_map(fn ($envelope) => $envelope->event, $eventsBeforeStart);
            $stateBefore = $aggregateFactory($aggregateId, $events);
        }

        // Get state after end time
        $eventsUntilEnd = array_filter(
            $allEvents,
            fn ($envelope) => $envelope->timestamp->isBefore($endTime)
                || $envelope->timestamp->equals($endTime)
        );

        $stateAfter = null;
        if (! empty($eventsUntilEnd)) {
            $events = array_map(fn ($envelope) => $envelope->event, $eventsUntilEnd);
            $stateAfter = $aggregateFactory($aggregateId, $events);
        }

        // Get events in range
        $eventsInRange = array_filter(
            $allEvents,
            function ($envelope) use ($startTime, $endTime) {
                $eventTime = $envelope->timestamp;

                return ($eventTime->isAfter($startTime) || $eventTime->equals($startTime))
                    && ($eventTime->isBefore($endTime) || $eventTime->equals($endTime));
            }
        );

        return [
            'before' => $stateBefore,
            'after' => $stateAfter,
            'events' => $eventsInRange,
        ];
    }

    /**
     * Get event history with state snapshots
     *
     * @return array<array{version: int, timestamp: Timestamp, event: object, state: AggregateRoot}>
     */
    public function getEventHistory(
        AggregateId $aggregateId,
        callable $aggregateFactory,
        int $limit = 100
    ): array {
        $allEvents = $this->eventStore->loadStream($aggregateId);

        if (empty($allEvents)) {
            return [];
        }

        $history = [];
        $accumulatedEvents = [];

        foreach (array_slice($allEvents, -$limit) as $envelope) {
            $accumulatedEvents[] = $envelope->event;

            // Reconstruct state at this point
            $state = $aggregateFactory($aggregateId, $accumulatedEvents);

            $history[] = [
                'version' => $envelope->version,
                'timestamp' => $envelope->timestamp,
                'event' => $envelope->event,
                'state' => $state,
            ];
        }

        return $history;
    }

    /**
     * Find when specific condition became true
     */
    public function findWhenConditionBecameTrue(
        AggregateId $aggregateId,
        callable $aggregateFactory,
        callable $condition
    ): ?array {
        $allEvents = $this->eventStore->loadStream($aggregateId);

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

        $accumulatedEvents = [];

        foreach ($allEvents as $envelope) {
            $accumulatedEvents[] = $envelope->event;
            $state = $aggregateFactory($aggregateId, $accumulatedEvents);

            if ($condition($state)) {
                return [
                    'version' => $envelope->version,
                    'timestamp' => $envelope->timestamp,
                    'event' => $envelope->event,
                    'state' => $state,
                ];
            }
        }

        return null; // Condition never became true
    }
}
