<?php

namespace XZD\AI\V5\Tools;

use XZD\AI\V5\Core\Config;
use XZD\AI\V5\Core\Logger;
use XZD\AI\V5\Core\PendingQueue;

/**
 * WordPress event trigger — listens to WP hooks and triggers pipeline / automation rules.
 *
 * All processing goes through PendingQueue for lightweight async enqueue (never blocks user requests).
 * Request-level static cache prevents duplicate enqueue from save_post firing multiple times.
 */
class EventTrigger
{
    private static bool $initialized = false;

    /** Request-level dedup cache: same post enqueued at most once per request */
    private static array $enqueued = [];

    /**
     * Register all WordPress hooks.
     */
    public static function init(): void
    {
        if (self::$initialized) {
            return;
        }
        self::$initialized = true;

        // Pipeline triggers
        add_action('publish_post', [self::class, 'handlePublish'], 20, 1);
        add_action('save_post', [self::class, 'handleSave'], 20, 1);

        // Import events
        add_action('import_post', [self::class, 'handleImportPost'], 20, 1);
        add_action('import_end', [self::class, 'handleImportEnd'], 20, 0);

        // Scheduled scan
        add_action('xzd_ai_schedule_scan', [self::class, 'scheduleScan']);

        // Automation rule triggers
        add_action('user_register', [self::class, 'handleUserRegister'], 20, 1);
        add_action('wp_login', [self::class, 'handleLogin'], 20, 2);
        add_action('comment_post', [self::class, 'handleComment'], 20, 2);
        add_action('transition_comment_status', [self::class, 'handleCommentApproved'], 20, 3);
        add_action('wp_insert_link', [self::class, 'handleLinkSubmitted'], 20, 1);

        // First post / daily event checks
        add_action('publish_post', [self::class, 'handleFirstPost'], 25, 1);
        add_action('xzd_ai_daily_event_check', [self::class, 'checkDailyEvents']);

        // Async pipeline stage
        add_action('xzd_ai_pipeline_async_stage', [PipelineEngine::class, 'executeAsyncStage'], 10, 1);

        // Async processing (cron callbacks)
        add_action(PipelineEngine::PROCESS_HOOK, [PipelineEngine::class, 'processPost'], 10, 1);
        add_action(RuleEngine::ASYNC_HOOK, [RuleEngine::class, 'evaluateFromCron'], 10, 1);
    }

    // Post events

    /**
     * Post published -> schedule automation rules + pipeline.
     * Rules and pipeline run in parallel; per-tool fill_empty markers prevent duplicate processing.
     */
    public static function handlePublish(int $postId): void
    {
        // Request-level dedup
        $dedupKey = "publish_{$postId}";
        if (isset(self::$enqueued[$dedupKey])) return;
        self::$enqueued[$dedupKey] = true;

        if (wp_is_post_autosave($postId) || wp_is_post_revision($postId)) {
            return;
        }

        $post = get_post($postId);
        if (!$post || $post->post_type !== 'post') {
            return;
        }

        $hasRules = class_exists(RuleEngine::class) && RuleEngine::instance()->hasEnabledRules('publish_post');

        // Check if pipeline is enabled
        $triggers = Config::instance()->get('pipeline.triggers', []);
        $defaultOn = empty($triggers);
        $publishOn = $defaultOn || in_array('publish', $triggers, true);

        // Rules and pipeline both enqueue (per-tool fill_empty handles dedup)
        if ($hasRules) {
            PendingQueue::addRuleEvaluation('publish_post', [
                'post_id'      => $postId,
                'post_title'   => $post->post_title,
                'post_type'    => $post->post_type,
                'post_author'  => $post->post_author,
                'post_status'  => $post->post_status,
                'category_ids' => wp_get_post_categories($postId),
            ]);
        }

        if ($publishOn) {
            // New posts fast-path: directly into PendingQueue, skipping WorkerQueue
            PipelineEngine::recoverStaleRunning($postId);
            if (!PipelineEngine::isFullyProcessed($postId, 'publish_post')) {
                PendingQueue::addPipeline($postId, 'publish_post');
            }
        }
    }

    /**
     * Post saved/updated -> schedule pipeline only when update trigger is enabled.
     *
     * If publish trigger is enabled but update is not explicitly off, update auto-activates.
     * Rules and pipeline run in parallel.
     */
    public static function handleSave(int $postId): void
    {
        // Request-level dedup
        $dedupKey = "save_{$postId}";
        if (isset(self::$enqueued[$dedupKey])) return;
        self::$enqueued[$dedupKey] = true;

        if (wp_is_post_autosave($postId) || wp_is_post_revision($postId)) {
            return;
        }

        $post = get_post($postId);
        if (!$post || $post->post_type !== 'post') {
            return;
        }

        // Skip new posts (handled by publish_post); only process actual updates
        if ($post->post_status === 'publish' && $post->post_date === $post->post_modified) {
            return;
        }

        if ($post->post_status !== 'publish') {
            return;
        }

        // Check update trigger — implicit linkage with publish trigger
        $triggers = Config::instance()->get('pipeline.triggers', []);
        $defaultOn = empty($triggers);
        $updateOn = $defaultOn || in_array('update', $triggers, true)
            || in_array('publish', $triggers, true);

        // Rules and pipeline run in parallel (consistent with handlePublish)
        $hasRules = class_exists(RuleEngine::class)
            && RuleEngine::instance()->hasEnabledRules('save_post');

        if ($hasRules) {
            PendingQueue::addRuleEvaluation('save_post', [
                'post_id'     => $postId,
                'post_title'  => $post->post_title,
                'post_type'   => $post->post_type,
                'post_author' => $post->post_author,
            ]);
        }

        if ($updateOn) {
            QueueManager::enqueue($postId, [], 'update');
        }
    }

    // Import events

    /**
     * Single post imported -> trigger pipeline.
     */
    public static function handleImportPost(int $postId): void
    {
        if (wp_is_post_autosave($postId) || wp_is_post_revision($postId)) {
            return;
        }
        $post = get_post($postId);
        if (!$post || $post->post_type !== 'post' || $post->post_status !== 'publish') {
            return;
        }
        Logger::info('event_trigger', "import_post triggered -> post {$postId}");
        self::handlePublish($postId);
    }

    /**
     * Import completed -> trigger full scan as safety net.
     */
    public static function handleImportEnd(): void
    {
        Logger::info('event_trigger', 'Import completed, triggering full scan');
        self::scheduleScan();
    }

    // Scheduled scan

    /**
     * Scheduled scan — batches of batchLimit posts, newest first, advancing by date cursor.
     *
     * Cursor = date of last post in previous batch. date_query handles pagination.
     * post__not_in excludes current batch IDs to avoid re-fetching same-date posts.
     */
    public static function scheduleScan(): void
    {
        $triggers = Config::instance()->get('pipeline.triggers', []);
        if (!in_array('schedule', $triggers, true)) {
            return;
        }

        // Watermark: pause historical scan when backlog exceeds limit
        if (!QueueManager::shouldResumeScan()) {
            $backlog = QueueManager::getTotalBacklog();
            // Log once when hitting limit (30min throttle)
            $logKey = 'xzd_scan_paused_log';
            if (!get_transient($logKey)) {
                set_transient($logKey, time(), 1800);
                Logger::info('event_trigger', "Backlog too high ({$backlog}/" . QueueManager::MAX_BACKLOG . "), pausing historical scan, waiting for " . QueueManager::RESUME_AT);
            }
            // Clear skip_ids during pause to prevent cursor deadlock on resume
            $state = Config::getArrayOption('xzd_scan_state', []);
            if (!empty($state['skip_ids'])) {
                $state['skip_ids'] = [];
                $state['updated_at'] = current_time('mysql');
                update_option('xzd_scan_state', $state);
            }
            return;
        }

        $batchLimit = Config::instance()->int('pipeline.scan_batch_limit', 10);
        $state = Config::getArrayOption('xzd_scan_state', []);
        $round = (int)($state['round'] ?? 1);
        $cursorDate = $state['cursor_date'] ?? '';
        $skipIds = $state['skip_ids'] ?? [];

        // Scan range: all (default, 3-round progressive) | 24h | 7d | 30d | since_install
        $scanRange = Config::instance()->get('pipeline.scan_range', 'all');

        // Scan range change detection: reset state when config changes
        $lastScanRange = $state['scan_range'] ?? '';
        if ($lastScanRange && $lastScanRange !== $scanRange) {
            Logger::info('event_trigger', "Scan range changed: {$lastScanRange} -> {$scanRange}, resetting scan state");
            $state = [];
            $round = 1;
            $cursorDate = '';
            $skipIds = [];
            delete_option('xzd_scan_state');
        }

        // Original 3-round scan windows (full mode)
        $scanWindowsAll = [
            1 => date('Y-m-d H:i:s', time() - 7 * DAY_IN_SECONDS),
            2 => date('Y-m-d H:i:s', time() - 30 * DAY_IN_SECONDS),
            3 => '',
        ];

        // Build windows by scan range
        $rangeDays = ['24h' => 1, '7d' => 7, '30d' => 30];
        if ($scanRange === 'since_install') {
            $installDate = get_option('xzd_plugin_install_date', '');
            if (empty($installDate)) {
                $installDate = current_time('mysql');
                update_option('xzd_plugin_install_date', $installDate);
            }
            $scanWindows = [1 => $installDate, 2 => $installDate, 3 => $installDate];
            $maxRound = 1;
        } elseif ($scanRange !== 'all' && isset($rangeDays[$scanRange])) {
            $days = $rangeDays[$scanRange];
            $hours = ($scanRange === '24h') ? 24 : $days * 24;
            $windowStart = date('Y-m-d H:i:s', time() - $hours * HOUR_IN_SECONDS);
            $scanWindows = [1 => $windowStart, 2 => $windowStart, 3 => $windowStart];
            $maxRound = 1;
        } else {
            $scanWindows = $scanWindowsAll;
            $maxRound = 3;
        }

        // First round: start from window start
        if (!$state) {
            $cursorDate = $scanWindows[1];
        }

        $queryArgs = [
            'post_type'      => 'post',
            'post_status'    => 'publish',
            'posts_per_page' => $batchLimit,
            'orderby'        => 'date',
            'order'          => 'DESC',
            'fields'         => 'ids',
            'no_found_rows'  => true,
        ];
        if ($cursorDate) {
            // Limited range modes use 'after'; full mode uses 'before' to go into older history
            if ($scanRange !== 'all') {
                $queryArgs['date_query'] = [['after' => $scanWindows[1], 'inclusive' => true]];
            } else {
                $queryArgs['date_query'] = [['before' => $cursorDate, 'inclusive' => true]];
            }
        }
        if (!empty($skipIds)) {
            $queryArgs['post__not_in'] = array_map('intval', $skipIds);
        }
        $postIds = get_posts($queryArgs);

        // Current round done, advance to next round (wider window)
        if (empty($postIds)) {
            if ($round < $maxRound) {
                update_option('xzd_scan_state', [
                    'round'       => $round + 1,
                    'cursor_date' => $scanWindows[$round + 1],
                    'skip_ids'    => [],
                    'scan_range'  => $scanRange,
                    'updated_at'  => current_time('mysql'),
                ]);
                Logger::info('event_trigger', "Round {$round} scan complete, entering round " . ($round + 1) . " (wider window)");
            } elseif ($scanRange !== 'all') {
                // Limited range: reset cursor to window start for continuous scanning
                update_option('xzd_scan_state', [
                    'round'       => 1,
                    'cursor_date' => $scanWindows[1],
                    'skip_ids'    => [],
                    'scan_range'  => $scanRange,
                    'updated_at'  => current_time('mysql'),
                ]);
                Logger::info('event_trigger', "Scan range [{$scanRange}] round complete, resetting cursor for continuous scan");
            } else {
                delete_option('xzd_scan_state');
                Logger::info('event_trigger', "All 3 rounds of historical scan complete");
            }
            return;
        }

        // Filter by isFullyProcessed
        $toEnqueue = [];
        foreach ($postIds as $pid) {
            if (!PipelineEngine::isFullyProcessed((int)$pid, 'publish_post')) {
                $toEnqueue[] = (int)$pid;
            }
        }

        $enqueued = 0;
        if (!empty($toEnqueue)) {
            $enqueued = QueueManager::enqueueBatch($toEnqueue, [], 'schedule_scan');
        }

        $lastPost = get_post(end($postIds));
        $newCursor = ($scanRange !== 'all')
            ? $scanWindows[1]
            : ($lastPost ? $lastPost->post_date : $cursorDate);

        $stateData = [
            'round'       => $round,
            'cursor_date' => $newCursor,
            'skip_ids'    => $postIds,
            'scan_range'  => $scanRange,
            'updated_at'  => current_time('mysql'),
        ];
        // update_option auto-creates if missing, avoids add_option/update_option race
        wp_cache_delete('xzd_scan_state', 'options');
        update_option('xzd_scan_state', $stateData, false);

        Logger::info('event_trigger', "Round {$round} scan: checked " . count($postIds) . " posts, enqueued {$enqueued}, cursor: {$newCursor}");
    }

    // User events -> automation rules

    /**
     * User registered.
     */
    public static function handleUserRegister(int $userId): void
    {
        if (!class_exists(RuleEngine::class)) {
            return;
        }

        if (!RuleEngine::instance()->hasEnabledRules('user_register')) {
            return;
        }

        $user = get_userdata($userId);

        PendingQueue::addRuleEvaluation('user_register', [
            'user_id'       => $userId,
            'user_login'    => $user ? $user->user_login : '',
            'user_email'    => $user ? $user->user_email : '',
            'user_roles'    => $user ? $user->roles : [],
            'registered_at' => $user ? $user->user_registered : '',
        ]);
    }

    /**
     * User logged in.
     */
    public static function handleLogin(string $login, \WP_User $user): void
    {
        if (!class_exists(RuleEngine::class)) {
            return;
        }

        if (!RuleEngine::instance()->hasEnabledRules('wp_login')) {
            return;
        }

        PendingQueue::addRuleEvaluation('wp_login', [
            'user_id'    => $user->ID,
            'user_login' => $login,
            'user_roles' => $user->roles,
        ]);
    }

    /**
     * New comment -> detect WP moderation status -> AI moderation (if needed) -> auto-reply.
     */
    public static function handleComment(int $commentId, int|string $approved): void
    {
        if (!class_exists(RuleEngine::class)) {
            return;
        }

        $comment = get_comment($commentId);
        if (!$comment) {
            return;
        }

        // Skip AI self-replies to prevent recursive loops
        if (!empty($comment->comment_parent)) {
            return;
        }

        // Phase 1: AI comment moderation — only when WP requires review
        $needsModeration = ($approved !== 1 && $approved !== '1');

        if ($needsModeration) {
            try {
                CommentModerator::moderate([
                    'comment_id'    => $commentId,
                    'auto_moderate' => true,
                    'strictness'    => \XZD\AI\V5\Core\Config::instance()->get('tool_defaults.wp_moderate_comment.strictness', 'medium'),
                ], 0);
            } catch (\Throwable $e) {
                // Moderation error must not block comment flow
                \XZD\AI\V5\Core\Logger::warning('event_trigger', "Comment moderation error: " . $e->getMessage(), ['comment_id' => $commentId]);
            }

            // Re-fetch comment (AI moderation may have changed approval status)
            $comment = get_comment($commentId);
        }

        // Phase 2: Only approved comments get auto-reply
        if (!$comment || $comment->comment_approved !== '1') {
            return;
        }

        self::scheduleCommentReply($commentId, $comment);
    }

    /**
     * Comment approved via transition_comment_status hook.
     * Handles the "manual approval -> auto-reply" scenario (no re-moderation).
     */
    public static function handleCommentApproved(string $newStatus, string $oldStatus, $comment): void
    {
        if ($newStatus !== 'approved' || $oldStatus === 'approved') {
            return;
        }

        $commentId = $comment->comment_ID;
        $commentObj = get_comment($commentId);

        // Skip AI self-replies
        if (!$commentObj || !empty($commentObj->comment_parent)) {
            return;
        }

        // Skip if already handled by handleComment (AI moderation meta present)
        $meta = get_comment_meta($commentId, '_xzd_ai_moderation', true);
        if (!empty($meta) && ($meta['moderated_by'] ?? '') === 'ai') {
            return;
        }

        self::scheduleCommentReply($commentId, $commentObj);
    }

    /**
     * Schedule comment auto-reply (into PendingQueue).
     *
     * Triggered when either global config or an enabled automation rule is active.
     */
    private static function scheduleCommentReply(int $commentId, \WP_Comment $comment): void
    {
        if (!class_exists(RuleEngine::class)) {
            return;
        }

        // Dual gate: global switch OR automation rules (either triggers auto-reply)
        $autoReplyEnabled = \XZD\AI\V5\Core\Config::instance()->bool('comment.auto_reply', false);
        $hasEnabledRules   = RuleEngine::instance()->hasEnabledRules('comment_post');

        if (!$autoReplyEnabled && !$hasEnabledRules) {
            return;
        }

        // Global switch on but no enabled rule -> auto-create/enable preset
        if ($autoReplyEnabled && !$hasEnabledRules) {
            RuleEngine::instance()->ensureAutoReplyRule();
        }

        // Lightweight enqueue, never blocks user comment request
        PendingQueue::addRuleEvaluation('comment_post', [
            'comment_id'       => $commentId,
            'comment_post_id'  => (int)$comment->comment_post_ID,
            'comment_author'   => $comment->comment_author,
            'comment_content'  => $comment->comment_content,
            'comment_approved' => $comment->comment_approved,
            'user_id'          => (int)$comment->user_id,
        ]);
    }

    /**
     * Link submitted -> schedule async rule evaluation.
     */
    public static function handleLinkSubmitted(int $linkId): void
    {
        if (!class_exists(RuleEngine::class)) {
            return;
        }

        if (!RuleEngine::instance()->hasEnabledRules('link_submitted')) {
            return;
        }

        $link = get_bookmark($linkId);
        if (!$link) {
            return;
        }

        PendingQueue::addRuleEvaluation('link_submitted', [
            'link_id'    => $linkId,
            'link_url'   => $link->link_url,
            'link_name'  => $link->link_name,
            'link_target'=> $link->link_target,
            'link_owner' => $link->link_owner,
        ]);
    }

    // First post / daily event checks

    /**
     * First post detection — publish_post hook (priority 25, after handlePublish).
     */
    public static function handleFirstPost(int $postId): void
    {
        if (!class_exists(RuleEngine::class)) {
            return;
        }
        if (!RuleEngine::instance()->hasEnabledRules('xzd_first_post')) {
            return;
        }

        $post = get_post($postId);
        if (!$post || $post->post_type !== 'post' || $post->post_status !== 'publish') {
            return;
        }
        if (wp_is_post_autosave($postId) || wp_is_post_revision($postId)) {
            return;
        }

        // Check if this is the author's first published post
        $count = (int) get_posts([
            'post_type'   => 'post',
            'post_status' => 'publish',
            'author'      => $post->post_author,
            'numberposts' => 1,
            'fields'      => 'ids',
            'post__not_in'=> [$postId],
        ]);

        if ($count > 0) {
            return;
        }

        $user = get_userdata($post->post_author);
        PendingQueue::addRuleEvaluation('xzd_first_post', [
            'user_id'       => $post->post_author,
            'user_login'    => $user ? $user->user_login : '',
            'user_email'    => $user ? $user->user_email : '',
            'post_id'       => $postId,
            'post_title'    => $post->post_title,
        ]);

        Logger::info('event_trigger', "First post event: user {$post->post_author}, post {$postId}");
    }

    /**
     * Daily event check — cron callback for VIP expiry / points threshold / views milestone.
     */
    public static function checkDailyEvents(): void
    {
        if (!class_exists(RuleEngine::class)) {
            return;
        }

        $engine = RuleEngine::instance();

        if ($engine->hasEnabledRules('vip_expiring')) {
            self::checkVipExpiring();
        }

        if ($engine->hasEnabledRules('points_threshold')) {
            self::checkPointsThreshold();
        }

        if ($engine->hasEnabledRules('views_threshold_1000')) {
            self::checkViewsThreshold();
        }
    }

    /**
     * VIP expiry check — queries users with VIP expiring within 3 days.
     */
    private static function checkVipExpiring(): void
    {
        global $wpdb;
        $threshold = time() + 3 * DAY_IN_SECONDS;

        $users = $wpdb->get_results($wpdb->prepare(
            "SELECT user_id, meta_value FROM {$wpdb->usermeta}
             WHERE meta_key = 'vip_end_time'
             AND meta_value > %d
             AND meta_value <= %d",
            time(), $threshold
        ));

        foreach ($users as $row) {
            $daysLeft = ceil(((int)$row->meta_value - time()) / DAY_IN_SECONDS);
            PendingQueue::addRuleEvaluation('vip_expiring', [
                'user_id'    => (int)$row->user_id,
                'vip_end'    => (int)$row->meta_value,
                'days_left'  => $daysLeft,
            ]);
        }
    }

    /**
     * Points threshold check — triggers when user points cross a milestone.
     */
    private static function checkPointsThreshold(): void
    {
        $thresholds = [100, 500, 1000, 5000, 10000];
        global $wpdb;

        foreach ($thresholds as $threshold) {
            $users = $wpdb->get_results($wpdb->prepare(
                "SELECT user_id, meta_value FROM {$wpdb->usermeta}
                 WHERE meta_key = 'points'
                 AND CAST(meta_value AS UNSIGNED) >= %d",
                $threshold
            ));

            foreach ($users as $row) {
                $points = (int)$row->meta_value;
                // Only trigger when near the threshold (within 5 points above)
                if ($points >= $threshold && $points < $threshold + 5) {
                    $alreadyTriggered = get_transient("xzd_points_t_{$row->user_id}_{$threshold}");
                    if ($alreadyTriggered) continue;

                    set_transient("xzd_points_t_{$row->user_id}_{$threshold}", 1, DAY_IN_SECONDS * 30);

                    PendingQueue::addRuleEvaluation('points_threshold', [
                        'user_id'   => (int)$row->user_id,
                        'points'    => $points,
                        'threshold' => $threshold,
                    ]);
                }
            }
        }
    }

    /**
     * Views threshold check — triggers for posts exceeding 1000 views.
     */
    private static function checkViewsThreshold(): void
    {
        global $wpdb;
        $posts = $wpdb->get_results(
            "SELECT post_id, meta_value FROM {$wpdb->postmeta}
             WHERE meta_key = 'views'
             AND CAST(meta_value AS UNSIGNED) >= 1000
             ORDER BY CAST(meta_value AS UNSIGNED) DESC
             LIMIT 50"
        );

        foreach ($posts as $row) {
            $postId = (int)$row->post_id;
            $views  = (int)$row->meta_value;

            // Check if already triggered for this threshold
            $triggered = get_post_meta($postId, '_xzd_views_triggered', true) ?: [];
            if (in_array(1000, (array)$triggered, true)) continue;

            $triggered[] = 1000;
            update_post_meta($postId, '_xzd_views_triggered', $triggered);

            $post = get_post($postId);
            if (!$post || $post->post_status !== 'publish') continue;

            PendingQueue::addRuleEvaluation('views_threshold_1000', [
                'post_id'    => $postId,
                'post_title' => $post->post_title,
                'views'      => $views,
                'threshold'  => 1000,
                'post_author'=> $post->post_author,
            ]);
        }
    }

    /**
     * Points changed hook entry — called by ThemeManager after points change.
     *
     * @internal Called by ThemeManager::addPoints / deductPoints
     */
    public static function onPointsChanged(int $userId, int $newPoints, string $changeType): void
    {
        if (!class_exists(RuleEngine::class)) return;
        if (!RuleEngine::instance()->hasEnabledRules('points_threshold')) return;

        $thresholds = [100, 500, 1000, 5000, 10000];
        foreach ($thresholds as $t) {
            if ($newPoints >= $t && ($newPoints - $t) <= 5) {
                $key = "xzd_points_t_{$userId}_{$t}";
                if (get_transient($key)) continue;
                set_transient($key, 1, DAY_IN_SECONDS * 30);

                PendingQueue::addRuleEvaluation('points_threshold', [
                    'user_id'   => $userId,
                    'points'    => $newPoints,
                    'threshold' => $t,
                    'change'    => $changeType,
                ]);
            }
        }
    }

    /**
     * Get hook registration status.
     */
    public static function getStatus(): array
    {
        return [
            'initialized' => self::$initialized,
            'hooks'       => [
                'publish_post'         => has_action('publish_post', [self::class, 'handlePublish']),
                'save_post'            => has_action('save_post', [self::class, 'handleSave']),
                'xzd_ai_schedule_scan' => has_action('xzd_ai_schedule_scan', [self::class, 'scheduleScan']),
                'user_register'        => has_action('user_register', [self::class, 'handleUserRegister']),
                'wp_login'             => has_action('wp_login', [self::class, 'handleLogin']),
                'comment_post'         => has_action('comment_post', [self::class, 'handleComment']),
                'first_post'           => has_action('publish_post', [self::class, 'handleFirstPost']),
                'daily_events'         => has_action('xzd_ai_daily_event_check', [self::class, 'checkDailyEvents']),
                'async_stage'          => has_action('xzd_ai_pipeline_async_stage', [PipelineEngine::class, 'executeAsyncStage']),
            ],
        ];
    }
}
