<?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\VitiTheme\DependencyInjection;

use Composer\InstalledVersions;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

final class CpCreationVitiThemeExtension extends Extension implements PrependExtensionInterface
{
    private const THEME_PACKAGE_TYPE = 'viti-theme';

    public function load(array $configs, ContainerBuilder $container): void
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);
        $themes = $this->resolveThemes($config, $container);

        $container->setParameter('cp_creation_viti_theme.active_theme', $this->normalizeThemeName($config['active_theme']));
        $container->setParameter('cp_creation_viti_theme.themes', $themes);

        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../../config'));
        $loader->load('services.yaml');
    }

    public function prepend(ContainerBuilder $container): void
    {
        $configuration = new Configuration();
        $configs = $container->getExtensionConfig($this->getAlias());
        $config = $this->processConfiguration($configuration, $configs);
        $themes = $this->resolveThemes($config, $container);
        $activeThemeName = $this->normalizeThemeName($config['active_theme']);
        $activeTheme = $themes[$activeThemeName] ?? null;
        $defaultTheme = $themes['theme-default'] ?? null;

        if (!isset($activeTheme['path']) || !\is_string($activeTheme['path']) || !is_dir($activeTheme['path'])) {
            throw new \RuntimeException(\sprintf('The active Viti theme "%s" is not installed or has no valid path.', $activeThemeName));
        }

        if ($container->hasExtension('twig')) {
            $twigPaths = [];
            $projectTemplatePath = $this->resolveProjectTemplatePath($container);

            if (null !== $projectTemplatePath) {
                $this->addTemplatePaths($twigPaths, $projectTemplatePath, true);
                $this->addNamespacedTemplatePath($twigPaths, $projectTemplatePath, 'CpCreationVitiCore');
                $this->addNamespacedTemplatePath($twigPaths, $projectTemplatePath.'/bundles/TwigBundle', 'Twig');
            }

            foreach (array_filter([$activeTheme, $defaultTheme]) as $theme) {
                $templatePath = $this->themeSubPath($theme, 'templates_path', 'templates');
                $this->addTemplatePaths($twigPaths, $templatePath);
                $this->addNamespacedTemplatePath($twigPaths, $templatePath.'/bundles/TwigBundle', 'Twig');
            }

            $activeTemplatePath = $this->themeSubPath($activeTheme, 'templates_path', 'templates');
            $this->addNamespacedTemplatePath($twigPaths, $activeTemplatePath, 'viti_theme');
            $this->addNamespacedTemplatePath($twigPaths, $activeTemplatePath, 'CpCreationVitiCore');

            if (InstalledVersions::isInstalled('cpcreation/viti-core')) {
                $vitiCoreInstallPath = InstalledVersions::getInstallPath('cpcreation/viti-core');
                if (\is_string($vitiCoreInstallPath)) {
                    $this->addNamespacedTemplatePath(
                        $twigPaths,
                        $vitiCoreInstallPath.'/src/Bridge/Symfony/Bundle/Resources/views',
                        'viti_core_default',
                    );
                }
            }

            if (isset($defaultTheme['path']) && \is_string($defaultTheme['path'])) {
                $defaultTemplatePath = $this->themeSubPath($defaultTheme, 'templates_path', 'templates');
                $this->addNamespacedTemplatePath($twigPaths, $defaultTemplatePath, 'viti_theme_default');
            }

            if ([] !== $twigPaths) {
                $container->prependExtensionConfig('twig', [
                    'paths' => $twigPaths,
                ]);
            }
        }

        if ($container->hasExtension('framework')) {
            $translationPaths = [];

            foreach (array_filter([$defaultTheme, $activeTheme]) as $theme) {
                $translationPath = $this->themeSubPath($theme, 'translations_path', 'translations');

                if (is_dir($translationPath)) {
                    $translationPaths[] = $translationPath;
                }
            }

            if ([] !== $translationPaths) {
                $container->prependExtensionConfig('framework', [
                    'translator' => [
                        'paths' => $translationPaths,
                    ],
                ]);
            }
        }
    }

    /**
     * @param array{active_theme: string, themes: array<string, array<string, mixed>>} $config
     *
     * @return array<string, array<string, string|null>>
     */
    private function resolveThemes(array $config, ContainerBuilder $container): array
    {
        $themes = $this->discoverBundledThemes();

        foreach ($this->discoverComposerThemes() as $name => $theme) {
            $themes[$name] = $theme;
        }

        foreach ($config['themes'] as $name => $themeConfig) {
            $themeName = $this->normalizeThemeName((string) $name);
            $existingTheme = $themes[$themeName] ?? [];
            $packageName = \is_string($themeConfig['package'] ?? null)
                ? $themeConfig['package']
                : ($existingTheme['package'] ?? null);
            $packageTheme = \is_string($packageName) ? $this->resolveComposerTheme($packageName) : null;

            $themes[$themeName] = [
                'package' => \is_string($packageName) ? $packageName : null,
                'path' => $this->resolveConfiguredPath($themeConfig['path'] ?? null, $container)
                    ?? $packageTheme['path'] ?? $existingTheme['path'] ?? null,
                'templates_path' => $this->stringConfig($themeConfig, 'templates_path')
                    ?? $packageTheme['templates_path'] ?? $existingTheme['templates_path'] ?? 'templates',
                'translations_path' => $this->stringConfig($themeConfig, 'translations_path')
                    ?? $packageTheme['translations_path'] ?? $existingTheme['translations_path'] ?? 'translations',
                'assets_path' => $this->stringConfig($themeConfig, 'assets_path')
                    ?? $packageTheme['assets_path'] ?? $existingTheme['assets_path'] ?? 'assets',
                'public_path' => $this->stringConfig($themeConfig, 'public_path')
                    ?? $packageTheme['public_path'] ?? $existingTheme['public_path'] ?? null,
            ];
        }

        return $themes;
    }

    /**
     * @param array<string, string|null> $twigPaths
     */
    private function addTemplatePaths(array &$twigPaths, string $templatePath, bool $includeProjectNamespaces = false): void
    {
        if (is_dir($templatePath)) {
            $twigPaths[$templatePath] = null;
        }

        $namespacedPaths = ['components'];
        if ($includeProjectNamespaces) {
            $namespacedPaths[] = 'stream';
            $namespacedPaths[] = 'extends';
        }

        foreach ($namespacedPaths as $namespace) {
            $path = $templatePath.'/'.$namespace;
            if (is_dir($path)) {
                $twigPaths[$path] = $namespace;
            }
        }
    }

    /**
     * @param array<string, string|null> $twigPaths
     */
    private function addNamespacedTemplatePath(array &$twigPaths, string $templatePath, string $namespace): void
    {
        if (!is_dir($templatePath)) {
            return;
        }

        // Twig paths are keyed by their filesystem path. Keep a distinct spelling
        // when the same directory must exist in both the default and named namespaces.
        $namespacedPath = $templatePath.'/.';
        while (\array_key_exists($namespacedPath, $twigPaths)) {
            $namespacedPath .= '/.';
        }

        $twigPaths[$namespacedPath] = $namespace;
    }

    private function resolveProjectTemplatePath(ContainerBuilder $container): ?string
    {
        $projectDir = $container->getParameter('kernel.project_dir');

        if (!\is_string($projectDir)) {
            return null;
        }

        $templatePath = $projectDir.'/templates';

        return is_dir($templatePath) ? $templatePath : null;
    }

    /**
     * @return array<string, array<string, string|null>>
     */
    private function discoverBundledThemes(): array
    {
        $themes = [];
        $bundleRoot = \dirname(__DIR__, 2);

        foreach (glob($bundleRoot.'/themes/theme-*', \GLOB_ONLYDIR) ?: [] as $themePath) {
            $themeName = basename($themePath);
            $publicPath = $bundleRoot.'/public/themes/'.$themeName;

            $themes[$themeName] = [
                'package' => null,
                'path' => $themePath,
                'templates_path' => 'templates',
                'translations_path' => 'translations',
                'assets_path' => 'assets',
                'public_path' => is_dir($publicPath) ? $publicPath : null,
            ];
        }

        return $themes;
    }

    /**
     * @return array<string, array<string, string|null>>
     */
    private function discoverComposerThemes(): array
    {
        $themes = [];

        if (!class_exists(InstalledVersions::class) || !method_exists(InstalledVersions::class, 'getInstalledPackagesByType')) {
            return $themes;
        }

        foreach (InstalledVersions::getInstalledPackagesByType(self::THEME_PACKAGE_TYPE) as $packageName) {
            $theme = $this->resolveComposerTheme($packageName);

            if (null === $theme) {
                continue;
            }

            $themes[$theme['name']] = $theme;
            unset($themes[$theme['name']]['name']);
        }

        return $themes;
    }

    /**
     * @return array{name: string, package: string, path: string, templates_path: string, translations_path: string, assets_path: string, public_path: string|null}|null
     */
    private function resolveComposerTheme(string $packageName): ?array
    {
        if (!class_exists(InstalledVersions::class) || !InstalledVersions::isInstalled($packageName)) {
            return null;
        }

        $installPath = InstalledVersions::getInstallPath($packageName);

        if (null === $installPath) {
            return null;
        }

        $composerJson = $this->readJsonFile($installPath.'/composer.json');
        $extra = \is_array($composerJson['extra']['viti-theme'] ?? null) ? $composerJson['extra']['viti-theme'] : [];
        $themePath = $this->joinPath($installPath, $this->extraString($extra, 'path') ?? '.');
        $metadata = $this->readJsonFile($themePath.'/theme.json');
        $themeName = $this->extraString($extra, 'name')
            ?? (\is_string($metadata['name'] ?? null) ? $metadata['name'] : basename($themePath));

        return [
            'name' => $this->normalizeThemeName($themeName),
            'package' => $packageName,
            'path' => $themePath,
            'templates_path' => $this->extraString($extra, 'templates_path') ?? 'templates',
            'translations_path' => $this->extraString($extra, 'translations_path') ?? 'translations',
            'assets_path' => $this->extraString($extra, 'assets_path') ?? 'assets',
            'public_path' => $this->extraString($extra, 'public_path'),
        ];
    }

    /**
     * @param array<string, mixed> $config
     */
    private function stringConfig(array $config, string $key): ?string
    {
        return \is_string($config[$key] ?? null) && '' !== $config[$key] ? $config[$key] : null;
    }

    /**
     * @param array<string, mixed> $extra
     */
    private function extraString(array $extra, string $key): ?string
    {
        $value = $extra[$key] ?? $extra[str_replace('_', '-', $key)] ?? null;

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

    private function resolveConfiguredPath(mixed $path, ContainerBuilder $container): ?string
    {
        if (!\is_string($path) || '' === $path) {
            return null;
        }

        $resolvedPath = $container->getParameterBag()->resolveValue($path);

        return \is_string($resolvedPath) ? $resolvedPath : $path;
    }

    /**
     * @param array<string, string|null> $theme
     */
    private function themeSubPath(array $theme, string $key, string $default): string
    {
        $themeRoot = mb_rtrim((string) $theme['path'], '/');
        $subPath = $theme[$key] ?? $default;

        if (null === $subPath || '' === $subPath || '.' === $subPath) {
            return $themeRoot;
        }

        if (str_starts_with($subPath, '/') || 1 === preg_match('#^[A-Za-z]:[\\\\/]#', $subPath)) {
            return mb_rtrim($subPath, '/');
        }

        return $themeRoot.'/'.mb_trim($subPath, '/');
    }

    private function normalizeThemeName(string $name): string
    {
        return str_replace('_', '-', $name);
    }

    /**
     * @return array<string, mixed>
     */
    private function readJsonFile(string $path): array
    {
        if (!is_file($path)) {
            return [];
        }

        $decoded = json_decode((string) file_get_contents($path), true, flags: \JSON_THROW_ON_ERROR);

        return \is_array($decoded) ? $decoded : [];
    }

    private function joinPath(string $rootPath, string $relativePath): string
    {
        if ('' === $relativePath || '.' === $relativePath) {
            return mb_rtrim($rootPath, '/');
        }

        if (str_starts_with($relativePath, '/') || 1 === preg_match('#^[A-Za-z]:[\\\\/]#', $relativePath)) {
            return mb_rtrim($relativePath, '/');
        }

        return mb_rtrim($rootPath, '/').'/'.mb_trim($relativePath, '/');
    }
}
