<?php

namespace App\Service\Typo3;

use App\Entity\Access\Access;
use App\Entity\Organization\Organization;
use App\Entity\Organization\Subdomain;
use App\Message\Command\MailerCommand;
use App\Message\Command\Typo3\Typo3UpdateCommand;
use App\Repository\Access\AccessRepository;
use App\Repository\Organization\SubdomainRepository;
use App\Service\Mailer\Model\SubdomainChangeModel;
use App\Service\Organization\Utils as OrganizationUtils;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Messenger\MessageBusInterface;

/**
 * Service de gestion des sous-domaines des utilisateurs
 */
class SubdomainService
{
    // Max number of subdomains that an organization can own
    const MAX_SUBDOMAINS_NUMBER = 3;

    // Validation regex for subdomains
    const RX_VALIDATE_SUBDOMAIN = '/^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$/';

    public function __construct(
        private readonly SubdomainRepository $subdomainRepository,
        private readonly EntityManagerInterface $em,
        private readonly MessageBusInterface $messageBus,
        private readonly OrganizationUtils $organizationUtils,
        private readonly BindFileService $bindFileService,
        private readonly AccessRepository $accessRepository
    ) {}

    /**
     * Is the organization allowed to register a new subdomain
     *
     * @param Organization $organization
     * @return bool
     */
    public function canRegisterNewSubdomain(Organization $organization): bool {
        return count($organization->getSubdomains()) < self::MAX_SUBDOMAINS_NUMBER;
    }

    /**
     * Is the input a valid value for a subdomain
     *
     * @see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2
     * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
     * @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
     *
     * @param string $subdomainValue
     * @return bool
     */
    public function isValidSubdomain(string $subdomainValue): bool
    {
        return (bool)preg_match(self::RX_VALIDATE_SUBDOMAIN, $subdomainValue);
    }

    /**
     * Register a new subdomain for the organization
     * Is $activate is true, makes this new subdomain the active one too.
     *
     * @param Organization $organization
     * @param string $subdomainValue
     * @param bool $activate
     * @return Subdomain
     */
    public function addNewSubdomain(
        Organization $organization,
        string $subdomainValue,
        bool $activate = false
    ): Subdomain {

        if (!$this->isValidSubdomain($subdomainValue)) {
            throw new \RuntimeException("Not a valid subdomain");
        }

        if (!$this->canRegisterNewSubdomain($organization)) {
            throw new \RuntimeException("This organization can not register new subdomains");
        }

        // Vérifie que le sous-domaine n'est pas déjà utilisé
        if ($this->subdomainRepository->findBy(['subdomain' => $subdomainValue])) {
            throw new \RuntimeException('This subdomain is already registered');
        }

        $subdomain = new Subdomain();
        $subdomain->setSubdomain($subdomainValue);
        $subdomain->setOrganization($organization);
        $subdomain->setActive(false);

        $this->em->persist($subdomain);
        $this->em->flush();

        // Register into the BindFile (takes up to 5min to take effect)
        $this->bindFileService->registerSubdomain($subdomain->getSubdomain());

        if ($activate) {
            $subdomain = $this->activateSubdomain($subdomain);
        }

        return $subdomain;
    }

    /**
     * Makes the $subdomain the active one for the organization.
     *
     * @param Subdomain $subdomain
     * @return Subdomain
     */
    public function activateSubdomain(Subdomain $subdomain): Subdomain {
        if ($subdomain->isActive()) {
            throw new \RuntimeException('The subdomain is already active');
        }
        if (!$subdomain->getId()) {
            throw new \RuntimeException('Can not activate a non-persisted subdomain');
        }

        $subdomain = $this->setOrganizationActiveSubdomain($subdomain);

        $this->renameAdminUserToMatchSubdomain($subdomain);

        // Update the typo3 website (asynchronously with messenger)
        $this->updateTypo3Website($subdomain->getOrganization());

        // Send confirmation email
        $this->sendConfirmationEmail($subdomain);

        return $subdomain;
    }

    /**
     * The subdomain becomes the only active subdomain of its organization.
     * New state is persisted is database.
     *
     * @param Subdomain $subdomain
     * @return Subdomain
     */
    protected function setOrganizationActiveSubdomain(Subdomain $subdomain): Subdomain {
        foreach ($subdomain->getOrganization()->getSubdomains() as $other) {
            if ($other !== $subdomain && $other->isActive()) {
                $other->setActive(false);
            }
        }
        $subdomain->setActive(true);

        // TODO: comprendre pourquoi ce refresh est indispensable pour que l'organisation soit à jour
        $this->em->flush();
        $this->em->refresh($subdomain->getOrganization());

        return $subdomain;
    }

    /**
     * Rename the admin user of the organization to match the given subdomain
     *
     * @param Subdomain $subdomain
     * @return void
     */
    protected function renameAdminUserToMatchSubdomain(Subdomain $subdomain): void {
        $adminAccess = $this->accessRepository->findAdminAccess($subdomain->getOrganization());

        $adminAccess->getPerson()->setUsername('admin' . $subdomain->getSubdomain());
        $this->em->flush();
    }

    /**
     * Trigger an update of the typo3 organization's website
     *
     * @param $organization
     * @return void
     */
    protected function updateTypo3Website(Organization $organization): void
    {
        $this->messageBus->dispatch(
            new Typo3UpdateCommand($organization->getId())
        );
    }

    /**
     * Build the data model for the confirmation email
     *
     * @param Subdomain $subdomain
     * @return SubdomainChangeModel
     */
    protected function getMailModel(Subdomain $subdomain): SubdomainChangeModel {
        $adminAccess = $this->accessRepository->findAdminAccess($subdomain->getOrganization());

        /** @phpstan-ignore-next-line */
        return (new SubdomainChangeModel())
            ->setOrganizationId($subdomain->getOrganization()->getId())
            ->setSubdomainId($subdomain->getId())
            ->setUrl($this->organizationUtils->getOrganizationWebsite($subdomain->getOrganization()))
            ->setSenderId($adminAccess->getId());
    }

    /**
     * Send the confirmation email to the organization after a new subdomain has been activated
     *
     * @param Subdomain $subdomain
     * @return void
     */
    protected function sendConfirmationEmail(Subdomain $subdomain): void {
        // TODO: revoir quel sender par défaut
        $model = $this->getMailModel($subdomain);

        // Envoi d'un email
        $this->messageBus->dispatch(
            new MailerCommand($model)
        );
    }
}
