<?php

namespace App\Command;

use App\Entity\BilletBuyer;
use App\Entity\Event;
use App\Entity\User;
use App\Service\EventIndexService;
use App\Service\MeilisearchService;
use App\Service\OrderIndexService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
 * @codeCoverageIgnore Integration with Meilisearch + DB
 */
#[AsCommand(
    name: 'app:meilisearch:check-consistency',
    description: 'Check and fix Meilisearch index consistency with the database',
)]
class MeilisearchConsistencyCommand extends Command
{
    private const INDEX_MISSING_MSG = '  [%s] Index missing';
    private const INDEXES_ENDPOINT = '/indexes';

    public function __construct(
        private MeilisearchService $meilisearch,
        private EntityManagerInterface $em,
        private EventIndexService $eventIndex,
        private OrderIndexService $orderIndex,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this->addOption('fix', null, InputOption::VALUE_NONE, 'Fix inconsistencies (re-index missing, delete orphans)');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $fix = $input->getOption('fix');
        $totalOrphans = 0;
        $totalMissing = 0;

        try {
            $this->meilisearch->listIndexes();
        } catch (\Throwable $e) {
            $io->error('Meilisearch unreachable: '.$e->getMessage());

            return Command::FAILURE;
        }

        [$totalOrphans, $totalMissing] = $this->checkAllIndexes($fix, $io);

        $this->reportSummary($io, $totalOrphans, $totalMissing, $fix);

        return Command::SUCCESS;
    }

    /**
     * @return array{int, int}
     */
    private function checkAllIndexes(bool $fix, SymfonyStyle $io): array
    {
        $totals = [0, 0];

        $io->section('Event indexes');
        $this->accumulate($totals, $this->checkEventIndex('event_global', $fix, $io));
        $this->accumulate($totals, $this->checkEventIndex('event_admin', $fix, $io));

        $organizers = array_filter(
            $this->em->getRepository(User::class)->findAll(),
            fn (User $u) => \in_array('ROLE_ORGANIZER', $u->getRoles(), true),
        );
        foreach ($organizers as $user) {
            $this->accumulate($totals, $this->checkEventIndex('event_'.$user->getId(), $fix, $io, $user->getId()));
        }

        $io->section('Order indexes');
        foreach ($this->em->getRepository(Event::class)->findAll() as $event) {
            $this->accumulate($totals, $this->checkOrderIndex($event, $fix, $io));
        }

        $io->section('User indexes');
        $this->accumulate($totals, $this->checkUserIndex('buyers', false, $fix, $io));
        $this->accumulate($totals, $this->checkUserIndex('organizers', true, $fix, $io));

        return $totals;
    }

    /**
     * @param array{int, int} $totals
     * @param array{int, int} $result
     */
    private function accumulate(array &$totals, array $result): void
    {
        $totals[0] += $result[0];
        $totals[1] += $result[1];
    }

    private function reportSummary(SymfonyStyle $io, int $totalOrphans, int $totalMissing, bool $fix): void
    {
        if (0 === $totalOrphans && 0 === $totalMissing) {
            $io->success('All indexes are consistent.');

            return;
        }

        $msg = sprintf('%d orphan(s), %d missing document(s).', $totalOrphans, $totalMissing);
        if ($fix) {
            $io->success('Fixed: '.$msg);
        } else {
            $io->warning($msg.' Run with --fix to repair.');
        }
    }

    /**
     * @param list<int|string> $meiliIds
     * @param list<int|string> $dbIds
     *
     * @return array{int, int}
     */
    private function diffAndReport(string $index, array $meiliIds, array $dbIds, bool $fix, SymfonyStyle $io, \Closure $fixMissing): array
    {
        $orphans = array_diff($meiliIds, $dbIds);
        $missing = array_diff($dbIds, $meiliIds);

        if (\count($orphans) > 0) {
            $io->text(sprintf('  [%s] %d orphan(s)', $index, \count($orphans)));
            if ($fix) {
                $this->meilisearch->deleteDocuments($index, array_values($orphans));
            }
        }

        if (\count($missing) > 0) {
            $io->text(sprintf('  [%s] %d missing', $index, \count($missing)));
            if ($fix) {
                $fixMissing($missing);
            }
        }

        if (0 === \count($orphans) && 0 === \count($missing)) {
            $io->text(sprintf('  [%s] OK (%d documents)', $index, \count($meiliIds)));
        }

        return [\count($orphans), \count($missing)];
    }

    /**
     * @return array{int, int}
     */
    private function checkEventIndex(string $index, bool $fix, SymfonyStyle $io, ?int $accountId = null): array
    {
        $indexExists = $this->meilisearch->indexExists($index);

        if (!$indexExists && !$fix) {
            $io->text(sprintf(self::INDEX_MISSING_MSG, $index));

            return [0, 0];
        }

        if ('event_global' === $index) {
            $dbEvents = $this->em->getRepository(Event::class)->findBy(['isOnline' => true, 'isSecret' => false]);
        } elseif (null !== $accountId) {
            $dbEvents = $this->em->getRepository(Event::class)
                ->createQueryBuilder('e')
                ->join('e.account', 'a')
                ->where('a.id = :aid')
                ->setParameter('aid', $accountId)
                ->getQuery()
                ->getResult();
        } else {
            $dbEvents = $this->em->getRepository(Event::class)->findAll();
        }

        if (!$indexExists) {
            $this->meilisearch->request('POST', self::INDEXES_ENDPOINT, ['uid' => $index, 'primaryKey' => 'id']);
            $io->text(sprintf('  [%s] Created index, indexing %d event(s)', $index, \count($dbEvents)));
            foreach ($dbEvents as $event) {
                $this->eventIndex->indexEvent($event);
            }

            return [0, \count($dbEvents)];
        }

        $meiliIds = $this->meilisearch->getAllDocumentIds($index);
        $dbIds = array_map(fn (Event $e) => $e->getId(), $dbEvents);

        $eventsById = [];
        foreach ($dbEvents as $e) {
            $eventsById[$e->getId()] = $e;
        }

        return $this->diffAndReport($index, $meiliIds, $dbIds, $fix, $io, function (array $missing) use ($eventsById): void {
            foreach ($missing as $id) {
                if (isset($eventsById[$id])) {
                    $this->eventIndex->indexEvent($eventsById[$id]);
                }
            }
        });
    }

    /**
     * @return array{int, int}
     */
    private function checkOrderIndex(Event $event, bool $fix, SymfonyStyle $io): array
    {
        $index = 'order_event_'.$event->getId();
        $indexExists = $this->meilisearch->indexExists($index);

        if (!$indexExists && !$fix) {
            $io->text(sprintf(self::INDEX_MISSING_MSG, $index));

            return [0, 0];
        }

        $dbOrders = $this->em->getRepository(BilletBuyer::class)->findBy(['event' => $event]);

        if (!$indexExists) {
            $this->meilisearch->request('POST', self::INDEXES_ENDPOINT, ['uid' => $index, 'primaryKey' => 'id']);
            $io->text(sprintf('  [%s] Created index, indexing %d order(s)', $index, \count($dbOrders)));
            foreach ($dbOrders as $order) {
                $this->orderIndex->indexOrder($order);
            }

            return [0, \count($dbOrders)];
        }

        $meiliIds = $this->meilisearch->getAllDocumentIds($index);
        $dbIds = array_map(fn (BilletBuyer $o) => $o->getId(), $dbOrders);

        $ordersById = [];
        foreach ($dbOrders as $o) {
            $ordersById[$o->getId()] = $o;
        }

        return $this->diffAndReport($index, $meiliIds, $dbIds, $fix, $io, function (array $missing) use ($ordersById): void {
            foreach ($missing as $id) {
                if (isset($ordersById[$id])) {
                    $this->orderIndex->indexOrder($ordersById[$id]);
                }
            }
        });
    }

    /**
     * @return array{int, int}
     */
    private function checkUserIndex(string $index, bool $isOrganizer, bool $fix, SymfonyStyle $io): array
    {
        $indexExists = $this->meilisearch->indexExists($index);

        if (!$indexExists && !$fix) {
            $io->text(sprintf(self::INDEX_MISSING_MSG, $index));

            return [0, 0];
        }

        $dbUsers = $this->filterUsers($isOrganizer);

        if (!$indexExists) {
            return $this->createUserIndex($index, $dbUsers, $isOrganizer, $io);
        }

        $meiliIds = $this->meilisearch->getAllDocumentIds($index);
        $dbIds = array_map(fn (User $u) => $u->getId(), $dbUsers);

        $usersById = [];
        foreach ($dbUsers as $u) {
            $usersById[$u->getId()] = $u;
        }

        return $this->diffAndReport($index, $meiliIds, $dbIds, $fix, $io, function (array $missing) use ($usersById, $isOrganizer, $index): void {
            $docs = array_filter(array_map(
                fn (int|string $id) => isset($usersById[$id]) ? $this->userToDocument($usersById[$id], $isOrganizer) : null,
                $missing,
            ));
            if ([] !== $docs) {
                $this->meilisearch->addDocuments($index, array_values($docs));
            }
        });
    }

    /**
     * @return list<User>
     */
    private function filterUsers(bool $isOrganizer): array
    {
        $allUsers = $this->em->getRepository(User::class)->findAll();

        if ($isOrganizer) {
            return array_values(array_filter($allUsers, fn (User $u) => $u->isApproved() && \in_array('ROLE_ORGANIZER', $u->getRoles(), true)));
        }

        return array_values(array_filter($allUsers, fn (User $u) => $u->isVerified() && !\in_array('ROLE_ORGANIZER', $u->getRoles(), true) && !\in_array('ROLE_ROOT', $u->getRoles(), true)));
    }

    /**
     * @param list<User> $dbUsers
     *
     * @return array{int, int}
     */
    private function createUserIndex(string $index, array $dbUsers, bool $isOrganizer, SymfonyStyle $io): array
    {
        $this->meilisearch->request('POST', self::INDEXES_ENDPOINT, ['uid' => $index, 'primaryKey' => 'id']);
        $docs = array_map(fn (User $u) => $this->userToDocument($u, $isOrganizer), $dbUsers);
        $io->text(sprintf('  [%s] Created index, indexing %d user(s)', $index, \count($docs)));
        if ([] !== $docs) {
            $this->meilisearch->addDocuments($index, $docs);
        }

        return [0, \count($docs)];
    }

    /**
     * @return array<string, mixed>
     */
    private function userToDocument(User $u, bool $isOrganizer): array
    {
        $doc = [
            'id' => $u->getId(),
            'firstName' => $u->getFirstName(),
            'lastName' => $u->getLastName(),
            'email' => $u->getEmail(),
        ];

        if ($isOrganizer) {
            $doc['companyName'] = $u->getCompanyName();
            $doc['siret'] = $u->getSiret();
            $doc['city'] = $u->getCity();
        } else {
            $doc['createdAt'] = $u->getCreatedAt()->format('d/m/Y');
        }

        return $doc;
    }
}
