<?php

declare(strict_types=1);

namespace App\Framework\Email;

use App\Framework\Template\Parser\DomTemplateParser;
use Dom\HTMLDocument;

/**
 * CSS Inliner for Email Templates
 * Converts <style> tags to inline styles for better email client compatibility
 */
final readonly class CssInliner
{
    public function __construct(
        private DomTemplateParser $parser
    ) {
    }

    /**
     * Inline CSS from style tags into HTML elements
     */
    public function inline(string $html): string
    {
        $dom = $this->parser->parse($html);

        // Extract CSS rules from style tags
        $cssRules = $this->extractCssRules($dom);

        if (empty($cssRules)) {
            return $html; // No CSS to inline
        }

        // Apply CSS rules to elements
        $this->applyCssRules($dom, $cssRules);

        // Remove style tags after inlining (optional, can be configured)
        $this->removeStyleTags($dom);

        return $dom->saveHTML();
    }

    /**
     * Extract CSS rules from style tags
     *
     * @return array<string, array<string, string>> Selector => [property => value]
     */
    private function extractCssRules(HTMLDocument $dom): array
    {
        $cssRules = [];
        $styleTags = $dom->getElementsByTagName('style');

        foreach ($styleTags as $styleTag) {
            $css = $styleTag->textContent ?? '';
            $rules = $this->parseCss($css);

            // Merge rules, later rules override earlier ones
            foreach ($rules as $selector => $declarations) {
                if (! isset($cssRules[$selector])) {
                    $cssRules[$selector] = [];
                }
                $cssRules[$selector] = array_merge($cssRules[$selector], $declarations);
            }
        }

        return $cssRules;
    }

    /**
     * Parse CSS text into rules
     *
     * @return array<string, array<string, string>>
     */
    private function parseCss(string $css): array
    {
        $rules = [];

        // Remove comments
        $cleaned = preg_replace('/\/\*.*?\*\//s', '', $css);
        $css = $cleaned !== null ? $cleaned : $css;

        // Remove @media queries and other @ rules (not supported in inline styles)
        $cleaned = preg_replace('/@[^{]+\{[^{}]*(\{[^{}]*\}[^{}]*)*\}/s', '', $css);
        $css = $cleaned !== null ? $cleaned : $css;

        $cleaned = preg_replace('/@[^;]+;/s', '', $css);
        $css = $cleaned !== null ? $cleaned : $css;

        // Match CSS rules: selector { declarations }
        preg_match_all('/([^{]+)\{([^}]+)\}/s', $css, $matches, PREG_SET_ORDER);

        foreach ($matches as $match) {
            $selector = trim($match[1]);
            $declarationsText = trim($match[2]);

            // Skip empty selectors or those containing @ symbols (leftover @ rules)
            if (empty($selector) || str_contains($selector, '@')) {
                continue;
            }

            $declarations = $this->parseDeclarations($declarationsText);

            if (! empty($declarations)) {
                // Handle multiple selectors (e.g., "h1, h2, h3")
                $selectors = array_map('trim', explode(',', $selector));
                foreach ($selectors as $sel) {
                    if (! empty($sel) && ! str_contains($sel, '@')) {
                        $rules[$sel] = $declarations;
                    }
                }
            }
        }

        return $rules;
    }

    /**
     * Parse CSS declarations
     *
     * @return array<string, string>
     */
    private function parseDeclarations(string $declarationsText): array
    {
        $declarations = [];
        $parts = explode(';', $declarationsText);

        foreach ($parts as $part) {
            $part = trim($part);
            if (empty($part)) {
                continue;
            }

            // Split property: value
            if (strpos($part, ':') !== false) {
                [$property, $value] = explode(':', $part, 2);
                $property = trim($property);
                $value = trim($value);

                // Remove !important for inline styles (they're already highest priority)
                $value = str_replace('!important', '', $value);
                $value = trim($value);

                $declarations[$property] = $value;
            }
        }

        return $declarations;
    }

    /**
     * Apply CSS rules to matching elements
     *
     * @param array<string, array<string, string>> $cssRules
     */
    private function applyCssRules(HTMLDocument $dom, array $cssRules): void
    {
        // Sort selectors by specificity (simple implementation)
        $sortedSelectors = $this->sortBySpecificity(array_keys($cssRules));

        foreach ($sortedSelectors as $selector) {
            $declarations = $cssRules[$selector];
            $elements = $this->querySelectorAll($dom, $selector);

            foreach ($elements as $element) {
                $this->applyStylesToElement($element, $declarations);
            }
        }
    }

    /**
     * Query elements by CSS selector
     *
     * @return array<\Dom\HTMLElement>
     */
    private function querySelectorAll(HTMLDocument $dom, string $selector): array
    {
        $nodeList = $dom->querySelectorAll($selector);

        return iterator_to_array($nodeList);
    }

    /**
     * Apply styles to an element
     *
     * @param array<string, string> $declarations
     */
    private function applyStylesToElement(\Dom\HTMLElement $element, array $declarations): void
    {
        // Get existing inline styles
        $existingStyle = $element->getAttribute('style') ?? '';
        $existingDeclarations = $this->parseInlineStyle($existingStyle);

        // Merge with new declarations (new ones have lower priority than inline)
        $mergedDeclarations = array_merge($declarations, $existingDeclarations);

        // Build new style string
        $styleString = $this->buildStyleString($mergedDeclarations);

        if (! empty($styleString)) {
            $element->setAttribute('style', $styleString);
        }
    }

    /**
     * Parse inline style attribute
     *
     * @return array<string, string>
     */
    private function parseInlineStyle(string $style): array
    {
        if (empty($style)) {
            return [];
        }

        return $this->parseDeclarations($style);
    }

    /**
     * Build style string from declarations
     *
     * @param array<string, string> $declarations
     */
    private function buildStyleString(array $declarations): string
    {
        $parts = [];
        foreach ($declarations as $property => $value) {
            $parts[] = $property . ': ' . $value;
        }

        return implode('; ', $parts);
    }

    /**
     * Sort selectors by specificity (simplified)
     * Higher specificity selectors should be applied last
     *
     * @param array<string> $selectors
     * @return array<string>
     */
    private function sortBySpecificity(array $selectors): array
    {
        usort($selectors, function ($a, $b) {
            return $this->calculateSpecificity($a) <=> $this->calculateSpecificity($b);
        });

        return $selectors;
    }

    /**
     * Calculate CSS selector specificity (simplified)
     * Returns a number where higher = more specific
     */
    private function calculateSpecificity(string $selector): int
    {
        $specificity = 0;

        // Count IDs (#id)
        $specificity += substr_count($selector, '#') * 100;

        // Count classes (.class) and attributes
        $specificity += substr_count($selector, '.') * 10;
        $specificity += substr_count($selector, '[') * 10;

        // Count elements and pseudo-elements
        $specificity += preg_match_all('/[a-zA-Z]+/', $selector);

        return $specificity;
    }

    /**
     * Remove style tags from document
     */
    private function removeStyleTags(HTMLDocument $dom): void
    {
        $styleTags = $dom->getElementsByTagName('style');

        // Iterate backwards to avoid issues with live NodeList
        for ($i = $styleTags->length - 1; $i >= 0; $i--) {
            $styleTag = $styleTags->item($i);
            $styleTag->parentNode->removeChild($styleTag);
        }
    }
}
