<?php
/*
 * Copyright (c) 2023. RTM Business
 */

namespace WordpressModels\ORM\Command;

use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Schema\MySQLSchemaManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\Driver\DatabaseDriver;
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
use Doctrine\ORM\Tools\EntityGenerator;
use Doctrine\ORM\Tools\Export\ClassMetadataExporter;
use Doctrine\ORM\Tools\Export\Driver\AnnotationExporter;
use WordpressModels\DependencyInjection\CliCommandAttributes\Attributes\WpCliCommand;
use WP_CLI;

#[WpCliCommand('doctrine')]
class DoctrineCommands
{

    public function __construct(private EntityManagerInterface $entityManager)
    {
    }


    /**
     * @param array $args
     * @param array $flags
     * @return void
     */
    #[WpCliCommand(name: 'generate-proxies',
        description: 'Generate Doctrine proxies for all entities registered in the entity manager.',
        shortDescription: 'Generate proxy classes')]
    public function generate_proxies(array $args, array $flags)
    {
        $em = $this->entityManager;

        // initialize plugin class table prefixes
        $pluginClasses = array_keys(apply_filters('doctrine_plugin_entity_table_prefixes', []));

        $meta = $em->getMetadataFactory()->getAllMetadata();
        $generatedAmount = $em->getProxyFactory()
            ->generateProxyClasses($meta);

        $extraMessage = '';
        if ($generatedAmount !== count($meta)) {
            $extraMessage = ' Generated amount of proxy classes is not equal to the total amount of entity classes. Check the proxy dir for more info.';
        }

        WP_CLI::success('Generated ' . $generatedAmount . ' proxy classes.' . $extraMessage);
    }

    /**
     * @return void
     * @throws WP_CLI\ExitException
     * @throws \Doctrine\DBAL\Exception
     * @throws \Doctrine\ORM\Exception\MissingMappingDriverImplementation
     * @throws \Doctrine\Persistence\Mapping\MappingException
     * @throws \ReflectionException
     */
    #[WpCliCommand(name: 'purge-proxies',
        description: 'Purge all Doctrine proxies from the proxy directory.',
        shortDescription: 'Purge proxy classes')]
    public function purge_proxies()
    {
        $em = $this->entityManager;
        $dir = $em->getConfiguration()->getProxyDir();

        if (!is_dir($dir)) {
            WP_CLI::error("Proxy directory \"$dir\" does not exist or is not a directory.");
        }

        $files = glob($dir . '/*');
        if (!$files) {
            WP_CLI::success("Proxy directory is already empty.");
        }

        $deletedAmount = 0;
        foreach ($files as $file) {
            WP_CLI::line("Deleting file \"$file\"");
            if (!unlink($file)) {
                WP_CLI::line("Unable to remove file \"$file\"");
            } else {
                $deletedAmount++;
            }
        }

        WP_CLI::success("Succesfully deleted $deletedAmount files");
    }

    /**
     * Generate an array of SQL statements to create the database schema for classes in the specified plugin.
     *
     * This is a modified version of the orm:update-schema command.
     *
     * @return void
     * @throws WP_CLI\ExitException
     * @throws \Doctrine\DBAL\Exception
     */
    #[WpCliCommand(name: 'dump-schema',
        description: 'Generate an array of SQL statements to create the database schema for classes in the specified plugin.',
        shortDescription: 'Dump the database schema')]
    public function dump_migration(array $args, array $flags)
    {
        $args = array_filter($args, fn($v) => $v !== '--');
        if ($args && ($path = current($args))) {
            $args = ['dest-path' => $path];
        }

        if (!isset($flags['prefix'])) {
            WP_CLI::error("You must set a plugin table namespace to update schemas");
        }
        $tablePrefix = $flags['prefix'];

        $em = $this->entityManager;

        $pluginClasses = array_keys(array_filter(
            apply_filters('doctrine_plugin_entity_table_prefixes', []),
            fn($prefix) => $prefix === $tablePrefix));

        WP_CLI::line("Dumping update schema for the following classes, filtered by plugin table prefix {$tablePrefix}: \n"
            . join(",\n", $pluginClasses));

        $tool = new DatabaseSchemaTool($em, $this->getEnhancedSchemaManager($em));

        try {
            global $wpdb;
            $sql = $tool->getUpdateSchemaSql($em->getMetadataFactory()->getAllMetadata(), $wpdb->prefix . $tablePrefix . '_');
        } catch (\Throwable $throwable) {
            WP_CLI::error($throwable->getMessage());
        }

        if (isset($args['dest-path'])) {
            $datetime = new \DateTime();
            $format = $datetime->format(\DateTimeInterface::ATOM);
            $path = $args['dest-path'] . '/schema_update_' . $format . '.sql';

            $dir = dirname($path);
            if (!is_dir($dir)) {
                mkdir($dir, 0775, true);
            }
            file_put_contents($path, "-- SCHEMA UPDATE $format \n\n", FILE_APPEND);

            foreach ($sql as $part) {
                file_put_contents($path, "$part;\n\n", FILE_APPEND);
            }
            chmod($path, 0664);
            WP_CLI::success("Succesfully exported the Update Schema to $path");
        } else {
            foreach ($sql as $part) {
                WP_CLI::line($part);
            }
            WP_CLI::success("Done!");
        }
    }

    /**
     * Dump Entity classes from the current database schema.
     *
     * This is a modified version of the orm:create-entities command, which may or may not work.
     */
    #[WpCliCommand(name: 'dump-entities',
        description: 'Dump Entity classes from the current database schema.',
        shortDescription: 'Dump entities')]
    public function dump_entities($args, $flags)
    {
        $args = array_filter($args, fn($v) => $v !== '--');
        if (!$args || !($destPath = current($args))) {
            WP_CLI::error("Missing destination path");
        }

        $namespace = $flags['namespace'] ?? 'Database\\Entities';

        $entityGenerator = new EntityGenerator();
        $entityGenerator->setGenerateAnnotations(true);
        $entityGenerator->setGenerateStubMethods(true);
        $entityGenerator->setRegenerateEntityIfExists(false);
        $entityGenerator->setUpdateEntityIfExists(false);
        $entityGenerator->setNumSpaces(4);

        /** @var AnnotationExporter $annotationExporter */
        $annotationExporter = (new ClassMetadataExporter())->getExporter('annotation');
        $annotationExporter->setEntityGenerator($entityGenerator);

        $em = $this->entityManager;

        // add the enum type as string (https://github.com/doctrine/dbal/issues/3161#issuecomment-635419448)
        $databaseDriver = new DatabaseDriver($this->getEnhancedSchemaManager($em));
        $em->getConfiguration()->setMetadataDriverImpl($databaseDriver);

        $cmf = new DisconnectedClassMetadataFactory();
        $cmf->setEntityManager($em);
        try {
            $metadata = $cmf->getAllMetadata();
        } catch (\Throwable $throwable) {
            WP_CLI::error($throwable->getMessage());
        }

        if ($metadata) {
            WP_CLI::line(sprintf('Importing mapping information from "<info>%s</info>" entity manager', 'default'));
            foreach ($metadata as $class) {
                $className = $class->name;
                $class->name = $namespace . '\\' . $className;
                $path = $destPath . '/' . str_replace('\\', '.', $className) . '.php';

                WP_CLI::colorize("  > writing %G$path%n \n");
                $code = $annotationExporter->exportClassMetadata($class);
                $dir = dirname($path);
                if (!is_dir($dir)) {
                    mkdir($dir, 0775, true);
                }

                file_put_contents($path, $code);
                chmod($path, 0664);
            }

            WP_CLI::success("Exported " . count($metadata) . ' entities!');
        }
        WP_CLI::error('Database does not have any mapping information.');
    }

    /**
     * Generate an array of SQL statements to update the database schema for classes in the specified plugin.
     *
     * This is a modified version of the orm:update-schema command.
     *
     * @return void
     * @throws WP_CLI\ExitException
     * @throws \Doctrine\DBAL\Exception
     */
    #[WpCliCommand(name: 'review',
        description: 'Generate an array of SQL statements to update the database schema for classes in the specified plugin.',
        shortDescription: 'Review the database schema')]
    public function review(array $args, array $flags)
    {
        $args = array_filter($args, fn($v) => $v !== '--');
        if ($args && ($path = current($args))) {
            $args = ['dest-path' => $path];
        }

        if (!isset($flags['prefix'])) {
            WP_CLI::error("You must set a plugin table namespace to update schemas");
        }
        $tablePrefix = $flags['prefix'];

        $em = $this->entityManager;

        $pluginClasses = array_keys(array_filter(
            apply_filters('doctrine_plugin_entity_table_prefixes', []),
            fn($prefix) => $prefix === $tablePrefix));

        WP_CLI::line("Dumping update schema for the following classes, filtered by plugin table prefix {$tablePrefix}: \n"
            . join(",\n", $pluginClasses));

        $tool = new DatabaseSchemaTool($em, $this->getEnhancedSchemaManager($em));

        try {
            global $wpdb;
            $sql = $tool->getUpdateSchemaSql($em->getMetadataFactory()->getAllMetadata(), $wpdb->prefix . $tablePrefix . '_');
        } catch (\Throwable $throwable) {
            WP_CLI::error($throwable->getMessage());
        }

        foreach ($sql as $part) {
            WP_CLI::line("$part;");
        }
    }


    /**
     * Execute the SQL statements to update the database schema for classes in the specified plugin.
     *
     * This is a modified version of the orm:update-schema command.
     *
     * @return void
     * @throws WP_CLI\ExitException
     * @throws \Doctrine\DBAL\Exception
     */
    #[WpCliCommand(name: 'migrate',
        description: 'Execute the SQL statements to update the database schema for classes in the specified plugin.',
        shortDescription: 'Migrate the database schema')]
    public function migrate(array $args, array $flags)
    {
        $args = array_filter($args, fn($v) => $v !== '--');
        if ($args && ($path = current($args))) {
            $args = ['dest-path' => $path];
        }

        if (!isset($flags['prefix'])) {
            WP_CLI::error("You must set a plugin table namespace to update schemas");
        }
        $tablePrefix = $flags['prefix'];

        $em = $this->entityManager;

        $pluginClasses = array_keys(array_filter(
            apply_filters('doctrine_plugin_entity_table_prefixes', []),
            fn($prefix) => $prefix === $tablePrefix));

        WP_CLI::line("Dumping update schema for the following classes, filtered by plugin table prefix {$tablePrefix}: \n"
            . join(",\n", $pluginClasses));

        $tool = new DatabaseSchemaTool($em, $this->getEnhancedSchemaManager($em));

        try {
            global $wpdb;
            $sql = $tool->getUpdateSchemaSql($em->getMetadataFactory()->getAllMetadata(), $wpdb->prefix . $tablePrefix . '_');
        } catch (\Throwable $throwable) {
            WP_CLI::error($throwable->getMessage());
        }

        $em->getConnection()->executeStatement('SET FOREIGN_KEY_CHECKS = 0;');
        foreach ($sql as $part) {
            WP_CLI::line("Executing: \"$part\"");
            $em->getConnection()->executeStatement("$part;");
        }
        $em->getConnection()->executeStatement('SET FOREIGN_KEY_CHECKS = 1;');

        WP_CLI::success("Done!");
    }

    /**
     * @param EntityManagerInterface $em
     * @return ClassMetadata[]
     */
    private function getNonStandardClassMetadata(EntityManagerInterface $em): array
    {
        try {
            $allMetadata = $em->getMetadataFactory()->getAllMetadata();
        } catch (\Throwable $throwable) {
            WP_CLI::error($throwable->getMessage());
        }
        $standardNamespaces = ['WordpressModels\\ORM\\Entity', 'WordpressModels\\ORM\\WooCommerceEntity'];
        return array_filter($allMetadata, function (ClassMetadata $meta) use ($standardNamespaces) {
            return !array_filter($standardNamespaces, fn(string $namespace) => str_starts_with($meta->namespace, $namespace));
        });
    }

    /**
     * Get an enhanced schema manager that can handle the enum type.
     *
     * @param EntityManagerInterface $entityManager
     * @return DatabaseDriver
     * @throws \Doctrine\DBAL\Exception
     */
    private function getEnhancedSchemaManager(EntityManagerInterface $entityManager): MySQLSchemaManager
    {
        $platform = new MySQL80Platform();
        $platform->registerDoctrineTypeMapping('enum', 'string');

        return new MySQLSchemaManager($entityManager->getConnection(), $platform);
    }

}
