<?php

declare(strict_types=1);

/*
 * This file is part of the VITICORE2 package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CpCreation\VitiCore\Bridge\Symfony\Bundle\Command;

use CpCreation\VitiCore\Bridge\Symfony\Bundle\Mailer\AbandonedCartReminderMailer;
use CpCreation\VitiCore\Cart\CartService;
use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Repository\CartRepository;
use CpCreation\VitiCore\Marketing\AbandonedCartReminderIssuer;
use CpCreation\VitiCore\Marketing\AbandonedCartRuleMatcher;
use CpCreation\VitiCore\Marketing\AbandonedCartRuleSelector;
use CpCreation\VitiCore\Marketing\Model\AbandonedCartConfiguration;
use CpCreation\VitiCore\Marketing\Model\AbandonedCartReminder;
use CpCreation\VitiCore\Marketing\Model\AbandonedCartRule;
use CpCreation\VitiCore\Marketing\Repository\AbandonedCartConfigurationRepository;
use CpCreation\VitiCore\Marketing\Repository\AbandonedCartReminderRepository;
use CpCreation\VitiCore\Marketing\Repository\AbandonedCartRuleRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'viti-core:marketing:test-abandoned-cart',
    description: 'Teste les règles de relance sur le dernier panier abandonné ou sur un panier précis.'
)]
final class TestAbandonedCartReminderCommand extends Command
{
    public function __construct(
        private readonly CartRepository $cartRepository,
        private readonly AbandonedCartConfigurationRepository $configurationRepository,
        private readonly AbandonedCartRuleRepository $ruleRepository,
        private readonly AbandonedCartReminderRepository $reminderRepository,
        private readonly AbandonedCartRuleMatcher $ruleMatcher,
        private readonly AbandonedCartRuleSelector $ruleSelector,
        private readonly AbandonedCartReminderIssuer $reminderIssuer,
        private readonly AbandonedCartReminderMailer $mailer,
        private readonly CartService $cartService,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addArgument('cart', InputArgument::OPTIONAL, 'UUID ou référence du panier. Le dernier panier abandonné est utilisé si omis.')
            ->addOption('rule', null, InputOption::VALUE_REQUIRED, 'UUID de la règle à tester ou à envoyer.')
            ->addOption('send-to', null, InputOption::VALUE_REQUIRED, 'Adresse email de destination pour un envoi de test. Sans cette option, la commande simule, sauf avec --force.')
            ->addOption('force', null, InputOption::VALUE_NONE, 'Ignore uniquement l’état d’abandon et les conditions de date ou de délai, puis remplace une éventuelle relance existante. Les conditions métier restent obligatoires. Sans --send-to, envoie la relance à l’adresse email du panier.');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $cartIdentifier = $this->normalizeString($input->getArgument('cart'));
        $ruleIdentifier = $this->normalizeString($input->getOption('rule'));
        $sendTo = $this->normalizeString($input->getOption('send-to'));
        $force = (bool) $input->getOption('force');
        $cart = $this->resolveCart(
            $cartIdentifier,
            force: $force,
            requireCartEmail: null === $sendTo,
        );

        if (!$cart instanceof Cart) {
            $io->error(null === $cartIdentifier
                ? ($force
                    ? 'Aucun panier en cours utilisable n’a été trouvé.'
                    : 'Aucun panier abandonné n’a été trouvé.')
                : sprintf('Aucun panier n’a été trouvé pour « %s ».', $cartIdentifier));

            return Command::FAILURE;
        }

        if (!$force && !$cart->isAbandoned()) {
            $io->error('Le panier sélectionné n’est pas abandonné.');

            return Command::FAILURE;
        }

        if (
            $force
            && (
                $cart->isCheckoutPayed()
                || $cart->isCheckoutOrdered()
                || null !== $cart->getInvoice()
                || $cart->getItems()->isEmpty()
            )
        ) {
            $io->error('Le panier sélectionné est déjà finalisé ou ne contient aucun article.');

            return Command::FAILURE;
        }

        $configuration = $this->configurationRepository->findOne();
        if (!$configuration instanceof AbandonedCartConfiguration) {
            $io->error('La campagne de paniers abandonnés n’est pas encore configurée.');

            return Command::FAILURE;
        }

        $rules = $this->ruleRepository->findForConfiguration($configuration);
        if ([] === $rules) {
            $io->error('Aucune règle de relance n’est configurée.');

            return Command::FAILURE;
        }

        $now = new \DateTimeImmutable();
        $recipientEmail = $sendTo ?? ($force ? $this->normalizeString($cart->getEmail()) : null);
        $io->title('Test de relance de panier abandonné');
        $io->definitionList(
            ['Panier' => sprintf('%s (%s)', $cart->getToken() ?: '-', $cart->getId()->toString())],
            ['Abandonné le' => $cart->getAbandonedAt()?->format('d/m/Y H:i') ?? '-'],
            ['Email du panier' => $cart->getEmail() ?? '-'],
            ['Montant produits TTC' => sprintf('%.2f €', $this->ruleMatcher->calculateSubtotalTTC($cart) / 100)],
            ['Campagne active' => $configuration->isEnabled() ? 'oui' : 'non'],
            ['Mode' => null === $recipientEmail ? 'simulation sans écriture' : sprintf('envoi réel vers %s', $recipientEmail)]
        );

        $rows = [];
        foreach ($rules as $rule) {
            $rows[] = [
                $rule->getId()->toString(),
                $rule->getName(),
                $rule->isEnabled() ? 'oui' : 'non',
                $this->isAfterActivation($configuration, $cart) ? 'oui' : 'non',
                $this->isDelayReached($rule, $cart, $now) ? 'oui' : 'non',
                $this->ruleMatcher->matches($rule, $cart) ? 'oui' : 'non',
                $this->reminderRepository->findOneBy(['cart' => $cart, 'rule' => $rule]) instanceof AbandonedCartReminder ? 'oui' : 'non',
                $this->describeBenefit($rule),
            ];
        }

        $io->table(
            ['Règle', 'Nom', 'Active', 'Après activation', 'Délai atteint', 'Conditions', 'Déjà créée', 'Avantage'],
            $rows,
        );

        if (null === $recipientEmail && !$force) {
            $io->success('Simulation terminée. Utilisez --force pour envoyer à l’adresse du panier ou --send-to=adresse@example.com pour rediriger l’email de test.');

            return Command::SUCCESS;
        }

        if (null === $recipientEmail || false === filter_var($recipientEmail, \FILTER_VALIDATE_EMAIL)) {
            $io->error(null !== $sendTo
                ? 'L’adresse fournie avec --send-to n’est pas valide.'
                : 'Le panier ne possède pas d’adresse email valide pour l’envoi forcé.');

            return Command::INVALID;
        }

        $rule = $this->selectRule($rules, $ruleIdentifier, $cart, $configuration, $now, $force);
        if (!$rule instanceof AbandonedCartRule) {
            $io->error(null !== $ruleIdentifier
                ? sprintf('La règle « %s » est introuvable ou n’est pas utilisable pour ce panier.', $ruleIdentifier)
                : 'Aucune règle active dont les conditions métier correspondent à ce panier n’a été trouvée. --force ignore uniquement les conditions de date et de délai.');

            return Command::FAILURE;
        }

        $existingReminder = $this->reminderRepository->findOneBy(['cart' => $cart]);
        if ($existingReminder instanceof AbandonedCartReminder && !$force) {
            $io->error('Une relance existe déjà pour ce panier. Utilisez --force pour relancer tout de même.');

            return Command::FAILURE;
        }

        if ($existingReminder instanceof AbandonedCartReminder) {
            $existingReminder->cancel();
            $this->reminderRepository->delete($existingReminder);
            $this->cartService->hydratePromos($cart);
            $this->cartService->hydratePricing($cart);
        }

        $issued = $this->reminderIssuer->issue($rule, $cart, $recipientEmail, $now);

        try {
            $this->mailer->send($issued->reminder, $issued->token);
            $issued->reminder->markSent($now);
            $this->reminderRepository->flush();
        } catch (\Throwable $exception) {
            $io->error(sprintf('L’email de test n’a pas pu être envoyé : %s', $exception->getMessage()));

            return Command::FAILURE;
        }

        $io->success(sprintf(
            'Relance créée avec la règle « %s » et envoyée à %s. Le lien et le coupon éventuel sont réels et valides.',
            $rule->getName(),
            $recipientEmail,
        ));
        $io->note('Cette relance est enregistrée : la campagne ne recréera pas la même règle pour ce panier.');

        return Command::SUCCESS;
    }

    /**
     * @param list<AbandonedCartRule> $rules
     */
    private function selectRule(
        array $rules,
        ?string $identifier,
        Cart $cart,
        AbandonedCartConfiguration $configuration,
        \DateTimeImmutable $now,
        bool $force,
    ): ?AbandonedCartRule {
        $eligibleRules = [];

        foreach ($rules as $rule) {
            if (null !== $identifier && $rule->getId()->toString() !== $identifier) {
                continue;
            }

            if (
                $configuration->isEnabled()
                && $rule->isEnabled()
                && $this->ruleMatcher->matches($rule, $cart)
                && (
                    $force
                    || (
                        $this->isAfterActivation($configuration, $cart)
                        && $this->isDelayReached($rule, $cart, $now)
                    )
                )
            ) {
                if (null !== $identifier) {
                    return $rule;
                }

                $eligibleRules[] = $rule;
            }
        }

        return $this->ruleSelector->selectMostAdvantageous($eligibleRules, $cart);
    }

    private function resolveCart(?string $identifier, bool $force, bool $requireCartEmail): ?Cart
    {
        if (null === $identifier) {
            return $force
                ? $this->cartRepository->findLatestCartForForcedAbandonedCartReminder($requireCartEmail)
                : $this->cartRepository->findLatestAbandonedCart();
        }

        try {
            $cart = $this->cartRepository->find($identifier);
        } catch (\Throwable) {
            $cart = null;
        }

        if ($cart instanceof Cart) {
            return $cart;
        }

        $cart = $this->cartRepository->findOneBy(['token' => mb_strtoupper($identifier)]);

        return $cart instanceof Cart ? $cart : null;
    }

    private function isDelayReached(AbandonedCartRule $rule, Cart $cart, \DateTimeImmutable $now): bool
    {
        $abandonedAt = $cart->getAbandonedAt();

        return null !== $abandonedAt
            && $abandonedAt <= $now->modify(sprintf('-%d hours', $rule->getDelayHours()));
    }

    private function isAfterActivation(AbandonedCartConfiguration $configuration, Cart $cart): bool
    {
        $enabledAt = $configuration->getEnabledAt();
        $abandonedAt = $cart->getAbandonedAt();

        return null !== $enabledAt && null !== $abandonedAt && $abandonedAt >= $enabledAt;
    }

    private function describeBenefit(AbandonedCartRule $rule): string
    {
        return match ($rule->getDiscountType()) {
            AbandonedCartRule::DISCOUNT_FIXED => sprintf('%.2f €', ($rule->getDiscountAmountTTC() ?? 0) / 100),
            AbandonedCartRule::DISCOUNT_PERCENT => sprintf('%s %%', $rule->getDiscountPercent() ?? 0),
            AbandonedCartRule::DISCOUNT_FREE_SHIPPING => 'frais de port offerts',
            default => 'aucun coupon',
        };
    }

    private function normalizeString(mixed $value): ?string
    {
        if (!\is_scalar($value) && !$value instanceof \Stringable) {
            return null;
        }

        $value = mb_trim((string) $value);

        return '' !== $value ? $value : null;
    }
}
