<?php

declare(strict_types=1);

namespace Mautic\InstallBundle\Install;

use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Configurator\Configurator;
use Mautic\CoreBundle\Configurator\Step\StepInterface;
use Mautic\CoreBundle\Doctrine\Loader\FixturesLoaderInterface;
use Mautic\CoreBundle\Helper\CacheHelper;
use Mautic\CoreBundle\Helper\EncryptionHelper;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\CoreBundle\Helper\PathsHelper;
use Mautic\CoreBundle\Loader\ParameterLoader;
use Mautic\CoreBundle\Release\ThisRelease;
use Mautic\InstallBundle\Configurator\Step\DoctrineStep;
use Mautic\InstallBundle\Exception\AlreadyInstalledException;
use Mautic\InstallBundle\Exception\DatabaseVersionTooOldException;
use Mautic\InstallBundle\Helper\SchemaHelper;
use Mautic\UserBundle\Entity\Role;
use Mautic\UserBundle\Entity\User;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

class InstallService
{
    public const CHECK_STEP = 0;

    public const DOCTRINE_STEP = 1;

    public const USER_STEP = 2;

    public const FINAL_STEP = 3;

    public function __construct(
        private Configurator $configurator,
        private CacheHelper $cacheHelper,
        protected PathsHelper $pathsHelper,
        private EntityManager $entityManager,
        private TranslatorInterface $translator,
        private KernelInterface $kernel,
        private ValidatorInterface $validator,
        private UserPasswordHasher $hasher,
        private FixturesLoaderInterface $fixturesLoader,
    ) {
    }

    /**
     * Get step object for given index or appropriate step index.
     *
     * @param int $index The step number to retrieve
     *
     * @return StepInterface the valid step given installation status
     *
     * @throws \InvalidArgumentException|AlreadyInstalledException
     */
    public function getStep(int $index = 0): StepInterface
    {
        // We're going to assume a bit here; if the config file exists already and DB info is provided, assume the app
        // is installed and redirect
        if ($this->checkIfInstalled()) {
            throw new AlreadyInstalledException();
        }

        $params = $this->configurator->getParameters();

        // Check to ensure the installer is in the right place
        if ((empty($params)
                || !isset($params['db_driver'])
                || empty($params['db_driver'])) && $index > 1) {
            return $this->configurator->getStep(self::DOCTRINE_STEP)[0];
        }

        return $this->configurator->getStep($index)[0];
    }

    /**
     * Get local config file location.
     */
    private function localConfig(): string
    {
        return ParameterLoader::getLocalConfigFile($this->pathsHelper->getSystemPath('root').'/app');
    }

    /**
     * Get local config parameters.
     */
    public function localConfigParameters(): array
    {
        $localConfigFile = $this->localConfig();

        if (file_exists($localConfigFile)) {
            /** @var array $parameters */
            $parameters = [];

            // Load local config to override parameters
            include $localConfigFile;
            $localParameters = $parameters;
        } else {
            $localParameters = [];
        }

        return $localParameters;
    }

    /**
     * Checks if the application has been installed and redirects if so.
     */
    public function checkIfInstalled(): bool
    {
        // If the config file doesn't even exist, no point in checking further
        $localConfigFile = $this->localConfig();
        if (!file_exists($localConfigFile)) {
            return false;
        }

        $params = $this->configurator->getParameters();

        // if db_driver and site_url are present then it is assumed all the steps of the installation have been
        // performed; manually deleting these values or deleting the config file will be required to re-enter
        // installation.
        if (empty($params['db_driver']) || empty($params['site_url'])) {
            return false;
        }

        return true;
    }

    /**
     * Translation messages array.
     */
    private function translateMessages(array $messages): array
    {
        if (empty($messages)) {
            return $messages;
        }

        foreach ($messages as $key => $value) {
            $messages[$key] = $this->translator->trans($value);
        }

        return $messages;
    }

    /**
     * Checks for step's requirements.
     */
    public function checkRequirements(StepInterface $step): array
    {
        $messages = $step->checkRequirements();

        return $this->translateMessages($messages);
    }

    /**
     * Checks for step's optional settings.
     */
    public function checkOptionalSettings(StepInterface $step): array
    {
        $messages = $step->checkOptionalSettings();

        return $this->translateMessages($messages);
    }

    public function saveConfiguration($params, ?StepInterface $step = null, $clearCache = false): array
    {
        if ($step instanceof StepInterface) {
            $params = $step->update($step);
        }

        $this->configurator->mergeParameters($params);

        $messages = [];
        try {
            $this->configurator->write();
        } catch (\RuntimeException) {
            $messages = [
                'error' => $this->translator->trans(
                    'mautic.installer.error.writing.configuration',
                    [],
                    'flashes'
                ),
            ];
        }

        if ($clearCache) {
            $this->cacheHelper->refreshConfig();
        }

        return $messages;
    }

    /**
     * @return array Validation errors
     */
    public function validateDatabaseParams(array $dbParams): array
    {
        $required = [
            'driver',
            'host',
            'name',
            'user',
        ];

        $messages = [];
        foreach ($required as $r) {
            if (!isset($dbParams[$r]) || empty($dbParams[$r])) {
                $messages[$r] = $this->translator->trans(
                    'mautic.core.value.required',
                    [],
                    'validators'
                );
            }
        }

        if (!isset($dbParams['port']) || (int) $dbParams['port'] <= 0) {
            $messages['port'] = $this->translator->trans(
                'mautic.install.database.port.invalid',
                [],
                'validators'
            );
        }

        if (!empty($dbParams['driver']) && !in_array($dbParams['driver'], DoctrineStep::getDriverKeys())) {
            $messages['driver'] = $this->translator->trans(
                'mautic.install.database.driver.invalid',
                ['%drivers%' => implode(', ', DoctrineStep::getDriverKeys())],
                'validators'
            );
        }

        return $messages;
    }

    /**
     * Create the database.
     */
    public function createDatabaseStep(StepInterface $step, array $dbParams): array
    {
        $messages = $this->validateDatabaseParams($dbParams);

        if (!empty($messages)) {
            return $messages;
        }

        // Check if connection works and/or create database if applicable
        $schemaHelper = new SchemaHelper($dbParams);

        try {
            $schemaHelper->testConnection();
            $schemaHelper->validateDatabaseVersion();

            if ($schemaHelper->createDatabase()) {
                $messages = $this->saveConfiguration($dbParams, $step, true);
                if (empty($messages)) {
                    return $messages;
                }
            }

            $messages['error'] = $this->translator->trans(
                'mautic.installer.error.creating.database',
                ['%name%' => $dbParams['name']],
                'flashes'
            );
        } catch (DatabaseVersionTooOldException $e) {
            $metadata = ThisRelease::getMetadata();

            $messages['error'] = $this->translator->trans(
                'mautic.installer.error.database.version',
                [
                    '%currentversion%'    => $e->getCurrentVersion(),
                    '%mysqlminversion%'   => $metadata->getMinSupportedMySqlVersion(),
                    '%mariadbminversion%' => $metadata->getMinSupportedMariaDbVersion(),
                ],
                'flashes'
            );
        } catch (\Exception $exception) {
            $messages['error'] = $this->translator->trans(
                'mautic.installer.error.connecting.database',
                ['%exception%' => $exception->getMessage()],
                'flashes'
            );
        }

        return $messages;
    }

    /**
     * Create the database schema.
     */
    public function createSchemaStep(array $dbParams): array
    {
        $schemaHelper = new SchemaHelper($dbParams);
        $schemaHelper->setEntityManager($this->entityManager);

        $messages = [];
        try {
            if (!$schemaHelper->installSchema()) {
                $messages['error'] = $this->translator->trans(
                    'mautic.installer.error.no.metadata',
                    [],
                    'flashes'
                );
            }
        } catch (\Exception $exception) {
            $messages['error'] = $this->translator->trans(
                'mautic.installer.error.installing.data',
                ['%exception%' => $exception->getMessage()],
                'flashes'
            );
        }

        return $messages;
    }

    /**
     * Load the database fixtures in the database.
     */
    public function createFixturesStep(): array
    {
        $messages = [];

        try {
            $this->installDatabaseFixtures();
        } catch (\Exception $exception) {
            $messages['error'] = $this->translator->trans(
                'mautic.installer.error.adding.fixtures',
                ['%exception%' => $exception->getMessage()],
                'flashes'
            );
        }

        return $messages;
    }

    /**
     * Installs data fixtures for the application.
     *
     * @throws \InvalidArgumentException
     */
    public function installDatabaseFixtures(): void
    {
        $fixtures = $this->fixturesLoader->getFixtures(['group_install']);

        if (!$fixtures) {
            throw new \InvalidArgumentException('Could not find any fixtures to load with the "group_install" group.');
        }

        $purger = new ORMPurger($this->entityManager);
        $purger->setPurgeMode(ORMPurger::PURGE_MODE_DELETE);
        $executor = new ORMExecutor($this->entityManager, $purger);
        /*
         * FIXME entity manager does not load configuration if local.php just created by CLI install
         * [error] An error occurred while attempting to add default data
         * An exception occured in driver:
         * SQLSTATE[HY000] [1045] Access refused for user: ''@'@localhost' (mot de passe: NON)
         */
        $executor->execute($fixtures, true);
    }

    /**
     * Create the administrator user.
     */
    public function createAdminUserStep(array $data): array
    {
        $entityManager = $this->entityManager;

        // ensure the username and email are unique
        try {
            /** @var User $existingUser */
            $existingUser = $entityManager->getRepository(User::class)->find(1);
        } catch (\Exception) {
            $existingUser = null;
        }

        if (null !== $existingUser) {
            $user = $existingUser;
        } else {
            $user = new User();
        }

        $required = [
            'firstname',
            'lastname',
            'username',
            'email',
            'password',
        ];

        $messages = [];
        foreach ($required as $r) {
            if (!isset($data[$r])) {
                $messages[$r] = $this->translator->trans(
                    'mautic.core.value.required',
                    [],
                    'validators'
                );
            }
        }

        if (!empty($messages)) {
            return $messages;
        }

        $validations = [];

        $emailConstraint          = new Assert\Email();
        $emailConstraint->message = $this->translator->trans('mautic.core.email.required', [], 'validators');

        $passwordConstraint             = new Assert\Length(['min' => 6]);
        $passwordConstraint->minMessage = $this->translator->trans('mautic.install.password.minlength', [], 'validators');

        $validations[] = $this->validator->validate($data['email'], $emailConstraint);
        $validations[] = $this->validator->validate($data['password'], $passwordConstraint);

        $messages = [];
        foreach ($validations as $errors) {
            foreach ($errors as $error) {
                $messages[] = $error->getMessage();
            }
        }

        if (!empty($messages)) {
            return $messages;
        }

        $hasher = $this->hasher;

        $user->setFirstName(InputHelper::clean($data['firstname']));
        $user->setLastName(InputHelper::clean($data['lastname']));
        $user->setUsername(InputHelper::clean($data['username']));
        $user->setEmail(InputHelper::email($data['email']));
        $user->setPassword($hasher->hashPassword($user, $data['password']));

        $adminRole = null;
        try {
            $adminRole = $entityManager->getReference(Role::class, 1);
        } catch (\Exception $exception) {
            $messages['error'] = $this->translator->trans(
                'mautic.installer.error.getting.role',
                ['%exception%' => $exception->getMessage()],
                'flashes'
            );
        }

        if (!empty($adminRole)) {
            $user->setRole($adminRole);

            try {
                $entityManager->persist($user);
                $entityManager->flush();
            } catch (\Exception $exception) {
                $messages['error'] = $this->translator->trans(
                    'mautic.installer.error.creating.user',
                    ['%exception%' => $exception->getMessage()],
                    'flashes'
                );
            }
        }

        return $messages;
    }

    /**
     * Create the final configuration.
     */
    public function createFinalConfigStep(string $siteUrl): array
    {
        // Merge final things into the config, wipe the container, and we're done!
        $finalConfigVars = [
            'secret_key' => EncryptionHelper::generateKey(),
            'site_url'   => $siteUrl,
        ];

        return $this->saveConfiguration($finalConfigVars, null, true);
    }

    /**
     * Final migration step for install.
     */
    public function finalMigrationStep(): void
    {
        // Add database migrations up to this point since this is a fresh install (must be done at this point
        // after the cache has been rebuilt
        $input  = new ArgvInput(['console', 'doctrine:migrations:version', '--add', '--all', '--no-interaction']);
        $output = new BufferedOutput();

        $application = new Application($this->kernel);
        $application->setAutoExit(false);
        $application->run($input, $output);
    }
}
