<?php
declare(strict_types=1);

namespace App\Theme;

use App\Support\FileLogger;
use Throwable;

final class ThemeManager
{
    private const DEFAULT_THEME = 'default';

    /** @return list<array{name:string,version:string,author:string,directions:list<string>,min_core:string,assets:array<string,list<string>>}> */
    public static function available(array $requiredDirections = []): array
    {
        $themes = [];
        $directory = app_public_path('themes');
        foreach (glob($directory . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR) ?: [] as $themeDirectory) {
            $name = basename($themeDirectory);
            $manifest = self::loadManifest($name);
            if ($manifest !== [] && self::isCompatible($manifest, $requiredDirections)) {
                $themes[] = $manifest;
            }
        }
        usort($themes, static fn (array $left, array $right): int => strcmp($left['name'], $right['name']));
        return $themes;
    }

    /** @return array{name:string,version:string,author:string,directions:list<string>,min_core:string,assets:array<string,list<string>>} */
    public static function loadManifest(string $name): array
    {
        $name = self::normalizeName($name);
        if ($name === null) {
            return [];
        }
        $file = app_public_path('themes', $name, 'theme.json');
        if (!is_file($file)) {
            return [];
        }

        try {
            $raw = json_decode((string) file_get_contents($file), true, 32, JSON_THROW_ON_ERROR);
            if (!is_array($raw)) {
                return [];
            }
            $directions = array_values(array_unique(array_filter(array_map(
                static fn (mixed $direction): string => strtolower(trim((string) $direction)),
                is_array($raw['directions'] ?? null) ? $raw['directions'] : []
            ), static fn (string $direction): bool => in_array($direction, ['rtl', 'ltr'], true))));
            if ($directions === []) {
                return [];
            }

            return [
                'name' => $name,
                'version' => trim((string) ($raw['version'] ?? '')) ?: '1.0.0',
                'author' => trim((string) ($raw['author'] ?? '')),
                'directions' => $directions,
                'min_core' => trim((string) ($raw['min_core'] ?? '')),
                'assets' => [
                    'css' => self::safeAssetList($name, $raw['assets']['css'] ?? []),
                    'js' => self::safeAssetList($name, $raw['assets']['js'] ?? []),
                ],
            ];
        } catch (Throwable $throwable) {
            (new FileLogger())->error('Theme manifest could not be loaded.', $throwable, ['theme' => $name]);
            return [];
        }
    }

    /** @param list<string> $requiredDirections */
    public static function resolveActiveName(string $name, array $requiredDirections = []): string
    {
        $name = self::normalizeName($name) ?? self::DEFAULT_THEME;
        $manifest = self::loadManifest($name);
        return $manifest !== [] && self::isCompatible($manifest, $requiredDirections)
            ? $name
            : self::DEFAULT_THEME;
    }

    /**
     * @param array{name:string,version:string,author:string,directions:list<string>,min_core:string,assets:array<string,list<string>>} $manifest
     * @param list<string> $requiredDirections
     */
    public static function isCompatible(array $manifest, array $requiredDirections = [], ?string $coreVersion = null): bool
    {
        $directions = array_values(array_unique(array_filter(array_map(
            static fn (mixed $direction): string => strtolower(trim((string) $direction)),
            $requiredDirections
        ), static fn (string $direction): bool => in_array($direction, ['rtl', 'ltr'], true))));

        foreach ($directions as $direction) {
            if (!in_array($direction, $manifest['directions'] ?? [], true)) {
                return false;
            }
        }

        $minimum = trim((string) ($manifest['min_core'] ?? ''));
        if ($minimum === '') {
            return true;
        }
        if (preg_match('/^\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9.-]+)?$/', $minimum) !== 1) {
            return false;
        }

        $current = trim($coreVersion ?? (function_exists('app_version') ? app_version() : '0.0.0'));
        return version_compare($current, $minimum, '>=');
    }

    public static function templatePath(string $template, string $themeName, string $direction = 'ltr'): ?string
    {
        $themeName = self::resolveActiveName($themeName);
        $manifest = self::loadManifest($themeName);
        if ($manifest === [] || !in_array($direction, $manifest['directions'], true)) {
            return null;
        }
        $template = trim(str_replace(['\\', '..'], ['', ''], $template), '/');
        if ($template === '' || preg_match('/^[A-Za-z0-9_\/-]+$/', $template) !== 1) {
            return null;
        }
        $file = app_public_path('themes', $themeName, 'views', $template . '.php');
        return is_file($file) ? $file : null;
    }

    /** @return list<array{path:string,url:string}> */
    public static function assets(string $themeName, string $type, string $direction = ''): array
    {
        $manifest = self::loadManifest(self::resolveActiveName($themeName));
        if ($manifest === [] || !in_array($type, ['css', 'js'], true)) {
            return [];
        }

        $declared = $manifest['assets'][$type];

        // A theme may ship assets/css/rtl.css (or ltr.css) for the handful of
        // rules a direction genuinely needs. It is loaded after the declared
        // assets and only in that direction, so a theme that does not provide
        // one is unaffected.
        if ($type === 'css' && in_array($direction, ['rtl', 'ltr'], true)) {
            $variant = 'assets/css/' . $direction . '.css';
            if (!in_array($variant, $declared, true)) {
                $declared[] = $variant;
            }
        }

        $assets = [];
        foreach ($declared as $relative) {
            $path = app_public_path('themes', $manifest['name'], $relative);
            $publicPath = 'themes/' . $manifest['name'] . '/' . $relative;
            if (!is_file($path)) {
                continue;
            }
            $assets[] = [
                'path' => $publicPath,
                'url' => quaeriva_url($publicPath, ['v' => quaeriva_asset_version($publicPath)]),
            ];
        }
        return $assets;
    }

    /**
     * Strings a theme ships for one locale.
     *
     * A theme may provide `lang/<locale>.php` returning a flat array. Keys are
     * forced into the `theme.` namespace so a theme can never redefine a core
     * string, and database translation overrides still win over both — an
     * administrator can always correct a theme's wording.
     *
     * @return array<string, string>
     */
    public static function translations(string $themeName, string $locale): array
    {
        $name = self::normalizeName(self::resolveActiveName($themeName));
        $locale = strtolower(trim($locale));

        if ($name === null || preg_match('/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/', $locale) !== 1) {
            return [];
        }

        $file = app_public_path('themes', $name, 'lang', $locale . '.php');

        if (!is_file($file)) {
            return [];
        }

        $raw = require $file;

        if (!is_array($raw)) {
            return [];
        }

        $strings = [];

        foreach ($raw as $key => $value) {
            if (!is_string($key) || !is_scalar($value)) {
                continue;
            }

            $key = str_starts_with($key, 'theme.') ? $key : 'theme.' . $key;
            $strings[$key] = (string) $value;
        }

        return $strings;
    }

    private static function normalizeName(string $name): ?string
    {
        $name = strtolower(trim($name));
        return preg_match('/^[a-z0-9][a-z0-9_-]{0,63}$/', $name) === 1 ? $name : null;
    }

    /** @return list<string> */
    private static function safeAssetList(string $themeName, mixed $values): array
    {
        if (!is_array($values)) {
            return [];
        }
        $result = [];
        foreach ($values as $value) {
            $relative = trim(str_replace('\\', '/', (string) $value), '/');
            if ($relative === '' || str_contains($relative, '..') || !preg_match('/^[A-Za-z0-9_\/-]+\.(?:css|js)$/', $relative)) {
                continue;
            }
            if (!is_file(app_public_path('themes', $themeName, $relative))) {
                continue;
            }
            $result[] = $relative;
        }
        return array_values(array_unique($result));
    }
}
