<?php


namespace AutoAdmin\Bundle\Content\DependencyInjection\Compiler;

use AutoAdmin\Bundle\Admin\DynamicEntity\Administrator;
use AutoAdmin\Bundle\Admin\Repository\AdministratorRepository;
use AutoAdmin\Bundle\Content\DependencyInjection\Traits\RegisterRepositoriesTrait;
use AutoAdmin\Bundle\Content\Doctrine\Repository\ContainerRepositoryFactory;
use AutoAdmin\Bundle\Content\Doctrine\Repository\RepositoryServiceIdGenerator;
use AutoAdmin\Bundle\Content\Entity\AbstractTranslatable;
use AutoAdmin\Bundle\Content\Entity\AbstractUntranslated;
use AutoAdmin\Bundle\Content\Entity\Page;
use AutoAdmin\Bundle\Content\Repository\AbstractContentRepository;
use AutoAdmin\Bundle\Content\Repository\DefaultContentRepository;
use AutoAdmin\Bundle\Content\Repository\DefaultPageRepository;
use AutoAdmin\Bundle\Content\Repository\DefaultTranslatableContentRepository;
use AutoAdmin\Bundle\Content\Site\SiteContextHelper;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass;
use Doctrine\Common\Annotations\PsrCachedReader;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\Persistence\ManagerRegistry;
use Exception;
use ReflectionClass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

class ContentRepositoryCompilerPass implements CompilerPassInterface
{
    use RegisterRepositoriesTrait;

    const CONTENT_REPOSITORY_TAG = 'aa_content.repository';

    /**
     * @inheritDoc
     */
    public function process(ContainerBuilder $container)
    {
        // replace default doctrine repository factory to be able to auto-link repositories when undeclared
        $definition = $container->getDefinition($defId = 'doctrine.orm.container_repository_factory');
        $definition->setClass(ContainerRepositoryFactory::class);
        $container->setDefinition($defId, $definition);


        $doctrineConfigs = $container->getExtensionConfig('doctrine');
        $entityPaths = [];
        foreach ($doctrineConfigs as $doctrineConfig) {

            if (!empty($doctrineConfig['orm']['mappings'])) {
                foreach ($doctrineConfig['orm']['mappings'] as $alias => $mapping) {
                    $entityPath = $mapping['dir'];
                    $entityPath = str_replace(
                        '%kernel.project_dir%',
                        $container->getParameter('kernel.project_dir'),
                        $entityPath
                    );
                    $entityPath = realpath($entityPath);
                    $entityPaths[$mapping['prefix']] = $entityPath;
                }
            }
        }

        $config = $container->getParameter('aa_content.config');
        if (empty($config['contents'])) {
            return;
        }

        /**
         * For our contents, auto-define repository if entity annotation does not already set one
         */
        /** @var PsrCachedReader $reader */
        $reader = $container->get('annotations.cached_reader');
        foreach ($entityPaths as $namespace => $entityPath) {
            $glob = $this->recursiveFiles($entityPath, '/.+\.php/');
            $glob = $this->filterEntityFiles($glob);

            foreach ($glob as $filepath) {
                $basename = basename($filepath);
                $namespaceSuffix = $filepath;
                $namespaceSuffix = str_replace($entityPath, '', $namespaceSuffix);
                $namespaceSuffix = str_replace(DIRECTORY_SEPARATOR.$basename, '', $namespaceSuffix);
                $namespaceSuffix = str_replace(DIRECTORY_SEPARATOR, "\\", $namespaceSuffix);

                $className = str_replace('.php', '', $basename);

                $entityClass = $namespace.$namespaceSuffix.'\\'.$className;

                $refl = new ReflectionClass($entityClass);
                /** @var Entity $annotation */
                $annotation = $reader->getClassAnnotation($refl, Entity::class);
                if (empty($annotation)) {
                    continue;
                }

                $idGenerator = new RepositoryServiceIdGenerator();
                $serviceId = null;

                if (!$refl->isSubclassOf(AbstractUntranslated::class)) {
                    continue;
                }

                $guessedRepo = $this->getRepoClassFromEntity($entityClass);
                if (empty($annotation->repositoryClass)) {

                    if (!class_exists($guessedRepo)) {
//                        throw new \Exception(sprintf('No repo on %s entity, and guessed repo does not exist', $entityClass));
//                        dump($guessedRepo);
                    }

                    $repoClass = $guessedRepo;
                    $serviceId = $repoClass;
                } else {
                    if ($refl->isSubclassOf(Page::class)) {
                        $repoClass = DefaultPageRepository::class;
                    } elseif ($refl->isSubclassOf(AbstractTranslatable::class)) {
                        $repoClass = DefaultTranslatableContentRepository::class;
                    } elseif ($refl->isSubclassOf(AbstractUntranslated::class)) {
                        $repoClass = DefaultContentRepository::class;
                    } else {
                        $repoClass = $this->getRepoClassFromEntity($entityClass);
                    }
                    $serviceId = $idGenerator->generate($entityClass);
                }

                if ($container->hasDefinition($serviceId)) {
                    $definition = $container->getDefinition($serviceId);
                } else {
                    $definition = new Definition($this->getRepoClassFromEntity($entityClass));
                    $definition->setClass($repoClass);

                }

                $this->configure($definition, $entityClass, $repoClass);
                if (!class_exists($repoClass)) {
                    $container->setAlias($idGenerator->generate($entityClass), $repoClass);
                }
            }
        }

        // if some services only define content repo tag, configure them properly
        $services = $container->findTaggedServiceIds(self::CONTENT_REPOSITORY_TAG);

        foreach ($services as $service => $v) {
            /** @var Definition $definition */
            $definition = $container->getDefinition($service);

            if (
                ($repoClass = $definition->getClass())
                && $repoClass === AdministratorRepository::class
            ) {
                $entityClass = Administrator::class;
            } else {
                $entityClass = $this->getEntityClassFromRepo($repoClass);
            }

            $this->configure($definition, $entityClass, $repoClass);
            if (!class_exists($repoClass)) {
                $container->setAlias($idGenerator->generate($entityClass), $repoClass);
            }
        }
        $this->reRegisterRepositories($container);
    }

    public function getRepoClassFromEntity(string $entityClass): string
    {
        $repoClass = str_replace('\\Entity\\', '\\Repository\\', $entityClass);
        $repoClass = $repoClass.'Repository';

        return $repoClass;
    }

    public function configure(Definition $definition, string $entityClass, string $repositoryClass): Definition
    {
        $definition->setPublic(true);

        $tags = [
            self::CONTENT_REPOSITORY_TAG,
            ServiceRepositoryCompilerPass::REPOSITORY_SERVICE_TAG,
        ];
        foreach ($tags as $tag) {
            if (!$definition->hasTag($tag)) {
                $definition->addTag($tag);
            }
        }

        // if ctor is not overriden by final class, setup arguments for parent _construct call
        if (!class_exists($repositoryClass)) {
            // if "fake" service because no repo class exists
            $overriddenCtor = false;
        } else {
            $ctor = (new ReflectionClass($repositoryClass))->getMethod('__construct');
            $overriddenCtor = $ctor->class === $definition->getClass();
        }

        if (
            empty($definition->getArguments())// no service definition overriding args
            && (
                // either no construct override or override keeping same arguments
                !$overriddenCtor || $ctor && (
                    count($ctor->getParameters()) === 2
                    && $ctor->getParameters()[0]->getName() === 'registry'
                    && $ctor->getParameters()[1]->getName() === 'entityClass'
                )
            )
        ) {
            $definition->setArgument(0, new Reference(ManagerRegistry::class));
            $definition->setArgument(1, $entityClass);
        }

        if (!$definition->hasMethodCall('setSiteContextHelper')) {
            $definition->addMethodCall('setSiteContextHelper', [new Reference(SiteContextHelper::class)]);
        }

        return $definition;
    }

    public function getEntityClassFromRepo(string $repositoryClass): string
    {
        $entityClass = str_replace('\\Repository\\', '\\Entity\\', $repositoryClass);
        $entityClass = substr($entityClass, 0, -strlen('Repository'));

        return $entityClass;
    }

    private function validateRepo(string $entityClass, string $repositoryClass): bool
    {
        $entityRefl = new ReflectionClass($entityClass);
        $repositoryRefl = new ReflectionClass($repositoryClass);
        $isAAContent = $entityRefl->isSubclassOf(AbstractUntranslated::class);
        $isAAContentRepository = $repositoryRefl->isSubclassOf(AbstractContentRepository::class);
        if (!$isAAContent && !$isAAContentRepository) {
            //dump('cannot define repo');

            return false;
        } elseif (!$isAAContent && !$isAAContentRepository) {
            //dump('cannot define repo');

            return false;
        } elseif (!$isAAContent || !$isAAContentRepository) {
            if (!$isAAContent) {
                $error = sprintf(
                    'Entity "%s" should extend at least "%s" (as its repository seems to properly be extending "%s")',
                    $entityClass,
                    AbstractUntranslated::class,
                    AbstractContentRepository::class
                );
            } else {
                $error = sprintf(
                    'Entity "%s" is extending "%s" but its repository (%s) isn\'t extending "%s"',
                    $entityClass,
                    AbstractUntranslated::class,
                    $repositoryClass,
                    AbstractContentRepository::class
                );
            }
            throw new Exception($error);
        }

        return true;
    }

    protected function recursiveFiles(string $folder, $regPattern): array
    {
        $dir = new \RecursiveDirectoryIterator($folder);
        $ite = new \RecursiveIteratorIterator($dir);
        $files = new \RegexIterator($ite, $regPattern, \RegexIterator::GET_MATCH);
        $fileList = array();
        foreach ($files as $file) {
            $fileList = array_merge($fileList, $file);
        }

        return $fileList;
    }

    protected function filterEntityFiles(array $files)
    {
        $filtered = [];
        foreach ($files as $filepath) {
            $basename = basename($filepath);
            if (
                // utils (abstracts, interfaces, traits,)
                str_starts_with($basename, 'Abstract')
                || str_ends_with($basename, 'Interface.php')
                || str_ends_with($basename, 'Trait.php')
                // ignore translation part of contents
                || str_ends_with($basename, 'Translation.php')
                // other
                || str_ends_with($basename, 'Duplicator.php')
            ) {
                continue;
            }

            $filtered[] = $filepath;
        }

        return $filtered;
    }
}
