<?php

namespace AutoAdmin\Bundle\BrickBuilder\Controller\Admin;

use AutoAdmin\Bundle\Admin\Controller\Admin\AbstractAdminController;
use AutoAdmin\Bundle\BrickBuilder\Brick\BrickHandlerManagerInterface;
use AutoAdmin\Bundle\BrickBuilder\Brick\Handler\ColumnBrickHandler;
use AutoAdmin\Bundle\BrickBuilder\Builder\BuilderService;
use AutoAdmin\Bundle\BrickBuilder\Entity\Brick;
use AutoAdmin\Bundle\BrickBuilder\Exception\BadSettingFormatException;
use AutoAdmin\Bundle\BrickBuilder\Form\BrickSortType;
use AutoAdmin\Bundle\BrickBuilder\Form\ColumnPatternType;
use AutoAdmin\Bundle\BrickBuilder\Model\Api\BrickCreate;
use AutoAdmin\Bundle\BrickBuilder\Model\Api\BrickSort;
use AutoAdmin\Bundle\BrickBuilder\Model\BrickInterface;
use AutoAdmin\Bundle\BrickBuilder\Repository\BrickRepository;
use AutoAdmin\Bundle\Content\Model\UpdatedAtInterface;
use DateTime;
use Doctrine\Persistence\ManagerRegistry;
use Exception;
use RuntimeException;
use Symfony\Component\Asset\Packages;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Constraints\Choice;
use AutoAdmin\Bundle\BrickBuilder\Brick\BrickAdminHelper;
use AutoAdmin\Bundle\BrickBuilder\Brick\Handler\SectionBrickHandler;

class ContentBuilderController extends AbstractAdminController
{
    private PropertyAccessor $accessor;
    private BrickHandlerManagerInterface $handlerManager;
    private BuilderService $builderService;
    private BrickAdminHelper $adminHelper;
    private SerializerInterface $serializer;
    private NormalizerInterface $normalizer;
    private ManagerRegistry $managerRegistry;
    private Packages $packages;
    private KernelInterface $kernel;

    public function __construct(
        BrickHandlerManagerInterface $handlerManager,
        BuilderService               $builderService,
        BrickAdminHelper             $adminHelper,
        SerializerInterface          $serializer,
        NormalizerInterface          $normalizer,
        ManagerRegistry              $managerRegistry,
        Packages                     $packages,
        KernelInterface              $kernel

    )
    {
        $this->accessor = new PropertyAccessor();
        $this->handlerManager = $handlerManager;
        $this->builderService = $builderService;
        $this->adminHelper = $adminHelper;
        $this->serializer = $serializer;
        $this->normalizer = $normalizer;
        $this->managerRegistry = $managerRegistry;
        $this->kernel = $kernel;
        $this->packages = $packages;
    }

    public function index(Request $request): JsonResponse
    {
        $entitySlug = $request->get('entitySlug');
        $id = $request->get('id');
        $fqcn = $this->entityHelper->findFullClassName($entitySlug);
        $manager = $this->entityHelper->getManager();
        $repo = $manager->getRepository($fqcn);
        $entityObject = $repo->find($id);

        $property = $request->get('property');

        $updateUrl = $this->adminRouter->generate($entityObject, 'update', ['id' => $id]);
        $previewUrl = $this->adminRouter->generateUrlFromRoute(
            'aa_admin_content_builder_preview',
            [
                'entitySlug' => $entitySlug,
                'id' => $id,
                'property' => $property,
            ]
        );

        return $this->json(
            [
                'html' => $this->renderView(
                    '@AutoAdminBrickBuilder/builder/index.html.twig',
                    [
                        'brick' => $this->accessor->getValue($entityObject, $property),
                        'updateUrl' => $updateUrl,
                        'previewUrl' => $previewUrl,
                    ]
                ),
            ]
        );
    }

    public function create(Request $request, BrickHandlerManagerInterface $manager)
    {
        $json = [
            'innerTitle' => 'Nouvelle brique',
        ];

        $brickRepo = $this->entityHelper->getManager()->getRepository(Brick::class);
        $form = $this->createForm(FormType::class, new BrickCreate(), ['csrf_protection' => false,]);
        $form->add(
            'brickType',
            TextType::class,
            [
                'constraints' => new Choice($manager->getHandlerTypes()),
            ]
        );
        $form->add('parentBrickId', IntegerType::class);
        $form->add('layout', TextType::class);

        $form->handleRequest($request);
        $form->submit($request->request->all());
        // TODO, enforce brickType with parent type before saving

        if ($form->isValid()) {
            /** @var BrickCreate $data */
            $data = $form->getData();
            $brick = new Brick();
            $brick->setType($data->getBrickType());

            /** @var BrickInterface $parentBrick */
            $parentBrick = $brickRepo->find($data->getParentBrickId());
            if ($parentBrick === null) {
                throw new RuntimeException('Could not find parent block.');
            }

            $brick->setParent($parentBrick);

            if ($parentBrick->getChildren()->count() === 0) {
                $brick->setPosition(1);
            } else {
                $positions = [];
                /** @var BrickInterface $child */
                foreach ($parentBrick->getChildren() as $child) {
                    $positions[] = $child->getPosition();
                }

                $brick->setPosition(max($positions) + 1);
            }
            $em = $this->entityHelper->getManager();
            $em->persist($brick);
            $em->flush();

            if ($brick->getType() === 'brick.section') {
                $rowBrick = (new Brick())->setType('brick.row')->setParent($brick);
                $em->persist($rowBrick);
                $em->flush();
                if ($data->getLayout()) {
                    for ($i = 0; $i <= substr_count($data->getLayout(), '_'); $i++) {
                        $columnBrick = (new Brick())->setType('brick.column')->setParent($rowBrick);
                        $rowBrick->setSetting('column_pattern', $data->getLayout());
                        $em->persist($columnBrick);
                    }
                } else {
                    $columnBrick = (new Brick())->setType('brick.column')->setParent($rowBrick);
                    $em->persist($columnBrick);
                }
                $em->flush();
            }

            // Reset manager for get fresh brick created
            $this->managerRegistry->resetManager();

            /** @var BrickRepository $brickRepo */
            $brickRepo = $this->entityHelper->getManager()->getRepository(Brick::class);
            $brick = $brickRepo->eagerFind($brick->getId());

            $json = array_merge(
                $json,
                [
                    'status' => true,
                    'message' => $this->notifSuccessFormat($brick, 'create'),
                    'brick' => $this->normalizeBrick($brick),
                ]
            );
        } else {
            $json = array_merge(
                $json,
                [
                    'status' => false,
                    'message' => $this->notifErrorFormat('create'),
                ]
            );
        }

        return $this->json($json);
    }

    public function createRoot(Request $request): RedirectResponse
    {
        $referer = filter_var($request->headers->get('referer'), FILTER_SANITIZE_URL);

        $entitySlug = $request->get('entitySlug');
        $id = $request->get('id');
        $property = $request->get('property');

        $fqcn = $this->entityHelper->findFullClassName($entitySlug);
        $manager = $this->entityHelper->getManager();
        $repo = $manager->getRepository($fqcn);
        $entityObject = $translatableEntity = $repo->find($id);

        $isTranslatedField = stripos($property, '_tr.') !== false;
        $property = str_replace('_tr.', '', $property);
        $entityObject = $isTranslatedField ? $entityObject->getCurrentTranslation() : $entityObject;

        if ($entityObject === null && $isTranslatedField) {
//            throw new \Exception('Entity does not have any translation.');
            $translatableEntity = $this->contentFactory->createTranslationForEntity($translatableEntity);
            $entityObject = $translatableEntity->getCurrentTranslation();
        }

        $propertyAccessor = new PropertyAccessor();
        $rootBrick = (new Brick())->setType('brick.root');
        $sectionBrick = (new Brick())->setType('brick.section')->setParent($rootBrick);
        $rowBrick = (new Brick())->setType('brick.row')->setParent($sectionBrick);
        $columnBrick = (new Brick())->setType('brick.column')->setParent($rowBrick);

        $propertyAccessor->setValue($entityObject, $property, $rootBrick);

        foreach ([$columnBrick, $rowBrick, $sectionBrick, $rootBrick,] as $item) {
            $manager->persist($item);
        }
        $manager->persist($entityObject);
        $manager->flush();

        return $this->redirect($referer);
    }

    public function treeData(Request $request): JsonResponse
    {
        $id = $request->get('rootBrickId');
        if (empty($id) || !is_numeric($id)) {
            return $this->json(['error' => 'invalid brick id']);
        }

        $json = [];
        try {
            $brickRepo = $this->entityHelper->getManager()->getRepository(Brick::class);
            /** @var BrickInterface $root */
            $root = $brickRepo->find($id);
            $serialized = $this->serializeBrick($root);

            $json['rootBrick'] = json_decode($serialized, true);
            $json['categories'] = $this->adminHelper->getBrickCategories();
            $json['brickTypes'] = $this->adminHelper->getBrickTypes();
            $json['isAdmin'] = $this->isGranted('ROLE_ADMIN');
        } catch (BadSettingFormatException $e) {
            $json['error'] = $e->getMessage();
        }

        return $this->json($json);
    }

    public function update(Request $request): JsonResponse
    {
        $json = ['status' => true,];

        $brickId = $request->get('brickId');
        $brickRepo = $this->entityHelper->getManager()->getRepository(Brick::class);
        /** @var BrickInterface|null $brick */
        $brick = $brickRepo->find($brickId);
        $json['title'] = 'Edition de la brique ' . sprintf('#%s', $brick->getId());

        $form = $this->buildUpdateForm($brick);
        $json['html'] = $this->renderView(
            '@AutoAdminBrickBuilder/builder_ajax/form.html.twig',
            ['form' => $form->createView(),]
        );


        $errors = [];

        if ($request->isMethod('POST')) {
            $form->handleRequest($request);
            if ($form->isSubmitted() && $form->isValid()) {
                $postAction = $form->get('submit_and_close')->isClicked() ? 'close-after-save' : null;
                $em = $this->entityHelper->getManager();
                /** @var BrickInterface $brick */
                $brick = $form->getData();
                if ($brick instanceof UpdatedAtInterface) {
                    $brick->setUpdatedAt(new DateTime());
                }

                $em->persist($brick);
                $em->flush();
                // settings may have changed because of content_references, update form
                $em->refresh($brick);
                $form = $this->buildUpdateForm($brick);
                $json['html'] = $this->renderView(
                    '@AutoAdminBrickBuilder/builder_ajax/form.html.twig',
                    ['form' => $form->createView(),]
                );

                return $this->json(
                    array_merge(
                        $json,
                        [
                            'message' => $this->notifSuccessFormat($brick, 'update'),
                            'brick' => $this->normalizeBrick($brick),
                            'post_action' => $postAction,
                        ]
                    )
                );
            } else {
                $formErrors = $form->getErrors(true, true);
                foreach ($formErrors as $formError) {
                    $errors[] = $formError->getMessage();
                }
                $json['errors'] = $errors;
                $json['message'] = $this->notifErrorFormat('update');
                $json['status'] = false;
                $json['html'] = $this->renderView(
                    '@AutoAdminBrickBuilder/builder_ajax/form.html.twig',
                    ['form' => $form->createView(),]
                );
            }
        }
        return $this->json($json);
    }

    protected function buildUpdateForm(BrickInterface $brick): ?FormInterface
    {
        return $this->handlerManager->buildForm($brick);
    }

    public function delete(Request $request)
    {
        $brickId = $request->get('brickId');
        $brickRepo = $this->entityHelper->getManager()->getRepository(Brick::class);
        /** @var BrickInterface|null $brick */
        $brick = $brickRepo->find($brickId);
        if (!empty($brick)) {
            $manager = $this->entityHelper->getManager();
            $manager->remove($brick);
            $manager->flush();

            return $this->json(
                [
                    'status' => true,
                    'message' => $this->notifSuccessFormat($brick, 'delete'),
                ]
            );
        }

        return $this->json(
            [
                'status' => false,
                'message' => $this->notifErrorFormat('delete', $brick),
            ]
        );
    }

    public function duplicate(Request $request): JsonResponse
    {
        $brickId = $request->get('brickId');
        $rootBrickId = $request->get('rootBrickId');
        $brickRepo = $this->entityHelper->getManager()->getRepository(Brick::class);

        /** @var BrickInterface|null $brick */
        $brick = $brickRepo->find($brickId);

        $rootBrick = null;
        if ($rootBrickId) {
            /** @var BrickInterface|null $rootBrick */
            $rootBrick = $brickRepo->find($rootBrickId);
        }

        if (empty($brick) || ($brick->getParent() === null && !$rootBrick)) {
            return $this->json(['status' => false, 'message' => 'Cannot duplicate root brick.',]);
        }

        try {
            $duplicate = $this->builderService->replicate($brick);
            if ($rootBrick) {
                $duplicate->setParent($rootBrick);
                $duplicate->setPosition($rootBrick->getChildren()->count() + 1);
            }
            $manager = $this->entityHelper->getManager();
            $manager->persist($duplicate);
            $manager->flush();

            return $this->json(
                [
                    'status' => true,
                    'message' => $this->notifSuccessFormat($brick, 'duplicate'),
                    'brick' => $this->normalizeBrick($duplicate),
                ]
            );
        } catch (Exception $e) {
            return $this->json(
                [
                    'status' => false,
                    'message' => $this->notifErrorFormat('duplicate', $brick),
                    'exception' => $e->getMessage(),
                ]
            );
        }
    }

    public function preview(Request $request): Response
    {
        $entitySlug = $request->get('entitySlug');
        $id = $request->get('id');
        $brickProperty = $request->get('property');
        $fqcn = $this->entityHelper->findFullClassName($entitySlug);
        $manager = $this->entityHelper->getManager();
        $repo = $manager->getRepository($fqcn);
        $entityObject = $repo->find($id);

        $contentUrl = $this->contentRouter->generate($entityObject);
        $client = HttpClient::create(['base_uri' => $request->getSchemeAndHttpHost(), 'verify_peer' => false,]);
        $response = $client->request('GET', $contentUrl);

        return new Response($response->getContent());
    }

    public function sort(Request $request)
    {
        $form = $this->createForm(BrickSortType::class);
        $form->handleRequest($request);
        $form->submit($request->request->all());

        $json = [
            'status' => false,
            'message' => 'Erreur lors du tri des briques',
        ];

        if ($form->isValid()) {
            /** @var BrickSort $data */
            $data = $form->getData();
            $eventType = $data->getEventType();
            $manager = $this->entityHelper->getManager();
            /** @var BrickRepository $brickRepo */
            $brickRepo = $manager->getRepository(Brick::class);
            /** @var BrickInterface $brick */
            $brick = $brickRepo->eagerFind($data->getBrickId());
            /** @var BrickInterface $parentBrick */
            $parentBrick = $brickRepo->eagerFind($data->getParentBrickId());

            if ($eventType === 'sorting') {
                $this->builderService->reorder($brick, $data->getIndex());
                $manager->persist($brick);
                $manager->persist($brick->getParent());
                $manager->flush();
                $json['status'] = true;
                $json['message'] = 'Tri des briques réussi';
            } elseif ($eventType === 'moving') {
                // todo: check parent brick type, are we allowed to move to this brick ? container/content...
                $brick->setParent($parentBrick);
                $this->builderService->reorder($brick, $data->getIndex());
                $manager->persist($brick);
                $manager->flush();
                $json['status'] = true;
                $json['message'] = 'Déplacement de la brique réussi';
            }
        } elseif ($errors = $form->getErrors()) {
            foreach ($errors as $error) {
                $json['errors'][] = $error->getMessage();
            }
        }

        return $this->json($json);
    }

    public function createTemplate(Request $request)
    {
        $brickId = $request->get('brickId');
        $brickRepo = $this->managerRegistry->getRepository(Brick::class);

        /** @var BrickInterface|null $brick */
        $brick = $brickRepo->find($brickId);

        if (empty($brick) || $brick->getParent() === null) {
            return $this->json([
                'status' => false,
                'message' => 'Impossible de créer un template depuis une brique racine',
            ]);
        }

        try {
            $duplicate = $this->builderService->replicate($brick);
            // Set null for considered as template
            $duplicate->setParent(null);
            $duplicate->setTemplateName($request->get('name'));
            $duplicate->setTemplatedAt(new DateTime());
            $manager = $this->entityHelper->getManager();
            $manager->persist($duplicate);
            $manager->flush();

            return $this->json(
                [
                    'status' => true,
                    'message' => 'Création du template réussie',
                ]
            );
        } catch (Exception $e) {
            return $this->json(
                [
                    'status' => false,
                    'message' => 'Création du template impossible',
                    'exception' => $e->getMessage(),
                ]
            );
        }
    }

    public function template()
    {
        $brickRepo = $this->managerRegistry->getRepository(Brick::class);

        $templates = $brickRepo->findBy(
            [
                'parent' => null,
                'type' => SectionBrickHandler::BRICK_TYPE,
            ],
            [
                'templatedAt' => 'DESC',
            ]
        );

        $templatesData = array_map(function (BrickInterface $brick) {
            $data = $this->normalizeBrick($brick);

            $iconPath = 'assets/images/brick-template-icons/template-' . $brick->getId() . '.svg';
            if (file_exists($this->kernel->getProjectDir() . '/public/' . $iconPath)) {
                $data['icon'] = $this->packages->getUrl($iconPath);
            } else {
                $data['icon'] = $this->packages->getUrl('assets/images/brick-template-icons/template-default.svg');
            }

            return $data;
        }, $templates);

        return $this->json($templatesData);
    }

    public function layout()
    {
        $choices = ColumnPatternType::getChoices();
        $choices = array_flip($choices);
        foreach ($choices as $key => $choice) {
            $choices[$key] = [
                'label' => $choice,
                'value' => $key,
                'image' => ColumnPatternType::getImages()[$key],
            ];
        }

        return $this->json($choices);
    }

    public function updateLayout(Request $request)
    {
        $brickId = $request->get('brickId');
        $layout = $request->get('layout');

        /** @var BrickRepository $brickRepo */
        $brickRepo = $this->managerRegistry->getRepository(Brick::class);

        /** @var BrickInterface|null $brick */
        $brick = $brickRepo->find($brickId);

        if (!$brick) {
            return $this->json([
                'status' => false,
                'message' => 'Brique introuvable',
            ]);
        }

        $rowBrick = $brick->getChildren()->first();

        if (!$rowBrick) {
            return $this->json([
                'status' => false,
                'message' => 'Ligne introuvable',
            ]);
        }

        $rowBrick->setSetting('column_pattern', $layout);
        $manager = $this->entityHelper->getManager();
        $manager->persist($rowBrick);
        $manager->flush();

        $wantedColumns = substr_count($layout, '_') + 1;
        $currentColumns = $rowBrick->getChildren()->count();

        if ($wantedColumns < $currentColumns) {
            $toBeRemoved = $currentColumns - $wantedColumns;
            $children = $rowBrick->getChildren()->toArray();
            $children = array_slice($children, -abs($toBeRemoved));
            foreach ($children as $child) {
                $rowBrick->removeChild($child);
                $manager->remove($child);
            }
        } elseif ($wantedColumns > $currentColumns) {
            $toBeAdded = $wantedColumns - $currentColumns;
            for ($i = 0; $i < $toBeAdded; ++$i) {
                $columnBrick = new Brick();
                $columnBrick->setType(ColumnBrickHandler::BRICK_TYPE);
                $columnBrick->setParent($rowBrick);
                $columnBrick->setPosition($currentColumns + 1);
                $manager->persist($columnBrick);
                $rowBrick->addChild($columnBrick);
            }
        }

        $manager->flush();

        return $this->json([
            'status' => true,
            'message' => 'Mise à jour de la disposition des colonnes effectuée',
            'brick' => $this->normalizeBrick($rowBrick),
        ]);
    }

    protected function normalizeBrick(BrickInterface $brick): array
    {
        return $this->normalizer->normalize(
            $brick,
            'json',
            [
                AbstractNormalizer::IGNORED_ATTRIBUTES => [
                    'parent',
                    'createdAt',
                    'updatedAt',
                    'template',
                ],
                AbstractNormalizer::CALLBACKS => [
                    // Limit settings return to column_pattern
                    'settings' => function ($settings) {
                        return array_filter($settings, function ($key) {
                            return $key === 'column_pattern';
                        }, ARRAY_FILTER_USE_KEY);
                    },
                ],
            ]
        );
    }

    protected function serializeBrick(BrickInterface $brick): string
    {
        return $this->serializer->serialize(
            $brick,
            'json',
            [
                AbstractNormalizer::IGNORED_ATTRIBUTES => [
                    'parent',
                    'createdAt',
                    'updatedAt',
                    'template',
                ],
                AbstractNormalizer::CALLBACKS => [
                    // Limit settings return to column_pattern
                    'settings' => function ($settings) {
                        return array_filter($settings, function ($key) {
                            return $key === 'column_pattern';
                        }, ARRAY_FILTER_USE_KEY);
                    },
                ],
            ]
        );
    }

    protected function notifSuccessFormat(BrickInterface $brick, string $action): string
    {
        $notif = '';

        if ($action === 'create') {
            $notif .= 'Création';
        } elseif ($action === 'update') {
            $notif .= 'Mise à jour';
        } elseif ($action === 'delete') {
            $notif .= 'Suppression';
        } elseif ($action === 'duplicate') {
            $notif .= 'Duplication';
        }

        if ($brick->getTemplateName() && $brick->getTemplatedAt()) {
            $notif .= ' du template "' . $brick->getTemplateName() . '"';
        } elseif ($brick->getType() === SectionBrickHandler::BRICK_TYPE) {
            $notif .= ' de la section';
        } else {
            $notif .= ' de la brique';
        }

        $notif .= ' réussie';

        return $notif;
    }

    protected function notifErrorFormat(string $action, ?BrickInterface $brick = null): string
    {
        $notif = '';

        if ($action === 'create') {
            $notif .= 'Création';
        } elseif ($action === 'update') {
            $notif .= 'Mise à jour';
        } elseif ($action === 'delete') {
            $notif .= 'Suppression';
        } elseif ($action === 'duplicate') {
            $notif .= 'Duplication';
        }

        if ($brick && $brick->getType() === SectionBrickHandler::BRICK_TYPE) {
            $notif .= ' de la section';
        }

        $notif .= ' échouée';

        return $notif;
    }
}
