<?php

declare(strict_types=1);

namespace App\Service\Typo3;

use App\Entity\Organization\Organization;
use App\Entity\Organization\Subdomain;
use App\Message\Message\Mailer;
use App\Message\Message\Typo3\Typo3Update;
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\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Messenger\MessageBusInterface;

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

    // Validation regex for subdomains
    public const RX_VALIDATE_SUBDOMAIN = '/^[a-z0-9](?:[a-z0-9\-]{0,61}[a-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,
        private readonly ParameterBagInterface $parameterBag,
    ) {
    }

    /**
     * Récupère le sous-domaine actif de l'organisation.
     */
    public function getSubdomain(string $subdomainValue): ?Subdomain
    {
        return $this->subdomainRepository->findOneBy(['subdomain' => $subdomainValue]);
    }

    /**
     * Is the organization allowed to register a new subdomain.
     */
    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
     */
    public function isValidSubdomain(string $subdomainValue): bool
    {
        return (bool) preg_match(self::RX_VALIDATE_SUBDOMAIN, $subdomainValue);
    }

    /**
     * Is the subdomain a reserved one.
     *
     * @see https://ressources.opentalent.fr/display/SPEC/Nom+de+sous+domaines+reserves+pour+2IOS
     *
     * @throws \Exception
     */
    public function isReservedSubdomain(string $subdomainValue): bool
    {
        $reservedSubdomains = $this->parameterBag->get('opentalent.subdomains')['reserved'];
        $subRegexes = array_map(
            function (string $s) { return '(\b'.trim($s, '^$/\s').'\b)'; },
            $reservedSubdomains
        );

        $regex = '/^'.strtolower(implode('|', $subRegexes)).'$/';

        return preg_match($regex, $subdomainValue) !== 0;
    }

    /**
     * Returns true if the subdomain has already been registered.
     */
    public function isRegistered(string $subdomainValue): bool
    {
        return count($this->subdomainRepository->findBy(['subdomain' => $subdomainValue])) !== 0;
    }

    /**
     * Register a new subdomain for the organization
     * Is $activate is true, makes this new subdomain the active one too.
     */
    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');
        }

        if ($this->isReservedSubdomain($subdomainValue)) {
            throw new \RuntimeException('This subdomain is not available');
        }

        if ($this->isRegistered($subdomainValue)) {
            throw new \RuntimeException('This subdomain is already registered');
        }

        $this->em->beginTransaction();

        try {
            $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);
            }

            $this->em->commit();
        } catch (\Throwable $e) {
            $this->em->rollback();
            throw $e;
        }

        return $subdomain;
    }

    /**
     * Makes the $subdomain the active one for the organization.
     */
    public function activateSubdomain(Subdomain $subdomain): Subdomain
    {
        $currentActiveSubdomain = $this->subdomainRepository->getActiveSubdomainOf($subdomain->getOrganization());

        if ($currentActiveSubdomain && $subdomain->getId() === $currentActiveSubdomain->getId()) {
            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.
     */
    protected function setOrganizationActiveSubdomain(Subdomain $subdomain): Subdomain
    {
        foreach ($subdomain->getOrganization()->getSubdomains() as $other) {
            if ($other !== $subdomain && $other->isActive()) {
                $other->setActive(false);
            }
        }
        $subdomain->setActive(true);

        // <--- Pour la rétrocompatibilité avec la v1; pourra être supprimé lorsque la migration sera achevée
        $parameters = $subdomain->getOrganization()->getParameters();
        $parameters->setSubDomain($subdomain->getSubdomain());
        $parameters->setOtherWebsite('https://'.$subdomain->getSubdomain().'.opentalent.fr');
        $this->em->persist($parameters);
        // --->

        // 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.
     */
    protected function renameAdminUserToMatchSubdomain(Subdomain $subdomain): void
    {
        $adminAccess = $this->accessRepository->findAdminAccess($subdomain->getOrganization());

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

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

    /**
     * Build the data model for the confirmation email.
     */
    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.
     */
    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 Mailer($model)
        );
    }
}
