<?php

declare(strict_types=1);

namespace App\Framework\LiveComponents;

use App\Framework\LiveComponents\Contracts\SupportsNesting;
use App\Framework\LiveComponents\ValueObjects\ComponentHierarchy;
use App\Framework\LiveComponents\ValueObjects\ComponentId;

/**
 * Nested Component Manager
 *
 * Manages parent-child relationships between LiveComponents.
 * Handles hierarchy tracking, event bubbling, and lifecycle coordination.
 *
 * Features:
 * - Parent-child relationship registry
 * - Hierarchy tracking and validation
 * - Event bubbling between nested components
 * - Lifecycle coordination for nested hierarchies
 * - Circular dependency detection
 *
 * Architecture:
 * - Registry stores ComponentId → ComponentHierarchy mappings
 * - Parent components implement SupportsNesting interface
 * - Events bubble up from child to parent
 * - State can flow down from parent to children
 */
final class NestedComponentManager
{
    /**
     * @var array<string, ComponentHierarchy> Component ID → Hierarchy mapping
     */
    private array $hierarchyRegistry = [];

    /**
     * @var array<string, array<string>> Parent ID → [Child IDs] mapping
     */
    private array $childrenRegistry = [];

    public function __construct()
    {
        // Arrays initialized with property defaults
    }

    /**
     * Register component hierarchy
     *
     * @param ComponentId $componentId Component to register
     * @param ComponentHierarchy $hierarchy Component's hierarchy
     * @throws \InvalidArgumentException If circular dependency detected
     */
    public function registerHierarchy(ComponentId $componentId, ComponentHierarchy $hierarchy): void
    {
        $componentIdString = $componentId->toString();

        // Check for circular dependencies
        // A component can appear in its own path as the last element (current component)
        // but not in the ancestor portion of the path
        $pathWithoutCurrent = $hierarchy->path;
        if (count($pathWithoutCurrent) > 0) {
            array_pop($pathWithoutCurrent); // Remove current component from path
        }

        foreach ($pathWithoutCurrent as $ancestorId) {
            if ($ancestorId === $componentIdString) {
                throw new \InvalidArgumentException(
                    "Circular dependency detected: Component cannot be its own ancestor"
                );
            }
        }

        // Store hierarchy
        $this->hierarchyRegistry[$componentIdString] = $hierarchy;

        // Update children registry if has parent
        if ($hierarchy->parentId !== null) {
            $parentIdString = $hierarchy->parentId->toString();

            if (! isset($this->childrenRegistry[$parentIdString])) {
                $this->childrenRegistry[$parentIdString] = [];
            }

            if (! in_array($componentIdString, $this->childrenRegistry[$parentIdString], true)) {
                $this->childrenRegistry[$parentIdString][] = $componentIdString;
            }
        }
    }

    /**
     * Get component hierarchy
     *
     * @param ComponentId $componentId Component ID
     * @return ComponentHierarchy|null Hierarchy or null if not registered
     */
    public function getHierarchy(ComponentId $componentId): ?ComponentHierarchy
    {
        return $this->hierarchyRegistry[$componentId->toString()] ?? null;
    }

    /**
     * Get parent component ID
     *
     * @param ComponentId $componentId Child component ID
     * @return ComponentId|null Parent ID or null if root component
     */
    public function getParentId(ComponentId $componentId): ?ComponentId
    {
        $hierarchy = $this->getHierarchy($componentId);

        return $hierarchy?->parentId;
    }

    /**
     * Get child component IDs
     *
     * @param ComponentId $componentId Parent component ID
     * @return array<ComponentId> Array of child component IDs
     */
    public function getChildIds(ComponentId $componentId): array
    {
        $parentIdString = $componentId->toString();
        $childIdStrings = $this->childrenRegistry[$parentIdString] ?? [];

        return array_map(
            fn (string $idString) => ComponentId::fromString($idString),
            $childIdStrings
        );
    }

    /**
     * Check if component has children
     *
     * @param ComponentId $componentId Component ID to check
     * @return bool True if has children, false otherwise
     */
    public function hasChildren(ComponentId $componentId): bool
    {
        $parentIdString = $componentId->toString();

        return isset($this->childrenRegistry[$parentIdString])
            && count($this->childrenRegistry[$parentIdString]) > 0;
    }

    /**
     * Check if component is root (no parent)
     *
     * @param ComponentId $componentId Component ID to check
     * @return bool True if root, false if has parent
     */
    public function isRoot(ComponentId $componentId): bool
    {
        $hierarchy = $this->getHierarchy($componentId);

        return $hierarchy?->isRoot() ?? true; // Unregistered = root
    }

    /**
     * Check if component is child (has parent)
     *
     * @param ComponentId $componentId Component ID to check
     * @return bool True if has parent, false if root
     */
    public function isChild(ComponentId $componentId): bool
    {
        $hierarchy = $this->getHierarchy($componentId);

        return $hierarchy?->isChild() ?? false;
    }

    /**
     * Check if component is descendant of another
     *
     * @param ComponentId $componentId Component to check
     * @param ComponentId $potentialAncestor Potential ancestor
     * @return bool True if component is descendant, false otherwise
     */
    public function isDescendantOf(ComponentId $componentId, ComponentId $potentialAncestor): bool
    {
        $hierarchy = $this->getHierarchy($componentId);

        return $hierarchy?->isDescendantOf($potentialAncestor) ?? false;
    }

    /**
     * Get nesting depth
     *
     * @param ComponentId $componentId Component ID
     * @return int Nesting depth (0 for root, 1 for first level child, etc.)
     */
    public function getDepth(ComponentId $componentId): int
    {
        $hierarchy = $this->getHierarchy($componentId);

        return $hierarchy?->depth ?? 0;
    }

    /**
     * Get all ancestors (parent, grandparent, etc.)
     *
     * @param ComponentId $componentId Component ID
     * @return array<ComponentId> Array of ancestor IDs (parent first, root last)
     */
    public function getAncestors(ComponentId $componentId): array
    {
        $hierarchy = $this->getHierarchy($componentId);
        if ($hierarchy === null || $hierarchy->isRoot()) {
            return [];
        }

        $ancestors = [];
        $pathIds = $hierarchy->path;

        // Remove current component from path to get ancestors
        array_pop($pathIds);

        // Convert path strings to ComponentIds
        foreach ($pathIds as $ancestorIdString) {
            $ancestors[] = ComponentId::fromString($ancestorIdString);
        }

        // Reverse to get parent first, root last
        return array_reverse($ancestors);
    }

    /**
     * Get all descendants (children, grandchildren, etc.)
     *
     * @param ComponentId $componentId Component ID
     * @return array<ComponentId> Array of descendant IDs (breadth-first order)
     */
    public function getDescendants(ComponentId $componentId): array
    {
        $descendants = [];
        $queue = [$componentId];

        while (! empty($queue)) {
            $currentId = array_shift($queue);
            $children = $this->getChildIds($currentId);

            foreach ($children as $childId) {
                $descendants[] = $childId;
                $queue[] = $childId;
            }
        }

        return $descendants;
    }

    /**
     * Bubble event up through component hierarchy
     *
     * Dispatches event to parent, grandparent, etc. until:
     * - Root is reached
     * - A parent returns false (stops bubbling)
     * - A parent doesn't implement SupportsNesting
     *
     * @param ComponentId $sourceId Component that dispatched the event
     * @param string $eventName Event name
     * @param array $payload Event payload
     * @param ComponentRegistry $registry Component registry for resolving parents
     * @return bool True if event bubbled to root, false if stopped
     */
    public function bubbleEvent(
        ComponentId $sourceId,
        string $eventName,
        array $payload,
        ComponentRegistry $registry
    ): bool {
        $currentId = $sourceId;

        while (true) {
            $parentId = $this->getParentId($currentId);

            // Reached root
            if ($parentId === null) {
                return true;
            }

            // Resolve parent component
            $parent = $registry->resolve($parentId, initialData: null);

            // Parent doesn't support nesting - stop bubbling
            if (! $parent instanceof SupportsNesting) {
                return false;
            }

            // Call parent's event handler
            $shouldContinue = $parent->onChildEvent($currentId, $eventName, $payload);

            // Parent stopped bubbling
            if (! $shouldContinue) {
                return false;
            }

            // Move up to next level
            $currentId = $parentId;
        }
    }

    /**
     * Unregister component and cleanup
     *
     * Removes component from hierarchy and children registries.
     *
     * @param ComponentId $componentId Component to unregister
     */
    public function unregister(ComponentId $componentId): void
    {
        $componentIdString = $componentId->toString();

        // Remove from hierarchy registry
        unset($this->hierarchyRegistry[$componentIdString]);

        // Remove from parent's children
        $hierarchy = $this->getHierarchy($componentId);
        if ($hierarchy !== null && $hierarchy->parentId !== null) {
            $parentIdString = $hierarchy->parentId->toString();
            if (isset($this->childrenRegistry[$parentIdString])) {
                $this->childrenRegistry[$parentIdString] = array_filter(
                    $this->childrenRegistry[$parentIdString],
                    fn (string $childId) => $childId !== $componentIdString
                );

                // Cleanup empty arrays
                if (empty($this->childrenRegistry[$parentIdString])) {
                    unset($this->childrenRegistry[$parentIdString]);
                }
            }
        }

        // Remove children registry entry if has children
        unset($this->childrenRegistry[$componentIdString]);
    }

    /**
     * Get hierarchy statistics
     *
     * @return array Statistics about registered components
     */
    public function getStats(): array
    {
        $rootCount = 0;
        $maxDepth = 0;
        $totalComponents = count($this->hierarchyRegistry);

        foreach ($this->hierarchyRegistry as $hierarchy) {
            if ($hierarchy->isRoot()) {
                $rootCount++;
            }
            $maxDepth = max($maxDepth, $hierarchy->depth);
        }

        return [
            'total_components' => $totalComponents,
            'root_components' => $rootCount,
            'child_components' => $totalComponents - $rootCount,
            'max_nesting_depth' => $maxDepth,
            'parent_components_with_children' => count($this->childrenRegistry),
        ];
    }

    /**
     * Validate component hierarchy
     *
     * Checks for common issues like circular dependencies, orphaned children, etc.
     *
     * @return array<string> Array of validation error messages (empty if valid)
     */
    public function validate(): array
    {
        $errors = [];

        // Check for orphaned children (parent not registered)
        foreach ($this->hierarchyRegistry as $componentIdString => $hierarchy) {
            if ($hierarchy->parentId !== null) {
                $parentIdString = $hierarchy->parentId->toString();
                if (! isset($this->hierarchyRegistry[$parentIdString])) {
                    $errors[] = "Orphaned child: {$componentIdString} has parent {$parentIdString} that is not registered";
                }
            }
        }

        // Check for inconsistent children registry
        foreach ($this->childrenRegistry as $parentIdString => $childIds) {
            foreach ($childIds as $childIdString) {
                if (! isset($this->hierarchyRegistry[$childIdString])) {
                    $errors[] = "Invalid child reference: Parent {$parentIdString} references non-existent child {$childIdString}";
                }
            }
        }

        return $errors;
    }
}
