<?php

declare(strict_types=1);

namespace Builder\Core\Foundation;

use Builder\Core\Foundation\Account\AccountExtension;
use Builder\Core\Foundation\Account\AccountTab;
use Builder\Core\Foundation\Auth\AuthenticationProvider;
use Builder\Core\Foundation\Dashboard\Widgets\Widget;
use Builder\Core\Foundation\Definition\JsModule;
use Builder\Core\Foundation\Definition\MenuItem;
use Builder\Core\Foundation\Resources\Extension;
use Builder\Core\Foundation\Resources\Resource;
use Builder\Core\Foundation\Resources\ResourceDefinition;
use Builder\Core\Foundation\Settings\BuilderSettings;
use Builder\Core\Foundation\Settings\SettingsDefinition;
use Builder\Core\Foundation\Settings\SettingsExtension;
use Builder\Core\Foundation\Settings\SettingsSection;

final class Registry
{
    /** @var array<string, JsModule> */
    private array $modules = [];

    /** @var array<string, class-string<resource>> */
    private array $resources = [];

    /** @var array<string, list<class-string<Extension>>> */
    private array $extensions = [];

    /** @var list<MenuItem> */
    private array $menuItems = [];

    /** @var array<string, list<MenuItem>> parentKey => list<child MenuItem> */
    private array $pendingMenuChildren = [];

    /** @var list<Widget> */
    private array $widgets = [];

    /** @var list<AccountTab> */
    private array $accountTabs = [];

    /** @var list<AccountExtension> */
    private array $accountExtensions = [];

    /** @var list<SettingsSection> */
    private array $settingsSections = [];

    /** @var array<string, class-string<BuilderSettings>> */
    private array $settingsPageClasses = [];

    /** @var array<class-string<BuilderSettings>, list<class-string<SettingsExtension>>> */
    private array $settingsExtensions = [];

    /** @var array<string, string> frontendKey => trans() key */
    private array $translationNamespaces = [];

    /** @var list<string> */
    private array $registeredPermissions = [];

    /** @var list<class-string<AuthenticationProvider>> */
    private array $authProviders = [];

    public function register(JsModule ...$modules): self
    {
        foreach ($modules as $module) {
            $this->modules[$module->getKey()] = $module;
        }

        return $this;
    }

    /** @param class-string<resource> ...$resources */
    public function resource(string ...$resources): self
    {
        foreach ($resources as $resource) {
            $this->resources[$resource::getSlug()] = $resource;

            $inst = new $resource;

            // Auto-collect widgets declared by the resource
            foreach ($inst->widgets() as $widget) {
                $this->widgets[] = $widget;
            }

            // Auto-register a sidebar menu item when the resource declares a priority
            $priority = $inst->getMenuPriority();
            if ($priority !== null) {
                $this->menuItems[] = MenuItem::make('resource-'.$resource::getSlug())
                    ->label(fn (): string => $resource::getLabel())
                    ->icon($inst->getMenuIcon()->value)
                    ->namedRoute('builder.resource.index', ['resource' => $resource::getSlug()])
                    ->priority($priority);
            }
        }

        return $this;
    }

    /** @param class-string<Extension> ...$extensions */
    public function extend(string ...$extensions): self
    {
        foreach ($extensions as $extension) {
            $target = $extension::getExtends();
            $this->extensions[$target] ??= [];
            $this->extensions[$target][] = $extension;

            // Auto-collect widgets declared by the extension
            foreach ((new $extension)->widgets() as $widget) {
                $this->widgets[] = $widget;
            }
        }

        return $this;
    }

    public function menuItem(MenuItem ...$items): self
    {
        foreach ($items as $item) {
            $this->menuItems[] = $item;
        }

        return $this;
    }

    /**
     * Append a child menu item under a top-level group registered elsewhere.
     *
     * Children are buffered until `menuItems()` is called, at which point they
     * are merged into the matching parent (if one exists). If the parent never
     * registers, buffered children are silently ignored.
     *
     * The append itself is idempotent on the child's key via MenuItem::appendChild,
     * so calling this multiple times during repeated boot cycles is safe.
     */
    public function appendToMenuGroup(string $parentKey, MenuItem $child): self
    {
        $this->pendingMenuChildren[$parentKey] ??= [];
        $this->pendingMenuChildren[$parentKey][] = $child;

        return $this;
    }

    public function widget(Widget ...$widgets): self
    {
        foreach ($widgets as $widget) {
            $this->widgets[] = $widget;
        }

        return $this;
    }

    public function accountTab(AccountTab ...$tabs): self
    {
        foreach ($tabs as $tab) {
            $this->accountTabs[] = $tab;
        }

        return $this;
    }

    public function accountExtension(AccountExtension ...$extensions): self
    {
        foreach ($extensions as $extension) {
            $this->accountExtensions[] = $extension;
        }

        return $this;
    }

    public function settingsSection(SettingsSection ...$sections): self
    {
        foreach ($sections as $section) {
            $this->settingsSections[] = $section;
        }

        return $this;
    }

    /** @param class-string<BuilderSettings> ...$pageClasses */
    public function settingsPage(string ...$pageClasses): self
    {
        foreach ($pageClasses as $pageClass) {
            $page = new $pageClass;
            // Sub-pages are stored under a composite key (parent.key) so that the
            // controller can resolve them from a {parent}/{page} URL pattern.
            $key = $page->getParent() !== null
                ? "{$page->getParent()}.{$page->getKey()}"
                : $page->getKey();

            $this->settingsPageClasses[$key] = $pageClass;
        }

        return $this;
    }

    /** @param class-string<SettingsExtension> ...$classes */
    public function extendSettings(string ...$classes): self
    {
        foreach ($classes as $class) {
            $target = $class::getExtends();
            $this->settingsExtensions[$target] ??= [];
            $this->settingsExtensions[$target][] = $class;
        }

        return $this;
    }

    public function resolveSettingsPage(string $key): ?SettingsDefinition
    {
        if (! isset($this->settingsPageClasses[$key])) {
            return null;
        }

        $pageClass = $this->settingsPageClasses[$key];
        $settings = app($pageClass);

        $extensionClasses = $this->settingsExtensions[$pageClass] ?? [];
        $extensions = array_map(fn (string $ext): SettingsExtension => new $ext, $extensionClasses);

        return SettingsDefinition::make($settings, $key, $extensions);
    }

    public function resolveBySlug(string $slug): ?ResourceDefinition
    {
        if (! isset($this->resources[$slug])) {
            return null;
        }

        return $this->resolveResource($this->resources[$slug]);
    }

    /** @param class-string<resource> $resourceClass */
    public function resolveResource(string $resourceClass): ResourceDefinition
    {
        $resource = new $resourceClass;
        $extensionClasses = $this->extensions[$resourceClass] ?? [];

        $extensions = array_map(
            fn (string $ext): Extension => new $ext,
            $extensionClasses,
        );

        return ResourceDefinition::make($resource, $extensions);
    }

    /** @return list<JsModule> */
    public function all(): array
    {
        return array_values($this->modules);
    }

    /** @return list<MenuItem> */
    public function menuItems(): array
    {
        foreach ($this->menuItems as $item) {
            foreach ($this->pendingMenuChildren[$item->getKey()] ?? [] as $child) {
                $item->appendChild($child);
            }
        }

        $items = $this->menuItems;
        usort($items, fn (MenuItem $a, MenuItem $b): int => $a->getPriority() <=> $b->getPriority());

        return $items;
    }

    /** @return array<string, class-string<resource>> */
    public function resources(): array
    {
        return $this->resources;
    }

    /**
     * Returns all registered widgets sorted by order.
     *
     * @return list<Widget>
     */
    public function widgets(): array
    {
        $widgets = $this->widgets;
        usort($widgets, fn (Widget $a, Widget $b): int => $a->getOrder() <=> $b->getOrder());

        return $widgets;
    }

    /**
     * Returns all registered account extensions.
     *
     * @return list<AccountExtension>
     */
    public function accountExtensions(): array
    {
        return $this->accountExtensions;
    }

    /**
     * Returns all registered account tabs sorted by order.
     *
     * @return list<AccountTab>
     */
    public function accountTabs(): array
    {
        $tabs = $this->accountTabs;
        usort($tabs, fn (AccountTab $a, AccountTab $b): int => $a->getOrder() <=> $b->getOrder());

        return $tabs;
    }

    /**
     * Returns all registered settings sections sorted by order.
     *
     * @return list<SettingsSection>
     */
    public function settingsSections(): array
    {
        $sections = $this->settingsSections;
        usort($sections, fn (SettingsSection $a, SettingsSection $b): int => $a->getOrder() <=> $b->getOrder());

        return $sections;
    }

    /**
     * Returns all registered settings pages sorted by priority.
     *
     * @return list<BuilderSettings>
     */
    public function settingsPages(): array
    {
        $pages = array_map(fn (string $class): BuilderSettings => new $class, $this->settingsPageClasses);
        usort($pages, fn (BuilderSettings $a, BuilderSettings $b): int => $a->getPriority() <=> $b->getPriority());

        return $pages;
    }

    public function permissions(string ...$permissions): self
    {
        foreach ($permissions as $permission) {
            $this->registeredPermissions[] = $permission;
        }

        return $this;
    }

    /** @return list<string> */
    public function getRegisteredPermissions(): array
    {
        return $this->registeredPermissions;
    }

    /** @param class-string<AuthenticationProvider> ...$providers */
    public function authProvider(string ...$providers): self
    {
        foreach ($providers as $provider) {
            $this->authProviders[] = $provider;
        }

        return $this;
    }

    /** @return list<class-string<AuthenticationProvider>> */
    public function getAuthProviders(): array
    {
        return $this->authProviders;
    }

    public function translationNamespace(string $key, string $transKey): self
    {
        $this->translationNamespaces[$key] = $transKey;

        return $this;
    }

    /** @return array<string, string> */
    public function translationNamespaces(): array
    {
        return $this->translationNamespaces;
    }
}
