<?php

/*
 * 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\Cart\LegacyOrderImport\LegacyOrderImporter;
use CpCreation\VitiCore\Cart\LegacyOrderImport\LegacyOrderImportMapping;
use CpCreation\VitiCore\Cart\LegacyOrderImport\LegacyOrderImportMappingGenerator;
use CpCreation\VitiCore\Cart\LegacyOrderImport\LegacyOrderImportMappingWizard;
use CpCreation\VitiCore\Cart\LegacyOrderImport\LegacyOrderSourceConnectionConfig;
use CpCreation\VitiCore\Cart\LegacyOrderImport\LegacyOrderSourceConnector;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\TransactionIsolationLevel;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\LockableTrait;
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:orders:import-master',
    description: 'Importe une fois les commandes d\'un VitiCore master MySQL dans le VitiCore courant.'
)]
final class ImportLegacyOrdersCommand extends Command
{
    use LockableTrait;

    public function __construct(
        private readonly LegacyOrderImporter $importer,
        private readonly LegacyOrderImportMappingGenerator $mappingGenerator,
        private readonly LegacyOrderImportMappingWizard $mappingWizard,
        private readonly LegacyOrderSourceConnector $sourceConnector,
        private readonly string $kernelProjectDir,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addOption('database', null, InputOption::VALUE_REQUIRED, 'Nom de la base MySQL/MariaDB source. Demande interactivement si omis.')
            ->addOption('user', null, InputOption::VALUE_REQUIRED, 'Utilisateur MySQL/MariaDB source. Demande interactivement si omis.')
            ->addOption('password', null, InputOption::VALUE_REQUIRED, 'Mot de passe MySQL/MariaDB source. Omettez cette option pour une saisie interactive masquee.')
            ->addOption(
                'host',
                null,
                InputOption::VALUE_REQUIRED,
                'Hote MySQL/MariaDB vu directement ou depuis le serveur SSH.',
                LegacyOrderSourceConnectionConfig::DEFAULT_HOST,
            )
            ->addOption(
                'port',
                null,
                InputOption::VALUE_REQUIRED,
                'Port MySQL/MariaDB source.',
                LegacyOrderSourceConnectionConfig::DEFAULT_MYSQL_PORT,
            )
            ->addOption('ssh-host', null, InputOption::VALUE_REQUIRED, 'Hote SSH. Sa presence active un tunnel vers MySQL.')
            ->addOption('ssh-user', null, InputOption::VALUE_REQUIRED, 'Utilisateur SSH. Demande interactivement quand --ssh-host est renseigne.')
            ->addOption('ssh-key', null, InputOption::VALUE_REQUIRED, 'Chemin de la cle privee SSH. Demande interactivement quand --ssh-host est renseigne.')
            ->addOption(
                'ssh-port',
                null,
                InputOption::VALUE_REQUIRED,
                'Port du serveur SSH.',
                LegacyOrderSourceConnectionConfig::DEFAULT_SSH_PORT,
            )
            ->addOption(
                'source-id',
                null,
                InputOption::VALUE_REQUIRED,
                'Identifiant stable de la source. Par defaut, le nom de sa base est utilise.',
            )
            ->addOption(
                'mapping',
                null,
                InputOption::VALUE_REQUIRED,
                'Fichier JSON de correspondance des variantes et methodes.',
                'config/legacy-order-map.json',
            )
            ->addOption(
                'scope',
                null,
                InputOption::VALUE_REQUIRED,
                'Perimetre source : all (checkout non supprime) ou paid.',
                'all',
            )
            ->addOption(
                'configure-mapping',
                null,
                InputOption::VALUE_NONE,
                'Ouvre l assistant interactif pour completer ou modifier le fichier de correspondance.',
            )
            ->addOption(
                'apply',
                null,
                InputOption::VALUE_NONE,
                'Applique l\'import. Sans cette option, la commande effectue uniquement le preflight.',
            )
            ->addOption(
                'force',
                null,
                InputOption::VALUE_NONE,
                'Autorise la reprise idempotente d\'une source deja importee, sans dupliquer les commandes.',
            )
            ->addOption(
                'allow-stock-impact',
                null,
                InputOption::VALUE_NONE,
                'Autorise explicitement un import qui entre dans le calcul du stock courant.',
            );
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $io->title('Import des commandes VitiCore master');

        try {
            $scope = mb_strtolower(mb_trim((string) $input->getOption('scope')));
            if (!\in_array($scope, ['all', 'paid'], true)) {
                throw new \InvalidArgumentException('L\'option --scope doit valoir "all" ou "paid".');
            }

            $sourceConfiguration = $this->resolveSourceConfiguration($input, $io);
            $sourceHandle = $this->sourceConnector->connect($sourceConfiguration);
            $source = $sourceHandle->connection();
            $locked = false;
            try {
                $sourceLabel = $this->normalizeOptionalString($input->getOption('source-id'))
                    ?? $sourceConfiguration->database;

                if (null === $sourceLabel) {
                    throw new \InvalidArgumentException('Renseignez --source-id : le nom de la base source est indisponible.');
                }

                $mappingPath = $this->normalizeOptionalString($input->getOption('mapping'));
                if (null === $mappingPath) {
                    throw new \InvalidArgumentException('L option --mapping doit designer un fichier JSON.');
                }
                $absoluteMappingPath = $this->absolutePath($mappingPath);
                $apply = (bool) $input->getOption('apply');
                $force = (bool) $input->getOption('force');
                $allowStockImpact = (bool) $input->getOption('allow-stock-impact');
                $configureMapping = (bool) $input->getOption('configure-mapping');
                if ($configureMapping && !$input->isInteractive()) {
                    throw new \InvalidArgumentException('L option --configure-mapping ne peut pas etre utilisee avec --no-interaction.');
                }
                $sourceKey = $this->importer->sourceKey($sourceLabel);

                if (!$this->lock('viti-core-legacy-order-import-'.$sourceKey)) {
                    $io->error('Un autre import de cette source est deja en cours.');

                    return Command::FAILURE;
                }
                $locked = true;

                $io->definitionList(
                    ['Source' => $sourceLabel],
                    ['MySQL source' => $sourceConfiguration->mysqlEndpoint()],
                    ['Mot de passe MySQL' => 'renseigne (valeur masquee)'],
                    ['Tunnel SSH' => $sourceConfiguration->sshEndpoint() ?? 'non'],
                    ['Correspondances' => $absoluteMappingPath],
                    ['Perimetre' => 'paid' === $scope ? 'commandes payees' : 'tous les checkouts non supprimes'],
                    ['Mode' => $apply ? 'ecriture' : 'preflight sans ecriture'],
                    ['Reprise forcee' => $force ? 'oui' : 'non'],
                );

                if (!file_exists($absoluteMappingPath) && !is_link($absoluteMappingPath)) {
                    $this->beginSourceSnapshot($source);
                    try {
                        $draft = $this->mappingGenerator->generate($source, $scope, $sourceLabel, $sourceKey);
                    } finally {
                        if ($source->isTransactionActive()) {
                            $source->rollBack();
                        }
                    }
                    $this->mappingGenerator->write($absoluteMappingPath, $draft);

                    $unresolved = $this->countUnresolvedMappings($draft);
                    $io->success(\sprintf(
                        'Fichier de correspondance cree avec %d valeur(s) a completer : %s',
                        $unresolved,
                        $absoluteMappingPath,
                    ));

                    if ($configureMapping || ($unresolved > 0 && $input->isInteractive() && $io->confirm('Voulez-vous completer les correspondances maintenant ?', true))) {
                        $result = $this->mappingWizard->run(
                            $draft,
                            $absoluteMappingPath,
                            $io,
                            includeResolved: $configureMapping,
                        );
                        $io->text(\sprintf(
                            '%d choix enregistre(s), %d correspondance(s) encore a completer.',
                            $result['updated'],
                            $result['remaining'],
                        ));
                        $io->note('Relancez la commande pour le preflight. La creation et la configuration initiales ne declenchent jamais l import.');
                    } elseif ($unresolved > 0) {
                        $io->note('Completez uniquement les valeurs de droite __A_COMPLETER__, puis relancez la commande. Aucun import n a ete execute.');
                    } else {
                        $io->note('Le fichier ne contient aucune valeur a completer. Relancez la commande pour le preflight ; aucun import n a ete execute.');
                    }

                    if ($apply) {
                        $io->warning('L option --apply a ete ignoree pendant la creation initiale du fichier. Un second lancement explicite est obligatoire.');

                        return Command::FAILURE;
                    }

                    return Command::SUCCESS;
                }

                $mapping = LegacyOrderImportMapping::fromFile($absoluteMappingPath);
                if (1 !== $mapping->schemaVersion) {
                    throw new \RuntimeException('Ce fichier n a pas ete genere par cette commande (schema_version 1 requis). Renommez-le puis relancez pour generer un fichier sur et complet.');
                }

                $mappingContextErrors = $mapping->contextErrors($sourceLabel, $sourceKey, $scope);
                if ([] !== $mappingContextErrors) {
                    throw new \RuntimeException(implode("\n", $mappingContextErrors));
                }

                $mappingDraft = $this->mappingGenerator->readDraft($absoluteMappingPath);
                $unresolved = $this->countUnresolvedMappings($mappingDraft);
                if ($configureMapping || ($unresolved > 0 && $input->isInteractive() && $io->confirm('Des correspondances restent a completer. Ouvrir l assistant interactif ?', true))) {
                    $this->beginSourceSnapshot($source);
                    try {
                        $refreshedMappingDraft = $this->mappingGenerator->refreshVariantContext($source, $scope, $mappingDraft);
                    } finally {
                        if ($source->isTransactionActive()) {
                            $source->rollBack();
                        }
                    }
                    if ($refreshedMappingDraft !== $mappingDraft) {
                        $this->mappingGenerator->replace($absoluteMappingPath, $refreshedMappingDraft);
                        $mappingDraft = $refreshedMappingDraft;
                    }

                    $result = $this->mappingWizard->run(
                        $mappingDraft,
                        $absoluteMappingPath,
                        $io,
                        includeResolved: $configureMapping,
                    );
                    $io->text(\sprintf(
                        '%d choix enregistre(s), %d correspondance(s) encore a completer.',
                        $result['updated'],
                        $result['remaining'],
                    ));
                    $io->note('Relancez explicitement la commande pour le preflight ou l import. Aucun import n a ete execute pendant la configuration.');

                    if ($apply) {
                        $io->warning('L option --apply a ete ignoree pendant la configuration interactive.');

                        return Command::FAILURE;
                    }

                    return Command::SUCCESS;
                }

                $existingRun = $this->importer->findRun($sourceKey);

                if (null !== $existingRun && $apply && !$force) {
                    $io->error(\sprintf(
                        'Cette source a deja ete importee le %s. Relancez avec --force uniquement pour une reprise idempotente.',
                        (string) ($existingRun['completed_at'] ?? $existingRun['started_at'] ?? 'date inconnue'),
                    ));

                    return Command::FAILURE;
                }

                if (null !== $existingRun && !$apply) {
                    $io->note('Un import est deja trace pour cette source. Le preflight reste autorise et n\'ecrit rien.');
                }

                $this->beginSourceSnapshot($source);
                $plan = $this->importer->prepare($source, $mapping, $sourceLabel, $scope);
                $this->renderPlan($io, $plan->summary, $plan->warnings, $plan->errors);

                if (!$plan->isReady()) {
                    $io->error('Preflight refuse : corrigez toutes les erreurs avant de relancer. Aucune donnee cible n\'a ete modifiee.');

                    return Command::FAILURE;
                }

                if (!$apply) {
                    $io->success('Preflight valide. Relancez avec --apply pour effectuer l\'import atomique.');

                    return Command::SUCCESS;
                }

                $result = $this->importer->apply($plan, $force, $allowStockImpact);
                $io->success(\sprintf(
                    'Import termine : %d commande(s) ajoutee(s), %d deja presente(s), total historique %s EUR.',
                    $result['imported'],
                    $result['skipped'],
                    number_format($result['total_ttc'] / 100, 2, ',', ' '),
                ));
                $io->text(\sprintf('Execution tracee sous l\'identifiant %s.', $result['run_id']));

                return Command::SUCCESS;
            } finally {
                if ($source->isTransactionActive()) {
                    $source->rollBack();
                }
                $sourceHandle->close();
                if ($locked) {
                    $this->release();
                }
            }
        } catch (\Throwable $exception) {
            $io->error($exception->getMessage());

            return Command::FAILURE;
        }
    }

    /**
     * @param array<string, int|string> $summary
     * @param list<string>              $warnings
     * @param list<string>              $errors
     */
    private function renderPlan(SymfonyStyle $io, array $summary, array $warnings, array $errors): void
    {
        $io->section('Synthese du preflight');
        $io->table(['Controle', 'Valeur'], [
            ['Commandes', (string) $summary['orders']],
            ['Payees', (string) $summary['paid']],
            ['Non payees', (string) $summary['unpaid']],
            ['Lignes', (string) $summary['lines']],
            ['Quantite', (string) $summary['quantity']],
            ['Lignes rattachees au catalogue', (string) $summary['mapped_lines']],
            ['Lignes historiques detachees', (string) $summary['detached_lines']],
            ['Cartes cadeau historiques', (string) $summary['gift_cards']],
            ['Impact potentiel sur le stock', (string) $summary['stock_impact_quantity']],
            ['Total historique TTC', number_format(((int) $summary['total_ttc']) / 100, 2, ',', ' ').' EUR'],
        ]);

        foreach ($warnings as $warning) {
            $io->warning($warning);
        }

        foreach ($errors as $error) {
            $io->error($error);
        }
    }

    private function beginSourceSnapshot(Connection $source): void
    {
        $source->setTransactionIsolation(TransactionIsolationLevel::REPEATABLE_READ);
        $source->executeStatement('SET TRANSACTION READ ONLY');
        $source->beginTransaction();
    }

    private function normalizeOptionalString(mixed $value): ?string
    {
        if (null === $value || false === $value || !\is_scalar($value)) {
            return null;
        }

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

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

    private function resolveSourceConfiguration(InputInterface $input, SymfonyStyle $io): LegacyOrderSourceConnectionConfig
    {
        $database = $this->requiredTextOption($input, $io, 'database', 'Nom de la base MySQL/MariaDB source');
        $user = $this->requiredTextOption($input, $io, 'user', 'Utilisateur MySQL/MariaDB source');
        $password = $this->passwordOption($input, $io);
        $host = $this->normalizeOptionalString($input->getOption('host'))
            ?? LegacyOrderSourceConnectionConfig::DEFAULT_HOST;
        $port = $this->portOption($input, 'port');
        $sshHost = $this->normalizeOptionalString($input->getOption('ssh-host'));
        $sshUserOption = $this->normalizeOptionalString($input->getOption('ssh-user'));
        $sshKeyOption = $this->normalizeOptionalString($input->getOption('ssh-key'));

        if (null === $sshHost) {
            if (null !== $sshUserOption || null !== $sshKeyOption) {
                throw new \InvalidArgumentException('Renseignez --ssh-host pour utiliser --ssh-user ou --ssh-key.');
            }

            return new LegacyOrderSourceConnectionConfig(
                database: $database,
                user: $user,
                password: $password,
                host: $host,
                port: $port,
            );
        }

        $sshUser = $sshUserOption
            ?? $this->requiredInteractiveValue($input, $io, 'ssh-user', 'Utilisateur SSH');
        $sshKey = $sshKeyOption
            ?? $this->requiredInteractiveValue($input, $io, 'ssh-key', 'Chemin de la cle privee SSH');

        return new LegacyOrderSourceConnectionConfig(
            database: $database,
            user: $user,
            password: $password,
            host: $host,
            port: $port,
            sshHost: $sshHost,
            sshUser: $sshUser,
            sshKey: $this->absoluteSshKeyPath($sshKey),
            sshPort: $this->portOption($input, 'ssh-port'),
        );
    }

    private function requiredTextOption(InputInterface $input, SymfonyStyle $io, string $option, string $question): string
    {
        return $this->normalizeOptionalString($input->getOption($option))
            ?? $this->requiredInteractiveValue($input, $io, $option, $question);
    }

    private function requiredInteractiveValue(InputInterface $input, SymfonyStyle $io, string $option, string $question): string
    {
        if (!$input->isInteractive()) {
            throw new \InvalidArgumentException(\sprintf('Renseignez --%s.', $option));
        }

        $value = $this->normalizeOptionalString($io->ask($question));
        if (null === $value) {
            throw new \InvalidArgumentException(\sprintf('La valeur --%s est obligatoire.', $option));
        }

        return $value;
    }

    private function passwordOption(InputInterface $input, SymfonyStyle $io): string
    {
        $password = $input->getOption('password');
        if (null !== $password) {
            if (!\is_scalar($password)) {
                throw new \InvalidArgumentException('La valeur --password est invalide.');
            }

            return (string) $password;
        }

        if (!$input->isInteractive()) {
            throw new \InvalidArgumentException('Renseignez --password ou relancez la commande en mode interactif pour une saisie masquee.');
        }

        return (string) ($io->askHidden('Mot de passe MySQL/MariaDB source') ?? '');
    }

    private function portOption(InputInterface $input, string $option): int
    {
        $value = $input->getOption($option);
        if (!\is_int($value) && (!\is_string($value) || 1 !== preg_match('/^[0-9]+$/', $value))) {
            throw new \InvalidArgumentException(\sprintf('La valeur --%s doit etre un port numerique.', $option));
        }

        $port = (int) $value;
        if ($port < 1 || $port > 65535) {
            throw new \InvalidArgumentException(\sprintf('La valeur --%s doit etre comprise entre 1 et 65535.', $option));
        }

        return $port;
    }

    private function absoluteSshKeyPath(string $path): string
    {
        if ('~' === $path || str_starts_with($path, '~/')) {
            $home = $this->normalizeOptionalString(getenv('HOME'));
            if (null === $home) {
                throw new \InvalidArgumentException('Impossible de resoudre ~ dans le chemin --ssh-key. Utilisez un chemin absolu.');
            }

            return $home.mb_substr($path, 1);
        }

        return $this->absolutePath($path);
    }

    private function absolutePath(string $path): string
    {
        if (str_starts_with($path, '/') || 1 === preg_match('/^[A-Za-z]:[\\\\\/]/', $path)) {
            return $path;
        }

        return $this->kernelProjectDir.'/'.mb_ltrim($path, '/');
    }

    /**
     * @param array<string, mixed> $draft
     */
    private function countUnresolvedMappings(array $draft): int
    {
        $count = 0;

        foreach (['variants', 'payment_methods', 'delivery_methods'] as $section) {
            $values = $draft[$section] ?? [];
            if (!\is_array($values)) {
                continue;
            }

            foreach ($values as $value) {
                if (LegacyOrderImportMapping::UNRESOLVED === $value) {
                    ++$count;
                }
            }
        }

        return $count;
    }
}
