<?php

namespace AutoAdmin\Bundle\Admin\Controller\Admin;

use AutoAdmin\Bundle\Admin\Builder\LayoutBuilder;
use AutoAdmin\Bundle\Admin\Builder\TableBuilder;
use AutoAdmin\Bundle\Admin\Controller\Admin\Traits\ListTrait;
use AutoAdmin\Bundle\Admin\Controller\Exception\NotFoundActionTemplateException;
use AutoAdmin\Bundle\Admin\Exception\ForbiddenActionException;
use AutoAdmin\Bundle\Admin\Exception\UnknownActionException;
use AutoAdmin\Bundle\Admin\Exception\UnknownEntityException;
use AutoAdmin\Bundle\Admin\Form\Type\AdminContentType;
use AutoAdmin\Bundle\Admin\Helper\FlashbagHelper;
use AutoAdmin\Bundle\Admin\Model\EntityFilterModel;
use AutoAdmin\Bundle\Content\Entity\AbstractContent;
use AutoAdmin\Bundle\Content\Entity\AbstractTranslatable;
use AutoAdmin\Bundle\Content\Model\NameInterface;
use AutoAdmin\Bundle\Content\Model\PublishableInterface;
use AutoAdmin\Bundle\Content\Model\TitleInterface;
use AutoAdmin\Bundle\Content\Model\TranslatableInterface;
use AutoAdmin\Bundle\Content\Model\UpdatedAtInterface;
use AutoAdmin\Bundle\Core\Doctrine\Traits\ListenerStateManagerTrait;
use Countable;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Exception;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use ReflectionClass;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\String\ByteString;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Service\Attribute\Required;

class EntityController extends AbstractAdminController
{
    use ListTrait;
    use ListenerStateManagerTrait;

    /**
     * @var array{
     *     update: string,
     *     create: string,
     *     list: string,
     *     view: string
     * }
     */
    private array $templates;

    private readonly ValidatorInterface $validator;

    public function __construct()
    {
        $this->templates = $this->setupDefaultTemplates();
    }

    #[Required]
    public function initialize(ValidatorInterface $validator): void
    {
        $this->validator = $validator;
    }

    /**
     * Can be overridden using inheritance to add/edit action template
     *
     * @return string[]
     */
    protected function setupDefaultTemplates(): array
    {
        return [
            'create' => '@AutoAdminAdmin/entity/create.html.twig',
            'list' => '@AutoAdminAdmin/entity/list.html.twig',
            'update' => '@AutoAdminAdmin/entity/update.html.twig',
            'view' => '@AutoAdminAdmin/entity/view.html.twig',
            // can be overridden in inherited controller to add/replace templates
            // custom => /admin/controllername/action.html.twig
        ];
    }

    protected function getTemplate(string $action): string
    {
        if (!array_key_exists($action, $this->templates)) {
            throw new NotFoundActionTemplateException($action, get_class($this));
        }

        return $this->templates[$action];
    }

    protected function createEntity($class): object
    {
        return $this->contentFactory->createEntity($class);
    }

    /**
     * @throws ForbiddenActionException
     * @throws UnknownEntityException
     */
    public function create(string $entitySlug, Request $request): RedirectResponse|Response
    {
        $this->ensureActionIsAllowed($entitySlug, 'create');

        $class = $this->entityHelper->findFullClassName($entitySlug);
        $entity = $this->createEntity($class);

        if ($entity instanceof PublishableInterface) {
            $entity->setPublishable(false);
        }

        $form = $this->createForm(
            AdminContentType::class,
            $entity,
            [
                'has_submit' => true,
                'entity_action' => 'create',
            ]
        );
        $form->handleRequest($request);

        if ($form->isSubmitted()) {
            $redirect = $this->handleUpsertForm($entity, $form, $request);

            if ($redirect instanceof RedirectResponse) {
                return $redirect;
            }
        }

        return $this->createHook(
            $this->getTemplate('create'),
            [
                'headActions' => [],
                'form' => $form->createView(),
            ]
        );
    }

    public function createHook(string $template, array $vars): Response
    {
        return $this->render($template, $vars);
    }

    /**
     * @throws ForbiddenActionException
     * @throws UnknownActionException
     * @throws UnknownEntityException
     */
    public function view(Request $request): Response
    {
        $entitySlug = $request->get('entitySlug');
        $id = $request->get('id');

        $this->ensureActionIsAllowed($entitySlug, 'view');

        $entity = $this->resolveEntity($entitySlug, $id);

        /** @var TableBuilder $tableBuilder */
        $tableBuilder = $this->builderFactory->get('table');

        /** @var LayoutBuilder $layoutBuilder */
        $layoutBuilder = $this->builderFactory->get('layout');

        $headActions = $layoutBuilder->buildTopbarActions($entitySlug, $request->get('action'));

        $infoTable = $tableBuilder->buildEntityArray($entity, $this->getUser());

        return $this->viewHook(
            $this->getTemplate('view'),
            [
                'headActions' => $headActions,
                'table' => $infoTable,
                'entity' => $entity,
            ]
        );
    }

    public function viewHook(string $template, array $vars): Response
    {
        return $this->render($template, $vars);
    }

    protected function handleUpsertForm($entity, FormInterface $form, Request $request)
    {
        try {
            $class = get_class($entity);
            $data = $form->getData();

            // manually trigger doctrine listeners using UpdatedAtInterface
            if ($data instanceof UpdatedAtInterface && $data->getUpdatedAt()) {
                $clone = clone $data->getUpdatedAt();
                $data->setUpdatedAt($clone);
            }

            $this->em->persist($data);

            // Validate entity
            $errors = $this->validator->validate($data);

            if (count($errors) > 0) {
                /** @var ConstraintViolation $error */
                foreach ($errors as $error) {
                    FlashbagHelper::addError($request, $error->getMessage());
                }

                return false;
            }

            // Validate translation if it exists
            if ($data instanceof AbstractTranslatable) {
                $translation = $data->getCurrentTranslation();

                $translationErrors = $this->validator->validate($translation);
                if (count($translationErrors) > 0) {
                    /** @var ConstraintViolation $error */
                    foreach ($translationErrors as $error) {
                        FlashbagHelper::addError($request, $error->getMessage());
                    }

                    return false;
                }
            }

            $this->em->flush();

            FlashbagHelper::addSuccess($request, 'Entité sauvegardée avec succès !');

            // redirect to default action
            $entityDefaultAction = $this->configManager->getEntityConfigByClass($class)['default_action'];

            $redirectActionSlug = $entityDefaultAction;
            if ($form->get('submit_and_return')->isClicked() === false) {
                $actionConfig = $this->configManager->getActionConfig($class, 'update');
                if ($actionConfig !== null) {
                    $redirectActionSlug = 'update';
                }
            }

            $redirectUrl = $this->adminRouter->generate(
                $entity,
                $redirectActionSlug,
                ['tab' => $request->get('tab', 'default'),]
            );

            return $this->redirect($redirectUrl);
        } catch (UniqueConstraintViolationException $exception) {
            $mapping = $this->entityHelper->getManager()->getClassMetadata($class);
            $possibleDuplicatedFields = [];
            foreach ($mapping->getFieldNames() as $name) {
                if (!empty($mapping->getFieldMapping($name)['unique'])) {
                    $possibleDuplicatedFields[] = $name;
                }
            }

            foreach ($mapping->getAssociationNames() as $name) {
                if ($mapping->isSingleValuedAssociation($name)) {
                    $possibleDuplicatedFields[] = $name;
                }
            }

            $errMessage = 'Erreur, une colonne unique est dupliquée ('.implode(', ', $possibleDuplicatedFields).')';
            FlashbagHelper::addError($request, $errMessage);
        }

        return false;
    }

    /**
     * @return RedirectResponse
     * @throws ForbiddenActionException
     * @throws UnknownEntityException
     */
    public function delete(Request $request): RedirectResponse
    {
        $entitySlug = $request->get('entitySlug');

        $ids = [];
        if ($request->getMethod() === 'POST') {
            $data = json_decode($request->getContent(), true);
            $ids = $data['ids'] ?? [];
        }

        $idFromRequest = $request->get('id');
        if ($idFromRequest !== null) {
            $ids[] = $idFromRequest;
        }

        $this->ensureActionisAllowed($entitySlug, 'delete');

        try {
            foreach ($ids as $id) {
                $entity = $this->resolveEntity($entitySlug, $id);

                $this->em->remove($entity);
            }

            $this->em->flush();

            FlashbagHelper::addSuccess($request, 'Entité supprimée avec succès');
        } catch (Exception $e) {
            $errorMessage = 'Une erreur est survenue lors de la suppression de l\'entité.';

            if ($e instanceof ForeignKeyConstraintViolationException) {
                $regex = '|(.+)a foreign key constraint fails \(`(?<db>[a-z-_]+)`.`(?<table>[a-z-_]+)`(.+)|';
                $matched = preg_match($regex, $e->getMessage(), $matches);
                if ($matched !== false) {
                    $table = $matches['table'];
                    $metadata = $this->entityHelper->getManager()->getMetadataFactory()->getAllMetadata();
                    foreach ($metadata as $metadatum) {
                        if ($table === $metadatum->table['name']) {
                            $fqcn = $metadatum->getReflectionClass()->getName();
                            $config = $this->configManager->getEntityConfigByClass($fqcn);
                            $errorMessage .= sprintf(
                                ' (Une entité "%s" a empêché la suppression en cours)',
                                $config['name'] ?? $metadatum->getReflectionClass()->getShortName()
                            );
                            break;
                        }
                    }
                }

                // full error message for super admin
                if ($this->isGranted('ROLE_SUPER_ADMIN')) {
                    $errorMessage .= sprintf(' <br/>(<span title="%s">code: %s</span>)', $e->getMessage(), 'ERRFK');
                }
            }

            FlashbagHelper::addError($request, $errorMessage);
        }

        $url = $this->adminRouter->generate($entitySlug, 'list');

        return $this->deleteHook([
            'url' => $url,
            'entity' => $entitySlug,
        ]);
    }

    public function deleteHook(array $vars): RedirectResponse
    {
        return $this->redirect($vars['url']);
    }

    /**
     * @throws ForbiddenActionException
     * @throws UnknownActionException
     * @throws UnknownEntityException
     */
    public function update(Request $request): RedirectResponse|Response
    {
        $entitySlug = $request->get('entitySlug');
        $id = $request->get('id');

        $this->ensureActionIsAllowed($entitySlug, 'update');

        $tab = $request->get('tab', 'default');

        $entity = $this->resolveEntity($entitySlug, $id);

        // grab builder info if we are on a builder tab
        $builderData = $this->handleBrickBuilderTab($tab, $entitySlug, $entity);

        if ($entity instanceof AbstractTranslatable && empty($entity->getCurrentTranslation())) {
            $entity = $this->contentFactory->createTranslationForEntity($entity, $request->get('_admin_locale'));
        }

        $form = $this->createForm(
            AdminContentType::class,
            $entity,
            [
                'has_submit' => true,
                'admin_tab' => $tab,
                'entity_action' => 'update',
            ]
        );
        $form->handleRequest($request);

        if ($form->isSubmitted()) {
            $redirect = $this->handleUpsertForm($entity, $form, $request);

            if ($redirect instanceof RedirectResponse) {
                return $redirect;
            }
        }

        /** @var LayoutBuilder $topbarBuilder */
        $topbarBuilder = $this->builderFactory->get('layout');
        $headActions = $topbarBuilder->buildTopbarActions($entity, $request->get('action'));

        $vars = array_merge(
            [
                'headActions' => $headActions,
                'form' => empty($builderData) ? $form->createView() : null,
                'entity' => $entity,
            ],
            $builderData
        );

        return $this->updateHook($this->getTemplate('update'), $vars);
    }

    public function updateHook(string $template, array $vars): Response
    {
        return $this->render($template, $vars);
    }

    protected function handleBrickBuilderTab(string $tab, string $entitySlug, object $entity): array
    {
        // prefix and root brick for builder
        $builderField = null;
        $prefix = 'builder*';
        $tabString = new ByteString($tab);
        $builderUrl = null;
        $builderData = [];
        if ($tabString->startsWith($prefix)) {
            $builderField = $tabString->replace($prefix, '');
            $builderUrl = $this->adminRouter->generateUrlFromRoute(
                'aa_admin_content_builder_index',
                [
                    'entitySlug' => $entitySlug,
                    'id' => $entity->getId(),
                    'property' => $builderField,
                ]
            );

            $builderPreviewUrl = false;
            try {
                $builderPreviewUrl = $this->contentRouter->generate($entity);
            } catch (Exception $e) {
            }

            $isTranslatedField = stripos($builderField, '_tr.') !== false;
            $builderField = str_replace('_tr.', '', $builderField);
            $builderEntity = $isTranslatedField ? $entity->getCurrentTranslation() : $entity;
            if (!empty($builderEntity)) {
                $builderRootBrick = (new PropertyAccessor())->getValue($builderEntity, $builderField);
                if (!empty($builderRootBrick)) {

                    $builderDataUrl = $this->adminRouter->generateUrlFromRoute(
                        'aa_admin_content_builder_data',
                        ['rootBrickId' => $builderRootBrick->getId(),]
                    );
                }
            }

            $builderData = [
                'builderUrl' => $builderUrl,
                'builderRootBrick' => $builderRootBrick ?? null,
                'builderDataUrl' => $builderDataUrl ?? null,
                'builderPreviewUrl' => $builderPreviewUrl ?? null,
            ];
        }

        return $builderData;
    }

    /**
     * @throws ForbiddenActionException
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     */
    public function export(Request $request, ?EntityFilterModel $entityFilterModel = null): BinaryFileResponse
    {
        $entitySlug = $request->get('entitySlug');

        $this->ensureActionIsAllowed($entitySlug, 'export');

        $request->query->set('items', 10000);
        if (!$entityFilterModel) {
            $entityFilterModel = new EntityFilterModel($request);
        }

        $config = $this->getEntityConfigFromRequest($request);
        $entities = $this->entityFinder->find($entityFilterModel, $config);
        $fieldsConfig = $config['export']['fields'];

        $spreadsheet = new Spreadsheet();
        $sheet = $spreadsheet->getActiveSheet();

        $data = [];

        $_data = [];
        foreach ($fieldsConfig as $fieldConfig) {
            $_data[] = $fieldConfig['label'];
        }
        $data[] = $_data;
        $pa = new PropertyAccessor();
        foreach ($entities as $entity) {
            $_data = [];
            foreach ($fieldsConfig as $fieldConfig) {
                $value = $pa->getValue($entity, $fieldConfig['property']);
                $_data[] = $this->stringify($value);
            }
            $data[] = $_data;
        }

        $sheet->fromArray($data);

        $writer = new Xlsx($spreadsheet);
        $file = tempnam(sys_get_temp_dir(), "export_{$entitySlug}_").'.xlsx';
        $writer->save($file);

        $response = new BinaryFileResponse($file);
        $response->setContentDisposition(
            ResponseHeaderBag::DISPOSITION_ATTACHMENT,
            sprintf('%s_%s.xlsx', $entitySlug, date('YmdHis'))
        );

        return $response;
    }

    private function stringify($value): ?string
    {
        // Countable : array & collection
        if (is_countable($value)) {
            if (is_array($value)) {
                return implode(', ', $value);
            }

            if ($value instanceof Countable) {
                return implode(', ', $value->toArray());
            }
        }

        // Objects
        if (is_object($value)) {
            if ($value instanceof \DateTimeInterface) {
                return $value->format('d/m/Y H:i:s');
            }

            if ($value instanceof AbstractContent) {
                $entityTitle = '#'.$value->getId();
                if ($value instanceof TranslatableInterface) {
                    $translation = $value->getCurrentTranslation();
                    if (!empty($translation) && $translation instanceof TitleInterface) {
                        $entityTitle = $translation->getTitle();
                    } elseif (!empty($translation) && $translation instanceof NameInterface) {
                        $entityTitle = $translation->getName();
                    }
                }

                return $entityTitle;
            }

            $refl = new ReflectionClass($value);

            if ($refl->hasMethod('__toString')) {
                return (string)$value;
            }

            return '#ERROR';
        }

        return $value;
    }

    public function show(Request $request)
    {
        try {
            $entitySlug = $request->get('entitySlug');
            $id = $request->get('id');

            $this->ensureActionIsAllowed($entitySlug, 'show');

            $entity = $this->resolveEntity($entitySlug, $id);

            $route = $this->contentRouter->generate($entity);

            if (!$route) {
                throw new Exception("No route found");
            }

            return $this->redirect($route);
        } catch (Exception $e) {
            $this->addFlash('error', 'Aucune URL trouvé');

            return $this->redirect($this->adminRouter->generate($entity, 'list'));
        }
    }

    public function duplicate(Request $request)
    {
        $entitySlug = $request->get('entitySlug');
        $id = $request->get('id');

        $this->ensureActionIsAllowed($entitySlug, 'duplicate');

        $entity = $this->resolveEntity($entitySlug, $id);

        try {
            $this->listenerStateManager->disable();// disable for slug subscriber etc.

            $duplicate = $this->duplicatorService->duplicate($entity);
            $this->em->persist($duplicate);
            $this->em->flush();

            $this->listenerStateManager->enable();// re-enable after duplication

            $this->addFlash('success', 'Dupliqué avec succès');
        } catch (Exception $e) {
            if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'dev') {
                throw $e;
            }
            $this->addFlash('error', 'Erreur lors de la duplication');
        }

        return $this->redirect($request->headers->get('referer'));
    }
}
