<?php

namespace AutoAdmin\Bundle\Admin\Builder;

use AutoAdmin\Bundle\Admin\Configuration\ConfigManager;
use AutoAdmin\Bundle\Admin\Exception\UnknownActionException;
use AutoAdmin\Bundle\Admin\Exception\UnknownEntityException;
use AutoAdmin\Bundle\Admin\Finder\EntityFinder;
use AutoAdmin\Bundle\Admin\Helper\AccessHelper;
use AutoAdmin\Bundle\Admin\Helper\EntityHelper;
use AutoAdmin\Bundle\Admin\Helper\StringifierHelper;
use AutoAdmin\Bundle\Admin\Model\EntityFilterModel;
use AutoAdmin\Bundle\Admin\Router\AdminRouter;
use AutoAdmin\Bundle\Admin\Search\EntitySearchHelper;
use AutoAdmin\Bundle\Content\Model\TranslatableInterface;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
use Exception;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Security\Core\User\UserInterface;

class TableBuilder
{
    const SEARCHABLE_TYPE = [
        'string',
        'text',
        'integer',
    ];

    const SORTABLE_TYPE = [
        'string',
        'text',
        'int',
        'integer',
        'float',
        'datetime',
        'boolean',
    ];

    const FILTERABLE_TYPE = [
        'boolean',
        'date',
        'datetime',
    ];

    private EntityManager $entityManager;
    private ConfigManager $configManager;
    private EntityHelper $entityHelper;
    private PropertyAccessor $accessor;
    private array $entityConfig;
    private AdminRouter $adminRouter;
    private EntityFinder $finder;
    private AccessHelper $accessHelper;
    private StringifierHelper $stringifierHelper;
    private FormFactoryInterface $formFactory;
    private EntitySearchHelper $entitySearchHelper;

    public function __construct(
        EntityManagerInterface $entityManager,
        ConfigManager          $configManager,
        EntityHelper           $entityHelper,
        AdminRouter            $adminRouter,
        AccessHelper           $accessHelper,
        StringifierHelper      $stringifierHelper,
        EntitySearchHelper     $entitySearchHelper
    )
    {
        $this->entityManager = $entityManager;
        $this->configManager = $configManager;
        $this->entityHelper = $entityHelper;
        $this->adminRouter = $adminRouter;

        $this->finder = new EntityFinder($this->entityManager);
        $this->accessor = new PropertyAccessor(
            PropertyAccessor::DISALLOW_MAGIC_METHODS,
            PropertyAccessor::THROW_ON_INVALID_INDEX | PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH
        );
        $this->accessHelper = $accessHelper;
        $this->stringifierHelper = $stringifierHelper;
        $this->entitySearchHelper = $entitySearchHelper;
    }

    public function buildEntityArray($entity, UserInterface $user = null): array
    {
        $class = get_class($entity);
        $this->entityConfig = $this->configManager->getEntityConfigByClass($class);

        $return = [
            'head' => ['entity.view.label', 'entity.view.value'],
            'body' => [],
            'foot' => [],
        ];
        $viewConfig = $this->accessor->getValue($this->entityConfig, '[view][fields]');
        foreach ($viewConfig as $item) {
            $label = $item['label'] ?? $item['property'] ?? '--';
            try {
                $value = $this->accessor->getValue($entity, $item['property']);
            } catch (Exception $e) {
                $value = '--';
            }
            $return['body'][$label] = $this->stringifierHelper->stringify($value);
        }

        if (empty($return['body'])) {
            $return['body']['--'] = '--';
        }

        return $return;
    }

    public function buildEntityDatatableArray(array $entityConfig, EntityFilterModel $filterModel, array $entities): array
    {
        $metadata = $this->entityManager->getClassMetadata($class = $entityConfig['class']);
        $translationMetadata = null;
        if ($entityConfig['translation_class']) {
            $translationMetadata = $this->entityManager->getClassMetadata($entityConfig['translation_class']);
        }

        $this->entityConfig = $entityConfig;

        $fields = $this->getFieldsToDisplay($metadata);

        return [
            'search' => $this->buildSearch($fields, $metadata, $translationMetadata),
            'head' => $this->buildHead($fields, $metadata, $translationMetadata),
            'body' => $this->buildBody($fields, $entities),
            'foot' => $this->buildFoot($filterModel),
        ];
    }

    private function getFieldsToDisplay(ClassMetadata $metadata)
    {
        $fields = [];
        if ($this->accessor->getValue($this->entityConfig, '[list][group_actions]')) {
            $fields[] = [
                'property' => 'group_actions',
                'label' => 'Actions Groupées',
            ];
        }

        if (empty($this->accessor->getValue($this->entityConfig, '[list][fields]'))) {
            return array_merge($fields, array_map(
                function ($field) {
                    return [
                        'label' => $field,
                        'property' => $field,
                    ];
                },
                $metadata->getFieldNames()
            ));
        }

        return array_merge($fields, $this->accessor->getValue($this->entityConfig, '[list][fields]'));
    }

    public function buildSearchConfigArray(array $entityConfig): array
    {
        $metadata = $this->entityManager->getClassMetadata($class = $entityConfig['class']);
        $translationMetadata = null;
        if ($entityConfig['translation_class']) {
            $translationMetadata = $this->entityManager->getClassMetadata($entityConfig['translation_class']);
        }

        $this->entityConfig = $entityConfig;

        $fields = $this->getFieldsToDisplay($metadata);

        return $this->buildSearch($fields, $metadata, $translationMetadata);
    }

    private function buildSearch(
        array          $fields,
        ClassMetadata  $metadata,
        ?ClassMetadata $translationMetadata
    ): array
    {
        $search = [];
        foreach ($fields as $field) {
            $property = $realProperty = $field['property'];
            $name = !empty($field['label']) ? $field['label'] : $property;
            $meta = $metadata;

            if (false !== stripos($property, '_tr.')) {
                $meta = $translationMetadata;
                $property = str_replace('_tr.', '', $property);
            }

            $data = [
                'name' => $name,
                'property' => $property,
                'real_property' => $realProperty,
                'searchable' => false,
                'sortable' => false,
                'filterable' => false,
            ];

            if ($realProperty === 'group_actions') {
                $search[$realProperty] = $data;
                continue;
            }

            try {
                if ($meta->hasAssociation($property)) {
                    $fieldMapping = $meta->getAssociationMapping($property);
                } else {
                    $fieldMapping = $meta->getFieldMapping($property);
                }

                $data = array_merge(
                    $data,
                    [
                        'mapping' => $fieldMapping,
                    ]
                );

                $data = $this->createSearchDataForField($data, $meta);


            } catch (MappingException $exception) {
            }
            $search[$realProperty] = $data;
        }


        // Add fake last col
        $search['actions'] = [
            'name' => 'Actions',
            'searchable' => false,
            'sortable' => false,
            'filterable' => false,
        ];

        return $search;
    }

    private function createSearchDataForField(array $data, ClassMetadata $meta): array
    {
        $property = $data['property'];

        $data = array_merge(
            $data,
            [
                'searchable' => false,
                'sortable' => true,
                'filterable' => true,
            ]
        );

        if ($meta->hasAssociation($property)) {
            $fieldMapping = $meta->getAssociationMapping($property);
        } else {
            $fieldMapping = $meta->getFieldMapping($property);
            $data = array_merge(
                $data,
                [
                    'searchable' => in_array($fieldMapping['type'], self::SEARCHABLE_TYPE),
                    'sortable' => in_array($fieldMapping['type'], self::SORTABLE_TYPE),
                    'filterable' => in_array($fieldMapping['type'], self::FILTERABLE_TYPE),
                ]
            );
        }

        $form = $this->entitySearchHelper->createForm($fieldMapping);
        $data['formView'] = $form ? $form->createView() : null;
        $data['form'] = $form ?? null;

        return $data;
    }

    private function buildHead(
        array          $fields,
        ClassMetadata  $metadata,
        ?ClassMetadata $translationMetadata
    ): array
    {
        $headLabels = [];
        foreach ($fields as $field) {
            $property = $realProperty = $field['property'];
            $name = !empty($field['label']) ? $field['label'] : $property;
            $meta = $metadata;
            if (false !== stripos($property, '_tr.')) {
                $meta = $translationMetadata;
                $property = str_replace('_tr.', '', $property);
            }

            $data = [
                'name' => $name,
                'property' => $property,
                'searchable' => false,
                'sortable' => false,
                'filterable' => false,
            ];

            try {
                $fieldMapping = $meta->getFieldMapping($property);
                $data = array_merge(
                    $data,
                    [
                        'name' => $name,
                        'sortable' => in_array($fieldMapping['type'], self::SORTABLE_TYPE),
                    ]
                );
            } catch (MappingException $exception) {
            }

            $headLabels[$realProperty] = $data;

        }

        $actions = $this->accessor->getValue($this->entityConfig, '[list][item_actions]');
        if (!empty($actions)) {
            // Add Actions
            $headLabels['actions'] = [
                'name' => 'Actions',
                'searchable' => false,
                'sortable' => false,
                'filterable' => false,
            ];
        }

        return $headLabels;
    }

    private function buildBody(array $fields, array $entities): array
    {
        $bodyValues = [];

        foreach ($entities as $entity) {
            $entityValuesArray = [];
            foreach ($fields as $fieldNameAndLabel) {
                if ($fieldNameAndLabel['property'] === 'group_actions') {
                    $entityValuesArray[$fieldNameAndLabel['property']] = '<input type="checkbox" class="form-control group-actions-checkbox" data-id="' . $entity->getId() . '">';
                    continue;
                }
                $subject = $entity;
                if (
                    $entity instanceof TranslatableInterface
                    && strpos($fieldNameAndLabel['property'], '_tr.') !== false
                ) {

                    $subject = $entity->getCurrentTranslation() ?? $entity;

                }
                $prop = str_replace('_tr.', '', $fieldNameAndLabel['property']);
                try {
                    $value = $this->accessor->getValue($subject, $prop);
                } catch (Exception $e) {
                    $value = '--';
                }
                $visualValue = $this->stringifierHelper->stringify($value, $prop === 'id');
                $entityValuesArray[$fieldNameAndLabel['property']] = $visualValue;
            }
            $actions = $this->buildColAction($entity, 'list');
            if (!empty($actions)) {
                $entityValuesArray['actions'] = $actions;
            }
            $bodyValues[] = $entityValuesArray;
        }

        return $bodyValues;
    }

    private function buildFoot(EntityFilterModel $filterModel)
    {
        $totalResults = (int)$this->finder->find($filterModel, $this->entityConfig, true);

        $minPage = 1;
        $currentPage = $filterModel->getCurrentPage();
        $maxPage = max(1, (int)ceil($totalResults / $filterModel->getMaxItems()));

        $hasPrevPage = !($minPage === $currentPage);
        $hasNextPage = $currentPage + 1 <= $maxPage;

        return [
            'hasPrevPage' => $hasPrevPage,
            'hasNextPage' => $hasNextPage,
            'minPage' => $minPage,
            'currentPage' => $currentPage,
            'maxPage' => $maxPage,
            'perPage' => $filterModel->getMaxItems(),
            'totalResults' => $totalResults,
        ];
    }

    /**
     * @throws UnknownEntityException
     * @throws UnknownActionException
     */
    private function buildColAction($entity, string $zone): bool|array
    {
        $class = $this->entityHelper->guessFqcn($entity);
        $actions = $this->accessor->getValue($this->entityConfig, '[' . $zone . '][item_actions]');
            $class = $this->entityHelper->guessFqcn($entity);
            $actions = $this->accessor->getValue($this->entityConfig, '['.$zone.'][item_actions]');
            if (empty($actions)) {
                return false;
            }

            foreach ($actions as $action => $config) {
                $isGranted = $this->accessHelper->isActionAllowed($class, $action);
                if ($isGranted) {
                    $actions[$action]['link'] = $this->adminRouter->generate($entity, $action);
                } else {
                    unset($actions[$action]);
                }
        }

        return $actions;
    }

    /**
     * Return fieldName or label (if set) if field is displayable
     * else return FALSE
     */
    private function getLabelForDisplayableField($fieldName): bool|string
    {
        $listConfig = $this->accessor->getValue($this->entityConfig, '[view]');

        // Field key is not set, display field by default
        if (empty($listConfig['fields'])) {
            return false;
        }

        foreach ($listConfig['fields'] as $fieldConfig) {
            if (!empty($fieldConfig['property']) && $fieldConfig['property'] === $fieldName) {
                return $fieldConfig['label'] ?? $fieldConfig['property'];
            }
        }

        return false;
    }
}
