<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator\Listeners;

use Ekesto\CacheInvalidator\Core\GlobalInvalidationExecutor;
use Ekesto\CacheInvalidator\InvalidatorRuntime;
use Ekesto\CacheInvalidator\Integrations\Caching\AdapterManager;
use Ekesto\CacheInvalidator\Integrations\Multilingual\LanguageResolverFactory;
use Ekesto\CacheInvalidator\Support\Debug;

/**
 * Handles system/lifecycle invalidation triggers owned by ECI.
 */
final class SystemInvalidationListener
{
    public const LIFECYCLE_CRON_HOOK = 'ekesto_eci_system_lifecycle_invalidation';

    /**
     * @var array<string,bool> Request-local dedupe map for lifecycle events.
     */
    private static array $eventDedupe = [];

    /**
     * Register system/lifecycle trigger hooks.
     *
     * @return void
     */
    public static function registerHooks(): void
    {
        add_action('upgrader_process_complete', [self::class, 'handleUpgraderProcessComplete'], 20, 2);
        add_action('activated_plugin', [self::class, 'handlePluginActivated'], 20, 2);
        add_action('deactivated_plugin', [self::class, 'handlePluginDeactivated'], 20, 2);
        add_action('ekesto_ci_manual_full_refresh', [self::class, 'handleManualFullRefresh'], 20, 1);
        add_action(self::LIFECYCLE_CRON_HOOK, [self::class, 'runDeferredLifecycleInvalidation'], 10, 2);
    }

    /**
     * Handle upgrader events and map to unified lifecycle toggles.
     *
     * @param mixed $upgrader Unused upgrader object.
     * @param array<string,mixed> $data Upgrader payload.
     * @return void
     */
    public static function handleUpgraderProcessComplete($upgrader, array $data): void
    {
        if (self::shouldSkipForMaintenance('upgrader_process_complete')) {
            return;
        }

        if (!InvalidatorRuntime::settingsRepository()->isInvalidationEnabled()) {
            return;
        }

        $action = sanitize_key((string) ($data['action'] ?? ''));
        $type = sanitize_key((string) ($data['type'] ?? ''));
        if (!in_array($action, ['update', 'upgrade'], true) || !in_array($type, ['core', 'plugin', 'theme'], true)) {
            return;
        }

        $global = InvalidatorRuntime::settingsRepository()->getGlobalInvalidation();
        $reason = '';
        $dedupeKey = '';

        if ($type === 'core') {
            if (!((bool) ($global['core_update'] ?? false))) {
                return;
            }
            $reason = 'core_update';
            $dedupeKey = 'upgrade:core';
        } elseif ($type === 'theme') {
            if (!((bool) ($global['theme_update'] ?? false))) {
                return;
            }
            $reason = 'theme_update';
            $dedupeKey = 'upgrade:theme';
        } elseif ($type === 'plugin') {
            if (!((bool) ($global['plugin_update'] ?? false))) {
                return;
            }
            $reason = 'plugin_update';
            $dedupeKey = 'upgrade:plugin';
        }

        if ($reason === '' || self::isDuplicateEvent($dedupeKey)) {
            return;
        }

        self::scheduleGlobalLifecycleInvalidation($reason, [
            'trigger_event' => $dedupeKey,
            'upgrader_action' => $action,
            'upgrader_type' => $type,
        ]);
    }

    /**
     * Handle plugin activation as plugin_update lifecycle toggle.
     *
     * @param string $plugin Plugin basename.
     * @param bool   $networkWide Whether network-wide.
     * @return void
     */
    public static function handlePluginActivated(string $plugin, bool $networkWide): void
    {
        if (self::shouldSkipForMaintenance('activated_plugin')) {
            return;
        }

        if (!InvalidatorRuntime::settingsRepository()->isInvalidationEnabled()) {
            return;
        }

        $global = InvalidatorRuntime::settingsRepository()->getGlobalInvalidation();
        if (!((bool) ($global['plugin_update'] ?? false))) {
            return;
        }

        $plugin = trim($plugin);
        $dedupeKey = 'plugin_activated:' . $plugin;
        if (self::isDuplicateEvent($dedupeKey)) {
            return;
        }

        self::executeGlobalLifecycleInvalidation('plugin_update', [
            'trigger_event' => $dedupeKey,
            'plugin' => $plugin,
            'network_wide' => $networkWide,
        ]);
    }

    /**
     * Handle plugin deactivation as plugin_update lifecycle toggle.
     *
     * @param string $plugin Plugin basename.
     * @param bool   $networkWide Whether network-wide.
     * @return void
     */
    public static function handlePluginDeactivated(string $plugin, bool $networkWide): void
    {
        if (self::shouldSkipForMaintenance('deactivated_plugin')) {
            return;
        }

        if (!InvalidatorRuntime::settingsRepository()->isInvalidationEnabled()) {
            return;
        }

        $global = InvalidatorRuntime::settingsRepository()->getGlobalInvalidation();
        if (!((bool) ($global['plugin_update'] ?? false))) {
            return;
        }

        $plugin = trim($plugin);
        $dedupeKey = 'plugin_deactivated:' . $plugin;
        if (self::isDuplicateEvent($dedupeKey)) {
            return;
        }

        self::executeGlobalLifecycleInvalidation('plugin_update', [
            'trigger_event' => $dedupeKey,
            'plugin' => $plugin,
            'network_wide' => $networkWide,
        ]);
    }

    /**
     * Handle manual full refresh requests routed through ECI.
     *
     * @param array<string,mixed> $context
     * @return void
     */
    public static function handleManualFullRefresh(array $context = []): void
    {
        if (self::shouldSkipForMaintenance('manual_full_refresh')) {
            return;
        }

        if (!InvalidatorRuntime::settingsRepository()->isInvalidationEnabled()) {
            return;
        }

        if (self::isDuplicateEvent('manual_full_refresh')) {
            return;
        }

        $triggerEvent = (string) ($context['trigger_event'] ?? '');
        $triggerEvent = strtolower(preg_replace('/[^a-z0-9:_-]/', '', $triggerEvent));
        if ($triggerEvent === '') {
            $triggerEvent = 'manual_full_refresh';
        }

        self::executeGlobalLifecycleInvalidation('manual_full_refresh', array_merge($context, [
            'trigger_event' => $triggerEvent,
        ]));
    }

    /**
     * Deferred cron handler for system lifecycle invalidation.
     *
     * Executes the canonical global invalidation outside of the upgrader request
     * so purge/emission never runs while WordPress is in an unstable upgrade state.
     *
     * IMPORTANT:
     * - Cron args must be deterministic and scalar-only.
     * - Full context is reconstructed inside this handler.
     *
     * @param mixed $reason
     * @param mixed $trigger_event
     * @return void
     */
    public static function runDeferredLifecycleInvalidation($reason = '', $trigger_event = ''): void
    {
        $reason = sanitize_key((string) $reason);
        if ($reason === '') {
            return;
        }

        $trigger_event = sanitize_key((string) $trigger_event);

        if (self::shouldSkipForMaintenance('deferred_lifecycle_invalidation')) {
            return;
        }

        if (!InvalidatorRuntime::settingsRepository()->isInvalidationEnabled()) {
            return;
        }

        // Reconstruct minimal deterministic context for execution
        $context = [
            'trigger_class' => 'system',
            'trigger_event' => $trigger_event !== '' ? $trigger_event : 'upgrade',
            'reason' => $reason,
            'intent' => 'GLOBAL_INVALIDATION',
            'resolution' => 'GLOBAL_INVALIDATION',
        ];

        self::executeGlobalLifecycleInvalidation($reason, $context);
    }

    /**
     * Defer a global lifecycle invalidation to a single cron event.
     *
     * Must not trigger purge, adapter lookup, or emission — upgrade-safety doctrine
     * forbids operational work during the upgrader request.
     *
     * @param string              $reason
     * @param array<string,mixed> $context
     * @return void
     */
    private static function scheduleGlobalLifecycleInvalidation(string $reason, array $context): void
    {
        $reason = sanitize_key($reason);
        if ($reason === '') {
            return;
        }

        $triggerEvent = (string) ($context['trigger_event'] ?? '');
        $triggerEvent = strtolower(preg_replace('/[^a-z0-9:_-]/', '', $triggerEvent));

        $args = [
            'reason' => $reason,
            'trigger_event' => $triggerEvent,
        ];

        $alreadyQueued = wp_next_scheduled(self::LIFECYCLE_CRON_HOOK, $args) !== false;

        if ($alreadyQueued) {
            return;
        }

        $result = wp_schedule_single_event(
            time() + 15,
            self::LIFECYCLE_CRON_HOOK,
            $args,
            true
        );

        $result = wp_schedule_single_event(
            time() + 15,
            self::LIFECYCLE_CRON_HOOK,
            $args,
            true
        );

        if ($result === false || is_wp_error($result)) {
            Debug::log('eci_system_invalidation_schedule_failed', [
                'error' => is_wp_error($result) ? $result->get_error_message() : 'false_return',
                'trigger_event' => sanitize_key((string) ($context['trigger_event'] ?? $reason)),
                'reason' => $reason,
            ]);
            return;
        }

        Debug::log('eci_system_invalidation_scheduled', [
            'trigger_event' => sanitize_key((string) ($context['trigger_event'] ?? $reason)),
            'upgrade_type' => sanitize_key((string) ($context['upgrader_type'] ?? '')),
            'correlation_id' => (string) ($context['correlation_id'] ?? ''),
            'reason' => $reason,
        ]);
    }

    /**
     * Execute one global lifecycle invalidation via the canonical executor.
     *
     * @param string              $reason
     * @param array<string,mixed> $context
     * @return void
     */
    private static function executeGlobalLifecycleInvalidation(string $reason, array $context): void
    {
        $adapter = AdapterManager::getOperationalAdapter();
        if ($adapter === null) {
            return;
        }

        $resolver = LanguageResolverFactory::create();
        $baseContext = [
            'trigger_class' => 'system',
            'trigger_kind' => 'system',
            'trigger_event' => sanitize_key((string) ($context['trigger_event'] ?? $reason)),
            'reason' => sanitize_key($reason),
            'trigger_post_id' => 0,
            'trigger_post_type' => '',
            'full_flush' => true,
            'target_mode' => 'full_flush',
        ];

        GlobalInvalidationExecutor::executeGlobalInvalidation(
            $adapter,
            $resolver,
            null,
            sanitize_key($reason),
            array_merge($baseContext, $context)
        );
    }

    /**
     * Determine whether the current event key has already been processed.
     *
     * @param string $dedupeKey
     * @return bool
     */
    private static function isDuplicateEvent(string $dedupeKey): bool
    {
        if ($dedupeKey === '') {
            return true;
        }

        if (isset(self::$eventDedupe[$dedupeKey])) {
            Debug::log('eci_system_trigger_deduped', [
                'key' => $dedupeKey,
            ]);
            return true;
        }

        self::$eventDedupe[$dedupeKey] = true;
        return false;
    }

    /**
     * Enforce maintenance guard before any adapter lookup/purge path.
     *
     * @param string $trigger
     * @return bool
     */
    private static function shouldSkipForMaintenance(string $trigger): bool
    {
        if (file_exists(ABSPATH . '.maintenance') || (function_exists('wp_is_maintenance_mode') && wp_is_maintenance_mode())) {
            Debug::log('eci_maintenance_guard_skip', [
                'trigger' => sanitize_key($trigger),
            ]);
            return true;
        }

        return false;
    }
}
