<?php

namespace App\Framework\View\Processors;

use App\Framework\DI\Container;
use App\Framework\View\Functions\ImageSlotFunction;
use App\Framework\View\Functions\UrlFunction;
use App\Framework\View\RenderContext;
use App\Framework\View\StringProcessor;
use App\Framework\View\TemplateFunctions;
use DateTimeZone;

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

    // Erlaubte Template-Funktionen für zusätzliche Sicherheit
    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);

        // Standard Variablen und Methoden: {{ item.getRelativeFile() }}
        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, 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 resolveEscaped(array $data, string $expr, int $flags): string
    {
        $value = $this->resolveValue($data, $expr);
        if ($value === null) {
            // Bleibt als Platzhalter stehen
            return '{{ ' . $expr . ' }}';
        }
        return htmlspecialchars((string)$value, $flags, 'UTF-8');
    }

    private function resolveValue(array $data, string $expr): mixed
    {
        $keys = explode('.', $expr);
        $value = $data;

        foreach ($keys as $key) {
            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;
            }
        }

        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;
                }
            }

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

        return $params;
    }

    /**
     * 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;
    }
}
