<?php
/**
 * Copyright since 2025 InPost S.A.
 *
 * For the full license information, please view the LICENSE file bundled with the module.
 *
 * @author InPost S.A.
 * @copyright since 2025 InPost S.A.
 * @license MIT
 */

declare(strict_types=1);

namespace InPost\International\PrestaShop\ObjectModel\Form\Type;

use InPost\International\PrestaShop\ObjectModel\Form\ChoiceLoader\ObjectModelChoiceLoader;
use InPost\International\PrestaShop\ObjectModel\Form\DataTransformer\ObjectModelCollectionToIdsTransformer;
use InPost\International\PrestaShop\ObjectModel\Form\DataTransformer\ObjectModelToIdTransformer;
use InPost\International\PrestaShop\ObjectModel\ObjectManagerInterface;
use InPost\International\PrestaShop\ObjectModel\QueryBuilder;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

if (!defined('_PS_VERSION_')) {
    exit;
}

final class ObjectModelType extends AbstractType
{
    /**
     * @var ObjectManagerInterface
     */
    private $manager;

    /**
     * @var \Context
     */
    private $context;

    /**
     * @var array<string, ObjectModelChoiceLoader>
     */
    private $choiceLoaders = [];

    public function __construct(ObjectManagerInterface $manager, \Context $context)
    {
        $this->context = $context;
        $this->manager = $manager;
    }

    public function getParent(): string
    {
        return ChoiceType::class;
    }

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        if ('id' !== $options['input']) {
            return;
        }

        if ($options['multiple']) {
            $builder->addModelTransformer(new ReversedTransformer(
                new ObjectModelCollectionToIdsTransformer(
                    $this->manager,
                    $options['class'],
                    $options['language_id'],
                    $options['shop_id']
                )
            ));
        } else {
            $builder->addModelTransformer(new ReversedTransformer(
                new ObjectModelToIdTransformer(
                    $this->manager,
                    $options['class'],
                    $options['language_id'],
                    $options['shop_id']
                )
            ));
        }
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver
            ->setRequired(['class'])
            ->setDefaults([
                'language_id' => (int) $this->context->language->id,
                'shop_id' => null,
                'input' => 'object',
                'query_builder' => null,
                'choices' => null,
                'choice_loader' => function (Options $options) {
                    if (null !== $options['choices']) {
                        return null;
                    }

                    $cacheKey = $this->getChoiceLoaderCacheKey($options['class'], $options['query_builder'], $options['language_id'], $options['shop_id']);

                    return $this->choiceLoaders[$cacheKey] ?? $this->choiceLoaders[$cacheKey] = new ObjectModelChoiceLoader(
                        $this->manager,
                        $options['class'],
                        $options['query_builder'],
                        $options['language_id'],
                        $options['shop_id']
                    );
                },
                'choice_label' => function (\ObjectModel $model) {
                    if ($model instanceof \Stringable) {
                        return (string) $model;
                    }

                    $label = null;

                    // if the model's "name" property exists, use it by default
                    if (property_exists($model, 'name')) {
                        if (!is_array($model->name)) {
                            $label = $model->name;
                        } elseif ([] !== $model->name) {
                            $label = $model->name[$this->context->language->id] ?? current($model->name);
                        }
                    }

                    return $label ?? sprintf('"%s" #%d', get_class($model), $model->id);
                },
                'choice_name' => static function (\ObjectModel $model) {
                    return (string) $model->id;
                },
                'choice_value' => static function (?\ObjectModel $model) {
                    if (null === $model) {
                        return '';
                    }

                    return (string) $model->id;
                },
            ])
            ->setAllowedTypes('language_id', ['null', 'int'])
            ->setAllowedTypes('shop_id', ['null', 'int'])
            ->setAllowedTypes('query_builder', ['null', 'callable', QueryBuilder::class])
            ->setAllowedValues('input', ['object', 'id'])
            ->setNormalizer('query_builder', function (Options $options, $value) {
                if (is_callable($value)) {
                    $value = $value(
                        $this->manager->getRepository($options['class']),
                        $options['language_id'],
                        $options['shop_id']
                    );

                    if (null !== $value && !$value instanceof QueryBuilder) {
                        throw new UnexpectedTypeException($value, QueryBuilder::class);
                    }
                }

                return $value;
            });
    }

    private function getChoiceLoaderCacheKey(string $class, ?QueryBuilder $queryBuilder, ?int $languageId, ?int $shopId): string
    {
        if (null === $queryBuilder) {
            return sprintf('%s_%d_%d', $class, (int) $languageId, (int) $shopId);
        }

        $sql = $queryBuilder->build()->getSql();

        return hash('md5', $sql . (int) $languageId);
    }
}
