<?php

declare(strict_types=1);

namespace App\Module\Commercial\Infrastructure\DataFixtures;

use App\Module\Catalog\Infrastructure\DataFixtures\CategoryFixtures;
use App\Module\Commercial\Application\Service\ClientFieldNormalizer;
use App\Module\Commercial\Domain\Entity\Bank;
use App\Module\Commercial\Domain\Entity\Client;
use App\Module\Commercial\Domain\Entity\ClientAddress;
use App\Module\Commercial\Domain\Entity\ClientContact;
use App\Module\Commercial\Domain\Entity\ClientRib;
use App\Module\Commercial\Domain\Entity\PaymentType;
use App\Module\Sites\Infrastructure\DataFixtures\SitesFixtures;
use App\Shared\Domain\Contract\CategoryInterface;
use App\Shared\Domain\Contract\SiteInterface;
use App\Shared\Domain\Contract\SiteProviderInterface;
use DateTimeImmutable;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use RuntimeException;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * Fixtures dev/test du module Commercial : ~14 clients de demonstration couvrant
 * l'ensemble des cas metier RG-1.xx du repertoire clients (M1) :
 *  - client basique ; dependant distributeur / courtier (RG-1.03) ;
 *  - reglement LCR avec 2 RIB (RG-1.13) ; reglement Cheque sans RIB ;
 *  - multi-adresses Prospect / Livraison / Facturation (RG-1.06/07/08/11) ;
 *  - prospect seul ; 3 contacts dont un avec telephone secondaire (RG-1.05/1.02) ;
 *  - client archive (RG-1.22) ; onglet Information complet ; multi-categories M2M.
 *
 * Resolution inter-modules conforme a la regle n°1 (pas d'import direct) :
 *  - categories resolues via le contrat Shared CategoryInterface
 *    (resolve_target_entities -> Category) ;
 *  - sites resolus via le contrat Shared SiteProviderInterface.
 *
 * Normalisation : les valeurs sont fournies BRUTES (casse libre, telephones
 * formates) et normalisees par ClientFieldNormalizer avant persist, exactement
 * comme le ferait le ClientProcessor via l'API (companyName UPPERCASE,
 * first/last Capitalize, telephones chiffres seuls, emails lowercase).
 *
 * Distributeur / courtier auto-references (RG-1.03) : les tiers referencables
 * (GSO distributeur, Cabinet Leonard courtier) sont crees AVANT les clients qui
 * les referencent ; un unique flush en fin de load ordonne correctement les
 * inserts auto-references.
 *
 * Idempotence : lookup par companyName normalise (coherent avec l'index unique
 * partiel uq_client_company_name_active). Un client deja present n'est pas
 * reconstruit (ses sous-collections ne sont pas redupliquees). Rejouable sans
 * doublon meme si le purger Doctrine est desactive.
 *
 * Audit / Blamable : persist hors contexte HTTP -> created_by / updated_by
 * restent null (« Systeme » cote front), c'est attendu. Les donnees respectent
 * les CHECK BDD ET les validators applicatifs (exclusivite Prospect, billingEmail
 * ssi facturation, aucune categorie de code DISTRIBUTEUR/COURTIER sur une adresse
 * — RG-1.29, ERP-78).
 *
 * Depend de CategoryFixtures (categories), SitesFixtures (sites) et
 * CommercialReferentialFixtures (referentiels comptables Bank / PaymentType).
 *
 * Portee : DONNEES DE DEMONSTRATION (dev uniquement). En environnement `test`,
 * la fixture ne charge rien : les tests seedent et nettoient leurs propres
 * clients et comptent sur une table `client` vierge — y injecter 14 clients de
 * demo casserait les comptages de liste et les cleanups. Meme garde-fou que
 * CategoryFixtures.
 */
class ClientFixtures extends Fixture implements DependentFixtureInterface
{
    /** Cache des categories resolues par nom (evite des requetes repetees). */
    private array $categoryCache = [];

    /** Cache des sites resolus par nom. */
    private array $siteCache = [];

    /** ObjectManager courant, capture en debut de load (resolution categories). */
    private ObjectManager $manager;

    public function __construct(
        private readonly ClientFieldNormalizer $normalizer,
        private readonly SiteProviderInterface $siteProvider,
        #[Autowire('%kernel.environment%')]
        private readonly string $environment,
    ) {}

    /**
     * @return array<int, class-string>
     */
    public function getDependencies(): array
    {
        return [
            CategoryFixtures::class,
            SitesFixtures::class,
            CommercialReferentialFixtures::class,
        ];
    }

    public function load(ObjectManager $manager): void
    {
        // Donnees de demo : dev uniquement. En test, on laisse la table vierge.
        if ('test' === $this->environment) {
            return;
        }

        $this->manager = $manager;

        // === Tiers referencables (RG-1.03) : crees en premier ===

        // Distributeur reference par d'autres clients.
        [$gso, $gsoIsNew] = $this->ensureClient(
            $manager,
            companyName: 'Distrib Grand Sud-Ouest',
            categoryNames: ['Distributeur'],
        );
        if ($gsoIsNew) {
            $this->addContact($gso, 'Paul', 'Garnier', 'Directeur commercial', '05 56 10 20 30', null, 'paul.garnier@distrib-gso.fr');
            $this->addAddress($gso, ['Pommevic'], '82400', 'Pommevic', '1 Av. Jean Duquesne', isDelivery: true, categoryNames: ['Transport/Logistique']);
        }

        // Courtier reference par d'autres clients.
        [$leonard, $leonardIsNew] = $this->ensureClient(
            $manager,
            companyName: 'Cabinet Léonard Assurances',
            categoryNames: ['Courtier'],
        );
        if ($leonardIsNew) {
            $this->addContact($leonard, 'Sophie', 'Léonard', 'Gérante', '05 49 11 22 33', null, 'sophie.leonard@cabinet-leonard.fr');
            $this->addAddress($leonard, ['Chatellerault'], '86100', 'Châtellerault', '5 rue des Courtiers', isBilling: true, billingEmail: 'Factures@Cabinet-Leonard.FR');
        }

        // === Client basique ===
        [$dubois, $isNew] = $this->ensureClient(
            $manager,
            companyName: 'Menuiserie Dubois',
            categoryNames: ['BTP'],
        );
        if ($isNew) {
            $dubois->setPaymentType($this->paymentType($manager, 'VIREMENT'));
            $dubois->setBank($this->bank($manager, 'SG'));
            $this->addContact($dubois, 'Jean', 'Dubois', 'Gérant', '05 49 00 00 01', null, 'jean.dubois@menuiserie-dubois.fr');
            $this->addAddress($dubois, ['Chatellerault'], '86100', 'Châtellerault', '12 rue de l\'Atelier', isDelivery: true, categoryNames: ['BTP']);
        }

        // === Dependant d'un distributeur (RG-1.03) ===
        [$garage, $isNew] = $this->ensureClient(
            $manager,
            companyName: 'Garage Martin',
            categoryNames: ['Services'],
        );
        if ($isNew) {
            $garage->setDistributor($gso);
            $this->addContact($garage, 'Luc', 'Martin', 'Gérant', '05 56 44 55 66', null, 'luc.martin@garage-martin.fr');
            $this->addAddress($garage, ['Pommevic'], '82400', 'Pommevic', '8 route de Moissac', isDelivery: true);
        }

        // === Dependant d'un courtier (RG-1.03) ===
        [$boulangerie, $isNew] = $this->ensureClient(
            $manager,
            companyName: 'Boulangerie Lemoine',
            categoryNames: ['Agro-alimentaire'],
        );
        if ($isNew) {
            $boulangerie->setBroker($leonard);
            $this->addContact($boulangerie, 'Marie', 'Lemoine', 'Gérante', '05 49 77 88 99', null, 'marie.lemoine@boulangerie-lemoine.fr');
            $this->addAddress($boulangerie, ['Chatellerault'], '86100', 'Châtellerault', '3 place du Marché', isDelivery: true);
        }

        // === Reglement LCR avec 2 RIB (RG-1.13) ===
        [$transports, $isNew] = $this->ensureClient(
            $manager,
            companyName: 'Transports Rapides',
            categoryNames: ['Transport/Logistique'],
        );
        if ($isNew) {
            $transports->setPaymentType($this->paymentType($manager, 'LCR'));
            $this->addContact($transports, null, 'Bernard', 'Responsable exploitation', '05 56 12 13 14', null, 'expl@transports-rapides.fr');
            $this->addAddress($transports, ['Saint-Jean'], '17400', 'Fontenet', '2 zone industrielle', isDelivery: true, categoryNames: ['Transport/Logistique']);
            $this->addRib($transports, 'Compte principal', 'BNPAFRPPXXX', 'FR1420041010050500013M02606', 0);
            $this->addRib($transports, 'Compte secondaire', 'SOGEFRPPXXX', 'FR7630006000011234567890189', 1);
        }

        // === Multi-adresses Prospect / Livraison / Facturation (RG-1.06/07/08/11) ===
        [$industries, $isNew] = $this->ensureClient(
            $manager,
            companyName: 'Industries Vertes',
            categoryNames: ['Industrie'],
        );
        if ($isNew) {
            $this->addContact($industries, 'Claire', 'Moreau', 'Directrice', '05 49 21 22 23', null, 'claire.moreau@industries-vertes.fr');
            // Prospect : exclusif de livraison/facturation (sans billingEmail).
            $this->addAddress($industries, ['Chatellerault'], '86100', 'Châtellerault', '1 avenue de la Prospection', isProspect: true, position: 0);
            // Livraison.
            $this->addAddress($industries, ['Saint-Jean'], '17400', 'Fontenet', '4 rue de la Livraison', isDelivery: true, categoryNames: ['Industrie'], position: 1);
            // Facturation : billingEmail obligatoire.
            $this->addAddress($industries, ['Chatellerault'], '86100', 'Châtellerault', '7 boulevard des Factures', isBilling: true, billingEmail: 'Compta@Industries-Vertes.FR', position: 2);
        }

        // === 3 contacts dont un avec telephone secondaire (RG-1.05/1.02) ===
        [$agro, $isNew] = $this->ensureClient(
            $manager,
            companyName: 'Agro Distribution Sud',
            categoryNames: ['Agro-alimentaire'],
        );
        if ($isNew) {
            $this->addContact($agro, 'Thomas', 'Petit', 'Directeur des achats', '05 56 31 32 33', '06 01 02 03 04', 'thomas.petit@agro-sud.fr', 0);
            $this->addContact($agro, 'Julie', 'Roux', 'Assistante commerciale', '05 56 31 32 34', null, 'julie.roux@agro-sud.fr', 1);
            $this->addContact($agro, 'Marc', 'Girard', 'Logistique', '05 56 31 32 35', null, 'marc.girard@agro-sud.fr', 2);
            $this->addAddress($agro, ['Pommevic'], '82400', 'Pommevic', '10 rue des Producteurs', isDelivery: true);
        }

        // === Client archive (RG-1.22) ===
        [$ancienne, $isNew] = $this->ensureClient(
            $manager,
            companyName: 'Ancienne Société Oubliée',
            categoryNames: ['Association'],
            isArchived: true,
        );
        if ($isNew) {
            $this->addContact($ancienne, null, 'Durand', 'Ancien contact', '05 49 99 99 99', null, 'contact@ancienne-societe.fr');
            $this->addAddress($ancienne, ['Chatellerault'], '86100', 'Châtellerault', '99 rue Fermée', isDelivery: true);
        }

        // === Reglement Cheque sans RIB ===
        [$services, $isNew] = $this->ensureClient(
            $manager,
            companyName: 'Services Pro Conseil',
            categoryNames: ['Services'],
        );
        if ($isNew) {
            $services->setPaymentType($this->paymentType($manager, 'CHEQUE'));
            $this->addContact($services, 'Nadia', 'Benali', 'Consultante', '05 49 41 42 43', null, 'nadia.benali@services-pro.fr');
            $this->addAddress($services, ['Chatellerault'], '86100', 'Châtellerault', '15 rue du Conseil', isDelivery: true);
        }

        // === Onglet Information complet (exemple de fiche renseignee) ===
        [$holding, $isNew] = $this->ensureClient(
            $manager,
            companyName: 'Holding Premium Invest',
            categoryNames: ['Industrie'],
        );
        if ($isNew) {
            $holding->setDescription('Holding industrielle diversifiée, présente sur le Grand Sud-Ouest.');
            $holding->setCompetitors('Groupe Atlantique, Sud Industries');
            $holding->setFoundedAt(new DateTimeImmutable('2005-03-15'));
            $holding->setEmployeesCount(240);
            $holding->setRevenueAmount('18500000.00');
            $holding->setDirectorName('Antoine Lefèvre');
            $holding->setProfitAmount('1250000.00');
            $this->addContact($holding, 'Antoine', 'Lefèvre', 'PDG', '05 56 51 52 53', null, 'antoine.lefevre@holding-premium.fr');
            $this->addAddress($holding, ['Pommevic'], '82400', 'Pommevic', '1 allée des Investisseurs', isDelivery: true, categoryNames: ['Industrie']);
        }

        // === Multi-categories M2M ===
        [$conglo, $isNew] = $this->ensureClient(
            $manager,
            companyName: 'Conglomérat Multi Activités',
            categoryNames: ['BTP', 'Industrie', 'Services'],
        );
        if ($isNew) {
            $this->addContact($conglo, 'Hélène', 'Faure', 'Directrice générale', '05 49 61 62 63', null, 'helene.faure@conglomerat-multi.fr');
            $this->addAddress($conglo, ['Chatellerault', 'Saint-Jean'], '86100', 'Châtellerault', '20 rue des Activités', isDelivery: true, categoryNames: ['BTP', 'Services']);
        }

        // === Prospect seul ===
        [$prospect, $isNew] = $this->ensureClient(
            $manager,
            companyName: 'Prospect Futur Client',
            categoryNames: ['BTP'],
        );
        if ($isNew) {
            $this->addContact($prospect, 'Olivier', 'Renard', 'Responsable projet', '05 56 71 72 73', null, 'olivier.renard@prospect-futur.fr');
            $this->addAddress($prospect, ['Chatellerault'], '86100', 'Châtellerault', '30 rue de la Découverte', isProspect: true);
        }

        // === Categorie AUTRE ===
        [$association, $isNew] = $this->ensureClient(
            $manager,
            companyName: 'Association des Riverains',
            categoryNames: ['Association'],
        );
        if ($isNew) {
            $this->addContact($association, null, 'Caron', 'Président', '05 49 81 82 83', null, 'president@asso-riverains.fr');
            $this->addAddress($association, ['Saint-Jean'], '17400', 'Fontenet', '6 chemin du Village', isDelivery: true, categoryNames: ['Association']);
        }

        $manager->flush();
    }

    /**
     * Cree un client (base normalisee + categories) s'il n'existe pas encore,
     * sinon retourne l'existant. Retourne [Client, isNew] : isNew=false bloque la
     * reconstruction des sous-collections (idempotence sans doublon).
     *
     * Le contact principal n'est plus porte par le Client (refonte contact) : les
     * coordonnees de contact sont fournies via addContact() dans le bloc isNew.
     *
     * @param list<string> $categoryNames
     *
     * @return array{0: Client, 1: bool}
     */
    private function ensureClient(
        ObjectManager $manager,
        string $companyName,
        array $categoryNames,
        bool $isArchived = false,
    ): array {
        $normalizedName = (string) $this->normalizer->normalizeCompanyName($companyName);

        $existing = $manager->getRepository(Client::class)->findOneBy(['companyName' => $normalizedName]);
        if ($existing instanceof Client) {
            return [$existing, false];
        }

        $client = new Client();
        $client->setCompanyName($normalizedName);

        foreach ($categoryNames as $categoryName) {
            $client->addCategory($this->category($manager, $categoryName));
        }

        if ($isArchived) {
            $client->setIsArchived(true);
            $client->setArchivedAt(new DateTimeImmutable());
        }

        $manager->persist($client);

        return [$client, true];
    }

    /**
     * Ajoute un contact normalise au client (cascade persist via Client.contacts).
     * Au moins lastName est toujours fourni (RG-1.05, chk_client_contact_name).
     */
    private function addContact(
        Client $client,
        ?string $firstName,
        ?string $lastName,
        ?string $jobTitle,
        ?string $phonePrimary,
        ?string $phoneSecondary,
        ?string $email,
        int $position = 0,
    ): void {
        $contact = new ClientContact();
        $contact->setFirstName($this->normalizer->normalizePersonName($firstName));
        $contact->setLastName($this->normalizer->normalizePersonName($lastName));
        $contact->setJobTitle($jobTitle);
        $contact->setPhonePrimary($this->normalizer->normalizePhone($phonePrimary));
        $contact->setPhoneSecondary($this->normalizer->normalizePhone($phoneSecondary));
        $contact->setEmail($this->normalizer->normalizeEmail($email));
        $contact->setPosition($position);

        $client->addContact($contact);
    }

    /**
     * Ajoute une adresse au client (cascade persist via Client.addresses). Les
     * donnees respectent les validators : exclusivite Prospect, billingEmail ssi
     * facturation, aucune categorie de code DISTRIBUTEUR/COURTIER (RG-1.29).
     *
     * @param list<string> $siteNames     au moins un site (RG-1.10)
     * @param list<string> $categoryNames categories hors DISTRIBUTEUR/COURTIER (RG-1.29)
     */
    private function addAddress(
        Client $client,
        array $siteNames,
        string $postalCode,
        string $city,
        string $street,
        bool $isProspect = false,
        bool $isDelivery = false,
        bool $isBilling = false,
        ?string $billingEmail = null,
        array $categoryNames = [],
        int $position = 0,
    ): void {
        $address = new ClientAddress();
        $address->setIsProspect($isProspect);
        $address->setIsDelivery($isDelivery);
        $address->setIsBilling($isBilling);
        $address->setBillingEmail($this->normalizer->normalizeEmail($billingEmail));
        $address->setPostalCode($postalCode);
        $address->setCity($city);
        $address->setStreet($street);
        $address->setPosition($position);

        foreach ($siteNames as $siteName) {
            $address->addSite($this->site($siteName));
        }

        foreach ($categoryNames as $categoryName) {
            $address->addCategory($this->category($this->manager, $categoryName));
        }

        $client->addAddress($address);
    }

    /**
     * Ajoute un RIB au client (cascade persist via Client.ribs). IBAN/BIC valides
     * (Assert\Iban/Bic non rejouee sur persist direct mais donnees coherentes).
     */
    private function addRib(Client $client, string $label, string $bic, string $iban, int $position = 0): void
    {
        $rib = new ClientRib();
        $rib->setLabel($label);
        $rib->setBic($bic);
        $rib->setIban($iban);
        $rib->setPosition($position);

        $client->addRib($rib);
    }

    /**
     * Resout une categorie par son nom via le contrat Shared CategoryInterface
     * (resolve_target_entities -> Category), sans importer le module Catalog
     * (regle n°1). Mise en cache par nom.
     */
    private function category(ObjectManager $manager, string $name): CategoryInterface
    {
        if (isset($this->categoryCache[$name])) {
            return $this->categoryCache[$name];
        }

        $category = $manager->getRepository(CategoryInterface::class)->findOneBy([
            'name'      => $name,
            'deletedAt' => null,
        ]);

        if (!$category instanceof CategoryInterface) {
            throw new RuntimeException(sprintf(
                'Categorie "%s" introuvable : CategoryFixtures doit tourner avant ClientFixtures.',
                $name,
            ));
        }

        return $this->categoryCache[$name] = $category;
    }

    /**
     * Resout un site par son nom via le contrat Shared SiteProviderInterface,
     * sans importer le module Sites (regle n°1). Mise en cache par nom.
     */
    private function site(string $name): SiteInterface
    {
        if (isset($this->siteCache[$name])) {
            return $this->siteCache[$name];
        }

        $site = $this->siteProvider->findByName($name);

        if (!$site instanceof SiteInterface) {
            throw new RuntimeException(sprintf(
                'Site "%s" introuvable : SitesFixtures doit tourner avant ClientFixtures.',
                $name,
            ));
        }

        return $this->siteCache[$name] = $site;
    }

    private function paymentType(ObjectManager $manager, string $code): PaymentType
    {
        $type = $manager->getRepository(PaymentType::class)->findOneBy(['code' => $code]);

        if (!$type instanceof PaymentType) {
            throw new RuntimeException(sprintf(
                'PaymentType "%s" introuvable : CommercialReferentialFixtures doit tourner avant ClientFixtures.',
                $code,
            ));
        }

        return $type;
    }

    private function bank(ObjectManager $manager, string $code): Bank
    {
        $bank = $manager->getRepository(Bank::class)->findOneBy(['code' => $code]);

        if (!$bank instanceof Bank) {
            throw new RuntimeException(sprintf(
                'Bank "%s" introuvable : CommercialReferentialFixtures doit tourner avant ClientFixtures.',
                $code,
            ));
        }

        return $bank;
    }
}
