<?php

declare(strict_types=1);

namespace App\Framework\View\Processors;

use App\Framework\LiveComponents\Contracts\ComponentRegistryInterface;
use App\Framework\LiveComponents\Performance\ComponentMetadataCacheInterface;
use App\Framework\LiveComponents\ValueObjects\ComponentData;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\DomComponentService;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use Dom\HTMLElement;

/**
 * XComponentProcessor - Unified component syntax processor
 *
 * Handles BOTH LiveComponents AND StaticComponents with <x-*> syntax:
 *
 * LiveComponents (Interactive/Stateful):
 *   <x-datatable id="users" page="1" pageSize="25" />
 *   ↓
 *   <div data-component-id="datatable:users" data-component-state="...">...</div>
 *
 * StaticComponents (Server-rendered):
 *   <x-button variant="primary">Click me</x-button>
 *   ↓
 *   <button class="btn btn--primary">Click me</button>
 *
 * Auto-Detection Logic:
 * 1. Check if component with #[LiveComponent] or #[ComponentName] exists in ComponentRegistry
 * 2. LiveComponent → Render with state wrapper
 * 3. StaticComponent → Render via ComponentRegistry.renderStatic()
 * 4. Neither → Error with helpful message
 *
 * Framework Pattern: readonly class, composition over inheritance
 */
final readonly class XComponentProcessor implements DomProcessor
{
    public function __construct(
        private ComponentRegistryInterface $componentRegistry,
        private ComponentMetadataCacheInterface $metadataCache,
        private DomComponentService $componentService
    ) {
    }

    public function process(DomWrapper $dom, RenderContext $context): DomWrapper
    {
        // Find all <x-*> elements (e.g., <x-datatable>, <x-button>)
        $xComponents = $this->findXComponents($dom);

        foreach ($xComponents as $element) {
            try {
                $this->processXComponent($dom, $element, $context);
            } catch (\Throwable $e) {
                // Replace with error message in development, silently fail in production
                $this->handleProcessingError($dom, $element, $e);
            }
        }

        return $dom;
    }

    /**
     * Find all <x-*> elements using CSS selectors
     *
     * @return list<HTMLElement>
     */
    private function findXComponents(DomWrapper $dom): array
    {
        // Use modern CSS selector API - much cleaner than XPath
        // This will match all elements starting with "x-"
        #$nodes = $dom->document->querySelectorAll('[class*="x-"], x-counter, x-datatable, x-search, x-button, x-badge, x-card, x-timer, x-chart, x-tabs, x-modal, x-notification-center, x-shopping-cart, x-product-filter, x-autocomplete, x-dynamic-form, x-infinite-scroll, x-comment-thread, x-activity-feed, x-metrics-dashboard, x-live-presence, x-image-uploader');

        // Better: iterate through all elements and check tagName
        $xComponents = [];
        $allElements = $dom->document->getElementsByTagName('*');

        foreach ($allElements as $element) {
            if ($element instanceof HTMLElement && str_starts_with(strtolower($element->tagName), 'x-')) {
                $xComponents[] = $element;
            }
        }

        return $xComponents;
    }

    /**
     * Process single <x-*> component with auto-detection
     */
    private function processXComponent(
        DomWrapper $dom,
        HTMLElement $element,
        RenderContext $context
    ): void {
        // Extract component name: <x-datatable> → "datatable"
        $componentName = $this->extractComponentName($element);

        // Check if component is registered
        if (!$this->componentRegistry->isRegistered($componentName)) {
            throw new \InvalidArgumentException(
                "Unknown component: <x-{$componentName}>\n\n" .
                "Available components: " . implode(', ', $this->componentRegistry->getAllComponentNames())
            );
        }

        // Get component class name to determine type
        $className = $this->componentRegistry->getClassName($componentName);

        if ($className === null) {
            throw new \InvalidArgumentException("Component '{$componentName}' class not found");
        }

        // Check if it's a LiveComponent (has state) or StaticComponent
        $metadata = $this->metadataCache->get($className);
        $isLiveComponent = isset($metadata->state);

        if ($isLiveComponent) {
            // Process as LiveComponent (Interactive/Stateful)
            $this->processAsLiveComponent($dom, $element, $componentName);
        } else {
            // Process as StaticComponent (Server-rendered)
            $this->processAsStaticComponent($dom, $element, $componentName);
        }
    }

    /**
     * Process as LiveComponent (Interactive)
     */
    private function processAsLiveComponent(
        DomWrapper $dom,
        HTMLElement $element,
        string $componentName
    ): void {
        // Extract props from HTML attributes
        $props = $this->extractProps($element);

        // Validate props against ComponentMetadata
        $this->validateLiveComponentProps($componentName, $props);

        // Create ComponentId (use 'id' prop or generate unique ID)
        $instanceId = isset($props['id']) && is_string($props['id'])
            ? $props['id']
            : $this->generateInstanceId($componentName);
        unset($props['id']); // Remove 'id' from props as it's used for ComponentId

        $componentId = ComponentId::create($componentName, $instanceId);

        // Create ComponentData from props
        $initialState = ComponentData::fromArray($props);

        // Resolve component via ComponentRegistry
        $component = $this->componentRegistry->resolve($componentId, $initialState);

        // Render component with wrapper (for initial page load)
        $componentHtml = $this->componentRegistry->renderWithWrapper($component);

        // Replace <x-datatable> with rendered component HTML
        $this->componentService->replaceComponent($dom, $element, $componentHtml);
    }

    /**
     * Process as StaticComponent (Server-rendered)
     */
    private function processAsStaticComponent(
        DomWrapper $dom,
        HTMLElement $element,
        string $componentName
    ): void {
        // Get element content and attributes
        $content = $element->textContent ?? '';
        $attributes = $this->extractAttributesAsArray($element);

        // Render via ComponentRegistry.renderStatic()
        $rendered = $this->componentRegistry->renderStatic(
            $componentName,
            $content,
            $attributes
        );

        // Replace element with rendered HTML
        $this->componentService->replaceComponent($dom, $element, $rendered);
    }

    /**
     * Extract component name from tag: <x-datatable> → "datatable"
     */
    private function extractComponentName(HTMLElement $element): string
    {
        $tagName = strtolower($element->tagName);

        if (! str_starts_with($tagName, 'x-')) {
            throw new \InvalidArgumentException(
                "Invalid x-component tag: '{$tagName}' (must start with 'x-')"
            );
        }

        return substr($tagName, 2); // Remove "x-" prefix
    }

    /**
     * Extract props from HTML attributes with type coercion (for LiveComponents)
     *
     * @return array<string, mixed>
     */
    private function extractProps(HTMLElement $element): array
    {
        $props = [];

        foreach ($element->attributes as $attr) {
            $name = $attr->nodeName;
            $value = $attr->nodeValue ?? '';

            // Type coercion: "123" → 123, "true" → true, "[1,2]" → [1,2]
            $props[$name] = $this->coerceType($value);
        }

        return $props;
    }

    /**
     * Extract attributes as array (for StaticComponents)
     *
     * @return array<string, string>
     */
    private function extractAttributesAsArray(HTMLElement $element): array
    {
        $attributes = [];

        foreach ($element->attributes as $attr) {
            $attributes[$attr->nodeName] = $attr->nodeValue ?? '';
        }

        return $attributes;
    }

    /**
     * Type coercion for prop values (LiveComponents only)
     *
     * Examples:
     * - "123" → 123 (int)
     * - "12.5" → 12.5 (float)
     * - "true" → true (bool)
     * - "false" → false (bool)
     * - "null" → null
     * - "[1,2,3]" → [1,2,3] (array via JSON)
     * - '{"key":"value"}' → ["key" => "value"] (object via JSON)
     * - "text" → "text" (string fallback)
     */
    private function coerceType(string $value): mixed
    {
        // Boolean literals
        if ($value === 'true') {
            return true;
        }
        if ($value === 'false') {
            return false;
        }

        // Null literal
        if ($value === 'null') {
            return null;
        }

        // Numeric values
        if (is_numeric($value)) {
            return str_contains($value, '.') ? (float)$value : (int)$value;
        }

        // JSON arrays or objects: "[1,2,3]" or '{"key":"value"}'
        if (str_starts_with($value, '[') || str_starts_with($value, '{')) {
            try {
                $decoded = json_decode($value, true, 512, JSON_THROW_ON_ERROR);

                return $decoded;
            } catch (\JsonException) {
                // Not valid JSON, treat as string
            }
        }

        // Fallback: string
        return $value;
    }

    /**
     * Validate props against ComponentMetadata (LiveComponents only)
     *
     * @param array<string, mixed> $props
     */
    private function validateLiveComponentProps(string $componentName, array $props): void
    {
        // Get component class name from registry
        $className = $this->componentRegistry->getClassName($componentName);

        if ($className === null) {
            return; // Component not found - will be caught in processXComponent
        }

        // Get metadata from cache
        $metadata = $this->metadataCache->get($className);

        // Validate each prop exists in component
        foreach ($props as $propName => $value) {
            // Skip 'id' - it's used for ComponentId, not a component property
            if ($propName === 'id') {
                continue;
            }

            if (! $metadata->hasProperty($propName)) {
                throw new \InvalidArgumentException(
                    "LiveComponent '{$componentName}' has no property '{$propName}'. " .
                    "Available properties: " . implode(', ', $this->getPropertyNames($metadata))
                );
            }
        }
    }

    /**
     * Get property names from metadata
     *
     * @param object{properties?: array<string, mixed>} $metadata
     * @return array<int, string>
     */
    private function getPropertyNames(object $metadata): array
    {
        $properties = $metadata->properties ?? [];

        return array_keys($properties);
    }

    /**
     * Generate unique instance ID for component
     */
    private function generateInstanceId(string $componentName): string
    {
        return $componentName . '-' . bin2hex(random_bytes(4));
    }

    /**
     * Handle processing errors
     *
     * In development: Show error message
     * In production: Silently fail (or log)
     */
    private function handleProcessingError(
        DomWrapper $dom,
        HTMLElement $element,
        \Throwable $e
    ): void {
        // Check if we're in debug mode (via environment or config)
        $isDebug = ($_ENV['APP_ENV'] ?? 'production') === 'development';

        if ($isDebug) {
            // Show error message in place of component
            $errorHtml = sprintf(
                '<div style="border:2px solid red;padding:1rem;background:#fee;color:#c00;">' .
                '<strong>XComponentProcessor Error:</strong><br>' .
                '<pre>%s</pre>' .
                '<small>Component: %s</small>' .
                '</div>',
                htmlspecialchars($e->getMessage()),
                htmlspecialchars($element->tagName)
            );

            $this->componentService->replaceComponent($dom, $element, $errorHtml);
        } else {
            // Production: Remove element silently (or log error)
            $element->remove();

            // Optional: Log error for monitoring
            // error_log("XComponentProcessor error: " . $e->getMessage());
        }
    }
}
