<?php
/**
 * Cron Manager - Chainable Cron Scheduler for Ultraguard Pro
 *
 * A fluent, chainable interface for registering, scheduling, and managing WordPress cron jobs.
 *
 * @package    UltraguardPro\core
 * @author     Vikas Bhardwaj
 * @version    2.0.0
 * @since      6.3.0
 */

namespace UltraguardPro\support;

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * =============================================================================
 * QUICK START EXAMPLES
 * =============================================================================
 *
 * // 1. BASIC USAGE - Simple daily job
 * CronManager::make()
 *     ->hook('my_plugin_daily_cleanup')
 *     ->label('Daily Cleanup')
 *     ->recurrence(CronManager::DAILY)
 *     ->callback([$cleaner, 'cleanup'])
 *     ->register();
 *
 * // 2. WITH ARGUMENTS AND DELAY
 * CronManager::make()
 *     ->hook('my_plugin_backup')
 *     ->label('Database Backup')
 *     ->recurrence(CronManager::WEEKLY)
 *     ->callback([$backup, 'create'])
 *     ->args(['type' => 'full', 'compress' => true])
 *     ->delay(3600) // Start in 1 hour
 *     ->register();
 *
 * // 3. ERROR HANDLING AND TIMEOUT
 * CronManager::make()
 *     ->hook('my_plugin_scan')
 *     ->label('Security Scan')
 *     ->recurrence(CronManager::TWICEDAILY)
 *     ->callback([$scanner, 'full_scan'])
 *     ->timeout(300) // 5 minutes max
 *     ->onError(function($e, $meta) {
 *         error_log('Scan failed: ' . $e->getMessage());
 *         wp_mail('admin@site.com', 'Scan Failed', $e->getMessage());
 *     })
 *     ->register();
 *
 * // 4. PREVENT CONCURRENT EXECUTION
 * CronManager::make()
 *     ->hook('my_plugin_long_task')
 *     ->label('Long Running Task')
 *     ->recurrence(CronManager::HOURLY)
 *     ->callback([$processor, 'process'])
 *     ->withLock('task_lock', 1800) // 30 min lock
 *     ->logExecution(true)
 *     ->register();
 *
 * // 5. CHAIN MULTIPLE JOBS
 * CronManager::make()
 *     ->hook('job_one')->recurrence(CronManager::HOURLY)->callback([$obj, 'one'])->register()
 *     ->reset()
 *     ->hook('job_two')->recurrence(CronManager::DAILY)->callback([$obj, 'two'])->register()
 *     ->reset()
 *     ->hook('job_three')->recurrence(CronManager::WEEKLY)->callback([$obj, 'three'])->register();
 *
 * // 6. CONDITIONAL REGISTRATION
 * if (get_option('enable_feature')) {
 *     CronManager::make()
 *         ->hook('feature_job')
 *         ->label('Feature Processor')
 *         ->recurrence(CronManager::EVERY_5_MINUTES)
 *         ->callback([$feature, 'process'])
 *         ->register();
 * }
 *
 * // 7. CHECK JOB STATUS
 * $status = CronManager::make()
 *     ->hook('my_plugin_job')
 *     ->nextRunFormatted(); // "In 2 hours" or "Not scheduled"
 *
 * // 8. UNREGISTER A JOB
 * CronManager::make()->hook('old_job')->unregister();
 *
 * // 9. CLEAR ALL PLUGIN JOBS (on deactivation)
 * register_deactivation_hook(__FILE__, ['CronManager', 'clear_all']);
 *
 * // 10. GET STATUS REPORT
 * $report = CronManager::status_report();
 * foreach ($report as $hook => $data) {
 *     echo "{$hook}: {$data['next_run_formatted']}\n";
 * }
 *
 * // 11. ADD CUSTOM SCHEDULE
 * CronManager::add_custom_schedule('every_2_hours', 7200, 'Every 2 Hours');
 *
 * // 12. WITH METADATA
 * CronManager::make()
 *     ->hook('analytics_job')
 *     ->label('Analytics Processing')
 *     ->recurrence(CronManager::DAILY)
 *     ->callback([$analytics, 'process'])
 *     ->withMeta('priority', 'high')
 *     ->withMeta('version', '2.0')
 *     ->logExecution(true)
 *     ->register();
 */

class CronManager {

    // =========================================================================
    // CONSTANTS - Built-in recurrence intervals
    // =========================================================================

    /** @var string Run every minute */
    public const EVERY_MINUTE    = 'every_minute';

    /** @var string Run every 5 minutes */
    public const EVERY_5_MINUTES = 'every_5_minutes';

    /** @var string Run every 15 minutes */
    public const EVERY_15_MINUTES = 'every_15_minutes';

    /** @var string Run every 30 minutes */
    public const EVERY_30_MINUTES = 'every_30_minutes';

    /** @var string Run hourly */
    public const HOURLY          = 'hourly';

    /** @var string Run every 2 hours */
    public const EVERY_2_HOURS   = 'every_2_hours';

    /** @var string Run every 3 hours */
    public const EVERY_3_HOURS   = 'every_3_hours';

    /** @var string Run every 6 hours */
    public const EVERY_6_HOURS   = 'every_6_hours';

    /** @var string Run twice daily */
    public const TWICEDAILY      = 'twicedaily';

    /** @var string Run daily */
    public const DAILY           = 'daily';

    /** @var string Run weekly */
    public const WEEKLY          = 'weekly';

    /** @var string Run monthly */
    public const MONTHLY         = 'monthly';

    /** @var int Maximum execution time in seconds */
    private const MAX_TIMEOUT = 300;

    /** @var string Option name for storing registered hooks */
    private const REGISTERED_HOOKS_OPTION = 'ultraguard_pro_cron_hooks';

    /** @var string Option name for last run times */
    private const LAST_RUN_OPTION_PREFIX = 'cron_last_run_';

    // =========================================================================
    // PROPERTIES - Class-level registry
    // =========================================================================

    /**
     * Master list of all hook names registered through this class.
     *
     * @var string[]
     */
    private static array $registered_hooks = [];

    /**
     * Custom schedules added by this manager.
     *
     * @var array<string, array{interval: int, display: string}>
     */
    private static array $custom_schedules = [];

    /**
     * Logger instance
     *
     * @var callable|null
     */
    private static $logger = null;

    /**
     * Whether the class has been initialized
     *
     * @var bool
     */
    private static bool $initialized = false;

    // =========================================================================
    // PROPERTIES - Instance state
    // =========================================================================

    /** @var string|null WordPress action hook name */
    private ?string $hook = null;

    /** @var string Human-readable label */
    private string $label = '';

    /** @var string Recurrence interval key */
    private string $recurrence = self::DAILY;

    /** @var callable|null The callable to run */
    private $callback = null;

    /** @var array Optional arguments */
    private array $args = [];

    /** @var int Maximum execution time in seconds */
    private int $timeout = 0;

    /** @var callable|null Error handler */
    private $error_handler = null;

    /** @var bool Whether job is enabled */
    private bool $enabled = true;

    /** @var int|null First run timestamp */
    private ?int $start_at = null;

    /** @var bool Whether to log execution */
    private bool $log_execution = false;

    /** @var string|null Lock key for preventing concurrent execution */
    private ?string $lock_key = null;

    /** @var int Lock expiration in seconds */
    private int $lock_expiration = 300;

    /** @var array<string, mixed> Additional metadata */
    private array $metadata = [];

    // =========================================================================
    // INITIALIZATION
    // =========================================================================

    /**
     * Initialize the CronManager.
     *
     * @param callable|null $logger Optional logging callback
     * @return void
     */
    public static function init( $logger = null ): void {
        if ( self::$initialized ) {
            return;
        }

        self::$logger = $logger;
        self::load_registered_hooks();

        // Register shutdown handler
        register_shutdown_function( [ self::class, 'handle_shutdown' ] );

        self::$initialized = true;
    }

    // =========================================================================
    // FACTORY & CONSTRUCTOR
    // =========================================================================

    /**
     * Named constructor – preferred entry point.
     *
     * @return static
     */
    public static function make() {
        if ( ! self::$initialized ) {
            self::init();
        }

        return new static();
    }

    /**
     * Private constructor.
     */
    private function __construct() {
        $this->register_schedules_filter();
    }

    /**
     * Register the cron schedules filter.
     *
     * @return void
     */
    private function register_schedules_filter(): void {
        if ( ! has_filter( 'cron_schedules', [ static::class, 'register_custom_schedules' ] ) ) {
            add_filter( 'cron_schedules', [ static::class, 'register_custom_schedules' ] );
        }
    }

    // =========================================================================
    // FLUENT SETTERS
    // =========================================================================

    /**
     * Set the WordPress action hook name.
     *
     * @param string $hook
     * @return static
     * @throws \InvalidArgumentException
     */
    public function hook( string $hook ) {
        $hook = sanitize_key( $hook );

        if ( empty( $hook ) ) {
            throw new \InvalidArgumentException( esc_html( 'Hook name cannot be empty after sanitization.' ) );
        }

        $this->hook = $hook;
        return $this;
    }

    /**
     * Set a human-readable label.
     *
     * @param string $label
     * @return static
     */
    public function label( string $label ) {
        $this->label = sanitize_text_field( $label );
        return $this;
    }

    /**
     * Set the recurrence interval.
     *
     * Validation against wp_get_schedules() is intentionally deferred to
     * assert_ready() / register() time. Calling wp_get_schedules() here would
     * fire the 'cron_schedules' filter as a side-effect of a simple setter,
     * which happens too early in the boot cycle (plugins_loaded) and triggers
     * the WP 6.7 "translation loaded too early" notice.
     *
     * @param string $recurrence
     * @return static
     */
    public function recurrence( string $recurrence ) {
        $this->recurrence = $recurrence;
        return $this;
    }

    /**
     * Set the callable that fires when the cron event runs.
     *
     * @param callable $callback
     * @return static
     * @throws \InvalidArgumentException
     */
    public function callback( callable $callback ) {
        if ( ! is_callable( $callback ) ) {
            throw new \InvalidArgumentException( esc_html( 'Invalid callback provided.' ) );
        }

        $this->callback = $callback;
        return $this;
    }

    /**
     * Set optional arguments that will be passed to the callback.
     *
     * @param array $args
     * @return static
     */
    public function args( array $args ) {
        $this->args = $args;
        return $this;
    }

    /**
     * Set a per-job PHP execution timeout override (in seconds).
     *
     * @param int $seconds 0 = no override, max 300 seconds
     * @return static
     * @throws \InvalidArgumentException
     */
    public function timeout( int $seconds ) {
        if ( $seconds < 0 ) {
            throw new \InvalidArgumentException( esc_html( 'Timeout must be non-negative.' ) );
        }

        if ( $seconds > self::MAX_TIMEOUT ) {
            $seconds = self::MAX_TIMEOUT;
        }

        $this->timeout = $seconds;
        return $this;
    }

    /**
     * Register an error handler invoked when the callback throws.
     *
     * @param callable $handler function(\Throwable $e, array $metadata): void
     * @return static
     */
    public function onError( callable $handler ) {
        $this->error_handler = $handler;
        return $this;
    }

    /**
     * Enable or disable this cron job.
     *
     * @param bool $enabled
     * @return static
     */
    public function enabled( bool $enabled ) {
        $this->enabled = $enabled;
        return $this;
    }

    /**
     * Override the timestamp of the first run.
     *
     * @param int $timestamp Unix timestamp
     * @return static
     * @throws \InvalidArgumentException
     */
    public function startAt( int $timestamp ) {
        if ( $timestamp < 0 ) {
            throw new \InvalidArgumentException( esc_html( 'Start timestamp must be positive.' ) );
        }

        $this->start_at = $timestamp;
        return $this;
    }

    /**
     * Schedule the job to run once in the future.
     *
     * @param int $seconds_from_now
     * @return static
     * @throws \InvalidArgumentException
     */
    public function delay( int $seconds_from_now ) {
        if ( $seconds_from_now < 0 ) {
            throw new \InvalidArgumentException( esc_html( 'Delay must be non-negative.' ) );
        }

        $this->start_at = time() + $seconds_from_now;
        return $this;
    }

    /**
     * Enable logging for this job's execution.
     *
     * @param bool $log
     * @return static
     */
    public function logExecution( bool $log = true ) {
        $this->log_execution = $log;
        return $this;
    }

    /**
     * Prevent concurrent execution of this job.
     *
     * @param string|null $lock_key Unique key for this lock (defaults to hook name)
     * @param int $expiration Lock expiration in seconds
     * @return static
     */
    public function withLock( ?string $lock_key = null, int $expiration = 300 ) {
        $this->lock_key = $lock_key ?? $this->hook;
        $this->lock_expiration = max( 60, $expiration );
        return $this;
    }

    /**
     * Add metadata to the job.
     *
     * @param string $key
     * @param mixed $value
     * @return static
     */
    public function withMeta( string $key, $value ) {
        $this->metadata[ sanitize_key( $key ) ] = $value;
        return $this;
    }

    // =========================================================================
    // REGISTRATION & UNREGISTRATION
    // =========================================================================

    /**
     * Register (schedule) the cron job and bind its callback.
     *
     * @return static
     * @throws \LogicException|\RuntimeException
     */
    public function register() {
        $this->assert_ready();

        if ( ! $this->enabled ) {
            $this->unregister();
            $this->log( sprintf( 'Job "%s" is disabled, skipping registration.', $this->hook ), 'info' );
            return $this;
        }

        try {
            $newly_scheduled = $this->schedule_event();
            $this->bind_callback();
            $this->track_hook();

            if ( $newly_scheduled ) {
                // Log only on first-time creation, not on every subsequent page load.
                $this->log( sprintf( 'Successfully registered cron job "%s"', $this->hook ), 'info' );
            }
        } catch ( \Exception $e ) {
            $this->log( sprintf( 'Failed to register cron job "%s": %s', $this->hook, $e->getMessage() ), 'error' );
            // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception is chained for debugging, not rendered as output.
            throw new \RuntimeException( esc_html__( 'Failed to register cron job.', 'ultraguard-security-suite' ), 0, $e );
        }

        return $this;
    }

    /**
     * Schedule the cron event.
     *
     * @return bool True if the event was newly scheduled, false if it was already scheduled.
     * @throws \RuntimeException
     */
    private function schedule_event(): bool {
        if ( wp_next_scheduled( $this->hook, $this->args ) ) {
            // Event already exists in the WP cron queue — nothing to do.
            return false;
        }

        $start = $this->start_at ?? time();

        // Ensure start time is not in the past
        if ( $start < time() - 60 ) {
            $start = time();
        }

        $result = wp_schedule_event( $start, $this->recurrence, $this->hook, $this->args );

        if ( $result === false ) {
            throw new \RuntimeException( esc_html( 'Failed to schedule cron event.' ) );
        }

        if ( $this->log_execution ) {
            $this->log( sprintf( 'Scheduled job "%s" for %s',
                $this->hook,
                wp_date( 'Y-m-d H:i:s', $start )
            ), 'debug' );
        }

        return true;
    }

    /**
     * Bind the callback to the WordPress action hook.
     *
     * @return void
     */
    private function bind_callback(): void {
        // BUG-FIX: If no explicit callback was provided, skip binding.
        // WordPress cron fires do_action($hook); modules register their own
        // add_action() handlers and will be called automatically.
        if ( $this->callback === null ) {
            return;
        }

        $wrapped = $this->create_wrapped_callback();

        // Only add the action once per request
        if ( ! has_action( $this->hook ) ) {
            add_action( $this->hook, $wrapped, 10, count( $this->args ) );
        }
    }

    /**
     * Create the wrapped callback with error handling and features.
     *
     * @return callable
     */
    private function create_wrapped_callback(): callable {
        $callback   = $this->callback;
        $args       = $this->args;
        $timeout    = $this->timeout;
        $handler    = $this->error_handler;
        $label      = $this->label ?: $this->hook;
        $log_exec   = $this->log_execution;
        $lock_key   = $this->lock_key;
        $lock_exp   = $this->lock_expiration;
        $metadata   = $this->metadata;
        $hook       = $this->hook;

        return function() use ( $callback, $args, $timeout, $handler, $label, $log_exec, $lock_key, $lock_exp, $metadata, $hook ) {
            $this->execute_job( $callback, $args, $timeout, $handler, $label, $log_exec, $lock_key, $lock_exp, $metadata, $hook );
        };
    }

    /**
     * Execute the cron job with all safety measures.
     *
     * @param callable $callback
     * @param array $args
     * @param int $timeout
     * @param callable|null $handler
     * @param string $label
     * @param bool $log_exec
     * @param string|null $lock_key
     * @param int $lock_exp
     * @param array $metadata
     * @param string $hook
     * @return void
     */
    private function execute_job(
        callable $callback,
        array $args,
        int $timeout,
        $handler,
        string $label,
        bool $log_exec,
        ?string $lock_key,
        int $lock_exp,
        array $metadata,
        string $hook
    ): void {
        // Check if job is locked
        if ( $lock_key && ! $this->acquire_lock( $lock_key, $lock_exp ) ) {
            $this->log( sprintf( 'Job "%s" is already running, skipping execution.', $label ), 'warning' );
            return;
        }

        $start_time = microtime( true );
        $this->update_last_run( $hook );

        try {
            // Set execution timeout
            if ( $timeout > 0 ) {
                $this->set_timeout( $timeout );
            }

            // Execute the callback
            $result = call_user_func_array( $callback, $args );

            // Log success
            if ( $log_exec ) {
                $execution_time = microtime( true ) - $start_time;
                $this->log( sprintf(
                    'Job "%s" completed successfully in %.2f seconds',
                    $label,
                    $execution_time
                ), 'info' );
            }

            // Trigger success action
            do_action( 'cron_manager_job_success', $hook, $result, $metadata, $execution_time ?? 0 );

        } catch ( \Throwable $e ) {
            $this->handle_job_error( $e, $handler, $label, $metadata, $hook );
        } finally {
            // Release lock
            if ( $lock_key ) {
                $this->release_lock( $lock_key );
            }

            // Reset timeout
            if ( $timeout > 0 && function_exists( 'set_time_limit' ) ) {
                @set_time_limit( 0 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- cron runner is explicitly releasing temporary execution limits after controlled jobs.
            }
        }
    }

    /**
     * Handle job execution error.
     *
     * @param \Throwable $e
     * @param callable|null $handler
     * @param string $label
     * @param array $metadata
     * @param string $hook
     * @return void
     */
    private function handle_job_error( \Throwable $e, $handler, string $label, array $metadata, string $hook ): void {
        // Log error
        $this->log( sprintf(
            'Cron job "%s" failed: %s in %s on line %d',
            $label,
            $e->getMessage(),
            $e->getFile(),
            $e->getLine()
        ), 'error' );

        // Trigger error action
        do_action( 'cron_manager_job_error', $hook, $e, $metadata );

        // Call custom error handler if provided
        if ( $handler ) {
            try {
                call_user_func( $handler, $e, $metadata );
            } catch ( \Throwable $handler_error ) {
                $this->log(
                    sprintf( 'Error handler for job "%s" threw an exception: %s', $label, $handler_error->getMessage() ),
                    'error'
                );
            }
        }
    }

    /**
     * Acquire a lock for the job.
     *
     * @param string $key
     * @param int $expiration
     * @return bool
     */
    private function acquire_lock( string $key, int $expiration ): bool {
        $lock_key = 'cron_lock_' . md5( $key );

        // Use CacheManager (single control point) for cron locks
        $cm = CacheManager::get_instance();
        if ( $cm->get( $lock_key, 'short' ) ) {
            return false;
        }
        $cm->set( $lock_key, time(), 'short', $expiration );

        return true;
    }

    /**
     * Release a lock.
     *
     * @param string $key
     * @return void
     */
    private function release_lock( string $key ): void {
        $lock_key = 'cron_lock_' . md5( $key );

        CacheManager::get_instance()->delete( $lock_key, 'short' );
    }

    /**
     * Set PHP execution timeout.
     *
     * @param int $timeout
     * @return void
     */
    private function set_timeout( int $timeout ): void {
        if ( function_exists( 'set_time_limit' ) ) {
            @set_time_limit( $timeout ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- cron runner intentionally adjusts execution window for controlled background jobs.
        }
    }

    /**
     * Update last run time for a hook.
     *
     * @param string $hook
     * @return void
     */
    private function update_last_run( string $hook ): void {
        update_option( self::LAST_RUN_OPTION_PREFIX . $hook, time(), false );
    }

    /**
     * Track the hook in the registry.
     *
     * @return void
     */
    private function track_hook(): void {
        if ( ! in_array( $this->hook, self::$registered_hooks, true ) ) {
            self::$registered_hooks[] = $this->hook;
            $this->persist_registered_hooks();
        }
    }

    /**
     * Remove all scheduled occurrences of the current hook.
     *
     * @return static
     * @throws \LogicException
     */
    public function unregister() {
        if ( ! $this->hook ) {
            throw new \LogicException( esc_html( 'CronManager: call ->hook() before ->unregister().' ) );
        }

        $this->clear_scheduled_hook();
        $this->remove_from_registry();
        $this->log( sprintf( 'Unscheduled cron job "%s"', $this->hook ), 'info' );

        return $this;
    }

    /**
     * Clear all scheduled instances of the hook.
     *
     * @return void
     */
    private function clear_scheduled_hook(): void {
        $timestamp = wp_next_scheduled( $this->hook, $this->args );

        while ( $timestamp ) {
            wp_unschedule_event( $timestamp, $this->hook, $this->args );
            $timestamp = wp_next_scheduled( $this->hook, $this->args );
        }

        // Also clear any pending events without specific args
        wp_clear_scheduled_hook( $this->hook );
    }

    /**
     * Remove the hook from the registry.
     *
     * @return void
     */
    private function remove_from_registry(): void {
        self::$registered_hooks = array_values( array_filter(
            self::$registered_hooks,
            fn( $h ) => $h !== $this->hook
        ) );

        $this->persist_registered_hooks();
    }

    // =========================================================================
    // CHAINING HELPERS
    // =========================================================================

    /**
     * Reset all instance-level state for chaining multiple jobs.
     *
     * @return static
     */
    public function reset() {
        $this->hook             = null;
        $this->label            = '';
        $this->recurrence       = self::DAILY;
        $this->callback         = null;
        $this->args             = [];
        $this->timeout          = 0;
        $this->error_handler    = null;
        $this->enabled          = true;
        $this->start_at         = null;
        $this->log_execution    = false;
        $this->lock_key         = null;
        $this->lock_expiration  = 300;
        $this->metadata         = [];

        return $this;
    }

    // =========================================================================
    // QUERY HELPERS
    // =========================================================================

    /**
     * Return the next scheduled timestamp for the current hook.
     *
     * @return int|false
     */
    public function nextRun() {
        if ( ! $this->hook ) {
            return false;
        }
        return wp_next_scheduled( $this->hook, $this->args );
    }

    /**
     * Return a human-readable string describing when the job runs next.
     *
     * @return string
     */
    public function nextRunFormatted(): string {
        $ts = $this->nextRun();

        if ( ! $ts ) {
            return __( 'Not scheduled', 'ultraguard-security-suite' );
        }

        if ( $ts < time() ) {
            return sprintf(
                /* translators: Placeholder values are inserted into this string. */
                __( '%s ago (overdue)', 'ultraguard-security-suite' ),
                human_time_diff( $ts, time() )
            );
        }

        return sprintf(
            /* translators: Placeholder values are inserted into this string. */
            __( 'In %s', 'ultraguard-security-suite' ),
            human_time_diff( time(), $ts )
        );
    }

    /**
     * Check if the hook is currently scheduled.
     *
     * @return bool
     */
    public function isScheduled(): bool {
        return (bool) $this->nextRun();
    }

    /**
     * Get the schedule information for the current hook.
     *
     * @return array{timestamp: int, schedule: string|false, interval: int, args: array}|null
     */
    public function getScheduleInfo(): ?array {
        if ( ! $this->hook ) {
            return null;
        }

        $crons = _get_cron_array();
        if ( ! is_array( $crons ) ) {
            return null;
        }

        foreach ( $crons as $timestamp => $cron_events ) {
            if ( isset( $cron_events[ $this->hook ] ) ) {
                foreach ( $cron_events[ $this->hook ] as $cron_data ) {
                    if ( $cron_data['args'] === $this->args ) {
                        return [
                            'timestamp' => $timestamp,
                            'schedule'  => $cron_data['schedule'] ?? false,
                            'interval'  => $cron_data['interval'] ?? 0,
                            'args'      => $cron_data['args'] ?? [],
                        ];
                    }
                }
            }
        }

        return null;
    }

    /**
     * Get the last run time for this job.
     *
     * @return int|false
     */
    public function lastRun() {
        if ( ! $this->hook ) {
            return false;
        }
        return get_option( self::LAST_RUN_OPTION_PREFIX . $this->hook, false );
    }

    // =========================================================================
    // STATIC / CLASS-LEVEL HELPERS
    // =========================================================================

    /**
     * Unschedule every hook that was registered through CronManager.
     *
     * @return void
     */
    public static function clear_all(): void {
        self::init();

        foreach ( self::$registered_hooks as $hook ) {
            wp_clear_scheduled_hook( $hook );
            delete_option( self::LAST_RUN_OPTION_PREFIX . $hook );
        }

        self::$registered_hooks = [];
        delete_option( self::REGISTERED_HOOKS_OPTION );

        self::log( 'All cron jobs cleared.', 'info' );
    }

    /**
     * Return all currently registered hook names.
     *
     * @return string[]
     */
    public static function get_registered_hooks(): array {
        self::init();
        return self::$registered_hooks;
    }

    /**
     * Manually push additional custom schedules.
     *
     * @param string $key Schedule key (e.g., 'every_15_minutes')
     * @param int $interval Interval in seconds
     * @param string $display Human-readable label
     * @return void
     * @throws \InvalidArgumentException
     */
    public static function add_custom_schedule( string $key, int $interval, string $display ): void {
        if ( $interval < 60 ) {
            throw new \InvalidArgumentException( esc_html( 'Interval must be at least 60 seconds.' ) );
        }

        $key = sanitize_key( $key );
        if ( empty( $key ) ) {
            throw new \InvalidArgumentException( esc_html( 'Schedule key cannot be empty after sanitization.' ) );
        }

        self::$custom_schedules[ $key ] = [
            'interval' => $interval,
            'display'  => sanitize_text_field( $display ),
        ];
    }

    /**
     * WordPress 'cron_schedules' filter callback.
     *
     * @param array $schedules
     * @return array
     */
    public static function register_custom_schedules( array $schedules ): array {
        // Built-in plugin schedules.
        // NOTE: display strings intentionally use plain literals — NOT __().
        // This filter can fire before the 'init' hook (e.g. during wp_get_schedules()
        // called at plugins_loaded), so wrapping them in __() triggers the WP 6.7
        // "translation loaded too early" notice. Schedule display labels are shown
        // only in WP Admin › Tools › Scheduled Events and do not require translation
        // at this stage of the boot cycle.
        $defaults = [
            self::EVERY_MINUTE    => [
                'interval' => MINUTE_IN_SECONDS,
                'display'  => 'Every Minute',
            ],
            self::EVERY_5_MINUTES => [
                'interval' => 5 * MINUTE_IN_SECONDS,
                'display'  => 'Every 5 Minutes',
            ],
            self::EVERY_15_MINUTES => [
                'interval' => 15 * MINUTE_IN_SECONDS,
                'display'  => 'Every 15 Minutes',
            ],
            self::EVERY_30_MINUTES => [
                'interval' => 30 * MINUTE_IN_SECONDS,
                'display'  => 'Every 30 Minutes',
            ],
            self::EVERY_2_HOURS => [
                'interval' => 2 * HOUR_IN_SECONDS,
                'display'  => 'Every 2 Hours',
            ],
            self::EVERY_3_HOURS => [
                'interval' => 3 * HOUR_IN_SECONDS,
                'display'  => 'Every 3 Hours',
            ],
            self::EVERY_6_HOURS => [
                'interval' => 6 * HOUR_IN_SECONDS,
                'display'  => 'Every 6 Hours',
            ],
            self::MONTHLY => [
                'interval' => 30 * DAY_IN_SECONDS,
                'display'  => 'Monthly',
            ],
        ];

        return array_merge( $schedules, $defaults, self::$custom_schedules );
    }

    /**
     * Return a full status snapshot of every hook registered via this class.
     *
     * @return array<string, array{
     *     hook: string,
     *     args: array,
     *     next_run: int|false,
     *     next_run_formatted: string,
     *     recurrence: string|false,
     *     interval: int,
     *     last_run: int|false,
     *     is_scheduled: bool
     * }>
     */
    public static function status_report(): array {
        self::init();

        $report = [];
        $crons = _get_cron_array() ?: [];

        foreach ( self::$registered_hooks as $hook ) {
            $next = wp_next_scheduled( $hook );
            $schedule_info = self::get_schedule_info_for_hook( $hook, $crons );
            $last_run = get_option( self::LAST_RUN_OPTION_PREFIX . $hook, false );

            $report[ $hook ] = [
                'hook'               => $hook,
                'args'               => $schedule_info['args'] ?? [],
                'next_run'           => $next,
                'next_run_formatted' => self::format_next_run( $next ),
                'recurrence'         => $schedule_info['schedule'] ?? false,
                'interval'           => $schedule_info['interval'] ?? 0,
                'last_run'           => $last_run,
                'last_run_formatted' => $last_run ? wp_date( 'Y-m-d H:i:s', $last_run ) : __( 'Never', 'ultraguard-security-suite' ),
                'is_scheduled'       => (bool) $next,
            ];
        }

        return $report;
    }

    /**
     * Get schedule information for a specific hook.
     *
     * @param string $hook
     * @param array $crons
     * @return array
     */
    private static function get_schedule_info_for_hook( string $hook, array $crons ): array {
        foreach ( $crons as $timestamp => $cron_events ) {
            if ( isset( $cron_events[ $hook ] ) ) {
                foreach ( $cron_events[ $hook ] as $cron_data ) {
                    return [
                        'schedule' => $cron_data['schedule'] ?? false,
                        'interval' => $cron_data['interval'] ?? 0,
                        'args'     => $cron_data['args'] ?? [],
                    ];
                }
            }
        }

        return [];
    }

    /**
     * Format the next run time.
     *
     * @param int|false $next
     * @return string
     */
    private static function format_next_run( $next ): string {
        if ( ! $next ) {
            return __( 'Not scheduled', 'ultraguard-security-suite' );
        }

        if ( $next < time() ) {
            return sprintf(
                /* translators: Placeholder values are inserted into this string. */
                __( '%s ago (overdue)', 'ultraguard-security-suite' ),
                human_time_diff( $next, time() )
            );
        }

        return sprintf(
            /* translators: Placeholder values are inserted into this string. */
            __( 'In %s', 'ultraguard-security-suite' ),
            human_time_diff( time(), $next )
        );
    }

    /**
     * Handle shutdown (catch fatal errors).
     *
     * @return void
     */
    public static function handle_shutdown(): void {
        $error = error_get_last();

        if ( $error && in_array( $error['type'], [ E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR ] ) ) {
            self::log(
                sprintf( 'Fatal error during cron execution: %s in %s on line %d',
                    $error['message'],
                    $error['file'],
                    $error['line']
                ),
                'critical'
            );
        }
    }

    // =========================================================================
    // INTERNAL HELPERS
    // =========================================================================

    /**
     * Assert that the minimum required properties are set.
     *
     * @throws \LogicException
     */
    private function assert_ready(): void {
        if ( ! $this->hook ) {
            throw new \LogicException( esc_html( 'CronManager: call ->hook() before ->register().' ) );
        }

        // Validate recurrence here (deferred from recurrence() setter so that
        // wp_get_schedules() — and the 'cron_schedules' filter it fires — is
        // not triggered prematurely during the fluent setter chain.
        $schedules = wp_get_schedules();
        if ( ! isset( $schedules[ $this->recurrence ] ) ) {
            throw new \InvalidArgumentException(
                /* translators: %1\: schedule name, %2\: hook name */
                esc_html( sprintf( 'CronManager: invalid recurrence schedule "%s" for hook "%s".', $this->recurrence, $this->hook ) )
            );
        }

        // BUG-FIX: callback is OPTIONAL. WordPress fires do_action($hook) when a
        // cron event runs; plugin modules register their own add_action() handlers
        // independently. CronManager only needs to schedule the event.
        // If a callback IS provided, validate it is callable.
        if ( $this->callback !== null && ! is_callable( $this->callback ) ) {
            throw new \LogicException(
                /* translators: %s: hook name */
                esc_html( sprintf( 'CronManager: callback for hook "%s" is not callable.', $this->hook ) )
            );
        }
    }

    /**
     * Load registered hooks from database.
     *
     * @return void
     */
    private static function load_registered_hooks(): void {
        $hooks = get_option( self::REGISTERED_HOOKS_OPTION, [] );

        if ( is_array( $hooks ) ) {
            self::$registered_hooks = $hooks;
        }
    }

    /**
     * Persist registered hooks to database.
     *
     * @return void
     */
    private static function persist_registered_hooks(): void {
        update_option( self::REGISTERED_HOOKS_OPTION, self::$registered_hooks, false );
    }

    /**
     * Log a message.
     *
     * @param string $message
     * @param string $level
     * @return void
     */
    private static function log( string $message, string $level = 'info' ): void {
        if ( self::$logger ) {
            call_user_func( self::$logger, $level, $message );
        } else {
            $formatted_message = '[CronManager] ' . $message;

            switch ( $level ) {
                case 'debug':
                    \UltraguardPro\core\Logger::debug( $formatted_message );
                    break;
                case 'warning':
                    \UltraguardPro\core\Logger::warning( $formatted_message );
                    break;
                case 'error':
                    \UltraguardPro\core\Logger::error( $formatted_message );
                    break;
                case 'critical':
                    \UltraguardPro\core\Logger::critical( $formatted_message );
                    break;
                default:
                    \UltraguardPro\core\Logger::info( $formatted_message );
                    break;
            }
        }
    }

    // =========================================================================
    // MAGIC METHODS
    // =========================================================================

    /**
     * Prevent cloning of the instance.
     *
     * @return void
     */
    private function __clone() {}

    /**
     * Prevent unserializing of the instance.
     *
     * @return void
     */
    public function __wakeup() {
        throw new \Exception( esc_html( 'Cannot unserialize singleton' ) );
    }

    /**
     * Debug info.
     *
     * @return array
     */
    public function __debugInfo(): array {
        return [
            'hook'       => $this->hook,
            'label'      => $this->label,
            'recurrence' => $this->recurrence,
            'enabled'    => $this->enabled,
            'scheduled'  => $this->isScheduled(),
            'next_run'   => $this->nextRunFormatted(),
            'last_run'   => $this->lastRun() ? wp_date( 'Y-m-d H:i:s', $this->lastRun() ) : 'Never',
            'args_count' => count( $this->args ),
            'has_lock'   => ! is_null( $this->lock_key ),
        ];
    }
}
