<?php
/**
 * Replaces a global class id across Bricks element trees atomically.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Index;

use LolaCoreForBricks\Support\BricksMetaKeys;

/**
 * Cascades class id replacements through pages, components, and global elements.
 */
class ClassCascade
{
    /**
     * @return int[]
     */
    public function replaceClassId(string $fromId, string $toId): array
    {
        if ($fromId === '' || $toId === '' || $fromId === $toId) {
            return [];
        }

        $snapshots = [];
        $affected  = [];

        try {
            $snapshots['components'] = get_option(BricksMetaKeys::OPTION_COMPONENTS, []);
            $components              = is_array($snapshots['components']) ? $snapshots['components'] : [];
            $changedComponents       = $this->replaceInComponents($components, $fromId, $toId);
            if ($changedComponents) {
                update_option(BricksMetaKeys::OPTION_COMPONENTS, $components);
            }

            $snapshots['global_elements'] = get_option(BricksMetaKeys::OPTION_GLOBAL_ELEMENTS, []);
            $globalElements               = is_array($snapshots['global_elements']) ? $snapshots['global_elements'] : [];
            $changedGlobalElements        = $this->replaceInGlobalElements($globalElements, $fromId, $toId);
            if ($changedGlobalElements) {
                update_option(BricksMetaKeys::OPTION_GLOBAL_ELEMENTS, $globalElements);
            }

            foreach ($this->discoverPostIdsWithClass($fromId) as $postId) {
                $postSnapshots = $this->snapshotPostMeta($postId);
                $snapshots['posts'][$postId] = $postSnapshots;
                $changed                     = $this->replaceInPostMeta($postId, $fromId, $toId);
                if ($changed) {
                    $affected[] = $postId;
                }
            }

            return array_values(array_unique($affected));
        } catch (\Throwable $exception) {
            $this->restoreSnapshots($snapshots);
            throw $exception;
        }
    }

    /**
     * @param array<int|string, mixed> $components
     */
    private function replaceInComponents(array &$components, string $fromId, string $toId): bool
    {
        $changed = false;

        foreach ($components as $index => $component) {
            if (!is_array($component)) {
                continue;
            }

            $elements = $component['elements'] ?? $component['children'] ?? null;
            if (!is_array($elements)) {
                continue;
            }

            if ($this->replaceInElements($elements, $fromId, $toId)) {
                if (isset($component['elements'])) {
                    $components[$index]['elements'] = $elements;
                } else {
                    $components[$index]['children'] = $elements;
                }
                $changed = true;
            }
        }

        return $changed;
    }

    /**
     * @param array<int|string, mixed> $globalElements
     */
    private function replaceInGlobalElements(array &$globalElements, string $fromId, string $toId): bool
    {
        $changed = false;

        foreach ($globalElements as $index => $group) {
            if (!is_array($group)) {
                continue;
            }

            if (isset($group['elements']) && is_array($group['elements'])) {
                $elements = $group['elements'];
                if ($this->replaceInElements($elements, $fromId, $toId)) {
                    $globalElements[$index]['elements'] = $elements;
                    $changed = true;
                }
                continue;
            }

            if ($this->replaceInElements($group, $fromId, $toId)) {
                $globalElements[$index] = $group;
                $changed                = true;
            }
        }

        return $changed;
    }

    private function replaceInPostMeta(int $postId, string $fromId, string $toId): bool
    {
        $changed = false;

        foreach (BricksMetaKeys::pageMetaKeys() as $metaKey) {
            $elements = get_post_meta($postId, $metaKey, true);
            if (!is_array($elements) || $elements === []) {
                continue;
            }

            if ($this->replaceInElements($elements, $fromId, $toId)) {
                update_post_meta($postId, $metaKey, $elements);
                $changed = true;
            }
        }

        return $changed;
    }

    /**
     * @param array<int|string, array<string, mixed>> $elements
     */
    private function replaceInElements(array &$elements, string $fromId, string $toId): bool
    {
        $changed = false;

        foreach ($elements as $index => $element) {
            if (!is_array($element)) {
                continue;
            }

            $settings = is_array($element['settings'] ?? null) ? $element['settings'] : [];
            if ($this->replaceInSettings($settings, $fromId, $toId)) {
                $elements[$index]['settings'] = $settings;
                $changed                      = true;
            }
        }

        return $changed;
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function replaceInSettings(array &$settings, string $fromId, string $toId): bool
    {
        $changed = false;

        if (isset($settings['_cssGlobalClasses']) && is_array($settings['_cssGlobalClasses'])) {
            $updated = $this->replaceInIdList($settings['_cssGlobalClasses'], $fromId, $toId);
            if ($updated !== $settings['_cssGlobalClasses']) {
                $settings['_cssGlobalClasses'] = $updated;
                $changed                       = true;
            }
        }

        if (isset($settings['_cssGlobalClassesProps']) && is_array($settings['_cssGlobalClassesProps'])) {
            $updated = $this->replaceInIdList($settings['_cssGlobalClassesProps'], $fromId, $toId);
            if ($updated !== $settings['_cssGlobalClassesProps']) {
                $settings['_cssGlobalClassesProps'] = $updated;
                $changed                            = true;
            }
        }

        return $changed;
    }

    /**
     * @param array<int|string, mixed> $ids
     * @return array<int|string, mixed>
     */
    private function replaceInIdList(array $ids, string $fromId, string $toId): array
    {
        $updated = [];
        foreach ($ids as $id) {
            if ((string) $id === $fromId) {
                $updated[] = $toId;
                continue;
            }

            $updated[] = $id;
        }

        return array_values(array_unique($updated));
    }

    /**
     * @return int[]
     */
    private function discoverPostIdsWithClass(string $classId): array
    {
        $postIds = [];
        foreach ((new IndexRepository())->findByRef(BricksMetaKeys::REF_CLASS, $classId) as $edge) {
            $postId = (int) ($edge['post_id'] ?? 0);
            if ($postId > 0) {
                $postIds[$postId] = $postId;
            }
        }

        return array_values($postIds);
    }

    /**
     * @return array<string, mixed>
     */
    private function snapshotPostMeta(int $postId): array
    {
        $snapshot = [];
        foreach (BricksMetaKeys::pageMetaKeys() as $metaKey) {
            $snapshot[$metaKey] = get_post_meta($postId, $metaKey, true);
        }

        return $snapshot;
    }

    /**
     * @param array<string, mixed> $snapshots
     */
    private function restoreSnapshots(array $snapshots): void
    {
        if (isset($snapshots['components'])) {
            update_option(BricksMetaKeys::OPTION_COMPONENTS, $snapshots['components']);
        }

        if (isset($snapshots['global_elements'])) {
            update_option(BricksMetaKeys::OPTION_GLOBAL_ELEMENTS, $snapshots['global_elements']);
        }

        if (!isset($snapshots['posts']) || !is_array($snapshots['posts'])) {
            return;
        }

        foreach ($snapshots['posts'] as $postId => $meta) {
            if (!is_array($meta)) {
                continue;
            }

            foreach ($meta as $metaKey => $value) {
                update_post_meta((int) $postId, (string) $metaKey, $value);
            }
        }
    }
}
