<?php

namespace App\Framework\View\Processors;

use App\Framework\View\StringProcessor;
use App\Framework\View\RenderContext;

/**
 * Ersetzt PHP-Variable-Syntax durch tatsächliche Werte
 */
final class PhpVariableProcessor implements StringProcessor
{
    private array $allowedFunctions = [
        // String-Funktionen
        'strtoupper', 'strtolower', 'ucfirst', 'ucwords', 'trim',
        'strlen', 'substr', 'str_replace', 'htmlspecialchars',

        // Datum/Zeit
        'date', 'strftime', 'gmdate',

        // Formatierung
        'number_format', 'sprintf',

        // Array-Funktionen
        'count', 'implode', 'array_keys', 'array_values',

        // Custom Template-Funktionen
        'format_date', 'format_currency', 'format_filesize',

        'print_r', 'get_class'
    ];

    public function process(string $html, RenderContext $context): string
    {
        $data = $context->data ?? [];

        // Stellen wir sicher, dass HTML-Entities dekodiert sind
        // Dies deckt Fälle ab, in denen das Template bereits HTML-Entities enthält
        $html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');

        // 2. Funktions-Aufrufe: date('Y-m-d'), strtoupper($name)
        $html = $this->replaceFunctionCalls($html, $data);

        // 1. Methoden-Aufrufe: $user->getName(), $date->format('Y-m-d')
        $html = $this->replaceMethodCalls($html, $data);



        // 3. Objekt-Properties: $error->name
        $html = $this->replaceObjectProperties($html, $data);

        // 4. Array-Zugriffe: $data['key']
        $html = $this->replaceArrayAccess($html, $data);

        // 5. Einfache Variablen: $name
        $html = $this->replaceSimpleVariables($html, $data);

        // 6. Ternäre Operatoren: $condition ? 'true' : 'false'
        $html = $this->replaceTernaryOperators($html, $data);

        return $html;
    }

    /**
     * Ersetzt Methoden-Aufrufe: $user->getName(), $date->format('Y-m-d')
     */
    private function replaceMethodCalls(string $html, array $data): string
    {
        #debug(html_entity_decode($html));

        return preg_replace_callback(
            '/\$([a-zA-Z_][a-zA-Z0-9_]*)((?:->|(?:-&gt;))[a-zA-Z_][a-zA-Z0-9_]*)*(?:->|(?:-&gt;))([a-zA-Z_][a-zA-Z0-9_]*)\(([^)]*)\)/',
            function($matches) use ($data) {
                $varName = $matches[1];
                $propertyChain = $matches[2] ? str_replace('-&gt;', '->', $matches[2]) : '';
                $methodName = $matches[3];
                $argsString = $matches[4];

                if (!array_key_exists($varName, $data)) {
                    return $matches[0];
                }

                $object = $data[$varName];

                // Property-Chain navigieren falls vorhanden
                if (!empty($propertyChain)) {
                    $properties = explode('->', trim($propertyChain, '->'));
                    foreach ($properties as $property) {
                        if (is_object($object) && property_exists($object, $property)) {
                            $object = $object->$property;
                        } else {
                            return $matches[0];
                        }
                    }
                }

                if (!is_object($object)) {
                    return $matches[0];
                }

                // Methode ausführen
                try {
                    $args = $this->parseArguments($argsString, $data);

                    if (method_exists($object, $methodName)) {
                        $result = $object->$methodName(...$args);
                        return $this->formatValue($result);
                    }

                    // Getter-Fallback: getName() -> name Property
                    if (str_starts_with($methodName, 'get') && empty($args)) {
                        $propertyName = lcfirst(substr($methodName, 3));
                        if (property_exists($object, $propertyName)) {
                            return $this->formatValue($object->$propertyName);
                        }
                    }

                } catch (\Throwable $e) {
                    // Fehler ignorieren, Original-String zurückgeben
                    return $matches[0];
                }

                return $matches[0];
            },
            $html
        );
    }

    /**
     * Ersetzt Funktions-Aufrufe: date('Y-m-d'), strtoupper($name)
     */
    private function replaceFunctionCalls(string $html, array $data): string
    {
        // Verbesserte Regex, die sowohl -> als auch HTML-kodiertes -&gt; erkennt
        return preg_replace_callback(
            '/\b([a-zA-Z_][a-zA-Z0-9_]*)\(([^)]*)\)/',
            function($matches) use ($data) {
                $functionName = $matches[1];
                $argsString = $matches[2];

                // Nur erlaubte Funktionen ausführen
                if (!in_array($functionName, $this->allowedFunctions)) {
                    return $matches[0];
                }

                try {
                    $args = $this->parseArguments($argsString, $data);

                    // Custom Template-Funktionen
                    if (method_exists($this, 'function_' . $functionName)) {
                        $result = $this->{'function_' . $functionName}(...$args);
                        return $this->formatValue($result);
                    }

                    // Standard PHP-Funktionen
                    if (function_exists($functionName)) {
                        $result = $functionName(...$args);
                        return $this->formatValue($result);
                    }

                } catch (\Throwable $e) {
                    return $matches[0];
                }

                return $matches[0];
            },
            $html
        );
    }

    /**
     * Ersetzt Objekt-Properties: $error->name
     */
    private function replaceObjectProperties(string $html, array $data): string
    {
        // Verbesserte Regex, die sowohl -> als auch HTML-kodiertes -&gt; erkennt
        return preg_replace_callback(
            '/\$([a-zA-Z_][a-zA-Z0-9_]*)((?:->|(?:-&gt;))[a-zA-Z_][a-zA-Z0-9_]*)+/',
            function($matches) use ($data) {
                $varName = $matches[1];
                $propertyChain = str_replace('-&gt;', '->', $matches[2]);

                if (!array_key_exists($varName, $data)) {
                    return $matches[0]; // Variable nicht gefunden
                }

                $value = $data[$varName];

                // Property-Chain abarbeiten: ->name->email
                $properties = explode('->', trim($propertyChain, '->'));

                foreach ($properties as $property) {
                    if (is_object($value)) {
                        if (property_exists($value, $property)) {
                            $value = $value->$property;
                        } elseif (method_exists($value, '__get')) {
                            $value = $value->__get($property);
                        } else {
                            return $matches[0]; // Property nicht gefunden
                        }
                    } elseif (is_array($value)) {
                        if (array_key_exists($property, $value)) {
                            $value = $value[$property];
                        } else {
                            return $matches[0]; // Key nicht gefunden
                        }
                    } else {
                        return $matches[0]; // Kann nicht weiter navigieren
                    }
                }

                return $this->formatValue($value);
            },
            $html
        );
    }

    /**
     * Ersetzt Array-Zugriffe: $data['key'], $items[0]
     */
    private function replaceArrayAccess(string $html, array $data): string
    {
        return preg_replace_callback(
            "/\\$([a-zA-Z_][a-zA-Z0-9_]*)\[(['\"]?)([^'\"\]]+)\2\]/",
            function($matches) use ($data) {
                $varName = $matches[1];
                $key = $matches[3];

                if (!array_key_exists($varName, $data)) {
                    return $matches[0];
                }

                $array = $data[$varName];

                if (is_array($array)) {
                    // Numerischer oder String-Key
                    $actualKey = is_numeric($key) ? (int)$key : $key;

                    if (array_key_exists($actualKey, $array)) {
                        return $this->formatValue($array[$actualKey]);
                    }
                } elseif (is_object($array)) {
                    // Objekt wie Array behandeln
                    if (property_exists($array, $key)) {
                        return $this->formatValue($array->$key);
                    }
                }

                return $matches[0];
            },
            $html
        );
    }

    /**
     * Ersetzt einfache Variablen: $name
     */
    private function replaceSimpleVariables(string $html, array $data): string
    {
        return preg_replace_callback(
            '/\$([a-zA-Z_][a-zA-Z0-9_]*)(?![a-zA-Z0-9_\[\-])/',
            function($matches) use ($data) {
                $varName = $matches[1];

                if (array_key_exists($varName, $data)) {
                    return $this->formatValue($data[$varName]);
                }

                return $matches[0]; // Variable nicht gefunden
            },
            $html
        );
    }

    /**
     * Parst Argument-String zu Array von Werten
     */
    private function parseArguments(string $argsString, array $data): array
    {
        if (trim($argsString) === '') {
            return [];
        }

        $args = [];
        $parts = $this->splitArguments($argsString);

        foreach ($parts as $part) {
            $part = trim($part);

            // String-Literale: 'text' oder "text"
            if (preg_match('/^[\'"](.*)[\'"]$/s', $part, $matches)) {
                $args[] = $matches[1];
                continue;
            }

            // Zahlen
            if (is_numeric($part)) {
                $args[] = str_contains($part, '.') ? (float)$part : (int)$part;
                continue;
            }

            // Boolean
            if ($part === 'true') {
                $args[] = true;
                continue;
            }
            if ($part === 'false') {
                $args[] = false;
                continue;
            }

            // Null
            if ($part === 'null') {
                $args[] = null;
                continue;
            }

            // Variablen: $name
            if (preg_match('/^\$([a-zA-Z_][a-zA-Z0-9_]*)$/', $part, $matches)) {
                $varName = $matches[1];
                if (array_key_exists($varName, $data)) {
                    $args[] = $data[$varName];
                    continue;
                }
            }

            // Fallback: als String behandeln
            $args[] = $part;
        }

        return $args;
    }

    /**
     * Teilt Argument-String intelligent auf (berücksichtigt Quotes)
     */
    private function splitArguments(string $argsString): array
    {
        $args = [];
        $current = '';
        $inQuotes = false;
        $quoteChar = null;
        $length = strlen($argsString);

        for ($i = 0; $i < $length; $i++) {
            $char = $argsString[$i];

            if (!$inQuotes && ($char === '"' || $char === "'")) {
                $inQuotes = true;
                $quoteChar = $char;
                $current .= $char;
            } elseif ($inQuotes && $char === $quoteChar) {
                $inQuotes = false;
                $quoteChar = null;
                $current .= $char;
            } elseif (!$inQuotes && $char === ',') {
                $args[] = trim($current);
                $current = '';
            } else {
                $current .= $char;
            }
        }

        if ($current !== '') {
            $args[] = trim($current);
        }

        return $args;
    }

    /**
     * Formatiert einen Wert für die HTML-Ausgabe
     */
    private function formatValue(mixed $value): string
    {
        return match (true) {
            is_null($value) => '',
            is_bool($value) => $value ? '1' : '0',
            is_scalar($value) => htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'),
            is_array($value) => htmlspecialchars(json_encode($value, JSON_UNESCAPED_UNICODE), ENT_QUOTES, 'UTF-8'),
            is_object($value) => $this->formatObject($value),
            default => htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8')
        };
    }

    /**
     * Formatiert Objekte für die Ausgabe
     */
    private function formatObject(object $value): string
    {
        if (method_exists($value, '__toString')) {
            return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
        }

        if (method_exists($value, 'toArray')) {
            return htmlspecialchars(json_encode($value->toArray(), JSON_UNESCAPED_UNICODE), ENT_QUOTES, 'UTF-8');
        }

        return htmlspecialchars(get_class($value), ENT_QUOTES, 'UTF-8');
    }

    // Custom Template-Funktionen

    private function function_format_date(string|\DateTime $date, string $format = 'Y-m-d H:i:s'): string
    {
        if (is_string($date)) {
            $date = new \DateTime($date);
        }
        return $date->format($format);
    }

    private function function_format_currency(float $amount, string $currency = 'EUR'): string
    {
        return number_format($amount, 2, ',', '.') . ' ' . $currency;
    }

    private function function_format_filesize(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        $factor = floor((strlen((string)$bytes) - 1) / 3);
        return sprintf("%.1f %s", $bytes / pow(1024, $factor), $units[$factor]);
    }

    private function replaceTernaryOperators(string $html, array $data): string
    {
        return $html;
    }
}
