<?php

declare(strict_types=1);

namespace App\Framework\View\Dom\Transformer;

use App\Framework\Template\Processing\AstTransformer;
use App\Framework\View\Dom\DocumentNode;
use App\Framework\View\Dom\ElementNode;
use App\Framework\View\Dom\Node;
use App\Framework\View\Dom\Parser\HtmlParser;
use App\Framework\View\Dom\TextNode;
use App\Framework\View\Loading\TemplateLoader;
use App\Framework\View\Processors\PlaceholderReplacer;
use App\Framework\View\RenderContext;

/**
 * LayoutTagTransformer - Processes layout tags and inserts content into layout slots
 *
 * Finds <layout src="name"> or <layout name="name"> tags, loads the layout template,
 * and inserts the page content into the layout's <main> slot.
 *
 * Framework Pattern: readonly class, AST-based transformation
 */
final readonly class LayoutTagTransformer implements AstTransformer
{
    public function __construct(
        private TemplateLoader $loader,
        private HtmlParser $parser,
        private PlaceholderReplacer $placeholderReplacer
    ) {}

    public function transform(DocumentNode $document, RenderContext $context): DocumentNode
    {
        // Skip layout processing for partials
        if ($context->isPartial) {
            return $this->removeLayoutTag($document);
        }

        // Find layout tag
        $layoutTag = $this->findLayoutTag($document);

        if (!$layoutTag) {
            return $document;
        }

        // Apply layout
        return $this->applyLayout($layoutTag, $document, $context);
    }

    /**
     * Find layout tag in document
     */
    private function findLayoutTag(DocumentNode $document): ?ElementNode
    {
        return $this->searchForLayoutTag($document);
    }

    /**
     * Recursively search for layout tag
     */
    private function searchForLayoutTag(Node $node): ?ElementNode
    {
        if ($node instanceof ElementNode && strtolower($node->getTagName()) === 'layout') {
            // Check for name or src attribute
            if ($node->getAttribute('name') !== null || $node->getAttribute('src') !== null) {
                return $node;
            }
        }

        // Search in children
        if ($node instanceof ElementNode || $node instanceof DocumentNode) {
            foreach ($node->getChildren() as $child) {
                $result = $this->searchForLayoutTag($child);
                if ($result !== null) {
                    return $result;
                }
            }
        }

        return null;
    }

    /**
     * Apply layout to document
     */
    private function applyLayout(ElementNode $layoutTag, DocumentNode $document, RenderContext $context): DocumentNode
    {
        // Get layout file name (support both 'name' and 'src' attributes)
        $layoutFile = $layoutTag->getAttribute('name') ?? $layoutTag->getAttribute('src') ?? null;

        if (!$layoutFile) {
            return $document;
        }

        // Load layout template
        $layoutPath = $this->loader->getTemplatePath($layoutFile);
        $layoutContent = file_get_contents($layoutPath);

        // Process basic placeholders in layout (not components or loops yet)
        $layoutContext = new RenderContext(
            template: $layoutFile,
            metaData: $context->metaData,
            data: $context->data,
            isPartial: false
        );

        $processedLayoutHtml = $this->placeholderReplacer->process($layoutContent, $layoutContext);

        // Parse layout to AST
        $layoutDocument = $this->parser->parse($processedLayoutHtml);

        // Find slot (<main> element) in layout
        $slot = $this->findMainSlot($layoutDocument);

        if (!$slot) {
            // No slot found, return original document
            return $document;
        }

        // Get content to insert (either innerHTML of layout tag or content after it)
        $contentToInsert = $this->collectContent($layoutTag, $document);

        // Clear slot and insert content
        $this->clearChildren($slot);

        foreach ($contentToInsert as $node) {
            $slot->appendChild($node);
        }

        return $layoutDocument;
    }

    /**
     * Find main slot element in layout
     */
    private function findMainSlot(DocumentNode $document): ?ElementNode
    {
        return $this->searchForElement($document, 'main');
    }

    /**
     * Recursively search for element by tag name
     */
    private function searchForElement(Node $node, string $tagName): ?ElementNode
    {
        if ($node instanceof ElementNode && strtolower($node->getTagName()) === strtolower($tagName)) {
            return $node;
        }

        // Search in children
        if ($node instanceof ElementNode || $node instanceof DocumentNode) {
            foreach ($node->getChildren() as $child) {
                $result = $this->searchForElement($child, $tagName);
                if ($result !== null) {
                    return $result;
                }
            }
        }

        return null;
    }

    /**
     * Collect content to insert into layout slot
     *
     * @return Node[]
     */
    private function collectContent(ElementNode $layoutTag, DocumentNode $document): array
    {
        $children = $layoutTag->getChildren();

        // If layout tag has children, use those
        if (!empty($children)) {
            return $children;
        }

        // Otherwise, collect all content after layout tag (self-closing tag syntax)
        return $this->collectContentAfterLayoutTag($layoutTag, $document);
    }

    /**
     * Collect all nodes after the layout tag
     *
     * @return Node[]
     */
    private function collectContentAfterLayoutTag(ElementNode $layoutTag, DocumentNode $document): array
    {
        $content = [];
        $foundLayoutTag = false;

        // Find the layout tag and collect everything after it
        $this->collectNodesAfter($document, $layoutTag, $foundLayoutTag, $content);

        return $content;
    }

    /**
     * Recursively collect nodes after a specific node
     */
    private function collectNodesAfter(Node $node, ElementNode $target, bool &$found, array &$content): void
    {
        if ($node === $target) {
            $found = true;
            return;
        }

        if ($found && $node !== $target) {
            // Collect this node if we're past the target (sibling-level only)
            if (!($node instanceof TextNode && trim($node->getTextContent()) === '')) {
                $content[] = $node;
            }
            // Don't recurse into collected nodes - we collect them as-is
            return;
        }

        // Recurse into children ONLY if we haven't found the target yet
        if (!$found && ($node instanceof ElementNode || $node instanceof DocumentNode)) {
            foreach ($node->getChildren() as $child) {
                $this->collectNodesAfter($child, $target, $found, $content);
            }
        }
    }

    /**
     * Remove layout tag (for partials)
     */
    private function removeLayoutTag(DocumentNode $document): DocumentNode
    {
        $layoutTag = $this->findLayoutTag($document);

        if (!$layoutTag) {
            return $document;
        }

        // Find parent of layout tag and replace layout tag with its children
        $this->replaceNodeWithChildren($document, $layoutTag);

        return $document;
    }

    /**
     * Replace a node with its children in the tree
     */
    private function replaceNodeWithChildren(Node $parent, ElementNode $target): bool
    {
        if ($parent instanceof ElementNode || $parent instanceof DocumentNode) {
            $children = $parent->getChildren();
            $found = false;

            foreach ($children as $index => $child) {
                if ($child === $target) {
                    // Found the target - replace with its children
                    $targetChildren = $target->getChildren();

                    // Remove target
                    $parent->removeChild($target);

                    // Insert target's children at the same position
                    foreach ($targetChildren as $targetChild) {
                        $parent->appendChild($targetChild);
                    }

                    $found = true;
                    break;
                }

                // Recurse
                if ($this->replaceNodeWithChildren($child, $target)) {
                    $found = true;
                    break;
                }
            }

            return $found;
        }

        return false;
    }

    /**
     * Clear all children from a node
     */
    private function clearChildren(ElementNode $node): void
    {
        $children = $node->getChildren();

        foreach ($children as $child) {
            $node->removeChild($child);
        }
    }
}
