<?php

declare(strict_types=1);

namespace App\Module\Technique\Infrastructure\DataFixtures;

use App\Module\Catalog\Infrastructure\DataFixtures\CategoryFixtures;
use App\Module\Commercial\Domain\Entity\Bank;
use App\Module\Commercial\Domain\Entity\PaymentDelay;
use App\Module\Commercial\Domain\Entity\PaymentType;
use App\Module\Commercial\Domain\Entity\TvaMode;
use App\Module\Commercial\Infrastructure\DataFixtures\CommercialReferentialFixtures;
use App\Module\Sites\Infrastructure\DataFixtures\SitesFixtures;
use App\Module\Technique\Application\Service\ProviderFieldNormalizer;
use App\Module\Technique\Domain\Entity\Provider;
use App\Module\Technique\Domain\Entity\ProviderAddress;
use App\Module\Technique\Domain\Entity\ProviderContact;
use App\Module\Technique\Domain\Entity\ProviderRib;
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 Technique : prestataires de demonstration couvrant
 * les cas metier RG-3.xx du repertoire prestataires (M3), jumelles des fixtures
 * fournisseurs (M2). Theme : prestations techniques (maintenance, nettoyage,
 * transport).
 *
 * Cas pivots couverts (§ 8.4) :
 *  - prestataire COMPLET : >= 1 site sur le formulaire principal (RG-3.03), >= 1
 *    contact, >= 1 adresse multi-sites (RG-3.05), comptabilite + RIB ;
 *  - reglement LCR avec RIB (RG-3.08) ; reglement VIREMENT avec banque (RG-3.07) ;
 *  - 1 prestataire archive (isArchived + archivedAt) pour l'exclusion de la liste
 *    (RG-3.16) ;
 *  - prestataires repartis sur des sites DIFFERENTS (86 / 17 / 82) pour exercer le
 *    cloisonnement par site (RG-3.17) ;
 *  - mono et multi-categories de type PRESTATAIRE (RG-3.09).
 *
 * Resolution inter-modules conforme a la regle n°1 (pas d'import de logique) :
 *  - categories resolues via le contrat Shared CategoryInterface ;
 *  - sites resolus via le contrat Shared SiteProviderInterface.
 *
 * Normalisation : valeurs fournies BRUTES, normalisees par ProviderFieldNormalizer
 * avant persist, exactement comme le ferait le ProviderProcessor via l'API
 * (companyName UPPERCASE, first/last Capitalize, telephones chiffres seuls, emails
 * lowercase — RG-3.11).
 *
 * Idempotence : lookup par companyName normalise (coherent avec l'index unique
 * partiel uq_provider_company_name_active). Un prestataire deja present n'est pas
 * reconstruit (sous-collections non redupliquees). Rejouable sans doublon.
 *
 * Portee : DONNEES DE DEMONSTRATION (dev uniquement). En environnement `test`, la
 * fixture ne charge rien : les tests seedent et nettoient leurs propres
 * prestataires et comptent sur une table `provider` vierge. Meme garde-fou que
 * SupplierFixtures / CategoryFixtures.
 */
class ProviderFixtures extends Fixture implements DependentFixtureInterface
{
    /**
     * Type de categorie exige pour un prestataire et ses adresses (RG-3.09).
     * Miroir de Provider::REQUIRED_CATEGORY_TYPE_CODE (non importable — regle n°1).
     */
    private const string PROVIDER_CATEGORY_TYPE_CODE = 'PRESTATAIRE';

    /** Cache des categories resolues par nom. */
    private array $categoryCache = [];

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

    /** ObjectManager courant, capture en debut de load. */
    private ObjectManager $manager;

    public function __construct(
        private readonly ProviderFieldNormalizer $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;

        // === Prestataire COMPLET — VIREMENT + banque (RG-3.07), compta + RIB, ===
        // === multi-sites sur le formulaire principal ET sur l'adresse.        ===
        [$maintenance, $isNew] = $this->ensureProvider($manager, 'Maintenance Pro SAS', ['Maintenance industrielle'], ['Chatellerault', 'Saint-Jean']);
        if ($isNew) {
            $maintenance->setSiren('841611054');
            $maintenance->setAccountNumber('P0001');
            $maintenance->setTvaMode($this->tvaMode($manager, 'FRANCE_VENTES'));
            $maintenance->setNTva('FR12841611054');
            $maintenance->setPaymentDelay($this->paymentDelay($manager, 'J30'));
            $maintenance->setPaymentType($this->paymentType($manager, 'VIREMENT'));
            $maintenance->setBank($this->bank($manager, 'SG'));
            $this->addContact($maintenance, 'Marie', 'Martin', 'Responsable', '05 49 00 00 01', null, 'marie.martin@maintenance-pro.fr');
            $this->addAddress($maintenance, ['Chatellerault', 'Saint-Jean'], '86000', 'Poitiers', '12 rue des Acacias', categoryNames: ['Maintenance industrielle']);
            $this->addRib($maintenance, 'Compte principal', 'BNPAFRPPXXX', 'FR1420041010050500013M02606', 0);
        }

        // === LCR avec RIB (RG-3.08) — site Pommevic ===
        [$nettoyage, $isNew] = $this->ensureProvider($manager, 'Nettoyage Sud-Ouest', ['Nettoyage'], ['Pommevic']);
        if ($isNew) {
            $nettoyage->setSiren('775680459');
            $nettoyage->setTvaMode($this->tvaMode($manager, 'FRANCE_VENTES'));
            $nettoyage->setPaymentDelay($this->paymentDelay($manager, 'J15'));
            $nettoyage->setPaymentType($this->paymentType($manager, 'LCR'));
            $this->addContact($nettoyage, 'Sophie', 'Marchand', 'Directrice', '05 56 10 20 30', '06 11 22 33 44', 'sophie.marchand@nettoyage-so.fr', 0);
            $this->addContact($nettoyage, 'Marc', 'Girard', 'Chef d\'equipe', '05 56 10 20 31', null, 'marc.girard@nettoyage-so.fr', 1);
            $this->addAddress($nettoyage, ['Pommevic'], '82400', 'Pommevic', '8 route des Prestations');
            $this->addRib($nettoyage, 'Compte principal', 'BNPAFRPPXXX', 'FR7630006000011234567890189', 0);
        }

        // === Multi-categories PRESTATAIRE + reglement CHEQUE (sans banque ni RIB) ===
        [$transport, $isNew] = $this->ensureProvider($manager, 'Transport Express Atlantique', ['Transport', 'Maintenance industrielle'], ['Saint-Jean']);
        if ($isNew) {
            $transport->setPaymentDelay($this->paymentDelay($manager, 'A_RECEPTION'));
            $transport->setPaymentType($this->paymentType($manager, 'CHEQUE'));
            $this->addContact($transport, 'Thomas', 'Petit', 'Responsable logistique', '05 56 31 32 33', null, 'thomas.petit@transport-express.fr');
            $this->addAddress($transport, ['Saint-Jean'], '17400', 'Fontenet', '4 zone des Transporteurs', categoryNames: ['Transport']);
        }

        // === Prestataire minimal — contact par le seul nom (RG-3.04), site 86 ===
        [$petit, $isNew] = $this->ensureProvider($manager, 'Atelier Soudure Locale', ['Maintenance industrielle'], ['Chatellerault']);
        if ($isNew) {
            $this->addContact($petit, null, 'Caron', 'Gerant', '05 49 81 82 83', null, 'contact@atelier-soudure.fr');
            $this->addAddress($petit, ['Chatellerault'], '86100', 'Châtellerault', '6 chemin de l\'Atelier');
        }

        // === Prestataire archive (RG-3.16) ===
        [$ancien, $isNew] = $this->ensureProvider($manager, 'Ancien Prestataire Ferme', ['Nettoyage'], ['Chatellerault'], isArchived: true);
        if ($isNew) {
            $this->addContact($ancien, null, 'Lambert', 'Ancien contact', '05 49 99 99 99', null, 'contact@ancien-prestataire.fr');
            $this->addAddress($ancien, ['Chatellerault'], '86100', 'Châtellerault', '99 rue Fermée');
        }

        $manager->flush();
    }

    /**
     * Cree un prestataire (base normalisee + categories PRESTATAIRE + sites directs)
     * s'il n'existe pas, sinon retourne l'existant. Retourne [Provider, isNew] :
     * isNew=false bloque la reconstruction des sous-collections (idempotence).
     *
     * @param list<string> $categoryNames categories de type PRESTATAIRE (RG-3.09)
     * @param list<string> $siteNames     sites du formulaire principal (RG-3.03, >= 1)
     *
     * @return array{0: Provider, 1: bool}
     */
    private function ensureProvider(
        ObjectManager $manager,
        string $companyName,
        array $categoryNames,
        array $siteNames,
        bool $isArchived = false,
    ): array {
        $normalizedName = (string) $this->normalizer->normalizeCompanyName($companyName);

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

        $provider = new Provider();
        $provider->setCompanyName($normalizedName);

        foreach ($categoryNames as $categoryName) {
            $provider->addCategory($this->category($manager, $categoryName));
        }
        foreach ($siteNames as $siteName) {
            $provider->addSite($this->site($siteName));
        }

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

        $manager->persist($provider);

        return [$provider, true];
    }

    /**
     * Ajoute un contact normalise au prestataire (cascade persist via
     * Provider.contacts). Au moins un champ est rempli (RG-3.04).
     */
    private function addContact(
        Provider $provider,
        ?string $firstName,
        ?string $lastName,
        ?string $jobTitle,
        ?string $phonePrimary,
        ?string $phoneSecondary,
        ?string $email,
        int $position = 0,
    ): void {
        $contact = new ProviderContact();
        $contact->setProvider($provider);
        $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);

        $provider->addContact($contact);
    }

    /**
     * Ajoute une adresse au prestataire (cascade persist via Provider.addresses).
     * Adresse simplifiee M3 : PAS de addressType / bennes / triageProvider. Au
     * moins un site est rattache (RG-3.05) ; categories d'adresse de type
     * PRESTATAIRE (RG-3.09).
     *
     * @param list<string> $siteNames     au moins un site (RG-3.05)
     * @param list<string> $categoryNames categories de type PRESTATAIRE (RG-3.09)
     */
    private function addAddress(
        Provider $provider,
        array $siteNames,
        string $postalCode,
        string $city,
        string $street,
        ?string $streetComplement = null,
        array $categoryNames = [],
        int $position = 0,
    ): void {
        $address = new ProviderAddress();
        $address->setProvider($provider);
        $address->setCountry('France');
        $address->setPostalCode($postalCode);
        $address->setCity($city);
        $address->setStreet($street);
        $address->setStreetComplement($streetComplement);
        $address->setPosition($position);

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

        $provider->addAddress($address);
    }

    /**
     * Ajoute un RIB au prestataire (cascade persist via Provider.ribs).
     */
    private function addRib(Provider $provider, string $label, string $bic, string $iban, int $position = 0): void
    {
        $rib = new ProviderRib();
        $rib->setProvider($provider);
        $rib->setLabel($label);
        $rib->setBic($bic);
        $rib->setIban($iban);
        $rib->setPosition($position);

        $provider->addRib($rib);
    }

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

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

        foreach ($candidates as $candidate) {
            if ($candidate instanceof CategoryInterface
                && in_array(self::PROVIDER_CATEGORY_TYPE_CODE, $candidate->getCategoryTypeCodes(), true)) {
                return $this->categoryCache[$name] = $candidate;
            }
        }

        throw new RuntimeException(sprintf(
            'Categorie PRESTATAIRE "%s" introuvable : CategoryFixtures doit tourner avant ProviderFixtures.',
            $name,
        ));
    }

    /**
     * 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 ProviderFixtures.',
                $name,
            ));
        }

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

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

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

        return $mode;
    }

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

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

        return $delay;
    }

    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 ProviderFixtures.',
                $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 ProviderFixtures.',
                $code,
            ));
        }

        return $bank;
    }
}
