<?php

declare(strict_types=1);

namespace Mautic\ProjectBundle\Service;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Model\FormModel;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\ProjectBundle\DTO\EntityTypeConfig;
use Mautic\ProjectBundle\Entity\Project;
use Mautic\ProjectBundle\Event\EntityTypeDetailRouteEvent;
use Mautic\ProjectBundle\Event\EntityTypeModelMappingEvent;
use Mautic\ProjectBundle\Event\EntityTypeNormalizationEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

final class ProjectEntityLoaderService
{
    /** @var array<string, EntityTypeConfig> */
    private array $entityTypesCache = [];

    public function __construct(
        private EntityManagerInterface $em,
        private TranslatorInterface $translator,
        private ModelFactory $modelFactory,
        private CorePermissions $security,
        private EventDispatcherInterface $eventDispatcher,
    ) {
    }

    /**
     * @param array<string, EntityTypeConfig> $entityTypes
     *
     * @return array<string, array<string, mixed>>
     */
    public function getProjectEntities(Project $project, array $entityTypes): array
    {
        $results = [];

        foreach ($entityTypes as $entityType => $config) {
            $repository = $config->model->getRepository();

            $entities   = $repository->createQueryBuilder('e')
                ->join('e.projects', 'p')
                ->where('p.id = :projectId')
                ->setParameter('projectId', $project->getId())
                ->orderBy('e.dateModified', 'DESC')
                ->getQuery()
                ->getResult();

            $results[$entityType] = [
                'label'       => $config->label,
                'entities'    => $entities,
                'count'       => count($entities),
                'detailRoute' => $config->detailRoute,
            ];
        }

        return $results;
    }

    /**
     * Get entity types filtered by user view permissions.
     *
     * @return array<string, EntityTypeConfig>
     */
    public function getEntityTypesWithViewPermissions(): array
    {
        return $this->filterEntityTypesByPermission('view');
    }

    /**
     * Get entity types filtered by user edit permissions.
     *
     * @return array<string, EntityTypeConfig>
     */
    public function getEntityTypesWithEditPermissions(): array
    {
        return $this->filterEntityTypesByPermission('edit');
    }

    /**
     * Get lookup results for entity type (used by EntityLookupType).
     *
     * @return array<int|string, string>
     */
    public function getLookupResults(string $entityType, string $filter = '', int $limit = 10, int $start = 0, ?int $projectId = null): array
    {
        $entityTypes = $this->getEntityTypes();

        if (!isset($entityTypes[$entityType])) {
            return [];
        }

        $entityConfig = $entityTypes[$entityType];

        // Check permission before proceeding
        if (!$this->hasViewPermissionForEntityType($entityConfig)) {
            return [];
        }
        $repository   = $entityConfig->model->getRepository();

        // Get the label column for this entity type
        $labelColumn = $this->getEntityLabelColumn($entityType);

        $qb = $repository->createQueryBuilder('e')
            ->select('e.id, e.'.$labelColumn.' as name')
            ->setFirstResult($start);

        if ($limit > 0) {
            $qb->setMaxResults($limit);
        }

        // Exclude entities already assigned to the specific project if projectId is provided
        if ($projectId) {
            // Use subQuery to handle many to many join scenario to find entities that are NOT in the specific project
            $qb->andWhere($qb->expr()->notIn('e.id',
                $this->em->createQueryBuilder()
                    ->select('e2.id')
                    ->from($entityConfig->entityClass, 'e2')
                    ->join('e2.projects', 'p2')
                    ->where('p2.id = :projectId')
                    ->getDQL()
            ))
            ->setParameter('projectId', $projectId);
        }

        // Add filter if provided
        if (!empty($filter)) {
            $qb->andWhere('e.'.$labelColumn.' LIKE :filter')
                ->setParameter('filter', '%'.$filter.'%');
        }

        $results = $qb->getQuery()->getArrayResult();

        // Format results for ProjectModel (id => name associative array)
        $choices = [];
        foreach ($results as $result) {
            $choices[$result['id']] = $result['name'];
        }

        return $choices;
    }

    /**
     * Check if user has view permission for entity type config.
     */
    public function hasViewPermissionForEntityType(EntityTypeConfig $config): bool
    {
        $permissionBase = $config->model->getPermissionBase();
        $permissions    = [
            $permissionBase.':viewown',
            $permissionBase.':viewother',
        ];

        return $this->security->isGranted($permissions, 'MATCH_ONE');
    }

    /**
     * Check if user has edit permission for entity type config.
     */
    public function hasEditPermissionForEntityType(EntityTypeConfig $config): bool
    {
        $permissionBase = $config->model->getPermissionBase();
        $permissions    = [
            $permissionBase.':editown',
            $permissionBase.':editother',
        ];

        return $this->security->isGranted($permissions, 'MATCH_ONE');
    }

    /**
     * @return array<string, EntityTypeConfig>
     */
    private function getEntityTypes(): array
    {
        if (!empty($this->entityTypesCache)) {
            return $this->entityTypesCache;
        }

        $allMetadata = $this->em->getMetadataFactory()->getAllMetadata();

        // Example mapping; only register what actually exists / is permitted.
        $routeEvent = new EntityTypeDetailRouteEvent();
        $this->eventDispatcher->dispatch($routeEvent);

        foreach ($allMetadata as $metadata) {
            $entityClass = $metadata->getName();

            foreach ($metadata->getAssociationMappings() as $association) {
                if (
                    ClassMetadataInfo::MANY_TO_MANY === $association['type']
                    && Project::class === $association['targetEntity']
                ) {
                    $shortName  = $metadata->getReflectionClass()->getShortName();
                    $entityType = $this->normalizeEntityType(strtolower($shortName));

                    $this->entityTypesCache[$entityType] = new EntityTypeConfig(
                        entityClass: $entityClass,
                        label: $this->getEntityLabel($entityType),
                        model: $this->findModelForEntityType($entityType),
                        detailRoute: $routeEvent->getRoute($entityType),
                    );

                    break;
                }
            }
        }

        // Sort entity types alphabetically by label
        uasort($this->entityTypesCache, fn (EntityTypeConfig $a, EntityTypeConfig $b) => strcasecmp($a->label, $b->label));

        return $this->entityTypesCache;
    }

    /**
     * Filter entity types by permission type.
     *
     * @return array<string, EntityTypeConfig>
     */
    private function filterEntityTypesByPermission(string $permissionType): array
    {
        $allEntityTypes     = $this->getEntityTypes();
        $allowedEntityTypes = [];

        foreach ($allEntityTypes as $entityType => $config) {
            $hasPermission = 'view' === $permissionType
                ? $this->hasViewPermissionForEntityType($config)
                : $this->hasEditPermissionForEntityType($config);

            if ($hasPermission) {
                $allowedEntityTypes[$entityType] = $config;
            }
        }

        return $allowedEntityTypes;
    }

    /**
     * Get the label column name for an entity type.
     */
    private function getEntityLabelColumn(string $entityType): string
    {
        return match ($entityType) {
            'asset', 'page' => 'title',
            default         => 'name',
        };
    }

    /**
     * Normalize entity type names for consistent usage.
     */
    private function normalizeEntityType(string $entityType): string
    {
        // Create event with default mappings
        $event = new EntityTypeNormalizationEvent([
            'leadlist'       => 'segment',
            'lead'           => 'contact',
            'dynamiccontent' => 'dynamicContent',
            'trigger'        => 'pointtrigger',
        ]);

        // Dispatch event to allow bundles to add their mappings
        $this->eventDispatcher->dispatch($event);

        // Return normalized type or original if no mapping exists
        return $event->getNormalizedType($entityType);
    }

    private function getEntityLabel(string $entityType): string
    {
        // Try possible translation keys in order
        $keys = [
            "mautic.project.$entityType",
            "mautic.$entityType.$entityType",
        ];

        foreach ($keys as $key) {
            $translated = $this->translator->trans($key);
            if ($translated !== $key) {
                return $translated;
            }
        }

        // Fallback: Capitalize the entity type if no translation was found
        return ucfirst($entityType);
    }

    private function findModelForEntityType(string $entityType): FormModel
    {
        // Create event with default model key mappings
        $event = new EntityTypeModelMappingEvent([
            'segment'        => 'lead.list',
            'message'        => 'channel.message',
            'company'        => 'lead.company',
            'dynamicContent' => 'dynamicContent.dynamicContent',
            'pointtrigger'   => 'point.trigger',
        ]);

        // Dispatch event to allow bundles to add their model mappings
        $this->eventDispatcher->dispatch($event);

        // Get model key from event or use entity type as fallback
        $modelKey = $event->getModelKey($entityType);

        $model = $this->modelFactory->getModel($modelKey);
        \assert($model instanceof FormModel);

        return $model;
    }
}
