<?php

declare(strict_types=1);

namespace PpPhp\Core\Theme;

use PpPhp\Core\ModuleState;

/**
 * ThemeRegistry
 *
 * Merges the built-in preset themes (resources/Themes/presets/*.json) with a
 * project's own custom themes (ModuleState 'themes'), resolving `base`
 * inheritance so every theme in the catalogue carries a complete token set.
 *
 * A theme is pure data:
 *   { id, label, description?, base?, tokens:{…}, dark?:{…} }
 * Custom project themes may set `base` to a preset (or another custom theme) to
 * inherit its tokens and override a few.
 */
final class ThemeRegistry
{
    /** Raw built-in presets keyed by id (unresolved). */
    public static function presets(): array
    {
        $dir = self::basePath() . '/resources/Themes/presets';
        $raw = [];

        foreach (glob($dir . '/*.json') ?: [] as $file) {
            $theme = json_decode((string) file_get_contents($file), true);
            if (is_array($theme) && !empty($theme['id'])) {
                $raw[(string) $theme['id']] = $theme;
            }
        }

        // keep 'default' first, then the rest in their natural order
        if (isset($raw['default'])) {
            $raw = ['default' => $raw['default']] + $raw;
        }

        return $raw;
    }

    /** Raw custom themes for a project keyed by id (unresolved). */
    public static function projectThemes(string $slug): array
    {
        $state = ModuleState::load($slug, 'themes');
        $themes = $state['themes'] ?? [];

        return is_array($themes) ? $themes : [];
    }

    /** The project's default theme id (falls back to 'default'). */
    public static function activeDefault(string $slug): string
    {
        $state = ModuleState::load($slug, 'themes');
        $id = (string) ($state['default'] ?? 'default');

        return $id !== '' ? $id : 'default';
    }

    /**
     * Full catalogue for a project: presets + custom, each fully resolved.
     *
     * @return array<int, array<string, mixed>>
     */
    public static function catalogue(string $slug): array
    {
        $all = self::themeMap($slug);

        $out = [];
        foreach ($all as $theme) {
            $out[] = self::resolve($all, $theme);
        }

        return $out;
    }

    /** Resolve a single theme id within a project (or null if unknown). */
    public static function get(string $slug, string $id): ?array
    {
        $all = self::themeMap($slug);

        return isset($all[$id]) ? self::resolve($all, $all[$id]) : null;
    }

    /**
     * The project's theme map: shipped presets with the project's saved copies
     * overlaid.
     *
     * A saved copy of a SHIPPED preset is merged ONTO the preset with empty
     * values dropped, rather than replacing it wholesale. So a field the project
     * never customised (e.g. a blank `css`) falls back to the shipped preset —
     * which means a later fix shipped in a preset still reaches projects that
     * saved an older copy of it. A saved copy keeps its locked built-in identity;
     * genuinely custom themes (no matching preset) are kept verbatim as custom.
     *
     * @return array<string, array<string,mixed>>
     */
    private static function themeMap(string $slug): array
    {
        $all = self::presets();
        $presetIds = $all;
        foreach (self::projectThemes($slug) as $tid => $theme) {
            if (!is_array($theme) || empty($theme['id'])) {
                continue;
            }
            $tid = (string) $tid;
            if (isset($presetIds[$tid])) {
                $nonEmpty = array_filter(
                    $theme,
                    static fn ($v) => $v !== '' && $v !== null && $v !== []
                );
                $theme = array_replace($presetIds[$tid], $nonEmpty);
                $theme['custom'] = false;
            } else {
                $theme['custom'] = true;
            }
            $all[$tid] = $theme;
        }

        return $all;
    }

    /**
     * Flatten `base` inheritance so tokens/dark are complete.
     *
     * @param array<string, array<string,mixed>> $all
     */
    private static function resolve(array $all, array $theme, array $seen = []): array
    {
        $tokens = [];
        $dark = [];

        $base = $theme['base'] ?? null;
        if (is_string($base) && $base !== '' && isset($all[$base]) && !in_array($base, $seen, true)) {
            $seen[] = $base;
            $resolvedBase = self::resolve($all, $all[$base], $seen);
            $tokens = $resolvedBase['tokens'];
            $dark = $resolvedBase['dark'];
        }

        $shell = [];
        $grid = [];
        $density = 'normal';
        $surface = 'flat';
        $css = '';
        if (is_string($base) && $base !== '' && isset($all[$base])) {
            $rb = self::resolve($all, $all[$base], $seen);
            $shell = $rb['shell'];
            $grid = $rb['grid'];
            $density = $rb['density'];
            $surface = $rb['surface'] ?? 'flat';
            $css = $rb['css'];
        }

        $tokens = array_merge($tokens, (array) ($theme['tokens'] ?? []));
        $dark = array_merge($dark, (array) ($theme['dark'] ?? []));
        $shell = self::deepMerge($shell, (array) ($theme['shell'] ?? []));
        $grid = array_merge($grid, (array) ($theme['grid'] ?? []));
        if (isset($theme['density'])) { $density = (string) $theme['density']; }
        if (isset($theme['surface'])) { $surface = ((string) $theme['surface'] === 'glass') ? 'glass' : 'flat'; }
        if (isset($theme['css'])) { $css = (string) $theme['css']; }

        return [
            'id' => (string) ($theme['id'] ?? ''),
            'label' => (string) ($theme['label'] ?? ($theme['id'] ?? 'Theme')),
            'description' => (string) ($theme['description'] ?? ''),
            'base' => is_string($base) ? $base : null,
            'custom' => !empty($theme['custom']),
            'tokens' => $tokens,
            'dark' => $dark,
            'shell' => $shell,
            'grid' => $grid,
            'density' => $density,
            'surface' => $surface,
            'css' => $css,
        ];
    }

    /** Recursive array merge (for nested shell.{topbar,sidebar,…}). */
    private static function deepMerge(array $a, array $b): array
    {
        foreach ($b as $k => $v) {
            $a[$k] = (is_array($v) && isset($a[$k]) && is_array($a[$k])) ? self::deepMerge($a[$k], $v) : $v;
        }
        return $a;
    }

    /** The project navigation menu (tree of items), edited in the Style SPA. */
    public static function menu(string $slug): array
    {
        $state = ModuleState::load($slug, 'themes');
        $menu = $state['menu'] ?? null;
        return is_array($menu) ? $menu : ['items' => []];
    }

    /**
     * Project-level layout (shell) overrides — edited in the Style SPA's
     * Layout tab and independent of the chosen theme, so switching themes
     * never resets topbar/sidebar/logo/footer settings.
     */
    public static function projectShell(string $slug): array
    {
        $state = ModuleState::load($slug, 'themes');
        $shell = $state['shell'] ?? null;
        return is_array($shell) ? $shell : [];
    }

    /** A resolved theme with the project shell overlaid (what the Build uses). */
    public static function resolvedForBuild(string $slug, string $id): ?array
    {
        $theme = self::get($slug, $id);
        if ($theme === null) {
            return null;
        }
        $shell = self::projectShell($slug);
        if ($shell !== []) {
            $theme['shell'] = self::deepMerge((array) ($theme['shell'] ?? []), $shell);
        }
        return $theme;
    }

    private static function basePath(): string
    {
        return defined('BASE_PATH') ? constant('BASE_PATH') : dirname(__DIR__, 2);
    }
}
