<?php

/*
 * This file is part of the Symfony MakerBundle package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Bundle\MakerBundle\Maker;

use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
use Symfony\Bundle\MakerBundle\Doctrine\EntityClassGenerator;
use Symfony\Bundle\MakerBundle\Doctrine\ORMDependencyBuilder;
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\Common\UidTrait;
use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater;
use Symfony\Bundle\MakerBundle\Security\UserClassBuilder;
use Symfony\Bundle\MakerBundle\Security\UserClassConfiguration;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
use Symfony\Bundle\MakerBundle\Util\YamlManipulationFailedException;
use Symfony\Bundle\MakerBundle\Validator;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
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\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Yaml\Yaml;

/**
 * @author Ryan Weaver <weaverryan@gmail.com>
 *
 * @internal
 */
final class MakeUser extends AbstractMaker
{
    use UidTrait;

    public function __construct(
        private FileManager $fileManager,
        private UserClassBuilder $userClassBuilder,
        private SecurityConfigUpdater $configUpdater,
        private EntityClassGenerator $entityClassGenerator,
        private DoctrineHelper $doctrineHelper,
    ) {
    }

    public static function getCommandName(): string
    {
        return 'make:user';
    }

    public static function getCommandDescription(): string
    {
        return 'Create a new security user class';
    }

    public function configureCommand(Command $command, InputConfiguration $inputConfig): void
    {
        $command
            ->addArgument('name', InputArgument::OPTIONAL, 'The name of the security user class (e.g. <fg=yellow>User</>)')
            ->addOption('is-entity', null, InputOption::VALUE_NONE, 'Do you want to store user data in the database (via Doctrine)?')
            ->addOption('identity-property-name', null, InputOption::VALUE_REQUIRED, 'Enter a property name that will be the unique "display" name for the user (e.g. <comment>email, username, uuid</comment>)')
            ->addOption('with-password', null, InputOption::VALUE_NONE, 'Will this app be responsible for checking the password? Choose <comment>No</comment> if the password is actually checked by some other system (e.g. a single sign-on server)')
            ->setHelp($this->getHelpFileContents('MakeUser.txt'))
        ;

        $this->addWithUuidOption($command);

        $inputConfig->setArgumentAsNonInteractive('name');
    }

    public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
    {
        $this->checkIsUsingUid($input);

        if (null === $input->getArgument('name')) {
            $name = $io->ask(
                $command->getDefinition()->getArgument('name')->getDescription(),
                'User'
            );
            $input->setArgument('name', $name);
        }

        $userIsEntity = $io->confirm(
            'Do you want to store user data in the database (via Doctrine)?',
            class_exists(DoctrineBundle::class)
        );
        if ($userIsEntity) {
            $dependencies = new DependencyBuilder();
            ORMDependencyBuilder::buildDependencies($dependencies);

            $missingPackagesMessage = $dependencies->getMissingPackagesMessage(self::getCommandName(), 'Doctrine must be installed to store user data in the database');
            if ($missingPackagesMessage) {
                throw new RuntimeCommandException($missingPackagesMessage);
            }
        }
        $input->setOption('is-entity', $userIsEntity);

        $identityFieldName = $io->ask('Enter a property name that will be the unique "display" name for the user (e.g. <comment>email, username, uuid</comment>)', 'email', Validator::validatePropertyName(...));
        $input->setOption('identity-property-name', $identityFieldName);

        $io->text('Will this app need to hash/check user passwords? Choose <comment>No</comment> if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server).');
        $userWillHavePassword = $io->confirm('Does this app need to hash/check user passwords?');
        $input->setOption('with-password', $userWillHavePassword);
    }

    public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
    {
        $userClassConfiguration = new UserClassConfiguration(
            $input->getOption('is-entity'),
            $input->getOption('identity-property-name'),
            $input->getOption('with-password')
        );

        $userClassNameDetails = $generator->createClassNameDetails(
            $input->getArgument('name'),
            $userClassConfiguration->isEntity() ? 'Entity\\' : 'Security\\'
        );

        // A) Generate the User class
        if ($userClassConfiguration->isEntity()) {
            $classPath = $this->entityClassGenerator->generateEntityClass(
                entityClassDetails: $userClassNameDetails,
                apiResource: false, // api resource
                withPasswordUpgrade: $userClassConfiguration->hasPassword(), // security user
                useUuidIdentifier: $this->getIdType()
            );
        } else {
            $classPath = $generator->generateClass($userClassNameDetails->getFullName(), 'Class.tpl.php');
        }
        // need to write changes early so we can modify the contents below
        $generator->writeChanges();

        $entityUsesAttributes = ($isEntity = $userClassConfiguration->isEntity()) && $this->doctrineHelper->doesClassUsesAttributes($userClassNameDetails->getFullName());

        if ($isEntity && !$entityUsesAttributes) {
            throw new \RuntimeException('MakeUser only supports attribute mapping with doctrine entities.');
        }

        // B) Implement UserInterface
        $manipulator = new ClassSourceManipulator(
            sourceCode: $this->fileManager->getFileContents($classPath),
            overwrite: true,
            useAttributesForDoctrineMapping: $entityUsesAttributes
        );

        $manipulator->setIo($io);

        $this->userClassBuilder->addUserInterfaceImplementation($manipulator, $userClassConfiguration);

        $generator->dumpFile($classPath, $manipulator->getSourceCode());

        // C) Generate a custom user provider, if necessary
        if (!$userClassConfiguration->isEntity()) {
            $userClassConfiguration->setUserProviderClass($generator->getRootNamespace().'\\Security\\UserProvider');

            $useStatements = new UseStatementGenerator([
                UnsupportedUserException::class,
                UserNotFoundException::class,
                PasswordAuthenticatedUserInterface::class,
                PasswordUpgraderInterface::class,
                UserInterface::class,
                UserProviderInterface::class,
            ]);

            $customProviderPath = $generator->generateClass(
                $userClassConfiguration->getUserProviderClass(),
                'security/UserProvider.tpl.php',
                [
                    'use_statements' => $useStatements,
                    'user_short_name' => $userClassNameDetails->getShortName(),
                ]
            );
        }

        // D) Update security.yaml
        $securityYamlUpdated = false;
        $path = 'config/packages/security.yaml';
        if ($this->fileManager->fileExists($path)) {
            try {
                $newYaml = $this->configUpdater->updateForUserClass(
                    $this->fileManager->getFileContents($path),
                    $userClassConfiguration,
                    $userClassNameDetails->getFullName()
                );
                $generator->dumpFile($path, $newYaml);
                $securityYamlUpdated = true;
            } catch (YamlManipulationFailedException) {
            }
        }

        $generator->writeChanges();

        $this->writeSuccessMessage($io);

        $io->text('Next Steps:');
        $nextSteps = [
            \sprintf('Review your new <info>%s</info> class.', $userClassNameDetails->getFullName()),
        ];
        if ($userClassConfiguration->isEntity()) {
            $nextSteps[] = \sprintf(
                'Use <comment>make:entity</comment> to add more fields to your <info>%s</info> entity and then run <comment>make:migration</comment>.',
                $userClassNameDetails->getShortName()
            );
        } else {
            $nextSteps[] = \sprintf(
                'Open <info>%s</info> to finish implementing your user provider.',
                /* @phpstan-ignore-next-line - $customProviderPath is defined in this else statement */
                $this->fileManager->relativizePath($customProviderPath)
            );
        }

        if (!$securityYamlUpdated) {
            $yamlExample = $this->configUpdater->updateForUserClass(
                'security: {}',
                $userClassConfiguration,
                $userClassNameDetails->getFullName()
            );
            $nextSteps[] = "Your <info>security.yaml</info> could not be updated automatically. You'll need to add the following config manually:\n\n".$yamlExample;
        }

        $nextSteps[] = 'Create a way to authenticate! See https://symfony.com/doc/current/security.html';

        $nextSteps = array_map(static fn ($step) => \sprintf('  - %s', $step), $nextSteps);
        $io->text($nextSteps);
    }

    public function configureDependencies(DependencyBuilder $dependencies, ?InputInterface $input = null): void
    {
        // checking for SecurityBundle guarantees security.yaml is present
        $dependencies->addClassDependency(
            SecurityBundle::class,
            'security'
        );

        // needed to update the YAML files
        $dependencies->addClassDependency(
            Yaml::class,
            'yaml'
        );

        if (null !== $input && $input->getOption('is-entity')) {
            ORMDependencyBuilder::buildDependencies($dependencies);
        }
    }
}
