<?php

declare(strict_types=1);

namespace Galeas\Api\Common\Event;

use Galeas\Api\BoundedContext\CreditCard\Product;
use Galeas\Api\BoundedContext\Identity\TakenEmail;
use Galeas\Api\BoundedContext\Identity\User;
use Galeas\Api\BoundedContext\Security\Session;
use Galeas\Api\Common\Event\Exception as EventException;

abstract class EventReflectionBaseClass
{
    /**
     * @var array<string, class-string<Event>>
     */
    private static array $eventNamesToEventClasses = [
        'Identity_User_SignedUp' => User\Event\SignedUp::class,
        'Identity_User_PrimaryEmailVerificationCodeSent' => User\Event\PrimaryEmailVerificationCodeSent::class,
        'Identity_User_PrimaryEmailVerified' => User\Event\PrimaryEmailVerified::class,
        'Identity_User_PrimaryEmailChangeRequested' => User\Event\PrimaryEmailChangeRequested::class,
        'Identity_TakenEmail_EmailTaken' => TakenEmail\Event\EmailTaken::class,
        'Identity_TakenEmail_EmailAbandoned' => TakenEmail\Event\EmailAbandoned::class,
        'Identity_TakenEmail_AbandonedEmailRetaken' => TakenEmail\Event\AbandonedEmailRetaken::class,
        'Security_Session_SignedIn' => Session\Event\SignedIn::class,
        'Security_Session_TokenRefreshed' => Session\Event\TokenRefreshed::class,
        'Security_Session_SignedOut' => Session\Event\SignedOut::class,
        'CreditCard_Product_ProductDefined' => Product\Event\ProductDefined::class,
        'CreditCard_Product_ProductActivated' => Product\Event\ProductActivated::class,
        'CreditCard_Product_ProductDeactivated' => Product\Event\ProductDeactivated::class,
    ];

    /**
     * @var array<string, string>
     */
    private static array $eventClassesToEventNames = [];

    /**
     * @var array<string, string>
     */
    private static array $eventClassesToCreationMethodNames = [];

    /**
     * @var array<string, string>
     */
    private static array $eventClassesToTransformationMethodNames = [];

    /**
     * @var array<string, \ReflectionMethod>
     */
    private static array $eventNamesToReflectionConstructorMethodsCache = [];

    /**
     * @var array<string, \ReflectionClass<Event>>
     */
    private static array $eventNamesToReflectionClassCache = [];

    /**
     * @throws EventException\EventMappingReflectionError
     */
    private static function setup(): void
    {
        try {
            if ([] === self::$eventClassesToEventNames) {
                self::$eventClassesToEventNames = array_flip(self::$eventNamesToEventClasses);

                foreach (self::$eventNamesToEventClasses as $eventName => $eventClass) {
                    /** @var \ReflectionClass<Event> $reflectionClass */
                    $reflectionClass = new \ReflectionClass($eventClass);

                    self::$eventNamesToReflectionClassCache[$eventName] = $reflectionClass;

                    $creationMethod = self::findFirstMethodBeginningWith($reflectionClass, 'create');
                    if (null !== $creationMethod) {
                        self::$eventClassesToCreationMethodNames[$eventClass] = $creationMethod;
                    }

                    $transformationMethod = self::findFirstMethodBeginningWith($reflectionClass, 'transform');
                    if (null !== $transformationMethod) {
                        self::$eventClassesToTransformationMethodNames[$eventClass] = $transformationMethod;
                    }

                    $reflectionConstructorMethod = $reflectionClass->getMethod('reflectionConstructor');
                    $reflectionConstructorMethod->setAccessible(true);
                    self::$eventNamesToReflectionConstructorMethodsCache[$eventName] = $reflectionConstructorMethod;
                }
            }
        } catch (\ReflectionException $exception) {
            throw new EventException\EventMappingReflectionError('Reflection method failure');
        }
    }

    /**
     * @return array<class-string<Event>>
     *
     * @throws EventException\EventMappingReflectionError
     */
    public static function allEventClasses(): array
    {
        self::setup();

        return array_values(self::$eventNamesToEventClasses);
    }

    /**
     * @return array<string>
     *
     * @throws EventException\EventMappingReflectionError
     */
    public static function allEventNames(): array
    {
        self::setup();

        return array_values(self::$eventClassesToEventNames);
    }

    /**
     * @throws EventException\NoEventClassMappingFound
     * @throws EventException\EventMappingReflectionError
     */
    protected static function eventClassToEventName(string $eventClass): string
    {
        self::setup();

        if (\array_key_exists($eventClass, self::$eventClassesToEventNames)) {
            return self::$eventClassesToEventNames[$eventClass];
        }

        throw new EventException\NoEventClassMappingFound('No mapping found for class: '.$eventClass);
    }

    /**
     * @return \ReflectionClass<Event>
     *
     * @throws EventException\NoEventReflectionClassMappingFound
     * @throws EventException\EventMappingReflectionError
     */
    protected static function eventNameToReflectionClass(string $eventName): \ReflectionClass
    {
        self::setup();

        if (\array_key_exists($eventName, self::$eventNamesToReflectionClassCache)) {
            return self::$eventNamesToReflectionClassCache[$eventName];
        }

        throw new EventException\NoEventReflectionClassMappingFound('No reflection class mapping found for event name '.$eventName);
    }

    /**
     * @throws EventException\NoEventReflectionClassMappingMethodFound
     * @throws EventException\EventMappingReflectionError
     */
    protected static function eventNameToReflectionClassMethod(string $eventName): \ReflectionMethod
    {
        self::setup();

        if (\array_key_exists($eventName, self::$eventNamesToReflectionConstructorMethodsCache)) {
            return self::$eventNamesToReflectionConstructorMethodsCache[$eventName];
        }

        throw new EventException\NoEventReflectionClassMappingMethodFound('No mapping found for event name '.$eventName);
    }

    /**
     * @throws EventException\NoCreationMethodFound
     * @throws EventException\EventMappingReflectionError
     */
    protected static function eventClassToCreationMethodName(string $eventClass): string
    {
        self::setup();

        if (\array_key_exists($eventClass, self::$eventClassesToCreationMethodNames)) {
            return self::$eventClassesToCreationMethodNames[$eventClass];
        }

        throw new EventException\NoCreationMethodFound('No mapping found for class name '.$eventClass);
    }

    /**
     * @throws EventException\NoTransformationMethodFound
     * @throws EventException\EventMappingReflectionError
     */
    protected static function eventClassToTransformationMethodName(string $eventClass): string
    {
        self::setup();

        if (\array_key_exists($eventClass, self::$eventClassesToTransformationMethodNames)) {
            return self::$eventClassesToTransformationMethodNames[$eventClass];
        }

        throw new EventException\NoTransformationMethodFound('No mapping found for class name '.$eventClass);
    }

    /**
     * @param \ReflectionClass<Event> $reflection
     */
    private static function findFirstMethodBeginningWith(\ReflectionClass $reflection, string $beginningWith): ?string
    {
        $methods = $reflection->getMethods();
        foreach ($methods as $method) {
            if (str_starts_with($method->getName(), $beginningWith)) {
                return $method->getName();
            }
        }

        return null;
    }
}
