<?php

declare(strict_types=1);

namespace App\Module\Commercial\Infrastructure\Controller;

use App\Module\Commercial\Domain\Entity\Supplier;
use App\Module\Commercial\Domain\Entity\SupplierContact;
use App\Module\Commercial\Domain\Repository\SupplierRepositoryInterface;
use App\Shared\Domain\Contract\CategoryInterface;
use App\Shared\Domain\Contract\SiteInterface;
use App\Shared\Domain\Contract\SpreadsheetExporterInterface;
use DateTimeImmutable;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

/**
 * Export XLSX du repertoire fournisseurs (M2, spec-back § 4.6). Jumeau du
 * {@see ClientExportController} (M1).
 *
 * Controller Symfony custom (et non operation API Platform) car il produit un
 * binaire de fichier, pas une representation Hydra. `priority: 1` est
 * OBLIGATOIRE sur la route : sans cela API Platform capterait
 * `/api/suppliers/export.xlsx` comme l'item `GET /api/suppliers/{id}.{_format}`
 * (id="export", _format="xlsx") — cf. CLAUDE.md « controller custom sous /api ».
 *
 * Separation des responsabilites :
 *  - le COMMENT (generation du fichier) est delegue au service Shared
 *    {@see SpreadsheetExporterInterface} — generique, reutilisable, sans metier ;
 *  - le QUOI vit ICI : selection des fournisseurs (memes filtres que
 *    `GET /api/suppliers`, via {@see SupplierRepositoryInterface::createListQueryBuilder()})
 *    et mapping metier des colonnes.
 *
 * Colonnes de contact : depuis la suppression du contact inline (ERP-106), elles
 * sont alimentees par le CONTACT PRINCIPAL du fournisseur — le SupplierContact de
 * plus petit `position` (decision D2, spec § 4.6).
 *
 * La colonne SIREN n'est ajoutee que si l'utilisateur a la permission
 * `commercial.suppliers.accounting.view` (gating identique a la lecture).
 */
#[AsController]
final class SupplierExportController
{
    public function __construct(
        #[Autowire(service: 'App\Module\Commercial\Infrastructure\Doctrine\DoctrineSupplierRepository')]
        private readonly SupplierRepositoryInterface $repository,
        private readonly SpreadsheetExporterInterface $exporter,
        private readonly Security $security,
    ) {}

    #[Route('/api/suppliers/export.xlsx', name: 'commercial_suppliers_export_xlsx', methods: ['GET'], priority: 1)]
    #[IsGranted('commercial.suppliers.view')]
    public function __invoke(Request $request): Response
    {
        // Memes filtres d'archivage que la vue liste (SupplierProvider) pour que
        // l'export reflete exactement ce que l'utilisateur voit a l'ecran :
        //  - includeArchived : inclut les archives en plus des actifs ;
        //  - archivedOnly    : restreint aux seules archives (prioritaire, cf.
        //    createListQueryBuilder).
        $includeArchived = $this->readBool($request->query->get('includeArchived'));
        $archivedOnly    = $this->readBool($request->query->get('archivedOnly'));
        $search          = $request->query->getString('search') ?: null;

        // Memes filtres que la vue liste : categoryCode/siteId tolerent une valeur
        // unique ou une liste (?categoryCode[]=A&siteId[]=1). On lit via all() pour
        // ne pas lever d'exception sur une valeur scalaire.
        $query         = $request->query->all();
        $categoryCodes = $this->readStringList($query['categoryCode'] ?? []);
        $siteIds       = $this->readIntList($query['siteId'] ?? []);

        /** @var list<Supplier> $suppliers */
        $suppliers = $this->repository
            ->createListQueryBuilder($includeArchived, $search, $categoryCodes, $siteIds, $archivedOnly)
            ->getQuery()
            ->getResult()
        ;

        // Hydratation batchee des collections affichees (§ 2.12) : le QB de
        // selection ne fetch-join pas les to-many. On remplit categories + sites en
        // lot (colonnes « Catégories » / « Sites »), puis les contacts (colonnes du
        // contact principal) — chacune en requetes IN bornees, anti N+1.
        $this->repository->hydrateListCollections($suppliers);
        $this->repository->hydrateContacts($suppliers);

        $withSiren = $this->security->isGranted('commercial.suppliers.accounting.view');

        $binary = $this->exporter->export(
            'Répertoire fournisseurs',
            $this->buildHeaders($withSiren),
            $this->buildRows($suppliers, $withSiren),
        );

        return $this->buildResponse($binary);
    }

    /**
     * Colonnes de l'export (spec § 4.6). SIREN inseree avant la date de creation,
     * uniquement si l'utilisateur a accounting.view.
     *
     * @return list<string>
     */
    private function buildHeaders(bool $withSiren): array
    {
        $headers = [
            'Nom fournisseur',
            'Contact principal',
            'Téléphone principal',
            'Téléphone secondaire',
            'Email',
            'Catégories',
            'Sites',
        ];

        if ($withSiren) {
            $headers[] = 'SIREN';
        }

        $headers[] = 'Date de création';

        return $headers;
    }

    /**
     * @param list<Supplier> $suppliers
     *
     * @return iterable<list<null|scalar>>
     */
    private function buildRows(array $suppliers, bool $withSiren): iterable
    {
        foreach ($suppliers as $supplier) {
            $contact = $this->principalContact($supplier);

            $row = [
                $supplier->getCompanyName(),
                null !== $contact ? $this->formatContactName($contact) : '',
                $contact?->getPhonePrimary() ?? '',
                $contact?->getPhoneSecondary() ?? '',
                $contact?->getEmail() ?? '',
                $this->formatCategories($supplier),
                $this->formatSites($supplier),
            ];

            if ($withSiren) {
                $row[] = $supplier->getSiren();
            }

            $row[] = $supplier->getCreatedAt()?->format('d/m/Y');

            yield $row;
        }
    }

    /**
     * Contact principal du fournisseur : le SupplierContact de plus petit
     * `position` (decision D2, spec § 4.6). Null si le fournisseur n'a aucun
     * contact (les colonnes contact restent vides).
     */
    private function principalContact(Supplier $supplier): ?SupplierContact
    {
        $contacts = $supplier->getContacts()->toArray();
        if ([] === $contacts) {
            return null;
        }

        usort(
            $contacts,
            static fn (SupplierContact $a, SupplierContact $b): int => $a->getPosition() <=> $b->getPosition(),
        );

        return $contacts[0];
    }

    /**
     * Libelle du contact principal « Nom Prénom » (spec § 4.6). Les deux parties
     * sont optionnelles (RG-2.04 : au moins l'une des deux), d'ou le trim final.
     */
    private function formatContactName(SupplierContact $contact): string
    {
        return trim(sprintf('%s %s', $contact->getLastName() ?? '', $contact->getFirstName() ?? ''));
    }

    /**
     * Libelles des categories du fournisseur, dedupliques, tries, joints par
     * virgule.
     */
    private function formatCategories(Supplier $supplier): string
    {
        $names = [];
        foreach ($supplier->getCategories() as $category) {
            // @var CategoryInterface $category
            $name = $category->getName();
            if (null !== $name && '' !== $name) {
                $names[$name] = true;
            }
        }

        return $this->joinSorted($names);
    }

    /**
     * Le fournisseur ne porte pas de sites en propre : ils sont rattaches aux
     * adresses (RG-2.06). La colonne « Sites » agrege donc l'union distincte des
     * sites de toutes les adresses du fournisseur.
     */
    private function formatSites(Supplier $supplier): string
    {
        $names = [];
        foreach ($supplier->getAddresses() as $address) {
            foreach ($address->getSites() as $site) {
                // @var SiteInterface $site
                $name = $site->getName();
                if (null !== $name && '' !== $name) {
                    $names[$name] = true;
                }
            }
        }

        return $this->joinSorted($names);
    }

    /**
     * @param array<string, true> $names ensemble de libelles (cles)
     */
    private function joinSorted(array $names): string
    {
        $list = array_keys($names);
        sort($list);

        return implode(', ', $list);
    }

    private function buildResponse(string $binary): Response
    {
        $filename = sprintf('repertoire-fournisseurs-%s.xlsx', new DateTimeImmutable()->format('Ymd'));

        $response = new Response($binary);
        $response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
        $response->headers->set('Content-Disposition', sprintf('attachment; filename="%s"', $filename));

        return $response;
    }

    /**
     * Lit un flag booleen issu des query params. Accepte true / "true" / "1".
     * Aligne sur SupplierProvider pour un comportement identique a la liste.
     */
    private function readBool(mixed $raw): bool
    {
        return is_string($raw) && in_array(strtolower($raw), ['true', '1'], true);
    }

    /**
     * Normalise un filtre en liste de chaines (valeur unique ou liste).
     * Aligne sur SupplierProvider pour un comportement identique a la liste.
     *
     * @return list<string>
     */
    private function readStringList(mixed $raw): array
    {
        $values = is_array($raw) ? $raw : [$raw];

        $out = [];
        foreach ($values as $value) {
            if (is_string($value) && '' !== trim($value)) {
                $out[] = trim($value);
            }
        }

        return $out;
    }

    /**
     * Normalise un filtre en liste d'identifiants entiers positifs (valeur unique
     * ou liste). Aligne sur SupplierProvider.
     *
     * @return list<int>
     */
    private function readIntList(mixed $raw): array
    {
        $values = is_array($raw) ? $raw : [$raw];

        $out = [];
        foreach ($values as $value) {
            if ((is_int($value) || (is_string($value) && ctype_digit($value))) && (int) $value > 0) {
                $out[] = (int) $value;
            }
        }

        return $out;
    }
}
