<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CoreBundle\DependencyInjection;

use Contao\CoreBundle\DependencyInjection\Filesystem\ConfigureFilesystemInterface;
use Contao\CoreBundle\DependencyInjection\Filesystem\FilesystemConfiguration;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Ferienpass\CoreBundle\Attribute\AsOfferEntityListener;
use Ferienpass\CoreBundle\Attribute\AsParticipantEntityListener;
use Ferienpass\CoreBundle\Cron\SendRemindersListener;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Offer;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant;
use Ferienpass\CoreBundle\Entity\Payment;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use Ferienpass\CoreBundle\Enum\AttendanceTransition;
use Ferienpass\CoreBundle\Enum\OfferStatus;
use Ferienpass\CoreBundle\Enum\OfferTransition;
use Ferienpass\CoreBundle\Enum\PaymentStatus;
use Ferienpass\CoreBundle\Export\Offer\OffersExportInterface;
use Ferienpass\CoreBundle\Export\Offer\PrintSheet\PdfExport;
use Ferienpass\CoreBundle\Export\Offer\Xml\XmlExport;
use Ferienpass\CoreBundle\Export\ParticipantList\WordExport;
use Ferienpass\CoreBundle\Messenger\LocaleMiddleware;
use Ferienpass\CoreBundle\Messenger\MessageLogMiddleware;
use Ferienpass\CoreBundle\Messenger\TenantMiddleware;
use Ferienpass\CoreBundle\Monolog\MessengerLogHandler;
use Ferienpass\CoreBundle\Monolog\NotificationLogHandler;
use Ferienpass\CoreBundle\Repository\OfferRepositoryInterface;
use Ferienpass\CoreBundle\Repository\ParticipantRepositoryInterface;
use Ferienpass\CoreBundle\Repository\ResetPasswordRequestRepository;
use Ferienpass\CoreBundle\Tenant\TenantFilter;
use Ferienpass\CoreBundle\Webhook\BrevoInboundRequestParser;
use Ferienpass\CoreBundle\Webhook\PmPaymentRequestParser;
use Symfony\Bridge\Doctrine\Types\UlidType;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

final class FerienpassCoreExtension extends Extension implements PrependExtensionInterface, ConfigureFilesystemInterface
{
    use PersistenceExtensionTrait;

    /**
     * Options that describe one municipality and have a per-tenant equivalent on
     * PageConfig. `SystemSettingsManager`/`MailConfigManager` read
     * `$parameter ?? $pageConfig->…`, so leaving one of these set on a
     * multi-tenant build silently gives every municipality the same value —
     * every reminder period, every receipt prefix, every reply-to address.
     *
     * Key => the label the admin UI uses for the PageConfig field that replaces it.
     */
    private const PER_TENANT_OPTIONS = [
        'reply_address' => 'Antwort-E-Mail-Adresse',
        'receipt_number_prefix' => 'Belegnummer-Präfix',
        'friends_booking' => 'Freundschaftsbuchung',
        'require_postal_address' => 'Anschrift verpflichtend',
        'erase_data_after' => 'Löschfrist',
        'remind_attendance' => 'Erinnerung an Teilnahme',
        'remind_attendance_before' => 'Vorlauf der Teilnahme-Erinnerung',
        'payment_allow_select' => 'Zahlungsart wählbar',
        'age_tolerance' => 'Alterstoleranz',
        'withdraw_grace_period' => 'Kulanzfrist für Absagen',
        'no_withdraw_after_grace_period' => 'Keine Absage nach Kulanzfrist',
        'no_withdraw_after_application_deadline' => 'Keine Absage nach Anmeldeschluss',
        'preferred_postal_codes' => 'Bevorzugte Postleitzahlen',
        'payments_enabled' => 'Zahlungen aktiv',
        'participant_list_sort' => 'Sortierung der Teilnahmelisten',
    ];

    public function load(array $configs, ContainerBuilder $container): void
    {
        $config = $this->processConfiguration(new Configuration(), $configs);

        // Before anything else: a misconfigured multi-tenant build must not boot.
        if ($config['multi_tenancy']['enabled']) {
            $this->assertNothingFlattensTheTenants($config);
        }

        $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config'));
        $loader->load('services.php');

        $this->configurePersistence($config['entities'], $container);
        $container->addAliases([
            OfferRepositoryInterface::class => 'ferienpass.repository.offer',
            ParticipantRepositoryInterface::class => 'ferienpass.repository.participant',
        ]);

        // Parameters
        // `app.id` is the tenant identifier, set by the app Kernel before
        // compile (see app/src/Kernel.php). Provide an empty-string default so
        // services that inject `%app.id%` (e.g. the cache-tag wiring) still wire
        // up in standalone/test container boots that don't set it.
        if (!$container->hasParameter('app.id')) {
            $container->setParameter('app.id', '');
        }

        $container->setParameter('ferienpass.multi_tenancy.enabled', $config['multi_tenancy']['enabled']);
        $container->setParameter('ferienpass.multi_tenancy.platform_host', $config['multi_tenancy']['platform_host']);
        $container->setParameter('ferienpass.logos_dir', $config['logos_dir']);
        $container->setParameter('ferienpass.images_dir', $config['images_dir']);
        $container->setParameter('ferienpass.friends_booking', $config['friends_booking']);
        $container->setParameter('ferienpass.reply_address', $config['reply_address']);
        $container->setParameter('ferienpass.inbound_domain', $config['inbound_domain']);
        $container->setParameter('ferienpass.inbound_secret', $config['inbound_secret']);
        $container->setParameter('ferienpass.preferred_postal_codes', $config['preferred_postal_codes']);
        $container->setParameter('ferienpass.require_postal_address', $config['require_postal_address']);
        $container->setParameter('ferienpass.erase_data_period', $config['erase_data_after']);
        $container->setParameter('ferienpass.attendance_remind_period', $config['remind_attendance_before']);
        $container->setParameter('ferienpass.payments_enabled', $config['payments_enabled']);
        $container->setParameter('ferienpass.payment_allow_select', $config['payment_allow_select']);
        $container->setParameter('ferienpass.age_tolerance', $config['age_tolerance']);
        $container->setParameter('ferienpass.withdraw_grace_period', $config['withdraw_grace_period']);
        $container->setParameter('ferienpass.no_withdraw_after_grace_period', $config['no_withdraw_after_grace_period']);
        $container->setParameter('ferienpass.no_withdraw_after_application_deadline', $config['no_withdraw_after_application_deadline']);
        $container->setParameter('ferienpass.remind_attendance', $config['remind_attendance']);
        $container->setParameter('ferienpass.participant_list_sort', $config['participant_list_sort']);

        $expressionLanguage = new ExpressionLanguage();
        $container->setParameter('ferienpass.receipt_number_prefix', null === $config['receipt_number_prefix'] ? null : $expressionLanguage->evaluate($config['receipt_number_prefix'], ['date' => new \DateTimeImmutable()]));

        // Feature flags
        if (false === $config['remind_attendance']) {
            $container->removeDefinition(SendRemindersListener::class);
        }

        // Injection
        if (isset($config['export'])) {
            foreach ($config['export']['pdf'] as $name => $pdfConfig) {
                $definition = new Definition(PdfExport::class)
                    ->setAutowired(true)
                    ->setArguments([
                        $pdfConfig['mpdf_config'] ?? [],
                        $pdfConfig['template'] ?? '',
                    ])
                ;
                $definition->addTag(OffersExportInterface::class, ['name' => $name.'.pdf']);
                $container->setDefinition('ferienpass.exporter.pdf.'.$name, $definition);
            }

            foreach ($config['export']['xml'] as $name => $template) {
                $definition = new Definition(XmlExport::class)->setAutowired(true)->setArguments([$template]);
                $definition->addTag(OffersExportInterface::class, ['name' => $name.'.xml']);
                $container->setDefinition('ferienpass.exporter.xml.'.$name, $definition);
            }
        }

        $docxParticipantList = $container->getDefinition(WordExport::class);
        $docxParticipantList->setArgument(2, $config['export']['participant_list_docx'] ?? null);

        $container->registerAttributeForAutoconfiguration(AsOfferEntityListener::class, static function (ChildDefinition $definition, AsEntityListener $attribute) use ($container) {
            $definition->addTag('doctrine.orm.entity_listener', [
                'event' => $attribute->event,
                'method' => $attribute->method,
                'lazy' => $attribute->lazy,
                'entity_manager' => $attribute->entityManager,
                'entity' => $container->getParameter('ferienpass.model.offer.class'),
                'priority' => $attribute->priority,
            ]);
        });
        $container->registerAttributeForAutoconfiguration(AsParticipantEntityListener::class, static function (ChildDefinition $definition, AsEntityListener $attribute) use ($container) {
            $definition->addTag('doctrine.orm.entity_listener', [
                'event' => $attribute->event,
                'method' => $attribute->method,
                'lazy' => $attribute->lazy,
                'entity_manager' => $attribute->entityManager,
                'entity' => $container->getParameter('ferienpass.model.participant.class'),
                'priority' => $attribute->priority,
            ]);
        });
    }

    public function getAlias(): string
    {
        return 'ferienpass';
    }

    public function prepend(ContainerBuilder $container): void
    {
        $this->prependTwigBundle($container);

        $container->prependExtensionConfig('monolog', [
            // `ferienpass_webhook` carries the inbound-webhook audit trail
            // (request accepted/rejected + processing outcome). The app routes it
            // to an always-on handler so it survives the prod fingers_crossed
            // buffer (which would otherwise drop non-error records).
            'channels' => ['ferienpass_messages', 'ferienpass_notifications', 'ferienpass_webhook'],
            'handlers' => [
                'messenger' => [
                    'type' => 'service',
                    'id' => MessengerLogHandler::class,
                    'channels' => ['ferienpass_messages'],
                ],
                'notifications' => [
                    'type' => 'service',
                    'id' => NotificationLogHandler::class,
                    'channels' => ['ferienpass_notifications'],
                ],
            ],
        ]);

        $container->prependExtensionConfig('framework', [
            'webhook' => [
                'routing' => [
                    'pmPayment' => [
                        'service' => PmPaymentRequestParser::class,
                    ],
                ],
            ],
            'http_client' => [
                'scoped_clients' => [
                    'pmPayment.client' => [
                        'base_uri' => 'https://www.payment.govconnect.de',
                    ],
                    'brevo.client' => [
                        'base_uri' => 'https://api.brevo.com/v3',
                        'headers' => [
                            'accept' => 'application/json',
                            'api-key' => '%env(BREVO_API_KEY)%',
                        ],
                    ],
                    'deepl.client' => [
                        'base_uri' => 'https://api.deepl.com',
                        'headers' => [
                            'accept' => 'application/json',
                            'Authorization' => 'DeepL-Auth-Key %env(DEEPL_API_KEY)%',
                        ],
                    ],
                ],
            ],
            'rate_limiter' => [
                'anonymous_api' => [
                    'policy' => 'token_bucket',
                    'limit' => 5,
                    'rate' => ['interval' => '60 minutes', 'amount' => 5],
                ],
                'authenticated_api' => [
                    'policy' => 'token_bucket',
                    'limit' => 50,
                    'rate' => ['interval' => '60 minutes', 'amount' => 50],
                ],
                'friend_code' => [
                    'policy' => 'sliding_window',
                    'limit' => 10,
                    'interval' => '1 day',
                ],
                // Throttles the unauthenticated anonymous-application LiveAction
                // (ApplicationForm::applyAnonymous) which sends a confirmation
                // e-mail to a caller-supplied address — prevents mail-bombing of
                // arbitrary inboxes and protects the mailer reputation/quota.
                'anonymous_application' => [
                    'policy' => 'sliding_window',
                    'limit' => 5,
                    'interval' => '1 hour',
                ],
                // Throttles the unauthenticated auth-email endpoints (magic-link
                // request, password-reset request) per client IP to prevent
                // e-mail bombing / mailer-quota abuse — SECURITY_REVIEW FP-016.
                'auth_email' => [
                    'policy' => 'sliding_window',
                    'limit' => 5,
                    'interval' => '15 minutes',
                ],
            ],
        ]);

        // TODO only register if NOT customized
        $container->prependExtensionConfig('doctrine', [
            'dbal' => [
                'types' => [
                    'id' => UuidType::class,
                    'ulid' => UlidType::class,
                ],
            ],
            'orm' => [
                'entity_managers' => [
                    'default' => [
                        'schema_ignore_classes' => [
                            Offer::class,
                            Participant::class,
                        ],
                        // Registered on every build but never enabled by itself:
                        // TenantContext turns it on once a tenant is resolved, so
                        // single-tenant installations query exactly as before.
                        'filters' => [
                            TenantFilter::NAME => [
                                'class' => TenantFilter::class,
                                'enabled' => false,
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        // ROLE_GLOBAL_ADMIN sits above ROLE_SUPER_ADMIN: a super admin runs one
        // municipality, a global admin runs the installation they all share.
        // Prepended rather than documented so an app's security.yaml needs no
        // edit; anything the app declares for these roles still wins.
        $container->prependExtensionConfig('security', [
            'role_hierarchy' => [
                'ROLE_GLOBAL_ADMIN' => ['ROLE_SUPER_ADMIN'],
            ],
        ]);

        $container->prependExtensionConfig('framework', [
            'messenger' => [
                'default_bus' => 'command.bus',
                'buses' => [
                    // Only required for Contao, which requires `@messenger.bus.default`
                    'messenger.bus.default' => [
                        'middleware' => [
                            'doctrine_ping_connection',
                            'doctrine_close_connection',
                        ],
                    ],
                    'command.bus' => [
                        'middleware' => [
                            MessageLogMiddleware::class,
                            LocaleMiddleware::class,
                            TenantMiddleware::class,
                            'doctrine_ping_connection',
                            'doctrine_close_connection',
                            // router_context MUST wrap doctrine_transaction, not the
                            // other way round. doctrine_transaction flushes the EM
                            // (firing CacheInvalidationListener) AFTER the inner stack
                            // returns; if router_context were inner it would already
                            // have reset the RequestContext, so the listener's
                            // canonicalHost() would be '' in an async worker and every
                            // host-scoped BAN would be fail-closed skipped. With
                            // router_context outer, the dispatch-time RouterContextStamp
                            // (the originating request's host) is still applied during
                            // the flush, so worker-driven list/status BANs reach the
                            // right tenant's Varnish.
                            'router_context',
                            'doctrine_transaction',
                        ],
                    ],
                    'event.bus' => [
                        'default_middleware' => [
                            'enabled' => true,
                            'allow_no_handlers' => true,
                        ],
                        'middleware' => [
                            MessageLogMiddleware::class,
                            LocaleMiddleware::class,
                            TenantMiddleware::class,
                            'doctrine_close_connection',
                            'router_context',
                        ],
                    ],
                ],
            ],
        ]);

        // Brevo Inbound Parse webhook (email replies → threaded comments). The
        // app already routes the `mailer_brevo` engagement webhook; this is the
        // inbound counterpart, served at `/webhook/brevo_inbound`.
        $container->prependExtensionConfig('framework', [
            'webhook' => [
                'routing' => [
                    'brevo_inbound' => [
                        'service' => BrevoInboundRequestParser::class,
                    ],
                ],
            ],
        ]);

        $container->prependExtensionConfig('symfonycasts_reset_password', [
            'request_password_repository' => ResetPasswordRequestRepository::class,
        ]);

        $this->prependWorkflow($container);
    }

    public function configureFilesystem(FilesystemConfiguration $config): void
    {
        $config
            ->mountLocalAdapter('storage/media', 'storage/media', 'media')
            ->addVirtualFilesystem('media', 'storage/media')
        ;

        $config->addDefaultDbafs('media', 'DbafsMedia');

        $config
            ->mountLocalAdapter('storage/attachments', 'storage/attachments', 'attachments')
            ->addVirtualFilesystem('attachments', 'storage/attachments')
        ;

        $config->addDefaultDbafs('attachments', 'DbafsAttachment');

        $config
            ->mountLocalAdapter('storage/logos', 'storage/logos', 'logos')
            ->addVirtualFilesystem('logos', 'storage/logos')
        ;

        $config->addDefaultDbafs('logos', 'DbafsLogo');

        $config
            ->mountLocalAdapter('storage/export', 'storage/export', 'export')
            ->addVirtualFilesystem('export', 'storage/export')
        ;

        $config
            ->mountAdapter('aws', [
                'client' => 'Aws\S3\S3Client',
                'bucket' => '%env(S3_ATTACHMENTS_BUCKET)%',
                'prefix' => '',
                'options' => [],
                'streamReads' => true,
            ], 'inbound_attachments', 'inbound_attachments')
            ->addVirtualFilesystem('inbound_attachments', 'inbound_attachments')
        ;
    }

    /**
     * Fails the build when a per-municipality option is configured on a
     * multi-tenant installation.
     *
     * This is deliberately a hard error rather than a silent override: the
     * symptom otherwise is one municipality's reply-to address on another's
     * mail, which nobody notices until a parent replies to the wrong town hall.
     */
    private function assertNothingFlattensTheTenants(array $config): void
    {
        $offenders = [];

        foreach (self::PER_TENANT_OPTIONS as $key => $label) {
            $value = $config[$key] ?? null;

            if (null === $value || [] === $value) {
                continue;
            }

            $offenders[] = \sprintf('  - ferienpass.%s (%s)', $key, $label);
        }

        if ([] === $offenders) {
            return;
        }

        throw new InvalidConfigurationException(\sprintf("Multi-tenancy is enabled, but these options describe a single municipality and would apply to every tenant at once:\n\n%s\n\n".'Remove them from the bundle configuration; each tenant sets its own value in the admin under Werkzeuge → System → Allgemein (stored on PageConfig).', implode("\n", $offenders)));
    }

    private function prependTwigBundle(ContainerBuilder $container): void
    {
        $container->prependExtensionConfig('twig', [
            'paths' => [
                __DIR__.'/../../templates/email' => 'email',
                __DIR__.'/../../email/css' => 'email_styles',
            ],
            'form_themes' => [
                '@FerienpassCore/form/custom_types.html.twig',
                '@FerienpassCore/form/theme.html.twig',
            ],
        ]);
    }

    private function prependWorkflow(ContainerBuilder $container): void
    {
        $container->prependExtensionConfig('framework', config: [
            'workflows' => [
                'offer' => [
                    'type' => 'workflow',
                    'marking_store' => ['type' => 'method', 'property' => 'status'],
                    'supports' => [OfferInterface::class],
                    'initial_marking' => OfferStatus::DRAFT,
                    'places' => [OfferStatus::COMPLETED, OfferStatus::DRAFT, OfferStatus::FINALIZED, OfferStatus::REVIEWED, OfferStatus::PUBLISHED, OfferStatus::CANCELLED],
                    'transitions' => [
                        OfferTransition::COMPLETE => [
                            'guard' => "is_granted('complete', subject)",
                            'to' => OfferStatus::COMPLETED,
                        ],
                        OfferTransition::FINALIZE => [
                            'guard' => "is_granted('finalize', subject)",
                            'from' => OfferStatus::DRAFT,
                            'to' => OfferStatus::FINALIZED,
                        ],
                        OfferTransition::TO_DRAFT => [
                            'guard' => "is_granted('to_draft', subject)",
                            'from' => OfferStatus::FINALIZED,
                            'to' => OfferStatus::DRAFT,
                        ],
                        OfferTransition::TO_DRAFT.'_2' => [
                            'guard' => "is_granted('to_draft', subject)",
                            'from' => OfferStatus::REVIEWED,
                            'to' => OfferStatus::DRAFT,
                        ],
                        OfferTransition::APPROVE => [
                            'guard' => "is_granted('approve', subject)",
                            'from' => OfferStatus::FINALIZED,
                            'to' => OfferStatus::REVIEWED,
                        ],
                        OfferTransition::UNAPPROVE => [
                            'guard' => "is_granted('unapprove', subject)",
                            'from' => OfferStatus::REVIEWED,
                            'to' => OfferStatus::FINALIZED,
                        ],
                        OfferTransition::PUBLISH => [
                            'guard' => "is_granted('publish', subject)",
                            'from' => OfferStatus::REVIEWED,
                            'to' => OfferStatus::PUBLISHED,
                        ],
                        OfferTransition::CANCEL => [
                            'guard' => "is_granted('cancel', subject)",
                            'to' => [OfferStatus::CANCELLED],
                        ],
                        OfferTransition::RELAUNCH => [
                            'guard' => "is_granted('relaunch', subject)",
                            'from' => OfferStatus::CANCELLED,
                            'to' => OfferStatus::PUBLISHED,
                        ],
                        OfferTransition::UNPUBLISH => [
                            'guard' => "is_granted('unpublish', subject)",
                            'from' => OfferStatus::PUBLISHED,
                            'to' => OfferStatus::REVIEWED,
                        ],
                    ],
                ],
                'attendance' => [
                    'type' => 'state_machine',
                    'marking_store' => ['type' => 'method', 'property' => 'status'],
                    'supports' => [Attendance::class],
                    'initial_marking' => AttendanceStatus::WAITING,
                    'places' => [AttendanceStatus::WAITING, AttendanceStatus::CONFIRMED, AttendanceStatus::WAITLISTED, AttendanceStatus::REJECTED, AttendanceStatus::WITHDRAWN, AttendanceStatus::UNFULFILLED, AttendanceStatus::NO_SHOW, AttendanceStatus::PARTICIPATED, AttendanceStatus::EMAIL_UNCONFIRMED, AttendanceStatus::CONSENTS_OUTSTANDING],
                    'transitions' => [
                        // Anonymous application held until the e-mail is confirmed. It is
                        // released back to WAITING (and allocated) by AttendanceFacade::confirmEmail().
                        AttendanceTransition::AWAIT_EMAIL => [
                            'from' => [AttendanceStatus::WAITING],
                            'to' => AttendanceStatus::EMAIL_UNCONFIRMED,
                        ],
                        // Anonymous application by a minor applicant: held after the
                        // e-mail confirmation until the legal guardian signed the
                        // consents via the shared link. Released back to WAITING (and
                        // allocated) by AttendanceFacade::confirmConsents().
                        AttendanceTransition::AWAIT_CONSENTS => [
                            'from' => [AttendanceStatus::WAITING],
                            'to' => AttendanceStatus::CONSENTS_OUTSTANDING,
                        ],
                        AttendanceTransition::CONFIRM => [
                            'from' => [AttendanceStatus::WAITING, AttendanceStatus::WAITLISTED, AttendanceStatus::REJECTED, AttendanceStatus::WITHDRAWN, AttendanceStatus::UNFULFILLED],
                            'to' => AttendanceStatus::CONFIRMED,
                        ],
                        AttendanceTransition::WAITLIST => [
                            'from' => [AttendanceStatus::WAITING, AttendanceStatus::CONFIRMED, AttendanceStatus::REJECTED, AttendanceStatus::WITHDRAWN, AttendanceStatus::UNFULFILLED],
                            'to' => AttendanceStatus::WAITLISTED,
                        ],
                        AttendanceTransition::WITHDRAW => [
                            'from' => [AttendanceStatus::WAITING, AttendanceStatus::CONFIRMED, AttendanceStatus::REJECTED, AttendanceStatus::WAITLISTED, AttendanceStatus::EMAIL_UNCONFIRMED, AttendanceStatus::CONSENTS_OUTSTANDING],
                            'to' => AttendanceStatus::WITHDRAWN,
                        ],
                        AttendanceTransition::RESET => [
                            'from' => [AttendanceStatus::CONFIRMED, AttendanceStatus::REJECTED, AttendanceStatus::WITHDRAWN, AttendanceStatus::UNFULFILLED, AttendanceStatus::WAITLISTED],
                            'to' => AttendanceStatus::WAITING,
                        ],
                        AttendanceTransition::REJECT => [
                            'from' => [AttendanceStatus::CONFIRMED, AttendanceStatus::WAITLISTED, AttendanceStatus::WITHDRAWN, AttendanceStatus::UNFULFILLED, AttendanceStatus::WAITING, AttendanceStatus::EMAIL_UNCONFIRMED, AttendanceStatus::CONSENTS_OUTSTANDING],
                            'to' => AttendanceStatus::REJECTED,
                        ],
                        AttendanceTransition::UNFULFILL => [
                            'from' => [AttendanceStatus::CONFIRMED, AttendanceStatus::WAITLISTED, AttendanceStatus::WITHDRAWN, AttendanceStatus::REJECTED, AttendanceStatus::WAITING],
                            'to' => AttendanceStatus::UNFULFILLED,
                        ],
                        AttendanceTransition::NO_SHOW => [
                            'from' => [AttendanceStatus::CONFIRMED],
                            'to' => AttendanceStatus::NO_SHOW,
                        ],
                        AttendanceTransition::PARTICIPATED => [
                            'from' => [AttendanceStatus::CONFIRMED, AttendanceStatus::REJECTED, AttendanceStatus::WITHDRAWN, AttendanceStatus::UNFULFILLED, AttendanceStatus::WAITLISTED],
                            'to' => AttendanceStatus::PARTICIPATED,
                        ],
                    ],
                ],
                'payment' => [
                    'type' => 'state_machine',
                    'marking_store' => ['type' => 'method', 'property' => 'status'],
                    'supports' => [Payment::class],
                    'initial_marking' => PaymentStatus::CREATED,
                    'places' => [PaymentStatus::CREATED, PaymentStatus::PAID, 'unpaid', PaymentStatus::FAILED, PaymentStatus::ABANDONED],
                    'transitions' => [
                        PaymentStatus::TRANSITION_PAY => [
                            'from' => [PaymentStatus::CREATED, 'unpaid', PaymentStatus::FAILED],
                            'to' => PaymentStatus::PAID,
                        ],
                        PaymentStatus::TRANSITION_FAIL => [
                            'from' => [PaymentStatus::CREATED, PaymentStatus::PAID, 'unpaid'],
                            'to' => PaymentStatus::FAILED,
                        ],
                        PaymentStatus::TRANSITION_ABANDON => [
                            'from' => [PaymentStatus::CREATED, 'unpaid'],
                            'to' => PaymentStatus::ABANDONED,
                        ],
                    ],
                ],
            ],
        ]);
    }
}
