<?php

namespace WordpressModelsPlugin\DependencyInjection\FederatedPages;

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use WordpressModels\Assets;
use WordpressModels\DependencyInjection\HookAttributes\Attributes\Action;
use WordpressModels\DependencyInjection\Pages\PageRegistry;
use WordpressModels\Page\AbstractPage;
use WordpressModels\Page\AbstractPageStack;
use WordpressModels\Page\AsyncPageData;

class FederatedPluginApp
{

    /**
     * Data to be passed to the federated plugin app as wp_localize_script data.
     *
     * @var array
     */
    private array $fpaData;

    /**
     * Array of module script configs that are rendered as preload links.
     * These modules are hooked into the specified element id (the array key) as React portals. This is convienient
     * when using a React Context provider inside your module, as the context will be shared between the
     * portal and the main app.
     *
     * Note that the contents of the container elements are not cleared, and you are responsible for
     * ensuring that the correct content is rendered in the correct container.
     *
     * You can mount additional data for the module by adding it to the moduleData key in the fpaData array.
     *
     * @var array<string, array{
     *     id: string,
     *     url: string,
     *     dependencies: string[],
     *     version: string
     * }>
     */
    private array $portalModules = [];

    public function __construct(#[Autowire(service: 'assets.wordpress-models-plugin')]
                                private Assets       $assets,
                                private PageRegistry $pageRegistry)
    {
        $this->fpaData = [
            'asyncDataNonce' => wp_create_nonce(AsyncPageData::NONCE_ID),

            'moduleData' => [],
        ];
    }

    public function addData(string $key, mixed $value)
    {
        $this->fpaData[$key] = $value;
    }

    /**
     * Add a module to be rendered as a portal.
     *
     * This will render a preload link for the module, and add the module to the fpaData array.
     * The module will be mounted into the specified element id in the federated plugin app.
     *
     * @param string $moduleId
     * @param string $url
     * @param array $dependencies
     * @param string $version
     * @param string|null $elementId
     * @param array $data
     * @return void
     */
    public function addPortalModule(string  $moduleId,
                                    string  $url,
                                    array   $dependencies = [],
                                    string  $version = '',
                                    ?string $elementId = null,
                                    array   $data = []
    ): void
    {
        $elementId ??= $moduleId;
        $this->portalModules[$elementId] = [
            'id' => $moduleId,
            'url' => $url,
            'dependencies' => $dependencies,
            'version' => $version
        ];

        $this->fpaData['moduleData'][$moduleId] = $data;
    }

    /**
     * Add an anchor to the footer to be used in React to render portals onto.
     *
     * @return void
     */
    #[Action('admin_footer', admin: true)]
    public function renderPortalAnchor(): void
    {
        echo '<div id="portal-anchor" class="fpa-page" style="position: absolute; top: -32px"></div>';
    }


    /**
     * Replace script enqueues on page load hooks with a custom hook.
     *
     * @return void
     */
    #[Action('admin_menu', 11, admin: true)]
    public function enqueueIprOnPageLoadHook(): void
    {
        $loadHooks = $this->pageRegistry->getLoadHooks();
        foreach ($this->pageRegistry->getPages() as $page) {
            if ($loadHook = $loadHooks[$page->getPageId()] ?? false) {
                remove_action("load-$loadHook", [$page, 'enqueueScripts']);
                // replace the load hook with our own, to prepare the page load
                add_action("load-$loadHook", fn() => $this->preparePageLoad($loadHook, $page));
            }
        }
    }

    public function hideAdminNotices()
    {
        // capture notices in an output buffer, and throw them away
        add_action('admin_notices', fn() => ob_start(), PHP_INT_MIN);
        add_action('admin_notices', fn() => ob_end_clean(), PHP_INT_MAX);
    }

    #[Action('all_admin_notices', priority: PHP_INT_MAX, admin: true)]
    public function renderPageRoot()
    {
        echo "<div id='fpa-root' style='margin-left: -20px' class='fpa-page'></div>";
    }

    /**
     * Set up the enqueues for the federated plugin app.
     *
     * @return void
     */
    public function preparePageLoad(string $loadHook, AbstractPage $page): void
    {
        add_action('in_admin_header', [$this, 'hideAdminNotices']);
        /**
         * localize the page data
         * we place this on the load hook, so that the header is already rendered, and
         * we can inject data, such as admin notices
         * we register this action during load-$loadHook, so that {@see _wp_menu_output} doesn't append the
         * full url als query parameter
         */
        add_action($loadHook, fn() => $this->localizePageData($page));
    }

    /**
     * Enqueue the scripts for the federated plugin app.
     *
     * @return void
     */
    #[Action('admin_enqueue_scripts', priority: 1000, admin: true)]
    public function enqueueScripts(): void
    {
        // add script module entries for undelayed imports
        $allPagesScripts = $this->getAllPagesScripts();
        $this->addData('scriptEntries', array_map(fn($script) => $script['url'] . '?ver=' . $script['config']['version'] ?? '', $allPagesScripts));
        $this->addData('modules', array_map(fn($module) => $module['url'] . '?ver=' . $module['version'], $this->portalModules));

        $allPagesDependencies = array_unique(array_reduce($allPagesScripts, fn($acc, $script) => [...$acc, ...$script['config']['dependencies'] ?? []], []));
        $allModuleDependencies = array_unique(array_reduce($this->portalModules, fn($acc, $module) => [...$acc, ...$module['dependencies']], []));

        $this->assets->enqueueCompiledScript('ext-react-router');
        $this->assets->enqueueCompiledScript('fpa', additionalDependencies: [
            ...$allPagesDependencies,
            ...$allModuleDependencies
        ]);
    }

    public function localizePageData(?AbstractPage $page = null)
    {
        // add the route loader data
        if ($page) {
            $this->primeRouteLoaderData($page);
        }
    }

    /**
     * @return void
     */
    #[Action('admin_footer', admin: true)]
    public function localizeFpaScript()
    {
        wp_localize_script(
            'fpa',
            'fpa',
            apply_filters('wpm_fpa_localize_data', $this->fpaData)
        );
    }

    /**
     * Render preload links for the script entries.
     *
     * This optimizes load times, as the preloads are loaded before the scripts are requested in the browser.
     * This prevents the browser from have to first load the main script, and then request the chunks.
     *
     * @return void
     */
    #[Action('admin_print_scripts', admin: true)]
    public function renderPreloadLinks()
    {
        foreach ($this->getAllPagesScripts() as $script) {
            $url = $script['url'];
            $version = $script['config']['version'] ?? null;
            echo "<link rel='modulepreload' href='$url?ver=$version'>";
        }

        foreach ($this->portalModules as $module) {
            $url = $module['url'];
            $version = $module['version'];
            echo "<link rel='modulepreload' href='$url?ver=$version'>";
        }
    }

    /**
     * Get all valid page scripts urls, indexed by page id.
     *
     * @return array
     */
    public function getAllPagesScripts(): array
    {
        $pages = $this->pageRegistry->getPages();
        $scripts = [];
        foreach ($pages as $page) {
            if (!$assets = $page->getAssets()) {
                continue;
            }
            /** @var Assets $assets */
            $scripts[$page->getPageId()] = [
                'url' => $assets->getAssetsUrls($page->getPageId())['js'],
                'config' => $assets->getAssetConfig($page->getPageId())
            ];
        }
        return $scripts;
    }

    /**
     * Prime the routeloader data to prevent unnessecary requests.
     *
     * If the page is part of a stack, we add the parent page to the route loader data.
     * If the page is a stack, we try to match the current url to a page in the stack.
     *
     * @param AbstractPage $page
     * @return void
     */
    public function primeRouteLoaderData(AbstractPage $page): void
    {
        $routeLoaderData = [];
        if ($parent = $this->pageRegistry->getParentPage($page)) {
            $routeLoaderData[$parent->getPageId()] = $parent->buildContext();
        }

        $routeLoaderData[$page->getPageId()] = $page->buildContext();

        if ($page instanceof AbstractPageStack) {
            // try to match url to a page
            $currentUrl = urldecode($_SERVER['REQUEST_URI']);
            foreach ($page->getPages() as $routePage) {
                $routeUrl = $page->getRouteUrl($routePage->getPageId());
                $routePath = str_replace(get_home_url(), '', $routeUrl);
                if (str_starts_with($currentUrl, $routePath)) {
                    $routeLoaderData[$routePage->getPageId()] = $routePage->buildContext();
                    break;
                }
            }
        }

        $this->addData('routeLoaderData', $routeLoaderData);
    }

}
