<?php

declare(strict_types=1);

namespace CliqThemes\StoreLocator;

defined('ABSPATH') || exit;

use CliqThemes\StoreLocator\Admin\AdminDataLayer;
use CliqThemes\StoreLocator\Extensions\ExtensionLoader;
use CliqThemes\StoreLocator\Schema\SchemaInstaller;
use CliqThemes\StoreLocator\REST\AdminController;
use CliqThemes\StoreLocator\REST\RestWriteGuard;
use CliqThemes\StoreLocator\REST\CategoryIconsController;
use CliqThemes\StoreLocator\REST\SettingsController;
use CliqThemes\StoreLocator\REST\PublicController;
use CliqThemes\UI\Admin\AdminPage;
use CliqThemes\UI\Admin\AdminPageRegistry;
use CliqThemes\UI\Bootstrap;
use CliqThemes\UI\Data\DataProviderRegistry;
use CliqThemes\StoreLocator\Data\Definition\BrandProviderDefinition;
use CliqThemes\StoreLocator\Data\Definition\CategoryProviderDefinition;
use CliqThemes\StoreLocator\Data\Definition\CustomFieldProviderDefinition;
use CliqThemes\StoreLocator\Data\Definition\ProviderContext;
use CliqThemes\StoreLocator\Data\Definition\ProviderDefinition;
use CliqThemes\StoreLocator\Data\Definition\StoreProviderDefinition;
use CliqThemes\StoreLocator\Data\FormSelectOptions;
use CliqThemes\UI\REST\RouteRegistrar;
use CliqThemes\UI\WordPress\Integration;

final class Plugin
{
    private const MENU_SLUG = 'cliqthemes-store-locator';

    private string $pluginFile;

    private ?Integration $integration = null;

    private ?AdminPageRegistry $registry = null;

    private ?DataProviderRegistry $providers = null;

    private ?RouteRegistrar $restRoutes = null;

    /**
     * Cached form schemas keyed by provider identifier.
     *
     * @var array<string,array<string,mixed>>
     */
    private array $providerForms = [];

    public function __construct(string $pluginFile)
    {
        $this->pluginFile = $pluginFile;
    }

    public function register(): void
    {
        // Boot on `init` (not `plugins_loaded`) so provider/form labels can use
        // translation functions without tripping WP 6.7's "textdomain loaded too
        // early" notice. All hooks attached here (admin_menu, rest_api_init,
        // wp_enqueue_scripts) fire after init, so timing is unaffected.
        add_action('init', [$this, 'bootUiLibrary']);
        add_action('admin_enqueue_scripts', [$this, 'injectDataLayer'], 15, 1);
    }

    public function bootUiLibrary(): void
    {
        $this->integration = Bootstrap::wordpress([
            'plugin_file' => $this->pluginFile,
            'host_id' => 'cliq-store-locator',
        ]);

        // Schema is version-gated (cheap no-op once current). Demo seeding is
        // NOT on the boot path — it runs on activation / `wp cliq-sl reset`
        // only, so normal admin loads don't fire SHOW TABLES / COUNT probes.
        // Before the schema check, for the same reason as activation.
        ExtensionLoader::boot();

        $this->ensureSchemaIsCurrent();

        // Public surface — needed on every request (the locator shortcode/block,
        // its assets, and the unauthenticated REST endpoints).
        (new PublicController())->register();
        (new \CliqThemes\StoreLocator\Frontend\FrontendModule($this->pluginFile))->register();
        (new \CliqThemes\StoreLocator\Frontend\LocatorWideTemplate())->register();

        // Admin data layer (providers + admin page + admin REST + records grid)
        // is ONLY needed in wp-admin or for the admin React app's REST calls.
        // Registering the providers eagerly resolves their form-select options via
        // DB queries (categories/brands/custom fields/markers/…), so skipping this
        // on public page loads keeps the frontend off that query path entirely.
        if ($this->needsAdminLayer()) {
            $this->providers = $this->bootDataProviders();

            $this->registry = new AdminPageRegistry($this->integration);
            $this->registry->register($this->makeStoreLocatorPage());

            // The generic record endpoints hand the request body straight to a
            // provider, so what a caller may write is declared before they run.
            RestWriteGuard::register();

            $this->restRoutes = new RouteRegistrar($this->providers);
            $this->restRoutes->register();

            (new AdminController())->register();

            // The admin controllers this site runs, keyed by the job each does.
            // A feature takes over a slot by answering with its own class for
            // that key.
            $controllers = apply_filters('cliq_sl_admin_controllers', [
                'category-icons' => CategoryIconsController::class,
                'settings' => SettingsController::class,
            ]);
            foreach ((array) $controllers as $controller) {
                if (is_string($controller) && class_exists($controller)) {
                    (new $controller())->register();
                }
            }
        }
    }

    /**
     * Whether this request needs the admin data layer: wp-admin (incl. admin-ajax)
     * or a REST request (the admin app's data calls). Public page renders skip it.
     */
    private function needsAdminLayer(): bool
    {
        if (is_admin()) {
            return true;
        }
        // Only the admin SPA's REST routes need the providers — those live under
        // AdminController's `…/admin/…` paths and the framework provider namespace.
        // The PUBLIC locator endpoints (search / templates / stores / categories)
        // and normal page renders must NOT boot the admin providers (that's what
        // made every frontend search re-run the eager option queries). Matching the
        // path works for both pretty-permalink (/wp-json/…) and plain (?rest_route=)
        // REST forms.
        $uri = isset($_SERVER['REQUEST_URI'])
            ? esc_url_raw(wp_unslash((string) $_SERVER['REQUEST_URI']))
            : '';
        if ($uri === '') {
            return false;
        }

        return strpos($uri, AdminController::NAMESPACE . '/admin/') !== false
            || strpos($uri, 'cliqthemes-ui/') !== false;
    }

    public function injectDataLayer(string $hook): void
    {
        if (! $this->integration || ! $this->providers) {
            return;
        }

        (new AdminDataLayer(
            $this->integration,
            $this->providers,
            $this->providerForms,
            $this->pluginFile,
            self::MENU_SLUG
        ))->inject($hook);
    }

    private function makeStoreLocatorPage(): AdminPage
    {
        return AdminPage::make([
            'slug' => self::MENU_SLUG,
            'page_title' => __('Store Locator', 'cliq-store-locator'),
            'menu_title' => __('Store Locator', 'cliq-store-locator'),
            'capability' => 'manage_options',
            'icon_url' => 'dashicons-location-alt',
            'position' => 58,
            'description' => '',
        ]);
    }

    private function bootDataProviders(): DataProviderRegistry
    {
        $registry = new DataProviderRegistry();
        $context = new ProviderContext(new FormSelectOptions());
        // One definition per entity this plugin manages.
        /**
         * Filter the entity definitions the admin data layer registers.
         *
         * @param array<int, ProviderDefinition> $definitions
         */
        $definitions = (array) apply_filters('cliq_sl_provider_definitions', [
            new StoreProviderDefinition(),
            new BrandProviderDefinition(),
            new CategoryProviderDefinition(),
            new CustomFieldProviderDefinition(),
        ]);
        $definitions = array_filter(
            $definitions,
            static fn (mixed $d): bool => $d instanceof ProviderDefinition
        );

        foreach ($definitions as $definition) {
            $form = $definition->register($registry, $context);
            if ($form !== []) {
                $this->providerForms[$definition->key()] = $form;
            }
        }

        return $registry;
    }

    private function ensureSchemaIsCurrent(): void
    {
        // Dev-stage: just make sure every table exists (idempotent, no data
        // loss). Use SchemaInstaller::fresh() (wp cliq-sl reset) to wipe and
        // rebuild from scratch.
        SchemaInstaller::ensure();
    }
}
