<?php

declare(strict_types=1);

namespace App\Framework\LiveComponents\Polling;

use App\Framework\Template\Processing\AstTransformer;
use App\Framework\View\Dom\DocumentNode;
use App\Framework\View\Dom\ElementNode;
use App\Framework\View\Dom\TextNode;
use App\Framework\View\RenderContext;
use App\Framework\LiveComponents\ValueObjects\ComponentId;

/**
 * AST Transformer that detects and processes PollableClosure instances.
 *
 * Automatically finds PollableClosure instances in template data, registers them
 * for polling, executes them for initial render, and injects polling JavaScript.
 */
final readonly class PollableClosureTransformer implements AstTransformer
{
    public function __construct(
        private PollableClosureRegistry $registry
    ) {}

    /**
     * Transform AST to inject polling JavaScript for PollableClosure instances.
     */
    public function transform(DocumentNode $document, RenderContext $context): DocumentNode
    {
        // 1. Extract ComponentId if available from context
        $componentId = $this->extractComponentId($context);

        // 2. Process template data and find PollableClosure instances
        $registrations = $this->processTemplateData($context->data, $componentId);

        // 3. If no pollable closures found, return document as-is
        if (empty($registrations)) {
            return $document;
        }

        // 4. Generate polling script element
        $scriptElement = $this->createPollingScriptElement($registrations);

        // 5. Inject script before closing body tag
        $this->injectScriptBeforeBodyClose($document, $scriptElement);

        return $document;
    }

    /**
     * Process template data to find and register PollableClosure instances.
     *
     * @return array<PollableClosureRegistration>
     */
    private function processTemplateData(array $data, ?ComponentId $componentId): array
    {
        $registrations = [];

        foreach ($data as $key => $value) {
            if ($value instanceof PollableClosure) {
                // Register for polling
                $registration = $this->registry->register($key, $value, $componentId);
                $registrations[] = $registration;

                // Execute closure for initial render and update context data
                $data[$key] = $value->execute();
            } elseif (is_array($value)) {
                // Recursively process nested arrays
                $nestedRegistrations = $this->processTemplateData($value, $componentId);
                $registrations = array_merge($registrations, $nestedRegistrations);
            }
        }

        return $registrations;
    }

    /**
     * Create script element with polling JavaScript.
     *
     * @param array<PollableClosureRegistration> $registrations
     */
    private function createPollingScriptElement(array $registrations): ElementNode
    {
        $scriptContent = $this->generatePollingScript($registrations);

        $scriptElement = new ElementNode('script');
        $scriptElement->children[] = new TextNode($scriptContent);

        return $scriptElement;
    }

    /**
     * Generate JavaScript for polling registered closures.
     *
     * @param array<PollableClosureRegistration> $registrations
     */
    private function generatePollingScript(array $registrations): string
    {
        $scripts = [];

        foreach ($registrations as $registration) {
            if (!$registration->shouldPoll()) {
                continue;
            }

            $pollId = (string) $registration->pollId;
            $endpoint = $registration->getEndpoint();
            $interval = $registration->closure->interval;
            $templateKey = $registration->templateKey;
            $stopOnError = $registration->closure->stopOnError ? 'true' : 'false';

            // Generate polling script for this closure
            $scripts[] = <<<JS

// Poll: {$templateKey}
(function() {
    let pollInterval_{$pollId} = null;
    let errorCount_{$pollId} = 0;
    const maxErrors_{$pollId} = 3;
    const stopOnError_{$pollId} = {$stopOnError};

    function poll_{$pollId}() {
        fetch('{$endpoint}', {
            method: 'GET',
            headers: {
                'X-Requested-With': 'XMLHttpRequest',
                'Accept': 'application/json'
            }
        })
        .then(response => {
            if (!response.ok) {
                throw new Error('Poll request failed: ' + response.status);
            }
            return response.json();
        })
        .then(data => {
            // Reset error count on success
            errorCount_{$pollId} = 0;

            // Update DOM element(s) with template key
            const elements = document.querySelectorAll('[data-poll="{$templateKey}"]');
            elements.forEach(el => {
                if (typeof data === 'object' && data !== null) {
                    // If data is object, update as JSON or use data-poll-property
                    const property = el.getAttribute('data-poll-property');
                    if (property && data[property] !== undefined) {
                        el.textContent = data[property];
                    } else {
                        el.textContent = JSON.stringify(data);
                    }
                } else {
                    // Otherwise update as text
                    el.textContent = data;
                }
            });

            // Dispatch custom event
            document.dispatchEvent(new CustomEvent('poll:updated', {
                detail: {
                    pollId: '{$pollId}',
                    templateKey: '{$templateKey}',
                    data: data
                }
            }));
        })
        .catch(error => {
            console.error('Poll error for {$templateKey}:', error);
            errorCount_{$pollId}++;

            // Stop polling if max errors reached or stopOnError enabled
            if (stopOnError_{$pollId} || errorCount_{$pollId} >= maxErrors_{$pollId}) {
                clearInterval(pollInterval_{$pollId});
                console.warn('Polling stopped for {$templateKey} due to errors');

                // Dispatch error event
                document.dispatchEvent(new CustomEvent('poll:stopped', {
                    detail: {
                        pollId: '{$pollId}',
                        templateKey: '{$templateKey}',
                        reason: 'error',
                        error: error.message
                    }
                }));
            }
        });
    }

    // Start polling
    pollInterval_{$pollId} = setInterval(poll_{$pollId}, {$interval});

    // Initial poll
    poll_{$pollId}();

    // Cleanup on page unload
    window.addEventListener('beforeunload', () => {
        clearInterval(pollInterval_{$pollId});
    });
})();
JS;
        }

        return implode("\n", $scripts);
    }

    /**
     * Inject script element before closing body tag in AST.
     */
    private function injectScriptBeforeBodyClose(DocumentNode $document, ElementNode $script): void
    {
        // Find body element
        $bodyElement = $this->findBodyElement($document);

        if ($bodyElement !== null) {
            // Add script as last child of body
            $bodyElement->children[] = $script;
        } else {
            // No body element found, add to document root
            $document->children[] = $script;
        }
    }

    /**
     * Find body element in AST.
     */
    private function findBodyElement(DocumentNode $document): ?ElementNode
    {
        foreach ($document->children as $child) {
            if ($child instanceof ElementNode && strtolower($child->tag) === 'body') {
                return $child;
            }

            // Search recursively
            if ($child instanceof ElementNode) {
                $result = $this->findBodyElementRecursive($child);
                if ($result !== null) {
                    return $result;
                }
            }
        }

        return null;
    }

    /**
     * Recursively search for body element.
     */
    private function findBodyElementRecursive(ElementNode $element): ?ElementNode
    {
        if (strtolower($element->tag) === 'body') {
            return $element;
        }

        foreach ($element->children as $child) {
            if ($child instanceof ElementNode) {
                $result = $this->findBodyElementRecursive($child);
                if ($result !== null) {
                    return $result;
                }
            }
        }

        return null;
    }

    /**
     * Extract ComponentId from RenderContext if available.
     */
    private function extractComponentId(RenderContext $context): ?ComponentId
    {
        // Check if context has component ID in metadata
        if (isset($context->data['_componentId']) && $context->data['_componentId'] instanceof ComponentId) {
            return $context->data['_componentId'];
        }

        return null;
    }
}
