<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator;

use Ekesto\CacheInvalidator\Core\Lifecycle;
use Ekesto\CacheInvalidator\Settings\SettingsAdminPage;
use Ekesto\CacheInvalidator\Settings\SettingsPayloadBuilder;
use Ekesto\CacheInvalidator\Settings\SettingsRepository;
use Ekesto\CacheInvalidator\Settings\SettingsRestController;
use Ekesto\CacheInvalidator\Settings\SettingsSanitizer;
use Ekesto\CacheInvalidator\Settings\SupportDebugRestController;
use Ekesto\CacheInvalidator\Settings\AccessControlRestController;
use Ekesto\CacheInvalidator\Support\Debug;
use Ekesto\CacheAutopilot\AccessControl;
use Ekesto\CacheAutopilot\ProEntitlement;

/**
 * Invalidator runtime
 *
 * Stores engine context and wires Cache Invalidator modules.
 */
final class InvalidatorRuntime
{
    /**
     * Plugin base URL (with trailing slash).
     *
     * @var string
     */
    private static string $url = '';

    /**
     * Plugin base path (with trailing slash).
     *
     * @var string
     */
    private static string $path = '';

    /** @var SettingsSanitizer|null */
    private static ?SettingsSanitizer $settingsSanitizer = null;

    /** @var SettingsRepository|null */
    private static ?SettingsRepository $settingsRepository = null;

    /** @var SettingsPayloadBuilder|null */
    private static ?SettingsPayloadBuilder $settingsPayloadBuilder = null;

    /** @var SettingsRestController|null */
    private static ?SettingsRestController $settingsRestController = null;

    /** @var SettingsAdminPage|null */
    private static ?SettingsAdminPage $settingsAdminPage = null;

    /** @var SupportDebugRestController|null */
    private static ?SupportDebugRestController $supportDebugRestController = null;

    /** @var AccessControlRestController|null */
    private static ?AccessControlRestController $accessControlRestController = null;

    /**
     * Bootstraps plugin globals.
     *
     * @param string $url  Plugin base URL (from plugin_dir_url(__FILE__)).
     * @param string $path Plugin base path (from plugin_dir_path(__FILE__)).
     * @return void
     */
    public static function boot(string $url, string $path): void
    {
        self::$url = trailingslashit($url);
        self::$path = trailingslashit($path);

        self::registerSettingsHooks();

        add_action('plugins_loaded', static function (): void {
            self::registerOptionalRuntimeHooks();
            self::registerAcfHooks();
        }, 20);
    }

    /**
     * Return the minimum class set required to boot safely.
     *
     * @return string[]
     */
    public static function bootRequiredClasses(): array
    {
        return [
            self::class,
            Lifecycle::class,
            Settings::class,
            SettingsAdminPage::class,
            SettingsPayloadBuilder::class,
            SettingsRepository::class,
            SettingsRestController::class,
            SettingsSanitizer::class,
            AccessControl::class,
        ];
    }

    /**
     * Returns plugin base URL (trailing slash).
     *
     * @return string
     */
    public static function url(): string
    {
        return self::$url;
    }

    /**
     * Build a URL to the plugin's settings page, routing through the correct parent
     * based on the current user's admin status.
     *
     * Admins land on `options-general.php?page=<slug>`; non-admins with access
     * land on `admin.php?page=<slug>` (the top-level menu entry).
     *
     * @param array<string,string> $args Extra query args merged into the URL.
     */
    public static function settingsPageUrl(array $args = []): string
    {
        $isAdmin = (function_exists('is_super_admin') && is_super_admin())
            || (function_exists('current_user_can') && current_user_can('manage_options'));

        $base = $isAdmin
            ? admin_url('options-general.php')
            : admin_url('admin.php');

        return add_query_arg(array_merge(['page' => Settings::SETTINGS_PAGE_SLUG], $args), $base);
    }

    /**
     * Returns plugin base path (trailing slash).
     *
     * @return string
     */
    public static function path(): string
    {
        return self::$path;
    }

    /**
     * Settings repository singleton.
     *
     * @return SettingsRepository
     */
    public static function settingsRepository(): SettingsRepository
    {
        self::initSettingsServices();
        return self::$settingsRepository;
    }

    /**
     * Initialize settings services once.
     *
     * @return void
     */
    private static function initSettingsServices(): void
    {
        if (self::$settingsRepository !== null) {
            return;
        }

        self::$settingsSanitizer = new SettingsSanitizer();
        self::$settingsRepository = new SettingsRepository(self::$settingsSanitizer);
        self::$settingsPayloadBuilder = new SettingsPayloadBuilder(self::$settingsRepository);
        self::$settingsRestController = new SettingsRestController(
            self::$settingsRepository,
            self::$settingsPayloadBuilder,
            self::$settingsSanitizer
        );
        self::$settingsAdminPage = new SettingsAdminPage(self::$settingsPayloadBuilder);
        self::$supportDebugRestController = new SupportDebugRestController();
        self::$accessControlRestController = new AccessControlRestController();
    }

    /**
     * Register settings hooks using instance services.
     *
     * @return void
     */
    private static function registerSettingsHooks(): void
    {
        self::initSettingsServices();

        add_action('admin_menu', [self::$settingsAdminPage, 'registerSettingsPage'], 20);
        add_action('admin_init', [self::$settingsRepository, 'registerOption']);
        add_action('admin_enqueue_scripts', [self::$settingsAdminPage, 'enqueueAdminAssets']);
        add_action('admin_notices', [self::$settingsAdminPage, 'renderGlobalAdminNotice']);
        add_action('admin_post_ekesto_ci_dismiss_unconfigured_post_types_notice', [self::$settingsAdminPage, 'handleDismissUnconfiguredPostTypesNoticeAction']);
        add_action('rest_api_init', [self::$settingsRestController, 'registerRestRoutes']);
        add_action('rest_api_init', [self::$supportDebugRestController, 'registerRestRoutes']);
        add_action('rest_api_init', [self::$accessControlRestController, 'registerRestRoutes']);

        AccessControl::registerHooks();
        $pluginActionLinksFilter = 'plugin_action_links_' . plugin_basename(EKESTO_CA_PLUGIN_FILE);
        add_filter($pluginActionLinksFilter, [self::$settingsAdminPage, 'addPluginSettingsLink']);
        add_filter($pluginActionLinksFilter, [self::$settingsAdminPage, 'addPluginUpgradeLink'], PHP_INT_MAX);
    }

    /**
     * Register non-bootstrap-critical runtime modules if their classes are available.
     *
     * @return void
     */
    private static function registerOptionalRuntimeHooks(): void
    {
        // Core modules ship in every build (Lite and Pro). A missing core module
        // indicates a broken package, so absence is logged loudly via error_log.
        $coreRegistrars = [
            \Ekesto\CacheInvalidator\Integrations\Caching\AdapterManager::class,
            \Ekesto\CacheInvalidator\Core\LifecycleProbe::class,
            \Ekesto\CacheInvalidator\Core\InvalidationEngine::class,
            \Ekesto\CacheInvalidator\Listeners\TaxonomyInvalidationListener::class,
            \Ekesto\CacheInvalidator\Listeners\PresentationInvalidationListener::class,
            \Ekesto\CacheInvalidator\Listeners\SystemInvalidationListener::class,
            \Ekesto\CacheInvalidator\Listeners\CommentInvalidationListener::class,
            \Ekesto\CacheInvalidator\Listeners\MetaChangeInvalidationListener::class,
            \Ekesto\CacheInvalidator\Integrations\Forms\FormsManager::class,
            \Ekesto\CacheInvalidator\Async\AsyncInvalidationProcessor::class,
        ];

        // Optional/premium modules are intentionally absent from stripped Lite
        // builds. Their absence after entitlement passes is reported through the
        // debug-gated logger (silent in production) — not error_log.
        $optionalRegistrars = [
            \Ekesto\CacheInvalidator\Pro\Relationships\ProRelationshipRegistrar::class,
            \Ekesto\CacheInvalidator\Pro\Multilingual\ProMultilingualRegistrar::class,
            \Ekesto\CacheInvalidator\Pro\Access\ProAccessRegistrar::class,
            \Ekesto\CacheInvalidator\Pro\Timed\TimedInvalidationScheduler::class,
            \Ekesto\CacheInvalidator\Pro\Elementor\ProElementorRegistrar::class,
            \Ekesto\CacheInvalidator\Pro\WooCommerce\ProWooCommerceRegistrar::class,
        ];

        // Core first, optional second: keeps registration order stable and ensures
        // any provider-swapping optional registrar still runs before content-change
        // resolution (which happens on later hooks, not during this loop).
        self::registerRegistrars($coreRegistrars, false);
        if (!ProEntitlement::canUseProFeatures()) {
            Debug::verbose('optional premium runtime modules skipped: entitlement unavailable');
            return;
        }

        self::registerRegistrars($optionalRegistrars, true);
    }

    /**
     * Register a set of runtime module registrars with shared failure semantics.
     *
     * Missing-class handling depends on $optional: intentionally absent
     * optional/premium modules are reported through the debug-gated logger
     * (silent in production), while missing core modules are logged loudly. A
     * present module that is malformed or throws during registration is always
     * logged loudly and skipped so one bad module cannot fatal the rest.
     *
     * @param array<int,string> $registrarClasses Fully-qualified registrar class names.
     * @param bool              $optional         Whether absence is expected (quiet) or a packaging error (loud).
     * @return void
     */
    private static function registerRegistrars(array $registrarClasses, bool $optional): void
    {
        foreach ($registrarClasses as $registrarClass) {
            if (!class_exists($registrarClass, true)) {
                if ($optional) {
                    Debug::log('optional runtime module skipped', ['class' => $registrarClass]);
                } else {
                    if (defined('WP_DEBUG') && WP_DEBUG) {
                        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only package integrity fallback when runtime logger may not be fully initialized.
                        error_log(
                            sprintf(
                                'Cache Invalidator runtime module skipped: missing class %s',
                                $registrarClass
                            )
                        );
                    }
                }
                continue;
            }

            if (!method_exists($registrarClass, 'registerHooks')) {
                if (defined('WP_DEBUG') && WP_DEBUG) {
                    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only package integrity fallback when runtime logger may not be fully initialized.
                    error_log(
                        sprintf(
                            'Cache Invalidator runtime module skipped: %s has no registerHooks() method',
                            $registrarClass
                        )
                    );
                }
                continue;
            }

            try {
                $registrarClass::registerHooks();
            } catch (\Throwable $registrarError) {
                if (defined('WP_DEBUG') && WP_DEBUG) {
                    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only runtime fallback to isolate bad modules without requiring logger availability.
                    error_log(
                        sprintf(
                            'Cache Invalidator runtime module failed: %s (%s)',
                            $registrarClass,
                            $registrarError->getMessage()
                        )
                    );
                }
                continue;
            }
        }
    }

    /**
     * Register ACF integration hooks only when the cache map class is available.
     *
     * @return void
     */
    private static function registerAcfHooks(): void
    {
        if (!ProEntitlement::canUseProFeatures()) {
            Debug::verbose('optional ACF integration skipped: premium entitlement unavailable');
            return;
        }

        if (!class_exists(\Ekesto\CacheInvalidator\Pro\Relationships\AcfRelationshipMap::class, true)) {
            // Optional/premium integration: absence is expected in stripped Lite
            // builds, so report only through the debug-gated logger.
            Debug::log('optional ACF integration skipped', [
                'class' => \Ekesto\CacheInvalidator\Pro\Relationships\AcfRelationshipMap::class,
            ]);
            return;
        }

        add_action('acf/update_field_group', static function (): void {
            \Ekesto\CacheInvalidator\Pro\Relationships\AcfRelationshipMap::invalidate();
        });
        add_action('acf/delete_field_group', static function (): void {
            \Ekesto\CacheInvalidator\Pro\Relationships\AcfRelationshipMap::invalidate();
        });
    }

}
