<?php

declare(strict_types=1);

namespace App\Module\Commercial\Infrastructure\Controller;

use App\Module\Commercial\Domain\Entity\Client;
use App\Module\Commercial\Domain\Repository\ClientRepositoryInterface;
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 clients (M1, spec-back § 4.6).
 *
 * 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/clients/export.xlsx` comme l'item `GET /api/clients/{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 clients (memes filtres que
 *    `GET /api/clients`, via {@see ClientRepositoryInterface::createListQueryBuilder()})
 *    et mapping metier des colonnes.
 *
 * La colonne SIREN n'est ajoutee que si l'utilisateur a la permission
 * `commercial.clients.accounting.view` (gating identique a la lecture).
 */
#[AsController]
final class ClientExportController
{
    public function __construct(
        #[Autowire(service: 'App\Module\Commercial\Infrastructure\Doctrine\DoctrineClientRepository')]
        private readonly ClientRepositoryInterface $repository,
        private readonly SpreadsheetExporterInterface $exporter,
        private readonly Security $security,
    ) {}

    #[Route('/api/clients/export.xlsx', name: 'commercial_clients_export_xlsx', methods: ['GET'], priority: 1)]
    #[IsGranted('commercial.clients.view')]
    public function __invoke(Request $request): Response
    {
        $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<Client> $clients */
        $clients = $this->repository
            ->createListQueryBuilder($includeArchived, $search, $categoryCodes, $siteIds, $archivedOnly)
            ->getQuery()
            ->getResult()
        ;

        // Hydratation batchee des categories + adresses/sites (ERP-100) : le QB de
        // selection ne fetch-join plus, on remplit les collections en 2 requetes
        // IN bornees plutot que d'hydrater un produit cartesien sur tout le jeu.
        $this->repository->hydrateListCollections($clients);

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

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

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

    /**
     * Colonnes de l'export. Depuis la suppression du contact inline (refonte
     * contact, D2), les colonnes de contact principal sont retirees : l'export
     * ne porte plus que les donnees propres au Client. 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 entreprise',
            'Catégories',
            'Sites',
        ];

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

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

        return $headers;
    }

    /**
     * @param list<Client> $clients
     *
     * @return iterable<list<null|scalar>>
     */
    private function buildRows(array $clients, bool $withSiren): iterable
    {
        foreach ($clients as $client) {
            $row = [
                $client->getCompanyName(),
                $this->formatCategories($client),
                $this->formatSites($client),
            ];

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

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

            yield $row;
        }
    }

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

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

    /**
     * Le Client ne porte pas de sites en propre : ils sont rattaches aux
     * adresses (RG-1.10). La colonne « Sites » agrege donc l'union distincte des
     * sites de toutes les adresses du client (decision validee 01/06).
     */
    private function formatSites(Client $client): string
    {
        $names = [];
        foreach ($client->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-clients-%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 ClientProvider 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 ClientProvider 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 ClientProvider.
     *
     * @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;
    }
}
