<?php

declare(strict_types=1);

namespace App\Service\Organization;

use App\ApiResources\Organization\OrganizationCreationRequest;
use App\ApiResources\Organization\OrganizationDeletionRequest;
use App\ApiResources\Organization\OrganizationMemberCreationRequest;
use App\Entity\Access\Access;
use App\Entity\Access\OrganizationFunction;
use App\Entity\Core\AddressPostal;
use App\Entity\Core\ContactPoint;
use App\Entity\Education\Cycle;
use App\Entity\Network\NetworkOrganization;
use App\Entity\Organization\Organization;
use App\Entity\Organization\OrganizationAddressPostal;
use App\Entity\Organization\Parameters;
use App\Entity\Organization\Settings;
use App\Entity\Organization\Subdomain;
use App\Entity\Person\Person;
use App\Entity\Person\PersonAddressPostal;
use App\Enum\Access\FunctionEnum;
use App\Enum\Core\ContactPointTypeEnum;
use App\Enum\Education\CycleEnum;
use App\Enum\Network\NetworkEnum;
use App\Enum\Organization\AddressPostalOrganizationTypeEnum;
use App\Enum\Organization\SettingsProductEnum;
use App\Enum\Person\AddressPostalPersonTypeEnum;
use App\Repository\Access\FunctionTypeRepository;
use App\Repository\Core\CountryRepository;
use App\Repository\Organization\OrganizationIdentificationRepository;
use App\Repository\Organization\OrganizationRepository;
use App\Repository\Person\PersonRepository;
use App\Service\ApiLegacy\ApiLegacyRequestService;
use App\Service\Dolibarr\DolibarrApiService;
use App\Service\File\FileManager;
use App\Service\Organization\Utils as OrganizationUtils;
use App\Service\Typo3\BindFileService;
use App\Service\Typo3\SubdomainService;
use App\Service\Typo3\Typo3Service;
use App\Service\Utils\DatesUtils;
use App\Service\Utils\SecurityUtils;
use App\Service\Utils\UrlBuilder;
use Doctrine\ORM\EntityManagerInterface;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberUtil;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\String\ByteString;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\Service\Attribute\Required;

class OrganizationFactory
{
    private LoggerInterface $logger;

    protected PhoneNumberUtil $phoneNumberUtil;

    public function __construct(
        private readonly SubdomainService $subdomainService,
        private readonly OrganizationRepository $organizationRepository,
        private readonly CountryRepository $countryRepository,
        private readonly OrganizationUtils $organizationUtils,
        private readonly Typo3Service $typo3Service,
        private readonly DolibarrApiService $dolibarrApiService,
        private readonly EntityManagerInterface $entityManager,
        private readonly PersonRepository $personRepository,
        private readonly BindFileService $bindFileService,
        private readonly OrganizationIdentificationRepository $organizationIdentificationRepository,
        private readonly ApiLegacyRequestService $apiLegacyRequestService,
        private readonly FunctionTypeRepository $functionTypeRepository,
        private readonly FileManager $fileManager,
    ) {
        $this->phoneNumberUtil = PhoneNumberUtil::getInstance();
    }

    #[Required]
    /** @see https://symfony.com/doc/current/logging/channels_handlers.html#how-to-autowire-logger-channels */
    public function setLoggerInterface(LoggerInterface $adminLogger): void
    {
        $this->logger = $adminLogger;
    }

    /**
     * Créé une nouvelle organisation à partir des données contenues dans une OrganizationCreationRequest.
     *
     * @throws TransportExceptionInterface
     * @throws \Throwable
     */
    public function create(OrganizationCreationRequest $organizationCreationRequest): Organization
    {
        $this->logger->info(
            "Start the creation of a new organization named '".$organizationCreationRequest->getName()."'"
        );

        $this->entityManager->beginTransaction();

        try {
            // On vérifie si cette organisation n'existe pas déjà
            $this->interruptIfOrganizationExists($organizationCreationRequest);

            // On vérifie la validité et la disponibilité du sous domaine
            $this->validateSubdomain($organizationCreationRequest->getSubdomain());
            $this->logger->info("Subdomain is valid and available : '".$organizationCreationRequest->getSubdomain()."'");

            // On construit l'organisation et ses relations
            $organization = $this->makeOrganizationWithRelations($organizationCreationRequest);
            $this->logger->info('Organization created with all its relations');

            // On persiste et on commit, les objets liés seront persistés en cascade
            $this->entityManager->persist($organization);

            $this->entityManager->flush();

            $this->entityManager->commit();
            $this->logger->debug(' - New entities committed in DB');

            $this->logger->info('Organization persisted in the DB');
        } catch (\Throwable $e) {
            $this->logger->critical("An error happened, operation cancelled\n".$e);
            $this->entityManager->rollback();
            throw $e;
        }

        $withError = false;

        // Création de la société Dolibarr
        try {
            $dolibarrId = $this->dolibarrApiService->createSociety(
                $organization,
                $organizationCreationRequest->isClient()
            );
            $this->logger->info('New dolibarr structure created (uid : '.$dolibarrId.')');
        } catch (\Throwable $e) {
            $this->logger->critical('An error happened while creating the dolibarr society, please proceed manually.');
            $this->logger->debug((string) $e);
            $withError = true;
        }

        // Register the subdomain into the BindFile (takes up to 5min to take effect)
        try {
            $this->bindFileService->registerSubdomain($organizationCreationRequest->getSubdomain());
            $this->logger->info('Subdomain registered');
        } catch (\Throwable $e) {
            $this->logger->critical('An error happened while updating the bind file, please proceed manually.');
            $this->logger->debug((string) $e);
            $withError = true;
        }

        // Création du site typo3 (on est obligé d'attendre que l'organisation soit persistée en base)
        if ($organizationCreationRequest->getCreateWebsite()) {
            try {
                $rootUid = $this->createTypo3Website($organization);
                $this->logger->info('Typo3 website created (root uid: '.$rootUid.')');
            } catch (\Throwable $e) {
                $this->logger->critical('An error happened while creating the typo3 website, please proceed manually.');
                $this->logger->debug((string) $e);
                $withError = true;
            }
        } else {
            $this->logger->warning('Typo3 website creation was not required');
        }

        // Création de l'organisation dans la base adminassos (géré par la V1)
        try {
            $this->updateAdminassosDb($organization);
            $this->logger->info('Adminassos db updated');
        } catch (\Throwable $e) {
            $this->logger->critical('An error happened while updating the adminassos db, please proceed manually.');
            $this->logger->debug((string) $e);
            $withError = true;
        }

        if ($withError) {
            $organizationCreationRequest->setStatus(OrganizationCreationRequest::STATUS_OK_WITH_ERRORS);
            $this->logger->warning('-- Operation ended with errors, check the logs for more information --');
        } else {
            $organizationCreationRequest->setStatus(OrganizationCreationRequest::STATUS_OK);
            $this->logger->info('The organization has been created (id='.$organization->getId().').');
        }

        return $organization;
    }

    /**
     * Lève une exception si cette organisation existe déjà.
     */
    public function interruptIfOrganizationExists(OrganizationCreationRequest $organizationCreationRequest): void
    {
        if (
            $organizationCreationRequest->getSiretNumber()
            && $this->organizationIdentificationRepository->findOneBy(
                ['siretNumber' => $organizationCreationRequest->getSiretNumber()]
            )
        ) {
            throw new \RuntimeException("This siret number is already registered : '".$organizationCreationRequest->getSiretNumber()."'");
        }

        if (
            $organizationCreationRequest->getWaldecNumber()
            && $this->organizationIdentificationRepository->findOneBy(
                ['waldecNumber' => $organizationCreationRequest->getWaldecNumber()]
            )
        ) {
            throw new \RuntimeException("This RNA identifier (waldec number) is already registered : '".$organizationCreationRequest->getWaldecNumber()."'");
        }

        if (
            $organizationCreationRequest->getIdentifier()
            && $this->organizationIdentificationRepository->findOneBy(
                ['identifier' => $organizationCreationRequest->getIdentifier()]
            )
        ) {
            throw new \RuntimeException("This CMF identifier is already registered : '".$organizationCreationRequest->getIdentifier()."'");
        }

        $normalizedName = $this->normalizeIdentificationField($organizationCreationRequest->getName());

        if (
            $this->organizationIdentificationRepository->findOneBy(
                ['normalizedName' => $normalizedName, 'addressCity' => $organizationCreationRequest->getCity()]
            )
        ) {
            throw new \RuntimeException("An organization named '".$organizationCreationRequest->getName()."' already exists in ".$organizationCreationRequest->getCity());
        }

        $address = $this->normalizeIdentificationField(implode(' ', [
            $organizationCreationRequest->getStreetAddress1(),
            $organizationCreationRequest->getStreetAddress2(),
            $organizationCreationRequest->getStreetAddress3(),
        ]));

        if (
            $this->organizationIdentificationRepository->findOneBy(
                [
                    'normalizedAddress' => $address,
                    'addressCity' => $organizationCreationRequest->getCity(),
                    'postalCode' => $organizationCreationRequest->getPostalCode(),
                ]
            )
        ) {
            throw new \RuntimeException('An organization already exists at this address.');
        }
    }

    /**
     * Vérifie la disponibilité et la validité d'un sous domaine.
     *
     * @throws \Exception
     */
    protected function validateSubdomain(string $subdomainValue): void
    {
        if (!$this->subdomainService->isValidSubdomain($subdomainValue)) {
            throw new \RuntimeException('Not a valid subdomain : '.$subdomainValue);
        }

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

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

    /**
     * Créé une nouvelle instance d'organisation, et toutes les instances liées (paramètres, contact, adresses, ...),
     * selon le contenu de la requête de création.
     *
     * @throws \Throwable
     */
    protected function makeOrganizationWithRelations(
        OrganizationCreationRequest $organizationCreationRequest,
    ): Organization {
        // Création de l'organisation
        $organization = $this->makeOrganization($organizationCreationRequest);
        $this->logger->debug(' - Organization created');

        // Création des Parameters
        $parameters = $this->makeParameters($organizationCreationRequest);
        $organization->setParameters($parameters);
        $this->logger->debug(' - Parameters created');

        // Création des Settings
        $settings = $this->makeSettings($organizationCreationRequest);
        $organization->setSettings($settings);
        $this->logger->debug(' - Settings created');

        // Création de l'adresse postale
        $organizationAddressPostal = $this->makePostalAddress($organizationCreationRequest);
        $organization->addOrganizationAddressPostal($organizationAddressPostal);
        $this->logger->debug(' - OrganizationAddressPostal created');

        // Création du point de contact
        $contactPoint = $this->makeContactPoint($organizationCreationRequest);
        $organization->addContactPoint($contactPoint);
        $this->logger->debug(' - ContactPoint created');

        // Rattachement au réseau
        $networkOrganization = $this->makeNetworkOrganization($organizationCreationRequest);
        $organization->addNetworkOrganization($networkOrganization);
        $this->logger->debug(' - NetworkOrganization created');

        // Créé l'admin
        $adminAccess = $this->makeAdminAccess($organizationCreationRequest);
        $organization->addAccess($adminAccess);
        $this->logger->debug(' - Admin access created');

        // Création des cycles
        foreach ($this->makeCycles() as $cycle) {
            $organization->addCycle($cycle);
        }
        $this->logger->debug(' - Cycles created');

        // Création du président (si renseigné)
        $presidentCreationRequest = $organizationCreationRequest->getPresident();
        if ($presidentCreationRequest !== null) {
            $presidentAccess = $this->makeAccess(
                $presidentCreationRequest,
                FunctionEnum::PRESIDENT,
                $organizationCreationRequest->getCreationDate(),
                $organizationCreationRequest->getAuthorId()
            );

            $organization->addAccess($presidentAccess);
            $this->logger->debug(' - President access created');
        }

        // Création du directeur (si renseigné)
        $directorCreationRequest = $organizationCreationRequest->getDirector();
        if ($directorCreationRequest !== null) {
            $directorAccess = $this->makeAccess(
                $directorCreationRequest,
                FunctionEnum::DIRECTOR,
                $organizationCreationRequest->getCreationDate(),
                $organizationCreationRequest->getAuthorId()
            );

            $organization->addAccess($directorAccess);
            $this->logger->debug(' - Director access created');
        }

        // Création du sous-domaine
        $subdomain = $this->makeSubdomain($organizationCreationRequest);
        $organization->addSubdomain($subdomain);

        // <--- Pour la rétrocompatibilité avec la v1 ; pourra être supprimé lorsque la migration sera achevée
        $parameters = $organization->getParameters();
        $parameters->setSubDomain($organizationCreationRequest->getSubdomain());
        $parameters->setOtherWebsite('https://'.$organizationCreationRequest->getSubdomain().'.opentalent.fr');
        // --->
        $this->logger->debug(' - Subdomain created');

        return $organization;
    }

    /**
     * Créé une nouvelle instance d'organisation.
     */
    protected function makeOrganization(OrganizationCreationRequest $organizationCreationRequest): Organization
    {
        // Création de l'organisation
        $organization = new Organization();
        $organization->setName($organizationCreationRequest->getName());
        $organization->setLegalStatus($organizationCreationRequest->getLegalStatus());
        $organization->setPrincipalType($organizationCreationRequest->getPrincipalType());
        $organization->setIdentifier($organizationCreationRequest->getIdentifier());
        $organization->setCreationDate($organizationCreationRequest->getCreationDate());
        $organization->setCreateDate($organizationCreationRequest->getCreationDate());
        $organization->setCreatedBy($organizationCreationRequest->getAuthorId());

        return $organization;
    }

    /**
     * Create a new Parameters object from the data in an OrganizationCreationRequest.
     *
     * @param OrganizationCreationRequest $organizationCreationRequest The organization creation request
     *
     * @return Parameters The created Parameters object
     *
     * @throws \Throwable If there is an error
     */
    protected function makeParameters(OrganizationCreationRequest $organizationCreationRequest): Parameters
    {
        $parameters = new Parameters();

        return $parameters;
    }

    /**
     * Creates a new instance of the Settings class based on the given OrganizationCreationRequest object.
     *
     * @param OrganizationCreationRequest $organizationCreationRequest the OrganizationCreationRequest object containing the required data
     *
     * @return Settings the newly created instance of the Settings class
     */
    protected function makeSettings(OrganizationCreationRequest $organizationCreationRequest): Settings
    {
        $settings = new Settings();
        $settings->setProduct($organizationCreationRequest->getProduct());

        // TODO: à revoir, pour étendre à d'autres pays (voir à remplacer le champs 'country' par un champs 'currency'?)
        $settings->setCountry(
            $organizationCreationRequest->getCountryId() === 41 ? 'SWITZERLAND' : 'FRANCE'
        );

        $settings->setCreateDate($organizationCreationRequest->getCreationDate());
        $settings->setCreatedBy($organizationCreationRequest->getAuthorId());

        return $settings;
    }

    /**
     * Creates a new instance of the OrganizationAddressPostal class based on the given OrganizationCreationRequest object.
     *
     * @param OrganizationCreationRequest $organizationCreationRequest the OrganizationCreationRequest object containing the required data
     *
     * @return OrganizationAddressPostal the newly created instance of the OrganizationAddressPostal class
     */
    protected function makePostalAddress(OrganizationCreationRequest $organizationCreationRequest): OrganizationAddressPostal
    {
        $country = $this->countryRepository->find($organizationCreationRequest->getCountryId());
        if (!$country) {
            throw new \RuntimeException('No country found for id '.$organizationCreationRequest->getCountryId());
        }

        $addressPostal = new AddressPostal();
        $addressPostal->setStreetAddress($organizationCreationRequest->getStreetAddress1());
        $addressPostal->setStreetAddressSecond($organizationCreationRequest->getStreetAddress2());
        $addressPostal->setStreetAddressThird($organizationCreationRequest->getStreetAddress3());
        $addressPostal->setPostalCode($organizationCreationRequest->getPostalCode());
        $addressPostal->setAddressCity($organizationCreationRequest->getCity());
        $addressPostal->setAddressCountry($country);
        $addressPostal->setCreateDate($organizationCreationRequest->getCreationDate());
        $addressPostal->setCreatedBy($organizationCreationRequest->getAuthorId());

        $organizationAddressPostal = new OrganizationAddressPostal();
        $organizationAddressPostal->setAddressPostal($addressPostal);
        $organizationAddressPostal->setType(AddressPostalOrganizationTypeEnum::ADDRESS_HEAD_OFFICE);
        $organizationAddressPostal->setCreateDate($organizationCreationRequest->getCreationDate());
        $organizationAddressPostal->setCreatedBy($organizationCreationRequest->getAuthorId());

        return $organizationAddressPostal;
    }

    /**
     * Creates a new instance of the ContactPoint class based on the given OrganizationCreationRequest object.
     *
     * @param OrganizationCreationRequest $organizationCreationRequest the OrganizationCreationRequest object containing the required data
     *
     * @return ContactPoint the newly created instance of the ContactPoint class
     *
     * @throws NumberParseException
     */
    protected function makeContactPoint(OrganizationCreationRequest $organizationCreationRequest): ContactPoint
    {
        if (!$this->phoneNumberUtil->isPossibleNumber($organizationCreationRequest->getPhoneNumber())) {
            throw new \RuntimeException('Phone number is invalid or missing');
        }
        $phoneNumber = $this->phoneNumberUtil->parse($organizationCreationRequest->getPhoneNumber());

        $contactPoint = new ContactPoint();
        $contactPoint->setContactType(ContactPointTypeEnum::PRINCIPAL);
        $contactPoint->setEmail($organizationCreationRequest->getEmail());
        $contactPoint->setTelphone($phoneNumber);
        $contactPoint->setCreateDate($organizationCreationRequest->getCreationDate());
        $contactPoint->setCreatedBy($organizationCreationRequest->getAuthorId());

        return $contactPoint;
    }

    /**
     * Creates a new instance of the NetworkOrganization class based on the given OrganizationCreationRequest object.
     *
     * @param OrganizationCreationRequest $organizationCreationRequest the OrganizationCreationRequest object containing the required data
     *
     * @return NetworkOrganization the newly created instance of the NetworkOrganization class
     *
     * @throws \RuntimeException|\Exception if no parent organization is found for the given parent ID or if no network is found for the given network ID
     */
    protected function makeNetworkOrganization(OrganizationCreationRequest $organizationCreationRequest): NetworkOrganization
    {
        $parent = $this->organizationRepository->find($organizationCreationRequest->getParentId());
        if (!$parent) {
            throw new \RuntimeException('No parent organization found for id '.$organizationCreationRequest->getParentId());
        }

        if (!in_array($parent->getSettings()->getProduct(), [SettingsProductEnum::MANAGER, SettingsProductEnum::MANAGER_PREMIUM])) {
            throw new \RuntimeException("Parent organization must have the product 'manager' (actual product: '".$parent->getSettings()->getProduct()->value."')");
        }

        $networkOrganization = $this->organizationUtils->getActiveNetworkOrganization($parent);
        if (!$networkOrganization) {
            throw new \RuntimeException('No network found for parent '.$organizationCreationRequest->getParentId());
        }

        $network = $networkOrganization->getNetwork();

        // Si réseau CMF, on vérifie que le matricule est valide
        if ($network->getId() === NetworkEnum::CMF->value) {
            if (!preg_match("/FR\d{3}\w\d{8}/", $organizationCreationRequest->getIdentifier())) {
                throw new \RuntimeException('CMF identifier is missing or invalid.');
            }
        }

        $networkOrganization = new NetworkOrganization();
        $networkOrganization->setParent($parent);
        $networkOrganization->setNetwork($network);
        $networkOrganization->setStartDate(DatesUtils::new());
        $networkOrganization->setCreateDate($organizationCreationRequest->getCreationDate());
        $networkOrganization->setCreatedBy($organizationCreationRequest->getAuthorId());

        return $networkOrganization;
    }

    /**
     * Creates a new instance of the Access class with admin access based on the given OrganizationCreationRequest object.
     *
     * @param OrganizationCreationRequest $organizationCreationRequest the OrganizationCreationRequest object containing the required data
     *
     * @return Access the newly created instance of the Access class with admin access
     */
    protected function makeAdminAccess(OrganizationCreationRequest $organizationCreationRequest): Access
    {
        $admin = new Person();
        $admin->setUsername('admin'.strtolower($organizationCreationRequest->getSubdomain()));
        $randomString = ByteString::fromRandom(32)->toString();
        $admin->setPassword($randomString);
        $admin->setEnabled(true);

        $adminAccess = new Access();
        $adminAccess->setAdminAccess(true);
        $adminAccess->setPerson($admin);
        $adminAccess->setLoginEnabled(true);
        $adminAccess->setRoles(['ROLE_ADMIN', 'ROLE_ADMIN_CORE']);
        $adminAccess->setCreateDate($organizationCreationRequest->getCreationDate());
        $adminAccess->setCreatedBy($organizationCreationRequest->getAuthorId());

        $contactPoint = new ContactPoint();
        $contactPoint->setContactType(ContactPointTypeEnum::PRINCIPAL);
        $contactPoint->setEmail($organizationCreationRequest->getEmail());
        $admin->addContactPoint($contactPoint);

        return $adminAccess;
    }

    /**
     * Creates an array of Cycle objects based on a predefined set of data.
     *
     * @return Cycle[] an array of Cycle objects
     */
    protected function makeCycles(): array
    {
        $cyclesData = [
            ['Cycle initiation', 10, CycleEnum::INITIATION_CYCLE],
            ['Cycle 1', 20, CycleEnum::CYCLE_1],
            ['Cycle 2', 30, CycleEnum::CYCLE_2],
            ['Cycle 3', 40, CycleEnum::CYCLE_3],
            ['Cycle 4', 50, CycleEnum::CYCLE_4],
            ['Hors cycle', 60, CycleEnum::OUT_CYCLE],
        ];

        $cycles = [];

        foreach ($cyclesData as $cycleData) {
            $cycle = new Cycle();
            $cycle->setLabel($cycleData[0]);
            $cycle->setOrder($cycleData[1]);
            $cycle->setCycleEnum($cycleData[2]);
            $cycle->setIsSystem(false);
            $cycles[] = $cycle;
        }

        return $cycles;
    }

    /**
     * Creates an Access object based on the given OrganizationMemberCreationRequest.
     *
     * @param int|OrganizationMemberCreationRequest $creationRequestData the request object containing the
     *                                                                   necessary data for creating a Person object,
     *                                                                   or the id of an existing one
     *
     * @return Access the created Access object
     *
     * @throws NumberParseException
     */
    protected function makeAccess(
        int|OrganizationMemberCreationRequest $creationRequestData,
        FunctionEnum $function,
        \DateTime $creationDate,
        ?int $authorId,
    ): Access {
        if (is_int($creationRequestData)) {
            $person = $this->personRepository->find($creationRequestData);
        } else {
            $person = new Person();

            if (
                $creationRequestData->getUsername() !== null
                && $this->personRepository->findOneBy(['username' => $creationRequestData->getUsername()])
            ) {
                throw new \RuntimeException('Username already in use : '.$creationRequestData->getUsername());
            }

            $person->setUsername($creationRequestData->getUsername());
            $person->setPassword(ByteString::fromRandom(32)->toString());
            $person->setGender($creationRequestData->getGender());
            $person->setName(
                ucfirst(strtolower($creationRequestData->getName()))
            );
            $person->setGivenName(
                ucfirst(strtolower($creationRequestData->getGivenName()))
            );

            $personPostalAddress = $this->makePersonPostalAddress($creationRequestData, $creationDate, $authorId);
            $person->addPersonAddressPostal($personPostalAddress);

            $contactPoint = $this->makePersonContactPoint($creationRequestData, $creationDate, $authorId);
            $person->addContactPoint($contactPoint);

            $person->setCreateDate($creationDate);
            $person->setCreatedBy($authorId);
        }

        $access = new Access();
        $access->setPerson($person);

        $functionType = $this->functionTypeRepository->findOneBy(['mission' => $function]);

        $organizationFunction = new OrganizationFunction();
        $organizationFunction->setFunctionType($functionType);
        $organizationFunction->setStartDate($creationDate);
        $organizationFunction->setCreateDate($creationDate);
        $organizationFunction->setCreatedBy($authorId);

        $access->addOrganizationFunction($organizationFunction);

        $access->setCreateDate($creationDate);
        $access->setCreatedBy($authorId);

        return $access;
    }

    /**
     * Creates a PersonAddressPostal object based on the given OrganizationMemberCreationRequest.
     *
     * @param OrganizationMemberCreationRequest $organizationMemberCreationRequest the request object containing the
     *                                                                             necessary data for creating a
     *                                                                             PersonAddressPostal object
     *
     * @return PersonAddressPostal the created PersonAddressPostal object
     */
    protected function makePersonPostalAddress(
        OrganizationMemberCreationRequest $organizationMemberCreationRequest,
        \DateTime $creationDate,
        ?int $authorId,
    ): PersonAddressPostal {
        $addressPostal = new AddressPostal();
        $addressPostal->setStreetAddress($organizationMemberCreationRequest->getStreetAddress1());
        $addressPostal->setStreetAddressSecond($organizationMemberCreationRequest->getStreetAddress2());
        $addressPostal->setStreetAddressThird($organizationMemberCreationRequest->getStreetAddress3());
        $addressPostal->setPostalCode($organizationMemberCreationRequest->getPostalCode());
        $addressPostal->setAddressCity($organizationMemberCreationRequest->getCity());
        $addressPostal->setCreateDate($creationDate);
        $addressPostal->setCreatedBy($authorId);

        $country = $this->countryRepository->find($organizationMemberCreationRequest->getCountryId());
        $addressPostal->setAddressCountry($country);

        $personAddressPostal = new PersonAddressPostal();
        $personAddressPostal->setAddressPostal($addressPostal);
        $personAddressPostal->setType(AddressPostalPersonTypeEnum::ADDRESS_PRINCIPAL);
        $personAddressPostal->setCreateDate($creationDate);
        $personAddressPostal->setCreatedBy($authorId);

        return $personAddressPostal;
    }

    /**
     * Creates a new instance of the ContactPoint class based on the given OrganizationCreationRequest object.
     *
     * @param OrganizationMemberCreationRequest $organizationMemberCreationRequest the OrganizationMemberCreationRequest object containing the required data
     *
     * @return ContactPoint the newly created instance of the ContactPoint class
     *
     * @throws NumberParseException
     */
    protected function makePersonContactPoint(
        OrganizationMemberCreationRequest $organizationMemberCreationRequest,
        \DateTime $creationDate,
        ?int $authorId,
    ): ContactPoint {
        if (
            $organizationMemberCreationRequest->getPhone() !== null
            && !$this->phoneNumberUtil->isPossibleNumber($organizationMemberCreationRequest->getPhone())
        ) {
            throw new \RuntimeException('Phone number is invalid or missing (person: '.$organizationMemberCreationRequest->getUsername().')');
        }

        if (
            $organizationMemberCreationRequest->getMobile() !== null
            && !$this->phoneNumberUtil->isPossibleNumber($organizationMemberCreationRequest->getMobile())) {
            throw new \RuntimeException('Mobile phone number is invalid (person: '.$organizationMemberCreationRequest->getUsername().')');
        }

        $contactPoint = new ContactPoint();
        $contactPoint->setContactType(ContactPointTypeEnum::PRINCIPAL);
        $contactPoint->setEmail($organizationMemberCreationRequest->getEmail());

        if ($organizationMemberCreationRequest->getPhone() !== null) {
            $phoneNumber = $this->phoneNumberUtil->parse($organizationMemberCreationRequest->getPhone());
            $contactPoint->setTelphone($phoneNumber);
        }

        if ($organizationMemberCreationRequest->getMobile() !== null) {
            $mobileNumber = $this->phoneNumberUtil->parse($organizationMemberCreationRequest->getMobile());
            $contactPoint->setMobilPhone($mobileNumber);
        }

        $contactPoint->setCreateDate($creationDate);
        $contactPoint->setCreatedBy($authorId);

        return $contactPoint;
    }

    protected function makeSubdomain(OrganizationCreationRequest $organizationCreationRequest): Subdomain
    {
        $subdomain = new Subdomain();
        $subdomain->setSubdomain($organizationCreationRequest->getSubdomain());
        $subdomain->setActive(true);

        return $subdomain;
    }

    /**
     * Créé le site Typo3 et retourne l'id de la page racine du site nouvellement créé, ou null en cas d'erreur.
     *
     * @throws RedirectionExceptionInterface
     * @throws ClientExceptionInterface
     * @throws TransportExceptionInterface
     * @throws ServerExceptionInterface
     */
    protected function createTypo3Website(Organization $organization): ?int
    {
        $response = $this->typo3Service->createSite($organization->getId());
        $content = json_decode($response->getContent(), true);
        $rootPageUid = $content['root_uid'];

        if ($response->getStatusCode() === Response::HTTP_OK && $rootPageUid > 0) {
            // TODO: revoir l'utilité du champs cmsId
            $organization->setCmsId($rootPageUid);
            $this->entityManager->persist($organization);
            $this->entityManager->flush();

            return $rootPageUid;
        } else {
            $this->logger->critical("/!\ A critical error happened while creating the Typo3 website");
            $this->logger->debug($response->getContent());
        }

        return null;
    }

    protected function updateAdminassosDb(Organization $organization): void
    {
        $response = $this->apiLegacyRequestService->get(
            UrlBuilder::concatPath('/_internal/request/adminassos/create/organization/', [(string) $organization->getId()])
        );

        if ($response->getStatusCode() !== Response::HTTP_OK) {
            throw new \RuntimeException('An error happened while updating the adminassos database: '.$response->getContent());
        }
    }

    /**
     * Normalise la chaine comme sont normalisées les champs de l'entité OrganizationIdentification.
     *
     * @øee sql/schema-extensions/003-view_organization_identification.sql
     */
    protected function normalizeIdentificationField(string $value): string
    {
        $value = strtolower(trim($value));
        $value = preg_replace('/[éèê]/u', 'e', $value);
        $value = preg_replace('/[à]/u', 'a', $value);
        $value = preg_replace('/[ç]/u', 'c', $value);

        return preg_replace('/[^a-z0-9]+/u', '+', $value);
    }

    /**
     * /!\ Danger zone /!\.
     *
     * Supprime définitivement une organisation, ses données, ses fichiers, son site internet, et son profil Dolibarr.
     *
     * Pour éviter une suppression accidentelle, cette méthode ne doit pouvoir être exécutée que si la requête a été
     * envoyée depuis le localhost.
     *
     * @throws \Exception
     */
    public function delete(OrganizationDeletionRequest $organizationDeletionRequest): OrganizationDeletionRequest
    {
        SecurityUtils::preventIfNotLocalhost();

        $organization = $this->organizationRepository->find($organizationDeletionRequest->getOrganizationId());
        if (!$organization) {
            throw new \RuntimeException('No organization was found for id : '.$organizationDeletionRequest->getOrganizationId());
        }

        $this->logger->info(
            "Start the deletion of organization '".$organization->getName()."' [".$organization->getId().']'
        );

        $this->entityManager->beginTransaction();

        $withError = false;

        try {
            $orphanPersons = $this->getFutureOrphanPersons($organization);

            // On est obligé de supprimer manuellement les paramètres, car c'est l'entité Parameters qui est
            // propriétaire de la relation Organization ↔ Parameters.
            $this->entityManager->remove($organization->getParameters());

            // Toutes les autres entités liées seront supprimées en cascade
            $this->entityManager->remove($organization);

            // Supprime les personnes qui n'avaient pas d'autre Access attaché
            $deletedPersonIds = [];
            foreach ($orphanPersons as $person) {
                $deletedPersonIds[] = $person->getId();
                $this->entityManager->remove($person);
            }

            $this->entityManager->flush();
            $this->entityManager->commit();
            $this->logger->info('Organization deleted');
        } catch (\Exception $e) {
            $this->logger->critical("An error happened, operation cancelled\n".$e);
            $this->entityManager->rollback();
            throw $e;
        }

        try {
            $this->deleteTypo3Website($organizationDeletionRequest->getOrganizationId());
            $this->logger->info('Typo3 website deleted');
        } catch (\Exception $e) {
            $this->logger->critical('An error happened while deleting the Typo3 website, please proceed manually.');
            $this->logger->debug((string) $e);
            $withError = true;
        }

        try {
            $this->switchDolibarrSocietyToProspect($organizationDeletionRequest->getOrganizationId());
            $this->logger->info('Dolibarr society switched to prospect');
        } catch (\Exception $e) {
            $this->logger->critical('An error happened while updating the Dolibarr society, please proceed manually.');
            $this->logger->debug((string) $e);
            $withError = true;
        }

        try {
            $this->fileManager->deleteOrganizationFiles($organizationDeletionRequest->getOrganizationId());
            $this->logger->info('Organization files deleted');
        } catch (\RuntimeException $e) {
            // Nothing to delete
        } catch (\Exception $e) {
            $this->logger->critical("An error happened while deleting the organization's files, please proceed manually.");
            $this->logger->debug((string) $e);
            $withError = true;
        }

        foreach ($deletedPersonIds as $personId) {
            try {
                $this->fileManager->deletePersonFiles($personId);
            } catch (\RuntimeException $e) {
                // Nothing to delete
            } catch (\Exception $e) {
                $this->logger->critical("An error happened while deleting the person's files, please proceed manually (id=".$personId.').');
                $this->logger->debug((string) $e);
                $withError = true;
            }
        }
        $this->logger->info("Organization's persons files deleted");

        if ($withError) {
            $organizationDeletionRequest->setStatus(OrganizationDeletionRequest::STATUS_OK_WITH_ERRORS);
            $this->logger->warning('-- Operation ended with errors, check the logs for more information --');
        } else {
            $organizationDeletionRequest->setStatus(OrganizationDeletionRequest::STATUS_OK);
        }

        $this->logger->info(
            'The organization has been deleted.'
        );

        return $organizationDeletionRequest;
    }

    /**
     * Supprime tous les Access d'une organisation, ainsi que la Person
     * rattachée (si celle-ci n'est pas liée à d'autres Access).
     *
     * @return array<Person>
     */
    protected function getFutureOrphanPersons(Organization $organization): array
    {
        $orphans = [];

        foreach ($organization->getAccesses() as $access) {
            $person = $access->getPerson();
            if ($person->getAccesses()->count() === 1) {
                $orphans[] = $person;
            }
        }

        return $orphans;
    }

    protected function deleteTypo3Website(int $organizationId): void
    {
        $this->typo3Service->hardDeleteSite($organizationId);
    }

    protected function switchDolibarrSocietyToProspect(int $organizationId): void
    {
        $this->dolibarrApiService->switchSocietyToProspect($organizationId);
    }
}
