<?php

/*
 * This file is part of the VITICORE2 package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CpCreation\VitiCore\Bridge\Symfony\Bundle\LocalizedRouting;

use CpCreation\VitiCore\Bridge\Symfony\Bundle\Provider\LocaleProvider;
use CpCreation\VitiCore\News\Model\News;
use CpCreation\VitiCore\News\Model\Translation\NewsTranslation;
use CpCreation\VitiCore\News\Repository\NewsRepository;
use CpCreation\VitiCore\Product\Model\Product;
use CpCreation\VitiCore\Product\Model\ProductCategory;
use CpCreation\VitiCore\Product\Model\Translation\ProductCategoryTranslation;
use CpCreation\VitiCore\Product\Model\Translation\ProductTranslation;
use CpCreation\VitiCore\Product\Repository\ProductCategoryRepository;
use CpCreation\VitiCore\Product\Repository\ProductRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Intl\Languages;
use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouterInterface;

final class LocalizedPageRouteResolver
{
    private const string REQUEST_CACHE_ATTRIBUTE = '_viti_core_localized_page_routes';
    private const string REQUEST_RESOLUTION_CACHE_ATTRIBUTE = '_viti_core_localized_page_route_resolutions';

    /** @var array<string, string> */
    private const array OPEN_GRAPH_LOCALES = [
        'fr' => 'fr_FR',
        'en' => 'en_GB',
        'de' => 'de_DE',
        'nl' => 'nl_NL',
        'it' => 'it_IT',
        'es' => 'es_ES',
        'ru' => 'ru_RU',
    ];

    /**
     * @var array{
     *     canonicalByRoute: array<string, string>,
     *     exactRoutes: array<string, true>,
     *     localizedByCanonical: array<string, array<string, string>>,
     *     productSlugParameterByCanonical: array<string, string>
     * }|null
     */
    private ?array $routeIndex = null;

    public function __construct(
        private readonly RequestStack $requestStack,
        private readonly RouterInterface $router,
        private readonly LocaleProvider $localeProvider,
        private readonly NewsRepository $newsRepository,
        private readonly ProductCategoryRepository $productCategoryRepository,
        private readonly ProductRepository $productRepository,
    ) {
    }

    public function current(): LocalizedPageRoutes
    {
        $request = $this->requestStack->getCurrentRequest() ?? $this->requestStack->getMainRequest();
        if (!$request instanceof Request) {
            return $this->emptyRoutes($this->localeProvider->getDefaultLocale());
        }

        $cached = $request->attributes->get(self::REQUEST_CACHE_ATTRIBUTE);
        if ($cached instanceof LocalizedPageRoutes) {
            return $cached;
        }

        $routeName = $request->attributes->get('_canonical_route');
        if (!\is_string($routeName) || '' === $routeName) {
            $routeName = $request->attributes->get('_route');
        }

        if (!\is_string($routeName) || '' === $routeName) {
            $routes = $this->emptyRoutes($request->getLocale());
            $request->attributes->set(self::REQUEST_CACHE_ATTRIBUTE, $routes);

            return $routes;
        }

        $parameters = $request->attributes->get('_route_params', []);
        $routes = $this->resolve(
            $routeName,
            \is_array($parameters) ? $parameters : [],
            $request->getLocale(),
        );
        $request->attributes->set(self::REQUEST_CACHE_ATTRIBUTE, $routes);

        return $routes;
    }

    /**
     * @param array<string, mixed> $parameters
     */
    public function resolve(
        string $routeName,
        array $parameters = [],
        ?string $currentLocale = null,
    ): LocalizedPageRoutes {
        $defaultLocale = $this->localeProvider->getDefaultLocale();
        $currentLocale ??= $defaultLocale;
        $request = $this->requestStack->getCurrentRequest() ?? $this->requestStack->getMainRequest();
        $cacheKey = $this->resolutionCacheKey($routeName, $parameters, $currentLocale);
        $cachedResolutions = $request?->attributes->get(self::REQUEST_RESOLUTION_CACHE_ATTRIBUTE, []);
        if (null !== $cacheKey
            && \is_array($cachedResolutions)
            && ($cachedResolutions[$cacheKey] ?? null) instanceof LocalizedPageRoutes) {
            return $cachedResolutions[$cacheKey];
        }

        $canonicalRoute = $this->canonicalRouteName($routeName);
        $routeNames = $this->localizedRouteNames($canonicalRoute, $currentLocale);
        $locales = $this->enabledLocales();

        unset($parameters['_locale']);
        $localizedParameters = $this->localizedParameters($canonicalRoute, $parameters, $locales);

        $routes = [];
        foreach ($locales as $locale) {
            $localizedRouteName = $routeNames[$locale] ?? null;
            $targetParameters = $localizedParameters[$locale] ?? null;
            if (!\is_string($localizedRouteName) || !\is_array($targetParameters)) {
                continue;
            }

            try {
                $path = $this->router->generate(
                    $localizedRouteName,
                    $targetParameters,
                    UrlGeneratorInterface::ABSOLUTE_PATH,
                );
                $absoluteUrl = $this->router->generate(
                    $localizedRouteName,
                    $targetParameters,
                    UrlGeneratorInterface::ABSOLUTE_URL,
                );
            } catch (RoutingExceptionInterface) {
                continue;
            }

            $routes[$locale] = new LocalizedPageRoute(
                locale: $locale,
                languageName: Languages::getName($locale, $locale) ?: mb_strtoupper($locale),
                hreflang: str_replace('_', '-', $locale),
                openGraphLocale: $this->openGraphLocale($locale),
                path: $path,
                absoluteUrl: $absoluteUrl,
            );
        }

        $localizedPageRoutes = new LocalizedPageRoutes($currentLocale, $defaultLocale, $routes);
        if (null !== $request && null !== $cacheKey) {
            $cachedResolutions = \is_array($cachedResolutions) ? $cachedResolutions : [];
            $cachedResolutions[$cacheKey] = $localizedPageRoutes;
            $request->attributes->set(self::REQUEST_RESOLUTION_CACHE_ATTRIBUTE, $cachedResolutions);
        }

        return $localizedPageRoutes;
    }

    /**
     * @return list<string>
     */
    private function enabledLocales(): array
    {
        $locales = [];
        foreach ($this->localeProvider->getLocales() as $locale) {
            if (!\is_string($locale)) {
                continue;
            }

            $locale = mb_trim($locale);
            if ('' !== $locale && !\in_array($locale, $locales, true)) {
                $locales[] = $locale;
            }
        }

        return $locales;
    }

    private function canonicalRouteName(string $routeName): string
    {
        return $this->routeIndex()['canonicalByRoute'][$routeName] ?? $routeName;
    }

    /**
     * @return array<string, string>
     */
    private function localizedRouteNames(string $canonicalRoute, string $currentLocale): array
    {
        $routeIndex = $this->routeIndex();
        $localizedRouteNames = $routeIndex['localizedByCanonical'][$canonicalRoute] ?? [];

        if ([] !== $localizedRouteNames) {
            return $localizedRouteNames;
        }

        if (isset($routeIndex['exactRoutes'][$canonicalRoute])) {
            $localizedRouteNames[$currentLocale] = $canonicalRoute;
        }

        return $localizedRouteNames;
    }

    /**
     * @return array{
     *     canonicalByRoute: array<string, string>,
     *     exactRoutes: array<string, true>,
     *     localizedByCanonical: array<string, array<string, string>>,
     *     productSlugParameterByCanonical: array<string, string>
     * }
     */
    private function routeIndex(): array
    {
        if (null !== $this->routeIndex) {
            return $this->routeIndex;
        }

        $canonicalByRoute = [];
        $exactRoutes = [];
        $localizedByCanonical = [];
        $productSlugParameterByCanonical = [];

        foreach ($this->router->getRouteCollection() as $routeName => $route) {
            if (!$route instanceof Route) {
                continue;
            }

            $exactRoutes[$routeName] = true;
            $canonicalRoute = $route->getDefault('_canonical_route');
            if (!\is_string($canonicalRoute) || '' === $canonicalRoute) {
                continue;
            }

            $canonicalByRoute[$routeName] = $canonicalRoute;
            $locale = $route->getDefault('_locale');
            if (\is_string($locale) && '' !== $locale && !isset($localizedByCanonical[$canonicalRoute][$locale])) {
                $localizedByCanonical[$canonicalRoute][$locale] = $routeName;
            }

            $productSlugParameter = $route->getOption('viti_localized_product_slug');
            if (\is_string($productSlugParameter) && '' !== mb_trim($productSlugParameter)) {
                $productSlugParameterByCanonical[$canonicalRoute] = $productSlugParameter;
            }
        }

        return $this->routeIndex = [
            'canonicalByRoute' => $canonicalByRoute,
            'exactRoutes' => $exactRoutes,
            'localizedByCanonical' => $localizedByCanonical,
            'productSlugParameterByCanonical' => $productSlugParameterByCanonical,
        ];
    }

    /**
     * @param array<string, mixed> $parameters
     * @param list<string>         $locales
     *
     * @return array<string, array<string, mixed>>
     */
    private function localizedParameters(string $canonicalRoute, array $parameters, array $locales): array
    {
        if ('shop' === $canonicalRoute) {
            return $this->localizedShopParameters($parameters, $locales);
        }

        if ('shop_product' === $canonicalRoute) {
            return $this->localizedProductParameters($parameters, $locales);
        }

        if ('news_category' === $canonicalRoute) {
            return $this->localizedNewsParameters($parameters, $locales);
        }

        $productSlugParameter = $this->routeIndex()['productSlugParameterByCanonical'][$canonicalRoute] ?? null;
        if (\is_string($productSlugParameter)) {
            return $this->localizedSingleProductSlugParameters($parameters, $locales, $productSlugParameter);
        }

        return array_fill_keys($locales, $parameters);
    }

    /**
     * @param array<string, mixed> $parameters
     * @param list<string>         $locales
     *
     * @return array<string, array<string, mixed>>
     */
    private function localizedSingleProductSlugParameters(
        array $parameters,
        array $locales,
        string $slugParameter,
    ): array {
        $slug = $parameters[$slugParameter] ?? null;
        if (!\is_string($slug) || '' === mb_trim($slug)) {
            return [];
        }

        $product = $this->productRepository->findOneBySlug($slug);
        if (!$product instanceof Product) {
            return [];
        }

        $localizedParameters = [];
        foreach ($locales as $locale) {
            $localizedSlug = $this->productSlug($product, $locale);
            if (null === $localizedSlug) {
                continue;
            }

            $localizedParameters[$locale] = [...$parameters, $slugParameter => $localizedSlug];
        }

        return $localizedParameters;
    }

    /**
     * @param array<string, mixed> $parameters
     * @param list<string>         $locales
     *
     * @return array<string, array<string, mixed>>
     */
    private function localizedShopParameters(array $parameters, array $locales): array
    {
        $slug = $parameters['slug'] ?? null;
        if (!\is_string($slug) || '' === mb_trim($slug)) {
            return array_fill_keys($locales, $parameters);
        }

        $category = $this->productCategoryRepository->findOneBySlug($slug);
        if (!$category instanceof ProductCategory) {
            return [];
        }

        $localizedParameters = [];
        foreach ($locales as $locale) {
            $localizedSlug = $this->categorySlug($category, $locale);
            if (null === $localizedSlug) {
                continue;
            }

            $localizedParameters[$locale] = [...$parameters, 'slug' => $localizedSlug];
        }

        return $localizedParameters;
    }

    /**
     * @param array<string, mixed> $parameters
     * @param list<string>         $locales
     *
     * @return array<string, array<string, mixed>>
     */
    private function localizedProductParameters(array $parameters, array $locales): array
    {
        $slug = $parameters['slug'] ?? null;
        if (!\is_string($slug) || '' === mb_trim($slug)) {
            return [];
        }

        $product = $this->productRepository->findOneBySlug($slug);
        if (!$product instanceof Product || !$product->getCategory() instanceof ProductCategory) {
            return [];
        }

        $localizedParameters = [];
        foreach ($locales as $locale) {
            $productSlug = $this->productSlug($product, $locale);
            $categorySlug = $this->categorySlug($product->getCategory(), $locale);
            if (null === $productSlug || null === $categorySlug) {
                continue;
            }

            $localizedParameters[$locale] = [
                ...$parameters,
                'categorySlug' => $categorySlug,
                'slug' => $productSlug,
            ];
        }

        return $localizedParameters;
    }

    /**
     * @param array<string, mixed> $parameters
     * @param list<string>         $locales
     *
     * @return array<string, array<string, mixed>>
     */
    private function localizedNewsParameters(array $parameters, array $locales): array
    {
        $slug = $parameters['slug'] ?? null;
        if (!\is_string($slug) || '' === mb_trim($slug)) {
            return [];
        }

        $news = $this->newsRepository->findOneAvailableBySlug($slug);
        if (!$news instanceof News) {
            return [];
        }

        $localizedParameters = [];
        foreach ($locales as $locale) {
            $localizedSlug = $this->newsSlug($news, $locale);
            if (null === $localizedSlug) {
                continue;
            }

            $localizedParameters[$locale] = [...$parameters, 'slug' => $localizedSlug];
        }

        return $localizedParameters;
    }

    private function categorySlug(ProductCategory $category, string $locale): ?string
    {
        foreach ($category->getTranslations() as $translation) {
            if (!$translation instanceof ProductCategoryTranslation || $locale !== $translation->getLocale()) {
                continue;
            }

            $slug = $translation->getSlug();

            return \is_string($slug) && '' !== mb_trim($slug) ? $slug : null;
        }

        return null;
    }

    private function productSlug(Product $product, string $locale): ?string
    {
        foreach ($product->getTranslations() as $translation) {
            if (!$translation instanceof ProductTranslation || $locale !== $translation->getLocale()) {
                continue;
            }

            $slug = $translation->getSlug();

            return \is_string($slug) && '' !== mb_trim($slug) ? $slug : null;
        }

        return null;
    }

    private function newsSlug(News $news, string $locale): ?string
    {
        foreach ($news->getTranslations() as $translation) {
            if (!$translation instanceof NewsTranslation || $locale !== $translation->getLocale()) {
                continue;
            }

            $slug = $translation->getSlug();

            return \is_string($slug) && '' !== mb_trim($slug) ? $slug : null;
        }

        return null;
    }

    private function openGraphLocale(string $locale): ?string
    {
        $locale = str_replace('-', '_', $locale);
        if (1 === preg_match('/^[a-z]{2}_[A-Za-z]{2}$/', $locale)) {
            return mb_strtolower(mb_substr($locale, 0, 2)).'_'.mb_strtoupper(mb_substr($locale, 3, 2));
        }

        return self::OPEN_GRAPH_LOCALES[$locale] ?? null;
    }

    /**
     * @param array<string, mixed> $parameters
     */
    private function resolutionCacheKey(string $routeName, array $parameters, string $currentLocale): ?string
    {
        try {
            return hash('sha256', serialize([$routeName, $parameters, $currentLocale]));
        } catch (\Throwable) {
            return null;
        }
    }

    private function emptyRoutes(string $currentLocale): LocalizedPageRoutes
    {
        return new LocalizedPageRoutes(
            $currentLocale,
            $this->localeProvider->getDefaultLocale(),
            [],
        );
    }
}
