<?php
/**
 * Walks Bricks element trees and emits index edges.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Index;

use LolaCoreForBricks\Support\BricksMetaKeys;

/**
 * Extracts class, variable, component, and element references from Bricks data.
 */
class ElementWalker
{
    public const MAX_COMPONENT_DEPTH = 10;

    /**
     * CSS variable references are stored as var(--name) in string settings.
     * Verified against Bricks 2.3.10 Helpers::process_settings_for_variable_rename().
     */
    private const VARIABLE_PATTERN = '/var\(\s*--\s*([a-zA-Z0-9_-]+)\s*\)/';

    /**
     * @param array<int|string, array<string, mixed>> $elements
     * @return array<int, array<string, mixed>>
     */
    public function walk(
        int $postId,
        string $area,
        array $elements,
        int $componentDepth = 0
    ): array {
        if ($elements === []) {
            return [];
        }

        $edges      = [];
        $components = $this->loadComponents();

        foreach ($this->normalizeElements($elements) as $element) {
            if (!is_array($element)) {
                continue;
            }

            $elementId   = isset($element['id']) ? (string) $element['id'] : null;
            $elementName = isset($element['name']) ? (string) $element['name'] : null;
            $settings    = is_array($element['settings'] ?? null) ? $element['settings'] : [];

            if ($elementId !== null && $elementId !== '') {
                $edges[] = $this->edge(
                    BricksMetaKeys::REF_ELEMENT,
                    $elementId,
                    $postId,
                    $area,
                    $elementId,
                    $elementName
                );
            }

            foreach ($this->extractClassRefs($settings) as $classId) {
                $edges[] = $this->edge(
                    BricksMetaKeys::REF_CLASS,
                    $classId,
                    $postId,
                    $area,
                    $elementId,
                    $elementName
                );
            }

            foreach ($this->extractVariableRefs($settings) as $variableName) {
                $edges[] = $this->edge(
                    BricksMetaKeys::REF_VARIABLE,
                    $variableName,
                    $postId,
                    $area,
                    $elementId,
                    $elementName
                );
            }

            $componentId = isset($element['cid']) ? (string) $element['cid'] : '';
            if ($componentId !== '') {
                $edges[] = $this->edge(
                    BricksMetaKeys::REF_COMPONENT,
                    $componentId,
                    $postId,
                    $area,
                    $elementId,
                    $elementName
                );

                if ($componentDepth < self::MAX_COMPONENT_DEPTH) {
                    $nested = $this->componentElements($components, $componentId);
                    if ($nested !== []) {
                        $edges = array_merge(
                            $edges,
                            $this->walk($postId, $area, $nested, $componentDepth + 1)
                        );
                    }
                }
            }
        }

        return $this->dedupeEdges($edges);
    }

    /**
     * @param array<string, mixed> $settings
     * @return array<int, array<string, mixed>>
     */
    public function walkSettings(
        int $postId,
        string $area,
        array $settings,
        ?string $subjectId = null,
        ?string $subjectName = null
    ): array {
        $edges = [];

        foreach ($this->extractClassRefs($settings) as $classId) {
            $edges[] = $this->edge(
                BricksMetaKeys::REF_CLASS,
                $classId,
                $postId,
                $area,
                $subjectId,
                $subjectName
            );
        }

        foreach ($this->extractVariableRefs($settings) as $variableName) {
            $edges[] = $this->edge(
                BricksMetaKeys::REF_VARIABLE,
                $variableName,
                $postId,
                $area,
                $subjectId,
                $subjectName
            );
        }

        return $this->dedupeEdges($edges);
    }

    /**
     * @param array<string, mixed> $settings
     * @return string[]
     */
    public function extractVariableRefs(array $settings): array
    {
        $found = [];

        $this->scanValueForVariables($settings, $found);

        return array_values(array_unique($found));
    }

    /**
     * @param array<string, mixed> $settings
     * @return string[]
     */
    public function extractClassRefs(array $settings): array
    {
        $ids = [];
        foreach (['_cssGlobalClasses', '_cssGlobalClassesProps'] as $key) {
            $classes = $settings[$key] ?? [];
            if (is_string($classes) && $classes !== '') {
                $ids[] = $classes;
                continue;
            }

            if (!is_array($classes)) {
                continue;
            }

            foreach ($classes as $classId) {
                if (is_string($classId) && $classId !== '') {
                    $ids[] = $classId;
                }
            }
        }

        return array_values(array_unique($ids));
    }

    /**
     * @param mixed $value
     * @param string[] $found
     */
    private function scanValueForVariables(mixed $value, array &$found): void
    {
        if (is_array($value)) {
            foreach ($value as $item) {
                $this->scanValueForVariables($item, $found);
            }

            return;
        }

        if (!is_string($value) || $value === '') {
            return;
        }

        if (preg_match_all(self::VARIABLE_PATTERN, $value, $matches) !== false) {
            foreach ($matches[1] as $name) {
                if (is_string($name) && $name !== '') {
                    $found[] = $name;
                }
            }
        }
    }

    /**
     * @param array<int|string, array<string, mixed>> $elements
     * @return array<int, array<string, mixed>>
     */
    private function normalizeElements(array $elements): array
    {
        $normalized = [];
        foreach ($elements as $element) {
            if (is_array($element)) {
                $normalized[] = $element;
            }
        }

        return $normalized;
    }

    /**
     * @return array<string, array<string, mixed>>
     */
    private function loadComponents(): array
    {
        $components = get_option(BricksMetaKeys::OPTION_COMPONENTS, []);

        if (!is_array($components)) {
            return [];
        }

        $map = [];
        foreach ($components as $component) {
            if (!is_array($component)) {
                continue;
            }

            $id = isset($component['id']) ? (string) $component['id'] : '';
            if ($id !== '') {
                $map[$id] = $component;
            }
        }

        return $map;
    }

    /**
     * @param array<string, array<string, mixed>> $components
     * @return array<int, array<string, mixed>>
     */
    private function componentElements(array $components, string $componentId): array
    {
        $component = $components[$componentId] ?? null;
        if (!is_array($component)) {
            return [];
        }

        $elements = $component['elements'] ?? $component['children'] ?? [];

        return is_array($elements) ? $this->normalizeElements($elements) : [];
    }

  /**
     * @param array<int, array<string, mixed>> $edges
     * @return array<int, array<string, mixed>>
     */
    private function dedupeEdges(array $edges): array
    {
        $unique = [];
        foreach ($edges as $edge) {
            $key = implode(
                '|',
                [
                    (string) ($edge['ref_type'] ?? ''),
                    (string) ($edge['ref_id'] ?? ''),
                    (string) ($edge['post_id'] ?? 0),
                    (string) ($edge['area'] ?? ''),
                    (string) ($edge['element_id'] ?? ''),
                ]
            );
            $unique[$key] = $edge;
        }

        return array_values($unique);
    }

    /**
     * @return array<string, mixed>
     */
    private function edge(
        string $refType,
        string $refId,
        int $postId,
        string $area,
        ?string $elementId,
        ?string $elementName
    ): array {
        return [
            'ref_type'     => $refType,
            'ref_id'       => $refId,
            'post_id'      => $postId,
            'area'         => $area,
            'element_id'   => $elementId,
            'element_name' => $elementName,
        ];
    }
}
