<?php

declare(strict_types=1);

namespace App\Module\Commercial\Infrastructure\ApiPlatform\State\Processor;

use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\Validator\Exception\ValidationException;
use App\Module\Commercial\Application\Service\ClientFieldNormalizer;
use App\Module\Commercial\Domain\Entity\Client;
use App\Module\Commercial\Domain\Entity\ClientContact;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;

/**
 * Processor d'ecriture de la sous-ressource Contact d'un client (M1, § 4.5).
 *
 * Sequence :
 *  - POST / PATCH : normalisation serveur (RG-1.19 prenom/nom capitalize,
 *    RG-1.20 telephones reduits aux chiffres, RG-1.21 email lowercase) via le
 *    ClientFieldNormalizer partage (reutilise d'ERP-55), puis validation RG-1.05
 *    (au moins prenom OU nom) avant persistance.
 *  - DELETE : RG-1.14 — la suppression du DERNIER contact d'un client est
 *    refusee (409). Au M1, la completude de l'onglet Contact est purement front
 *    (pas de state machine back) : on garantit seulement qu'un client deja dote
 *    d'un contact n'en soit jamais vide via l'API.
 *
 * La security de l'operation (commercial.clients.manage) est deja appliquee par
 * API Platform en amont. La validation Symfony des contraintes d'attribut
 * (Assert\Email, Assert\Length...) est jouee avant ce processor.
 *
 * @implements ProcessorInterface<ClientContact, null|ClientContact>
 */
final class ClientContactProcessor implements ProcessorInterface
{
    public function __construct(
        #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
        private readonly ProcessorInterface $persistProcessor,
        #[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')]
        private readonly ProcessorInterface $removeProcessor,
        private readonly ClientFieldNormalizer $normalizer,
        private readonly EntityManagerInterface $em,
    ) {}

    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
    {
        if (!$data instanceof ClientContact) {
            return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
        }

        if ($operation instanceof DeleteOperationInterface) {
            $this->guardLastContactDeletion($data);

            return $this->removeProcessor->process($data, $operation, $uriVariables, $context);
        }

        $this->linkParent($data, $uriVariables);
        $this->normalize($data);
        $this->validateName($data);

        return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
    }

    /**
     * Rattache le contact au client parent de la sous-ressource POST
     * (/clients/{clientId}/contacts). La relation n'est pas peuplee
     * automatiquement par le Link sur une operation d'ecriture : on resout donc
     * le parent depuis l'uri variable. Sur PATCH (entite existante), le client
     * est deja present -> no-op.
     */
    private function linkParent(ClientContact $contact, array $uriVariables): void
    {
        if (null !== $contact->getClient()) {
            return;
        }

        $clientId = $uriVariables['clientId'] ?? null;
        if (null === $clientId) {
            return;
        }

        $client = $clientId instanceof Client
            ? $clientId
            : $this->em->getRepository(Client::class)->find($clientId);

        // read:false sur le POST : sans stade lecture, un parent introuvable n'est
        // plus intercepte en amont -> 404 explicite (sinon 500 au persist sur la
        // contrainte client_id NOT NULL).
        if (!$client instanceof Client) {
            throw new NotFoundHttpException('Client introuvable.');
        }

        $contact->setClient($client);
    }

    /**
     * Normalisation serveur (RG-1.19 / 1.20 / 1.21). Toutes les methodes du
     * normalizer sont null-safe : une chaine vide apres trim devient null.
     */
    private function normalize(ClientContact $contact): void
    {
        $contact->setFirstName($this->normalizer->normalizePersonName($contact->getFirstName()));
        $contact->setLastName($this->normalizer->normalizePersonName($contact->getLastName()));
        $contact->setPhonePrimary($this->normalizer->normalizePhone($contact->getPhonePrimary()));
        $contact->setPhoneSecondary($this->normalizer->normalizePhone($contact->getPhoneSecondary()));
        $contact->setEmail($this->normalizer->normalizeEmail($contact->getEmail()));
    }

    /**
     * RG-1.05 : au moins le prenom OU le nom est obligatoire (double garde avec
     * le CHECK BDD chk_client_contact_name — leve un 422 propre plutot qu'une
     * erreur SQL). Joue apres normalisation, donc les chaines vides sont deja
     * ramenees a null.
     */
    private function validateName(ClientContact $contact): void
    {
        if (null === $contact->getFirstName() && null === $contact->getLastName()) {
            $violations = new ConstraintViolationList();
            $violations->add(new ConstraintViolation(
                'Le prénom ou le nom du contact est obligatoire.',
                null,
                [],
                $contact,
                'firstName',
                null,
            ));

            throw new ValidationException($violations);
        }
    }

    /**
     * RG-1.14 : refuse la suppression du dernier contact d'un client (409). La
     * collection inclut le contact en cours de suppression : un effectif <= 1
     * signifie qu'il ne resterait aucun contact. Sans client rattache (cas
     * theorique), on laisse passer.
     */
    private function guardLastContactDeletion(ClientContact $contact): void
    {
        $client = $contact->getClient();
        if (null === $client) {
            return;
        }

        if ($client->getContacts()->count() <= 1) {
            throw new ConflictHttpException(
                'Impossible de supprimer le dernier contact du client : au moins un contact est requis.',
            );
        }
    }
}
