<?php

declare(strict_types=1);

namespace App\Module\Core\Infrastructure\Console;

use App\Module\Core\Domain\Entity\Permission;
use App\Module\Core\Domain\Repository\PermissionRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Throwable;

use function count;

#[AsCommand(
    name: 'app:sync-permissions',
    description: 'Synchronise les permissions RBAC declarees par les modules actifs.',
)]
final class SyncPermissionsCommand extends Command
{
    public function __construct(
        private readonly EntityManagerInterface $em,
        private readonly PermissionRepositoryInterface $permissionRepository,
        #[Autowire(param: 'kernel.project_dir')]
        private readonly string $projectDir,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        try {
            // Etape 1 : scan + validation stricte des modules actifs AVANT
            // tout acces en ecriture a la base, afin qu'une erreur de
            // declaration laisse la table `permission` intacte.
            $desiredPermissions = $this->collectDesiredPermissions();
        } catch (InvalidArgumentException $e) {
            $io->error($e->getMessage());

            return Command::FAILURE;
        }

        // Etape 2 : upsert transactionnel non destructif.
        $this->em->beginTransaction();

        try {
            // Indexation des permissions existantes par code pour un acces O(1).
            $existingByCode = [];
            foreach ($this->permissionRepository->findAll() as $permission) {
                $existingByCode[$permission->getCode()] = $permission;
            }

            $added   = 0;
            $updated = 0;
            $orphans = 0;

            // Upsert : chaque entree desiree est creee, revivee ou mise a jour.
            foreach ($desiredPermissions as $code => $entry) {
                $label  = $entry['label'];
                $module = $entry['module'];

                if (isset($existingByCode[$code])) {
                    $existing = $existingByCode[$code];

                    if ($existing->isOrphan()) {
                        // Revival : le code reapparait dans le source, on
                        // rafraichit ses metadonnees et on retire le flag.
                        $existing->revive($label, $module);
                        ++$updated;
                    } elseif ($existing->getLabel() !== $label || $existing->getModule() !== $module) {
                        // Mise a jour des metadonnees sans toucher au flag orphan.
                        $existing->updateMetadata($label, $module);
                        ++$updated;
                    }
                // Sinon : strictement identique, no-op.
                } else {
                    // Creation : on persiste directement via l'EM pour ne
                    // pas declencher un flush par appel (cf. save() repo).
                    $permission = new Permission($code, $label, $module);
                    $this->em->persist($permission);
                    ++$added;
                }
            }

            // Etape 3 : marquage orphelin des permissions absentes du source.
            foreach ($existingByCode as $code => $existing) {
                if (isset($desiredPermissions[$code])) {
                    continue;
                }

                if (!$existing->isOrphan()) {
                    $existing->markOrphan();
                    ++$orphans;
                }
            }

            // Un unique flush regroupe toutes les mutations de la transaction.
            $this->em->flush();
            $this->em->commit();
        } catch (Throwable $e) {
            $this->em->rollback();
            $io->error(sprintf('Echec de la synchronisation des permissions : %s', $e->getMessage()));

            return Command::FAILURE;
        }

        $totalInDb = count($this->permissionRepository->findAll());

        $io->success('Synchronisation des permissions RBAC terminee.');
        $io->table(
            ['Indicateur', 'Valeur'],
            [
                ['Permissions ajoutees', (string) $added],
                ['Permissions mises a jour ou revivees', (string) $updated],
                ['Permissions marquees orphelines', (string) $orphans],
                ['Total en base apres sync', (string) $totalInDb],
            ],
        );

        return Command::SUCCESS;
    }

    /**
     * Parcourt la liste des modules actifs declares dans `config/modules.php`,
     * extrait leurs permissions statiques, valide strictement chaque entree
     * puis renvoie une map indexee par code.
     *
     * Regles de validation appliquees :
     *  - chaque entree doit posseder exactement les cles `code` et `label`
     *  - le `code` doit etre prefixe par `<ModuleClass>::ID . '.'`
     *  - `code` et `label` ne peuvent pas etre des chaines vides
     *
     * Les modules ne definissant pas de methode statique `permissions()` sont
     * ignores silencieusement (compat ascendante pour les modules legacy).
     *
     * @return array<string, array{code: string, label: string, module: string}>
     */
    private function collectDesiredPermissions(): array
    {
        /** @var array<int, class-string> $moduleClasses */
        $moduleClasses = require $this->projectDir.'/config/modules.php';

        $desired = [];

        foreach ($moduleClasses as $moduleClass) {
            if (!method_exists($moduleClass, 'permissions')) {
                continue;
            }

            /** @var array<int, array<string, string>> $entries */
            $entries  = $moduleClass::permissions();
            $moduleId = $moduleClass::ID;

            foreach ($entries as $entry) {
                $keys = array_keys($entry);
                sort($keys);
                if (['code', 'label'] !== $keys) {
                    throw new InvalidArgumentException(sprintf(
                        'Permission malformee declaree par %s : chaque entree doit contenir exactement les cles [code, label], recu [%s].',
                        $moduleClass,
                        implode(', ', array_keys($entry)),
                    ));
                }

                $code  = $entry['code'];
                $label = $entry['label'];

                if ('' === $code) {
                    throw new InvalidArgumentException(sprintf(
                        'Permission invalide declaree par %s : le code ne peut pas etre vide.',
                        $moduleClass,
                    ));
                }
                if ('' === $label) {
                    throw new InvalidArgumentException(sprintf(
                        'Permission invalide declaree par %s (code "%s") : le libelle ne peut pas etre vide.',
                        $moduleClass,
                        $code,
                    ));
                }

                $expectedPrefix = $moduleId.'.';
                if (!str_starts_with($code, $expectedPrefix)) {
                    throw new InvalidArgumentException(sprintf(
                        'Permission invalide declaree par %s : le code "%s" doit etre prefixe par "%s" (ID du module).',
                        $moduleClass,
                        $code,
                        $expectedPrefix,
                    ));
                }

                $desired[$code] = [
                    'code'   => $code,
                    'label'  => $label,
                    'module' => $moduleId,
                ];
            }
        }

        return $desired;
    }
}
