<?php

declare(strict_types=1);

// StyleDefinitionRuntime + DesignSystemRegistry loaded by the app-local JSONM runtime.

final class MyronTokenReferenceService
{
    /**
     * @var array<string, array{title:string,description:string,renderMode:string}>
     */
    private const GROUP_SPECS = [
        'tokens.color.semantic' => [
            'title' => 'color.semantic',
            'description' => 'Live semantic color tokens from the currently applied design system.',
            'renderMode' => 'swatch',
        ],
        'tokens.color.derived' => [
            'title' => 'color.derived',
            'description' => 'Derived semantic helpers compiled from the active JSONM definition.',
            'renderMode' => 'swatch',
        ],
        'tokens.space.scale' => [
            'title' => 'space.scale',
            'description' => 'Base spacing scale values used throughout atoms, primitives, and components.',
            'renderMode' => 'table',
        ],
        'tokens.space.aliases' => [
            'title' => 'space.aliases',
            'description' => 'Semantic spacing aliases mapped onto the active scale.',
            'renderMode' => 'table',
        ],
        'tokens.size.control' => [
            'title' => 'size.control',
            'description' => 'Control heights for inputs, buttons, and other interactive surfaces.',
            'renderMode' => 'table',
        ],
        'tokens.size.icon' => [
            'title' => 'size.icon',
            'description' => 'Canonical icon sizes shared across the component registry.',
            'renderMode' => 'table',
        ],
        'tokens.size.hitTargetMin' => [
            'title' => 'size.hitTargetMin',
            'description' => 'Minimum interaction target size enforced by the current system.',
            'renderMode' => 'table',
        ],
        'tokens.radius.scale' => [
            'title' => 'radius.scale',
            'description' => 'Corner radius scale values from the applied design system.',
            'renderMode' => 'table',
        ],
        'tokens.radius.aliases' => [
            'title' => 'radius.aliases',
            'description' => 'Semantic radius aliases compiled from the active scale.',
            'renderMode' => 'table',
        ],
        'tokens.border' => [
            'title' => 'border',
            'description' => 'Border width primitives currently active in the runtime.',
            'renderMode' => 'table',
        ],
        'tokens.typography.families' => [
            'title' => 'typography.families',
            'description' => 'Typography family assignments for the active system.',
            'renderMode' => 'table',
        ],
        'tokens.typography.weights' => [
            'title' => 'typography.weights',
            'description' => 'Typography weight values exposed through the token vocabulary.',
            'renderMode' => 'table',
        ],
        'tokens.typography.sizes' => [
            'title' => 'typography.sizes',
            'description' => 'Typography sizes currently resolved by the applied design system.',
            'renderMode' => 'table',
        ],
        'tokens.typography.lineHeights' => [
            'title' => 'typography.lineHeights',
            'description' => 'Line-height values available to the component registry.',
            'renderMode' => 'table',
        ],
        'tokens.typography.tracking' => [
            'title' => 'typography.tracking',
            'description' => 'Tracking values exposed through the live token vocabulary.',
            'renderMode' => 'table',
        ],
        'tokens.motion.duration' => [
            'title' => 'motion.duration',
            'description' => 'Motion durations currently active outside the fixed shell chrome.',
            'renderMode' => 'table',
        ],
        'tokens.motion.easing' => [
            'title' => 'motion.easing',
            'description' => 'Motion easing values currently active outside the fixed shell chrome.',
            'renderMode' => 'table',
        ],
        'tokens.layout.containers' => [
            'title' => 'layout.containers',
            'description' => 'Container widths available to page and component layouts.',
            'renderMode' => 'table',
        ],
        'tokens.layout.gutters' => [
            'title' => 'layout.gutters',
            'description' => 'Layout gutter values used by design and content surfaces.',
            'renderMode' => 'table',
        ],
        'tokens.layout.breakpoints' => [
            'title' => 'layout.breakpoints',
            'description' => 'Responsive breakpoints exposed by the applied design system.',
            'renderMode' => 'table',
        ],
        'tokens.layout.widthModes' => [
            'title' => 'layout.widthModes',
            'description' => 'Constrained page and section width intent consumed by public layout envelopes.',
            'renderMode' => 'table',
        ],
        'tokens.focus' => [
            'title' => 'focus',
            'description' => 'Focus treatment tokens used by interactive surfaces.',
            'renderMode' => 'table',
        ],
        'tokens.zIndex' => [
            'title' => 'zIndex',
            'description' => 'Overlay and stacking context values for interactive layers.',
            'renderMode' => 'table',
        ],
        'fontRoles.roles' => [
            'title' => 'fontRoles.roles',
            'description' => 'Role-based type assignments compiled from the active design system.',
            'renderMode' => 'table',
        ],
        'fontRoles.delivery' => [
            'title' => 'fontRoles.delivery',
            'description' => 'External font delivery metadata for the current system.',
            'renderMode' => 'table',
        ],
    ];

    private readonly string $tokenMapPath;
    private readonly string $currentDefinitionPath;
    private readonly string $runtimeTokensPath;
    private readonly string $runtimeFontsPath;
    private readonly string $inventoryPath;

    public function __construct(?array $paths = null)
    {
        $root = StyleDefinitionRuntime::projectRoot();

        $this->tokenMapPath = (string) ($paths['tokenMapPath'] ?? ($root . '/app/jsonm/maps/token-map.json'));
        $this->currentDefinitionPath = (string) ($paths['currentDefinitionPath'] ?? ($root . '/app/jsonm/current-definition.json'));
        $this->runtimeTokensPath = (string) ($paths['runtimeTokensPath'] ?? ($root . '/app/assets/runtime/css/runtime-token-overrides.css'));
        $this->runtimeFontsPath = (string) ($paths['runtimeFontsPath'] ?? ($root . '/app/assets/runtime/css/runtime-fonts.css'));
        $this->inventoryPath = (string) ($paths['inventoryPath'] ?? ($root . '/' . 'dev-' . 'references/component-inventory.json'));
    }

    /**
     * @return array<string, mixed>
     */
    public function snapshot(): array
    {
        $registry = new DesignSystemRegistry([
            'tokenMapPath' => $this->tokenMapPath,
            'currentDefinitionPath' => $this->currentDefinitionPath,
            'runtimeTokensPath' => $this->runtimeTokensPath,
            'runtimeFontsPath' => $this->runtimeFontsPath,
        ]);
        $registry->ensureStorage();

        $tokenMap = StyleDefinitionRuntime::loadTokenMap($this->tokenMapPath);
        $current = $registry->currentDefinition();
        $definition = StyleDefinitionRuntime::loadJson((string) $current['path']);
        $runtimeVariables = $this->loadRuntimeVariables();
        $fontImportUrl = $this->loadFontImportUrl();
        // Component inventory is Class 3 (development reference; not shipped).
        // When the file is absent, return an empty inventory shape so the
        // snapshot remains valid for downstream consumers.
        $inventory = is_file($this->inventoryPath)
            ? StyleDefinitionRuntime::loadJson($this->inventoryPath)
            : ['id' => 'absent', 'sourceRevision' => 'absent', 'producedAt' => '', 'note' => 'Component inventory artifact not present in this install (Class 3 development reference; not shipped).'];
        $normalizedInventory = $this->normalizeInventory($inventory, $tokenMap);
        $dependentsByToken = $this->indexTokenDependents($normalizedInventory['sections']);

        return [
            'currentSystem' => [
                'id' => (string) ($current['id'] ?? ''),
                'name' => (string) ($current['name'] ?? ''),
                'kind' => (string) ($current['kind'] ?? ''),
                'description' => (string) ($current['description'] ?? ''),
                'source' => (string) ($current['source'] ?? ''),
                'filename' => (string) ($current['filename'] ?? ''),
                'path' => (string) ($current['path'] ?? ''),
                'appliedAt' => $current['appliedAt'] ?? null,
            ],
            'sourceOfTruth' => [
                [
                    'label' => 'Vocabulary map',
                    'path' => 'app/jsonm/maps/token-map.json',
                    'detail' => count($tokenMap['mappings']) . ' mapped CSS variables and ' . count($tokenMap['specialMappings'] ?? []) . ' special mappings',
                ],
                [
                    'label' => 'Current applied pointer',
                    'path' => 'app/jsonm/current-definition.json',
                    'detail' => (string) ($current['id'] ?? '') . ' → ' . (string) ($current['filename'] ?? ''),
                ],
                [
                    'label' => 'Runtime token output',
                    'path' => 'app/assets/runtime/css/runtime-token-overrides.css',
                    'detail' => count($runtimeVariables) . ' live CSS variables parsed for observability',
                ],
                [
                    'label' => 'Component inventory snapshot',
                    'path' => 'app/data/component-inventory.json',
                    'detail' => (string) ($inventory['id'] ?? 'component inventory') . ' · ' . (string) ($inventory['sourceRevision'] ?? 'unknown revision'),
                ],
            ],
            'overview' => [
                'mappedTokenCount' => count($tokenMap['mappings']),
                'groupCount' => count(self::GROUP_SPECS),
                'inventoryId' => (string) ($inventory['id'] ?? 'component inventory'),
                'inventoryProducedAt' => (string) ($inventory['producedAt'] ?? ''),
                'inventoryRevision' => (string) ($inventory['sourceRevision'] ?? ''),
                'fontImportUrl' => $fontImportUrl,
                'summary' => is_array($inventory['summary'] ?? null) ? $inventory['summary'] : [],
            ],
            'groups' => $this->buildGroups($tokenMap, $definition, $runtimeVariables, $fontImportUrl, $dependentsByToken),
            'registry' => $normalizedInventory,
        ];
    }

    /**
     * @return array<string, string>
     */
    private function loadRuntimeVariables(): array
    {
        $runtimeTokens = (string) file_get_contents($this->runtimeTokensPath);
        $runtimeFonts = (string) file_get_contents($this->runtimeFontsPath);

        return array_merge(
            StyleDefinitionRuntime::parseCssVariables($runtimeTokens),
            StyleDefinitionRuntime::parseCssVariables($runtimeFonts)
        );
    }

    private function loadFontImportUrl(): string
    {
        $runtimeFonts = (string) file_get_contents($this->runtimeFontsPath);
        return StyleDefinitionRuntime::extractImportUrl($runtimeFonts);
    }

    /**
     * @param array<string, mixed> $inventory
     * @param array<string, mixed> $tokenMap
     * @return array{summary:array<string,mixed>,sections:array<string,array<string,mixed>>}
     */
    private function normalizeInventory(array $inventory, array $tokenMap): array
    {
        $summary = is_array($inventory['summary'] ?? null) ? $inventory['summary'] : [];
        $compositionTree = is_array($inventory['compositionTree'] ?? null) ? $inventory['compositionTree'] : [];
        $reverseComposition = [];

        foreach ($compositionTree as $parent => $children) {
            if (!is_array($children)) {
                continue;
            }

            foreach ($children as $child) {
                $childId = (string) $child;
                if ($childId === '') {
                    continue;
                }
                $reverseComposition[$childId][] = (string) $parent;
            }
        }

        $sections = [];
        foreach (['atoms' => 'Atom', 'primitives' => 'Primitive', 'components' => 'Component'] as $bucket => $label) {
            $items = is_array($inventory[$bucket] ?? null) ? $inventory[$bucket] : [];
            $normalizedItems = [];

            foreach ($items as $item) {
                if (!is_array($item)) {
                    continue;
                }

                $id = (string) ($item['id'] ?? '');
                $tokenDependencies = array_values(array_filter(
                    array_map('strval', is_array($item['tokenDependencies'] ?? null) ? $item['tokenDependencies'] : []),
                    static fn (string $value): bool => $value !== ''
                ));

                $normalizedItems[] = [
                    'id' => $id,
                    'classification' => strtolower($label),
                    'registryAddress' => (string) ($item['registryAddress'] ?? ''),
                    'purpose' => (string) ($item['purpose'] ?? ''),
                    'tokenDependencies' => $tokenDependencies,
                    'tokenFamilies' => $this->tokenFamiliesForDependencies($tokenDependencies, $tokenMap),
                    'composedOf' => array_values(array_map('strval', is_array($item['composedOf'] ?? null) ? $item['composedOf'] : [])),
                    'composes' => array_values(array_map('strval', is_array($compositionTree[$id] ?? null) ? $compositionTree[$id] : [])),
                    'usedBy' => array_values(array_map('strval', is_array($reverseComposition[$id] ?? null) ? $reverseComposition[$id] : [])),
                ];
            }

            $sections[$bucket] = [
                'label' => $label,
                'count' => count($normalizedItems),
                'items' => $normalizedItems,
            ];
        }

        return [
            'summary' => $summary,
            'sections' => $sections,
        ];
    }

    /**
     * @param array<string, array<string, mixed>> $sections
     * @return array<string, list<array<string, mixed>>>
     */
    private function indexTokenDependents(array $sections): array
    {
        $index = [];

        foreach ($sections as $section) {
            $items = is_array($section['items'] ?? null) ? $section['items'] : [];
            foreach ($items as $item) {
                if (!is_array($item)) {
                    continue;
                }

                $surface = [
                    'id' => (string) ($item['id'] ?? ''),
                    'classification' => (string) ($item['classification'] ?? ''),
                    'registryAddress' => (string) ($item['registryAddress'] ?? ''),
                    'tokenFamilies' => array_values(array_map('strval', is_array($item['tokenFamilies'] ?? null) ? $item['tokenFamilies'] : [])),
                    'composedOf' => array_values(array_map('strval', is_array($item['composedOf'] ?? null) ? $item['composedOf'] : [])),
                    'usedBy' => array_values(array_map('strval', is_array($item['usedBy'] ?? null) ? $item['usedBy'] : [])),
                ];

                foreach ((array) ($item['tokenDependencies'] ?? []) as $cssVar) {
                    $token = (string) $cssVar;
                    if ($token === '') {
                        continue;
                    }
                    $index[$token][] = $surface;
                }
            }
        }

        return $index;
    }

    /**
     * @param array<string, mixed> $tokenMap
     * @param array<string, mixed> $definition
     * @param array<string, string> $runtimeVariables
     * @param array<string, list<array<string, mixed>>> $dependentsByToken
     * @return list<array<string, mixed>>
     */
    private function buildGroups(
        array $tokenMap,
        array $definition,
        array $runtimeVariables,
        string $fontImportUrl,
        array $dependentsByToken
    ): array {
        $groups = [];
        foreach (self::GROUP_SPECS as $groupKey => $spec) {
            $groups[$groupKey] = [
                'id' => $this->groupAnchor($groupKey),
                'key' => $groupKey,
                'title' => $spec['title'],
                'description' => $spec['description'],
                'renderMode' => $spec['renderMode'],
                'entries' => [],
                'surfaceCount' => 0,
            ];
        }

        foreach ($tokenMap['mappings'] as $mapping) {
            if (!is_array($mapping)) {
                continue;
            }

            $jsonPath = (string) ($mapping['jsonPath'] ?? '');
            $groupKey = $this->groupKeyForPath($jsonPath);
            if ($groupKey === null || !isset($groups[$groupKey])) {
                continue;
            }

            $entry = $this->buildTokenEntry($mapping, $definition, $runtimeVariables, $dependentsByToken);
            $groups[$groupKey]['entries'][] = $entry;
            $groups[$groupKey]['surfaceCount'] += (int) $entry['dependentSummary']['total'];
        }

        $googleFontsPath = 'fontRoles.googleFontsCss2Url';
        if ($this->pathExists($definition, $googleFontsPath)) {
            $sourceValue = $this->pathStringValue($definition, $googleFontsPath);
            $groups['fontRoles.delivery']['entries'][] = [
                'name' => 'googleFontsCss2Url',
                'cssVar' => null,
                'jsonPath' => $googleFontsPath,
                'kind' => 'url',
                'runtimeValue' => $fontImportUrl !== '' ? $fontImportUrl : $sourceValue,
                'sourceValue' => $sourceValue,
                'dependentSummary' => [
                    'atoms' => 0,
                    'primitives' => 0,
                    'components' => 0,
                    'total' => 0,
                ],
                'dependentPreview' => [],
            ];
        }

        foreach ($groups as &$group) {
            usort(
                $group['entries'],
                static fn (array $left, array $right): int => strcmp((string) $left['jsonPath'], (string) $right['jsonPath'])
            );
        }
        unset($group);

        return array_values($groups);
    }

    /**
     * @param array<string, mixed> $mapping
     * @param array<string, mixed> $definition
     * @param array<string, string> $runtimeVariables
     * @param array<string, list<array<string, mixed>>> $dependentsByToken
     * @return array<string, mixed>
     */
    private function buildTokenEntry(
        array $mapping,
        array $definition,
        array $runtimeVariables,
        array $dependentsByToken
    ): array {
        $cssVar = (string) ($mapping['cssVar'] ?? '');
        $jsonPath = (string) ($mapping['jsonPath'] ?? '');
        $kind = (string) ($mapping['kind'] ?? 'value');
        $dependents = array_values($dependentsByToken[$cssVar] ?? []);
        $classificationCounts = ['atoms' => 0, 'primitives' => 0, 'components' => 0, 'total' => count($dependents)];

        foreach ($dependents as $dependent) {
            $classification = (string) ($dependent['classification'] ?? '');
            if ($classification === 'atom') {
                $classificationCounts['atoms']++;
            } elseif ($classification === 'primitive') {
                $classificationCounts['primitives']++;
            } elseif ($classification === 'component') {
                $classificationCounts['components']++;
            }
        }

        return [
            'name' => $this->tokenNameFromPath($jsonPath),
            'cssVar' => $cssVar,
            'jsonPath' => $jsonPath,
            'kind' => $kind,
            'runtimeValue' => $runtimeVariables[$cssVar] ?? '',
            'sourceValue' => $this->pathStringValue($definition, $jsonPath),
            'dependentSummary' => $classificationCounts,
            'dependentPreview' => array_slice($dependents, 0, 6),
        ];
    }

    /**
     * @param list<string> $dependencies
     * @param array<string, mixed> $tokenMap
     * @return list<string>
     */
    private function tokenFamiliesForDependencies(array $dependencies, array $tokenMap): array
    {
        $mappingIndex = [];
        foreach ($tokenMap['mappings'] as $mapping) {
            if (!is_array($mapping)) {
                continue;
            }

            $cssVar = (string) ($mapping['cssVar'] ?? '');
            $jsonPath = (string) ($mapping['jsonPath'] ?? '');
            if ($cssVar === '' || $jsonPath === '') {
                continue;
            }

            $groupKey = $this->groupKeyForPath($jsonPath);
            if ($groupKey === null) {
                continue;
            }

            $mappingIndex[$cssVar] = self::GROUP_SPECS[$groupKey]['title'] ?? $groupKey;
        }

        $families = [];
        foreach ($dependencies as $cssVar) {
            $title = $mappingIndex[$cssVar] ?? 'unmapped';
            $families[$title] = true;
        }

        return array_keys($families);
    }

    private function groupKeyForPath(string $jsonPath): ?string
    {
        $groupKeys = array_keys(self::GROUP_SPECS);
        usort(
            $groupKeys,
            static fn (string $left, string $right): int => strlen($right) <=> strlen($left)
        );

        foreach ($groupKeys as $groupKey) {
            if ($jsonPath === $groupKey || str_starts_with($jsonPath, $groupKey . '.')) {
                return $groupKey;
            }
        }

        return null;
    }

    private function groupAnchor(string $groupKey): string
    {
        return preg_replace('/[^a-z0-9]+/i', '-', strtolower($groupKey)) ?: 'token-group';
    }

    private function tokenNameFromPath(string $jsonPath): string
    {
        $segments = explode('.', $jsonPath);
        return (string) end($segments);
    }

    /**
     * @param array<string, mixed> $data
     */
    private function pathExists(array $data, string $path): bool
    {
        $segments = explode('.', $path);
        $current = $data;

        foreach ($segments as $segment) {
            if (!is_array($current) || !array_key_exists($segment, $current)) {
                return false;
            }
            $current = $current[$segment];
        }

        return true;
    }

    /**
     * @param array<string, mixed> $data
     */
    private function pathStringValue(array $data, string $path): string
    {
        if (!$this->pathExists($data, $path)) {
            return '';
        }

        $segments = explode('.', $path);
        $current = $data;
        foreach ($segments as $segment) {
            $current = $current[$segment];
        }

        if (is_string($current)) {
            return $current;
        }
        if (is_int($current) || is_float($current)) {
            return (string) $current;
        }
        if (is_bool($current)) {
            return $current ? 'true' : 'false';
        }

        $json = json_encode($current, JSON_UNESCAPED_SLASHES);
        return is_string($json) ? $json : '';
    }
}
