<?php

declare(strict_types=1);

namespace Activetwist\Jsonm\Targets;

use Activetwist\Jsonm\JsonmProfile;
use Activetwist\Jsonm\Transpiler;
use Activetwist\Jsonm\Validator;
use Activetwist\Jsonm\Util\PathAccess;
use RuntimeException;

/**
 * Emits CSS-custom-property output consumed by MyronCMS's runtime. The
 * `--{prefix}-*` form is sourced from the JsonmProfile, so the same target
 * class can serve any consuming AT app that wants CSS-var output with a
 * different prefix.
 */
final class MyronCssTarget implements Transpiler
{
    public function transpile(array $definition, array $tokenMap, JsonmProfile $profile): string
    {
        $errors = Validator::validate($definition, $tokenMap, $profile);
        if ($errors !== []) {
            throw new RuntimeException("Definition is invalid:\n- " . implode("\n- ", $errors));
        }

        $lines = [];
        $lines[] = '/**';
        $lines[] = ' * Runtime token overrides generated from JSONM style definitions.';
        $lines[] = ' * Canonical portable source: app/jsonm/baselines/apollo-v0.1.0.jsonm';
        $lines[] = ' * This file is compiled output and may be regenerated.';
        $lines[] = ' */';
        $lines[] = '';

        $defaultMode = Validator::resolveDefaultMode($definition);
        $appearanceModes = Validator::appearanceModes($definition);

        $this->appendTokenBlock($lines, ':root', $definition, $tokenMap, $profile, $defaultMode);
        if (in_array('dark', $appearanceModes, true)) {
            $this->appendTokenBlock($lines, ':root[data-appearance="dark"]', $definition, $tokenMap, $profile, 'dark');
            $lines[] = '@media (prefers-color-scheme: dark) {';
            $this->appendTokenBlock($lines, '  :root:not([data-appearance="light"])', $definition, $tokenMap, $profile, 'dark', 2);
            $lines[] = '}';
            $lines[] = '';
        }
        if (in_array('light', $appearanceModes, true)) {
            $this->appendTokenBlock($lines, ':root[data-appearance="light"]', $definition, $tokenMap, $profile, 'light');
        }
        $lines[] = '';

        return implode("\n", $lines);
    }

    public function transpileFontRoles(array $definition, JsonmProfile $profile): string
    {
        $lines = [];
        $lines[] = '/**';
        $lines[] = ' * Runtime font assignments compiled from the canonical JSONM definition.';
        $lines[] = ' * This file is checked-in output and may be regenerated.';
        $lines[] = ' */';
        $lines[] = '';

        $prefix = $profile->cssVarPrefix;
        $lines[] = ':root {';
        foreach (array_merge(Validator::ROLE_NAMES, Validator::OPTIONAL_ROLE_NAMES) as $role) {
            $familyPath = $this->fontRolePropertyPath($definition, $role, 'family');
            $lines[] = sprintf(
                '  --%s-font-role-%s-family: %s;',
                $prefix,
                $role,
                $this->compileFontStack($definition, $familyPath)
            );
            $lines[] = sprintf('  --%s-font-role-%s-weight: %s;', $prefix, $role, (string) PathAccess::get($definition, $this->fontRolePropertyPath($definition, $role, 'weight')));
            $lines[] = sprintf('  --%s-font-role-%s-style: %s;', $prefix, $role, (string) PathAccess::get($definition, $this->fontRolePropertyPath($definition, $role, 'style')));
            $lines[] = sprintf('  --%s-font-role-%s-size: %s;', $prefix, $role, (string) PathAccess::get($definition, $this->fontRolePropertyPath($definition, $role, 'size')));
        }
        $lines[] = '}';
        $lines[] = '';

        return implode("\n", $lines);
    }

    private function appendTokenBlock(array &$lines, string $selector, array $definition, array $tokenMap, JsonmProfile $profile, ?string $mode = null, int $indent = 0): void
    {
        $linePrefix = str_repeat(' ', $indent);
        $lines[] = $linePrefix . $selector . ' {';
        foreach ($tokenMap['mappings'] as $mapping) {
            $cssVar = (string) $mapping['cssVar'];
            $jsonPath = (string) $mapping['jsonPath'];
            $kind = (string) ($mapping['kind'] ?? 'value');
            if (!PathAccess::exists($definition, $jsonPath) && ($mapping['optional'] ?? false) === true) {
                $fallbackPath = (string) ($mapping['fallbackJsonPath'] ?? '');
                if ($fallbackPath === '') {
                    continue;
                }
                $jsonPath = $fallbackPath;
            }
            $value = $this->compileMappedValue($definition, $jsonPath, $kind, $profile, $mode);
            $lines[] = $linePrefix . sprintf('  %s: %s;', $cssVar, $value);
        }
        $lines[] = $linePrefix . '}';
        $lines[] = '';
    }

    private function compileMappedValue(array $definition, string $jsonPath, string $kind, JsonmProfile $profile, ?string $mode = null): string
    {
        $semanticKey = Validator::semanticTokenKey($jsonPath);
        if ($semanticKey !== null) {
            $semantic = Validator::resolveSemanticColors($definition, $mode);
            if (!array_key_exists($semanticKey, $semantic)) {
                throw new RuntimeException("Path not found: {$jsonPath}");
            }
            return (string) $semantic[$semanticKey];
        }

        $derivedKey = Validator::derivedTokenKey($jsonPath);
        if ($derivedKey !== null) {
            $derived = Validator::resolveDerivedColors($definition, $mode);
            if (!array_key_exists($derivedKey, $derived)) {
                throw new RuntimeException("Path not found: {$jsonPath}");
            }
            return (string) $derived[$derivedKey];
        }

        $value = PathAccess::get($definition, $jsonPath);
        $prefix = $profile->cssVarPrefix;

        return match ($kind) {
            'integer' => (string) (int) $value,
            'float' => $this->stringifyFloat((float) $value),
            'spaceAlias' => 'var(--' . $prefix . '-space-' . $value . ')',
            'radiusAlias' => 'var(--' . $prefix . '-radius-' . $value . ')',
            'containerRef' => (string) PathAccess::get($definition, 'tokens.layout.containers.' . (string) $value),
            'fontFamilyPrimary' => $this->compileFontStack($definition, $jsonPath),
            default => (string) $value,
        };
    }

    private function compileFontStack(array $definition, string $jsonPath): string
    {
        $family = (string) PathAccess::get($definition, $jsonPath);
        $fallbackPath = preg_replace('/\.family$/', '.fallback', $jsonPath);
        if (!PathAccess::exists($definition, (string) $fallbackPath) && str_contains($jsonPath, 'fontRoles.roles.eyebrow.')) {
            $fallbackPath = 'fontRoles.roles.micro.fallback';
        }
        $fallback = (string) PathAccess::get($definition, $fallbackPath);
        $primary = $this->quoteFontFamily($family);

        return $fallback !== '' ? "{$primary}, {$fallback}" : $primary;
    }

    private function fontRolePropertyPath(array $definition, string $role, string $property): string
    {
        $path = "fontRoles.roles.{$role}.{$property}";
        if ($role === 'eyebrow' && !PathAccess::exists($definition, $path)) {
            return "fontRoles.roles.micro.{$property}";
        }
        return $path;
    }

    private function quoteFontFamily(string $family): string
    {
        $trimmed = trim($family);
        if ($trimmed === '') {
            return $trimmed;
        }
        if (str_contains($trimmed, ' ') && !str_starts_with($trimmed, '"')) {
            return '"' . $trimmed . '"';
        }
        return $trimmed;
    }

    private function stringifyFloat(float $value): string
    {
        if ((float) (int) $value === $value) {
            return (string) ((float) $value);
        }
        return rtrim(rtrim(sprintf('%.4F', $value), '0'), '.');
    }
}
