<?php

declare(strict_types=1);

namespace App\Framework\View\Processors;

use App\Framework\Config\AppConfig;
use App\Framework\DI\Container;
use App\Framework\LiveComponents\ComponentRegistry;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\Template\Processing\StringProcessor;
use App\Framework\View\Functions\ImageSlotFunction;
use App\Framework\View\Functions\LazyComponentFunction;
use App\Framework\View\Functions\UrlFunction;
use App\Framework\View\RawHtml;
use App\Framework\View\RenderContext;
use App\Framework\View\TemplateFunctions;
use DateTime;
use DateTimeZone;

final class PlaceholderReplacer implements StringProcessor
{
    public function __construct(
        private readonly Container $container,
        private readonly ComponentRegistry $componentRegistry
    ) {
    }

    // Erlaubte Template-Funktionen für zusätzliche Sicherheit

    /**
     * @var string[]
     */
    private array $allowedTemplateFunctions = [
        'date', 'format_date', 'format_currency', 'format_filesize',
        'strtoupper', 'strtolower', 'ucfirst', 'trim', 'count', /*'imageslot'*/
    ];

    public function process(string $html, RenderContext $context): string
    {
        // Template-Funktionen: {{ date('Y-m-d') }}, {{ format_currency(100) }}
        $html = $this->replaceTemplateFunctions($html, $context);

        // Triple curly braces for raw/unescaped HTML: {{{ $content }}} or {{{ content }}}
        // Supports both old and new syntax for backwards compatibility
        $html = preg_replace_callback(
            '/{{{\\s*\\$?([\\w.]+)\\s*}}}/',
            function ($matches) use ($context) {
                $expression = $matches[1];

                return $this->resolveRaw($context->data, $expression);
            },
            $html
        );

        // Standard Variablen und Methoden: {{ $item.getRelativeFile() }} or {{ item.getRelativeFile() }}
        // Supports both old and new syntax for backwards compatibility
        // Also supports array bracket syntax: {{ $model['key'] }} or {{ $model["key"] }}
        return preg_replace_callback(
            '/{{\\s*\\$?([\\w.\\[\\]\'\"]+)(?:\\(\\s*([^)]*)\\s*\\))?\\s*}}/',
            function ($matches) use ($context) {
                $expression = $matches[1];
                $params = isset($matches[2]) ? trim($matches[2]) : null;

                if ($params !== null) {
                    return $this->resolveMethodCall($context->data, $expression, $params);
                } else {
                    return $this->resolveEscaped($context->data, $expression, ENT_QUOTES | ENT_HTML5);
                }
            },
            $html
        );
    }

    private function replaceTemplateFunctions(string $html, RenderContext $context): string
    {
        return preg_replace_callback(
            '/{{\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\(([^)]*)\\)\\s*}}/',
            function ($matches) use ($context) {
                $functionName = $matches[1];
                $params = trim($matches[2]);

                $functions = new TemplateFunctions($this->container, ImageSlotFunction::class, LazyComponentFunction::class, UrlFunction::class);
                if ($functions->has($functionName)) {
                    $function = $functions->get($functionName);
                    $args = $this->parseParams($params, $context->data);
                    if (is_callable($function)) {
                        return $function(...$args);
                    }
                    #return $function(...$args);
                }

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

                try {
                    $args = $this->parseParams($params, $context->data);

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

                        return $result;
                        #return htmlspecialchars((string)$result, ENT_QUOTES | ENT_HTML5, 'UTF-8');
                    }

                    // Standard PHP-Funktionen (begrenzt)
                    if (function_exists($functionName)) {
                        $result = $functionName(...$args);

                        return htmlspecialchars((string)$result, ENT_QUOTES | ENT_HTML5, 'UTF-8');
                    }

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

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

    private function function_imageslot(string $slotName): string
    {
        $function = $this->container->get(ImageSlotFunction::class);

        return ($function('slot1'));
    }

    // 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 \DateTimeImmutable($date, new DateTimeZone('Europe/Berlin'));
        }

        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 resolveMethodCall(array $data, string $expression, string $params): string
    {
        $parts = explode('.', $expression);
        $methodName = array_pop($parts); // Letzter Teil ist der Methodenname
        $objectPath = implode('.', $parts); // Pfad zum Objekt

        // Objekt auflösen
        $object = $this->resolveValue($data, $objectPath);

        if (! is_object($object)) {
            return '{{ ' . $expression . '() }}'; // Platzhalter beibehalten
        }

        if (! method_exists($object, $methodName)) {
            return '{{ ' . $expression . '() }}'; // Platzhalter beibehalten
        }

        try {
            // Parameter parsen (falls vorhanden)
            $parsedParams = empty($params) ? [] : $this->parseParams($params, $data);

            // Methode aufrufen
            $result = $object->$methodName(...$parsedParams);

            return htmlspecialchars((string)$result, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        } catch (\Throwable $e) {
            // Bei Fehlern Platzhalter beibehalten
            return '{{ ' . $expression . '() }}';
        }
    }

    private function resolveRaw(array $data, string $expr): string
    {
        $value = $this->resolveValue($data, $expr);
        if ($value === null) {
            // Bleibt als Platzhalter stehen
            return '{{{ ' . $expr . ' }}}';
        }

        // LiveComponentContract - automatisch rendern mit Wrapper
        if ($value instanceof LiveComponentContract) {
            return $this->componentRegistry->renderWithWrapper($value);
        }

        // RawHtml-Objekte - direkt ausgeben
        if ($value instanceof RawHtml) {
            return $value->content;
        }

        // HtmlElement-Objekte - direkt ausgeben
        if ($value instanceof \App\Framework\View\ValueObjects\HtmlElement) {
            return (string) $value;
        }

        // Strings direkt ausgeben (KEIN escaping!)
        if (is_string($value)) {
            return $value;
        }

        // Arrays und komplexe Objekte können nicht direkt als String dargestellt werden
        if (is_array($value)) {
            return $this->handleArrayValue($expr, $value);
        }

        if (is_object($value) && ! method_exists($value, '__toString')) {
            return $this->handleObjectValue($expr, $value);
        }

        return (string)$value;
    }

    private function resolveEscaped(array $data, string $expr, int $flags): string
    {
        $value = $this->resolveValue($data, $expr);
        if ($value === null) {
            // Bleibt als Platzhalter stehen
            return '{{ ' . $expr . ' }}';
        }

        // LiveComponentContract - automatisch rendern mit Wrapper
        // WICHTIG: Wrapper-HTML ist bereits escaped, also NICHT nochmal escapen!
        if ($value instanceof LiveComponentContract) {
            $wrapperHtml = $this->componentRegistry->renderWithWrapper($value);
            // Wrap in RawHtml to prevent double-escaping by code below (line 271)
            return $wrapperHtml;
        }

        // RawHtml-Objekte nicht escapen
        if ($value instanceof RawHtml) {
            return $value->content;
        }

        // HtmlElement-Objekte nicht escapen (Framework Components)
        if ($value instanceof \App\Framework\View\ValueObjects\HtmlElement) {
            return (string) $value;
        }

        // Arrays und komplexe Objekte können nicht direkt als String dargestellt werden
        if (is_array($value)) {
            return $this->handleArrayValue($expr, $value);
        }

        if (is_object($value) && ! method_exists($value, '__toString')) {
            return $this->handleObjectValue($expr, $value);
        }

        // Zusätzlicher Schutz gegen Array-to-String Conversion
        if (is_array($value) || (is_object($value) && ! method_exists($value, '__toString'))) {
            // In Debug: Exception mit Details werfen
            if ($this->isDebugMode()) {
                $type = is_array($value) ? 'array[' . count($value) . ']' : 'object(' . get_class($value) . ')';

                throw new \InvalidArgumentException("Cannot convert {$type} to string in placeholder: {{ {$expr} }}");
            }

            // In Production: leerer String
            return '';
        }

        return htmlspecialchars((string)$value, $flags, 'UTF-8');
    }

    private function resolveValue(array $data, string $expr): mixed
    {
        // Handle array bracket syntax: $var['key'] or $var["key"]
        // Can be chained: $var['key1']['key2'] or mixed: $var.prop['key']
        $originalExpr = $expr;
        $value = $data;

        // Split expression into parts, handling both dot notation and bracket notation
        $pattern = '/([\\w]+)|\\[([\'"])([^\\2]+?)\\2\\]/';
        preg_match_all($pattern, $expr, $matches, PREG_SET_ORDER);

        foreach ($matches as $match) {
            if (!empty($match[1])) {
                // Dot notation: variable.property
                $key = $match[1];
                if (is_array($value) && array_key_exists($key, $value)) {
                    $value = $value[$key];
                } elseif (is_object($value) && isset($value->$key)) {
                    $value = $value->$key;
                } else {
                    return null;
                }
            } elseif (!empty($match[3])) {
                // Bracket notation: variable['key'] or variable["key"]
                $key = $match[3];
                if (is_array($value) && array_key_exists($key, $value)) {
                    $value = $value[$key];
                } else {
                    return null;
                }
            }
        }

        return $value;
    }

    /**
     * Parst Parameter aus einem Parameter-String
     */
    private function parseParams(string $paramsString, array $data): array
    {
        if (empty(trim($paramsString))) {
            return [];
        }

        $params = [];
        $parts = $this->splitParams($paramsString);

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

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

                continue;
            }

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

                continue;
            }

            // Boolean
            if ($part === 'true') {
                $params[] = true;

                continue;
            }
            if ($part === 'false') {
                $params[] = false;

                continue;
            }

            // Null
            if ($part === 'null') {
                $params[] = null;

                continue;
            }

            // Variablen-Referenzen: $variable oder Pfade: objekt.eigenschaft
            if (str_starts_with($part, '$')) {
                $varName = substr($part, 1);
                if (array_key_exists($varName, $data)) {
                    $params[] = $data[$varName];

                    continue;
                }
            } elseif (str_contains($part, '.')) {
                $value = $this->resolveValue($data, $part);
                if ($value !== null) {
                    $params[] = $value;

                    continue;
                }
            } else {
                // Einfache Variablennamen (ohne $ oder .) aus Template-Daten auflösen
                $value = $this->resolveValue($data, $part);
                if ($value !== null) {
                    $params[] = $value;

                    continue;
                }
            }

            // Fallback: Als String behandeln
            $params[] = $part;
        }

        return $params;
    }

    /**
     * Behandelt Array-Werte in Templates - Debug vs Production Verhalten
     */
    private function handleArrayValue(string $expr, array $value): string
    {
        // In Debug-Mode: Exception werfen um Entwickler auf das Problem hinzuweisen
        if ($this->isDebugMode()) {
            throw new \InvalidArgumentException(
                "Template placeholder '{{ {$expr} }}' resolved to an array, which cannot be displayed as string. " .
                "Use array access like '{{ {$expr}.0 }}' or iterate with <for> loop."
            );
        }

        // In Production: Sinnvolle Fallback-Werte für häufige Arrays
        if (empty($value)) {
            return ''; // Leere Arrays werden zu leerem String
        }

        // Für Arrays mit nur einem Element: das Element zurückgeben
        if (count($value) === 1 && ! is_array(reset($value)) && ! is_object(reset($value))) {
            return htmlspecialchars((string)reset($value), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        }

        // Für einfache Arrays: Anzahl anzeigen
        return '[' . count($value) . ' items]';
    }

    /**
     * Behandelt Object-Werte in Templates - Debug vs Production Verhalten
     */
    private function handleObjectValue(string $expr, object $value): string
    {
        // In Debug-Mode: Exception werfen
        if ($this->isDebugMode()) {
            throw new \InvalidArgumentException(
                "Template placeholder '{{ {$expr} }}' resolved to object of type '" . get_class($value) . "', " .
                "which cannot be displayed as string. Implement __toString() method or access specific properties."
            );
        }

        // In Production: Klassenname anzeigen
        $className = get_class($value);
        $shortName = substr($className, strrpos($className, '\\') + 1);

        return '[' . $shortName . ']';
    }

    /**
     * Erkennt ob wir im Debug-Modus sind über AppConfig
     */
    private function isDebugMode(): bool
    {
        try {
            /** @var AppConfig $appConfig */
            $appConfig = $this->container->get(AppConfig::class);

            return $appConfig->isDebug();
        } catch (\Throwable $e) {
            // Fallback zu Environment-Variable falls AppConfig nicht verfügbar
            return ($_ENV['APP_ENV'] ?? 'production') === 'development';
        }
    }

    /**
     * Teilt einen Parameter-String in einzelne Parameter auf
     */
    private function splitParams(string $paramsString): array
    {
        $params = [];
        $current = '';
        $inQuotes = false;
        $quoteChar = null;
        $length = strlen($paramsString);

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

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

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

        return $params;
    }
}
