<?php
/**
 * Internal managed clock for Niwa background automation.
 *
 * WordPress cron is cooperative: it only advances when something loads
 * WordPress, unless the host has a real server cron. This class starts a
 * short-lived CLI worker when the host allows local PHP process spawning.
 *
 * @package Niwa_AI
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class Niwa_AI_Admin_Autonomy_Background_Clock {

	const OPTION_STATE        = 'niwa_ai_admin_autonomy_background_clock_state';
	const OPTION_START_LOCK   = 'niwa_ai_admin_autonomy_background_clock_start_lock';
	const WATCHDOG_HOOK       = 'niwa_ai_admin_autonomy_background_clock_watchdog';
	const DEFAULT_SLEEP       = 30;
	const DEFAULT_LIMIT       = 20;
	const DEFAULT_MAX_RUNTIME = 1800;
	const START_THROTTLE      = 20;
	const EXPIRY_GRACE        = 180;

	/**
	 * Start the internal clock if possible and not already running.
	 *
	 * @param string $reason Start reason.
	 * @param bool   $force  Bypass start-attempt throttling.
	 * @return array<string, mixed>
	 */
	public function maybe_start( $reason = 'boot', $force = false ) {
		$reason = sanitize_key( (string) $reason );
		if ( '' === $reason ) {
			$reason = 'boot';
		}

		if ( defined( 'NIWA_AI_BACKGROUND_RUNNER' ) && NIWA_AI_BACKGROUND_RUNNER ) {
			return array(
				'success' => true,
				'status'  => 'current_background_runner',
			);
		}

		if ( self::loop_lock_is_held() ) {
			return array(
				'success' => true,
				'status'  => 'already_running_lock',
			);
		}

		$state = self::get_state();
		if ( self::state_is_running( $state ) ) {
			return array(
				'success' => true,
				'status'  => 'already_running',
				'pid'     => intval( isset( $state['pid'] ) ? $state['pid'] : 0 ),
				'token'   => isset( $state['token'] ) ? (string) $state['token'] : '',
			);
		}

		if ( ! self::has_background_runtime_work() ) {
			self::save_state(
				array_merge(
					$state,
					array(
						'status'        => 'idle',
						'last_idle_at'  => time(),
						'last_idle_reason' => $reason,
					)
				)
			);
			return array(
				'success' => true,
				'status'  => 'idle',
			);
		}

		if ( ! class_exists( 'Niwa_AI_Admin_Autonomy_Process_Runtime' ) || ! Niwa_AI_Admin_Autonomy_Process_Runtime::can_spawn_local_cli() ) {
			self::record_unavailable( 'local_cli_unavailable', $reason );
			return array(
				'success' => false,
				'status'  => 'unavailable',
				'error'   => 'local_cli_unavailable',
			);
		}

		$start_lock = self::acquire_start_lock( $reason, $force );
		if ( empty( $start_lock['success'] ) ) {
			return array(
				'success' => true,
				'status'  => 'start_in_progress',
				'error'   => isset( $start_lock['error'] ) ? (string) $start_lock['error'] : 'start_lock_active',
			);
		}

		try {
			$state = self::get_state();
			if ( self::state_is_running( $state ) ) {
				return array(
					'success' => true,
					'status'  => 'already_running',
					'pid'     => intval( isset( $state['pid'] ) ? $state['pid'] : 0 ),
					'token'   => isset( $state['token'] ) ? (string) $state['token'] : '',
				);
			}

			$now          = time();
			$last_attempt = intval( isset( $state['last_start_attempt_at'] ) ? $state['last_start_attempt_at'] : 0 );
			if ( ! $force && $last_attempt > 0 && ( $now - $last_attempt ) < self::START_THROTTLE ) {
				return array(
					'success' => true,
					'status'  => 'start_throttled',
					'next_try_after' => $last_attempt + self::START_THROTTLE,
				);
			}

			$token       = self::new_token();
			$sleep       = self::sleep_seconds();
			$limit       = self::limit();
			$max_runtime = self::max_runtime_seconds();
			$args        = self::runner_args( $token, $limit, $sleep, $max_runtime, 'internal_clock' );

			self::save_state(
				array_merge(
					$state,
					array(
						'status'                => 'starting',
						'token'                 => $token,
						'pid'                   => 0,
						'started_at'            => $now,
						'last_start_attempt_at' => $now,
						'last_start_reason'     => $reason,
						'sleep_seconds'         => $sleep,
						'limit'                 => $limit,
						'max_runtime_seconds'   => $max_runtime,
						'expires_at'            => $now + $max_runtime + self::EXPIRY_GRACE,
						'stop_requested_at'     => 0,
					)
				)
			);

			$result = Niwa_AI_Admin_Autonomy_Process_Runtime::spawn_background_php(
				NIWA_AI_DIR . 'bin/background-runner.php',
				$args,
				defined( 'ABSPATH' ) ? ABSPATH : null,
				$max_runtime + self::EXPIRY_GRACE
			);

			$state_update = array_merge(
				self::get_state(),
				array(
					'last_start_result' => $result,
					'last_start_at'     => $now,
					'last_start_reason' => $reason,
				)
			);

			if ( ! empty( $result['success'] ) ) {
				$state_update['status']     = 'running';
				$state_update['pid']        = intval( isset( $result['pid'] ) ? $result['pid'] : 0 );
				$state_update['token']      = $token;
				$state_update['expires_at'] = time() + $max_runtime + self::EXPIRY_GRACE;
				self::append_log( 'start', array( 'reason' => $reason, 'pid' => $state_update['pid'], 'token' => $token ) );
			} else {
				$state_update['status'] = 'spawn_failed';
				$state_update['error']  = isset( $result['error'] ) ? (string) $result['error'] : 'spawn_failed';
				self::append_log( 'spawn_failed', array( 'reason' => $reason, 'error' => $state_update['error'] ) );
			}

			self::save_state( $state_update );

			return array_merge(
				$result,
				array(
					'status' => ! empty( $result['success'] ) ? 'started' : 'spawn_failed',
					'token'  => $token,
				)
			);
		} finally {
			self::release_start_lock( $start_lock );
		}
	}

	/**
	 * Record that the runner process has booted.
	 *
	 * @param string $token Runtime token.
	 * @param int    $pid Worker PID.
	 * @param array  $context Context.
	 * @return void
	 */
	public static function record_start( $token, $pid, $context = array() ) {
		$token       = sanitize_text_field( (string) $token );
		$pid         = intval( $pid );
		$context     = is_array( $context ) ? $context : array();
		$now         = time();
		$sleep       = isset( $context['sleep_seconds'] ) ? max( 5, intval( $context['sleep_seconds'] ) ) : self::DEFAULT_SLEEP;
		$max_runtime = isset( $context['max_runtime_seconds'] ) ? max( 60, intval( $context['max_runtime_seconds'] ) ) : self::DEFAULT_MAX_RUNTIME;

		$state = self::get_state();
		$state_token = isset( $state['token'] ) ? (string) $state['token'] : '';
		if ( '' !== $token && '' !== $state_token && ! hash_equals( $state_token, $token ) ) {
			self::append_log( 'worker_start_ignored', array( 'pid' => $pid, 'token' => $token, 'state_token' => $state_token ) );
			return;
		}

		self::save_state(
			array_merge(
				$state,
				array(
					'status'              => 'running',
					'token'               => $token,
					'pid'                 => $pid,
					'worker_pid'          => $pid,
					'started_at'          => $now,
					'last_worker_start_at'=> $now,
					'sleep_seconds'       => $sleep,
					'max_runtime_seconds' => $max_runtime,
					'expires_at'          => $now + $max_runtime + self::EXPIRY_GRACE,
					'source'              => isset( $context['source'] ) ? sanitize_key( (string) $context['source'] ) : 'internal_clock',
					'stop_requested_at'   => 0,
				)
			)
		);

		self::append_log( 'worker_start', array( 'pid' => $pid, 'token' => $token ) );
	}

	/**
	 * Record one runner tick.
	 *
	 * @param string $token Runtime token.
	 * @param int    $pid Worker PID.
	 * @param array  $result Cron maintenance result.
	 * @param array  $context Context.
	 * @return void
	 */
	public static function record_tick( $token, $pid, $result, $context = array() ) {
		$token   = sanitize_text_field( (string) $token );
		$pid     = intval( $pid );
		$result  = is_array( $result ) ? $result : array();
		$context = is_array( $context ) ? $context : array();
		$state   = self::get_state();

		if ( ! self::token_matches_state( $state, $token ) ) {
			self::append_log( 'stale_tick', array( 'pid' => $pid, 'token' => $token ) );
			return;
		}

		$summary = array(
			'success'   => ! empty( $result['success'] ),
			'processed' => intval( isset( $result['processed'] ) ? $result['processed'] : 0 ),
			'skipped'   => isset( $result['skipped'] ) ? sanitize_key( (string) $result['skipped'] ) : '',
			'errors'    => isset( $result['errors'] ) && is_array( $result['errors'] ) ? array_slice( array_map( 'strval', $result['errors'] ), 0, 5 ) : array(),
			'source'    => isset( $result['source'] ) ? sanitize_key( (string) $result['source'] ) : '',
		);

		$tick_count = intval( isset( $state['tick_count'] ) ? $state['tick_count'] : 0 ) + 1;
		$now        = time();
		$state      = array_merge(
			$state,
			array(
				'status'          => 'running',
				'pid'             => $pid,
				'worker_pid'      => $pid,
				'token'           => $token,
				'last_tick_at'    => $now,
				'last_result'     => $summary,
				'tick_count'      => $tick_count,
				'expires_at'      => $now + max( 120, intval( isset( $context['max_runtime_seconds'] ) ? $context['max_runtime_seconds'] : self::DEFAULT_MAX_RUNTIME ) ) + self::EXPIRY_GRACE,
				'sleep_seconds'   => isset( $context['sleep_seconds'] ) ? max( 5, intval( $context['sleep_seconds'] ) ) : intval( isset( $state['sleep_seconds'] ) ? $state['sleep_seconds'] : self::DEFAULT_SLEEP ),
			)
		);

		self::save_state( $state );
		self::write_status_file( $state );

		if ( $summary['processed'] > 0 || ! empty( $summary['errors'] ) || 1 === $tick_count || 0 === ( $tick_count % 10 ) ) {
			self::append_log( 'tick', array_merge( array( 'pid' => $pid, 'token' => $token ), $summary ) );
		}
	}

	/**
	 * Spawn a replacement worker before the current one exits.
	 *
	 * @param string $current_token Current token.
	 * @param array  $context Context.
	 * @return array<string, mixed>
	 */
	public static function spawn_successor( $current_token, $context = array() ) {
		$current_token = sanitize_text_field( (string) $current_token );
		$context       = is_array( $context ) ? $context : array();
		$state         = self::get_state();
		if ( ! self::token_matches_state( $state, $current_token ) ) {
			return array(
				'success' => true,
				'skipped' => 'state_token_changed',
			);
		}

		if ( ! class_exists( 'Niwa_AI_Admin_Autonomy_Process_Runtime' ) || ! Niwa_AI_Admin_Autonomy_Process_Runtime::can_spawn_local_cli() ) {
			self::append_log( 'rotate_unavailable', array( 'token' => $current_token ) );
			return array(
				'success' => false,
				'error'   => 'local_cli_unavailable',
			);
		}

		$new_token   = self::new_token();
		$limit       = isset( $context['limit'] ) ? max( 1, intval( $context['limit'] ) ) : self::limit();
		$sleep       = isset( $context['sleep_seconds'] ) ? max( 5, intval( $context['sleep_seconds'] ) ) : self::sleep_seconds();
		$max_runtime = isset( $context['max_runtime_seconds'] ) ? max( 60, intval( $context['max_runtime_seconds'] ) ) : self::max_runtime_seconds();
		$source      = isset( $context['source'] ) ? sanitize_key( (string) $context['source'] ) : 'internal_clock';
		$args        = self::runner_args( $new_token, $limit, $sleep, $max_runtime, $source );

		$result = Niwa_AI_Admin_Autonomy_Process_Runtime::spawn_background_php(
			NIWA_AI_DIR . 'bin/background-runner.php',
			$args,
			defined( 'ABSPATH' ) ? ABSPATH : null,
			$max_runtime + self::EXPIRY_GRACE
		);

		if ( ! empty( $result['success'] ) ) {
			self::save_state(
				array_merge(
					self::get_state(),
					array(
						'status'              => 'running',
						'token'               => $new_token,
						'pid'                 => intval( isset( $result['pid'] ) ? $result['pid'] : 0 ),
						'last_rotation_at'    => time(),
						'last_rotation_from'  => $current_token,
						'last_rotation_result'=> $result,
						'sleep_seconds'       => $sleep,
						'limit'               => $limit,
						'max_runtime_seconds' => $max_runtime,
						'expires_at'          => time() + $max_runtime + self::EXPIRY_GRACE,
					)
				)
			);
			self::append_log( 'rotate', array( 'old_token' => $current_token, 'new_token' => $new_token, 'pid' => intval( isset( $result['pid'] ) ? $result['pid'] : 0 ) ) );
		} else {
			self::append_log( 'rotate_failed', array( 'token' => $current_token, 'error' => isset( $result['error'] ) ? (string) $result['error'] : 'spawn_failed' ) );
		}

		return array_merge( $result, array( 'token' => $new_token ) );
	}

	/**
	 * Record worker exit.
	 *
	 * @param string $token Runtime token.
	 * @param string $reason Exit reason.
	 * @return void
	 */
	public static function record_stop( $token, $reason = 'exit' ) {
		$token  = sanitize_text_field( (string) $token );
		$reason = sanitize_key( (string) $reason );
		if ( '' === $reason ) {
			$reason = 'exit';
		}

		$state = self::get_state();
		if ( self::token_matches_state( $state, $token ) ) {
			$state['status']      = $reason;
			$state['stopped_at']  = time();
			$state['stop_reason'] = $reason;
			self::save_state( $state );
			self::write_status_file( $state );
		}

		self::append_log( 'worker_stop', array( 'token' => $token, 'reason' => $reason ) );
	}

	/**
	 * Ask the current runner to stop and try to terminate its process.
	 *
	 * @param string $reason Stop reason.
	 * @return array<string, mixed>
	 */
	public static function request_stop( $reason = 'manual' ) {
		$state = self::get_state();
		$pid   = intval( isset( $state['pid'] ) ? $state['pid'] : 0 );
		$state['stop_requested_at']     = time();
		$state['stop_requested_reason'] = sanitize_key( (string) $reason );
		self::save_state( $state );

		$signalled = false;
		if ( $pid > 0 && function_exists( 'posix_kill' ) ) {
			$signalled = @posix_kill( $pid, 15 );
		}

		self::append_log( 'stop_requested', array( 'reason' => $reason, 'pid' => $pid, 'signalled' => $signalled ) );

		return array(
			'success'   => true,
			'pid'       => $pid,
			'signalled' => $signalled,
		);
	}

	/**
	 * Check whether current runner token was asked to stop.
	 *
	 * @param string $token Runtime token.
	 * @return bool
	 */
	public static function should_stop( $token ) {
		$token       = sanitize_text_field( (string) $token );
		$state       = self::get_state();
		$state_token = is_array( $state ) && isset( $state['token'] ) ? (string) $state['token'] : '';
		if ( '' !== $token && '' !== $state_token && ! hash_equals( $state_token, $token ) ) {
			return true;
		}

		return self::token_matches_state( $state, $token )
			&& ! empty( $state['stop_requested_at'] );
	}

	private static function record_unavailable( $error, $reason ) {
		$state = self::get_state();
		self::save_state(
			array_merge(
				$state,
				array(
					'status'                => 'unavailable',
					'error'                 => sanitize_key( (string) $error ),
					'last_start_reason'     => sanitize_key( (string) $reason ),
					'last_start_attempt_at' => time(),
				)
			)
		);
	}

	private static function has_background_runtime_work() {
		$admin_ui_queue_option = class_exists( 'Niwa_AI_Constants' ) ? Niwa_AI_Constants::OPT_ADMIN_UI_CHAT_QUEUE : 'niwa_ai_admin_ui_chat_queue';
		$admin_ui_queue = get_option( $admin_ui_queue_option, array() );
		if ( is_array( $admin_ui_queue ) && ! empty( $admin_ui_queue ) ) {
			return true;
		}
		$should_run = null;
		if ( function_exists( 'apply_filters' ) ) {
			$should_run = apply_filters( 'niwa_ai_admin_autonomy_background_clock_should_run', null );
		}

		if ( true === $should_run ) {
			return true;
		}
		if ( false === $should_run ) {
			return false;
		}

		foreach ( self::background_work_option_names() as $option_name ) {
			$config = get_option( $option_name, array() );
			if ( is_array( $config ) && ! empty( $config['enabled'] ) ) {
				return true;
			}
			if ( is_array( $config ) && isset( $config['whatsapp'] ) && is_array( $config['whatsapp'] ) && ! empty( $config['whatsapp']['enabled'] ) ) {
				return true;
			}
			if ( is_array( $config ) && isset( $config['telegram'] ) && is_array( $config['telegram'] ) && ! empty( $config['telegram']['enabled'] ) ) {
				return true;
			}
		}

		return self::has_active_scheduled_jobs();
	}

	private static function background_work_option_names() {
		$options = array(
			class_exists( 'Niwa_AI_Constants' ) ? Niwa_AI_Constants::OPT_AUTONOMY : 'niwa_ai_admin_autonomy',
			class_exists( 'Niwa_AI_Constants' ) ? Niwa_AI_Constants::OPT_AUTONOMY_OPERATIVE_HB_CONFIG : 'niwa_ai_admin_autonomy_operative_hb_config',
			class_exists( 'Niwa_AI_Constants' ) ? Niwa_AI_Constants::OPT_AUTONOMY_STOCK_OPERATIVE_HB_CONFIG : 'niwa_ai_admin_autonomy_stock_operative_hb_config',
		);

		if ( function_exists( 'apply_filters' ) ) {
			$options = apply_filters( 'niwa_ai_admin_autonomy_background_clock_work_options', $options );
		}

		return array_values( array_unique( array_filter( array_map( 'strval', (array) $options ) ) ) );
	}

	private static function has_active_scheduled_jobs() {
		global $wpdb;
		if ( ! $wpdb || empty( $wpdb->prefix ) ) {
			return false;
		}

		$table = $wpdb->prefix . 'niwa_ai_admin_scheduled_jobs';
		$exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
		if ( $exists !== $table ) {
			return false;
		}

		$count = $wpdb->get_var(
			"SELECT COUNT(1) FROM {$table} WHERE status IN ('active', 'actor_pending', 'extension_pending', 'running') LIMIT 1"
		);

		return intval( $count ) > 0;
	}

	private static function acquire_start_lock( $reason, $force = false ) {
		$token   = self::new_token();
		$now     = time();
		$payload = array(
			'token'      => $token,
			'started_at' => $now,
			'expires_at' => $now + self::START_THROTTLE,
			'reason'     => sanitize_key( (string) $reason ),
		);

		if ( add_option( self::OPTION_START_LOCK, $payload, '', false ) ) {
			return array(
				'success' => true,
				'token'   => $token,
			);
		}

		$current = get_option( self::OPTION_START_LOCK, array() );
		$expires = is_array( $current ) && isset( $current['expires_at'] ) ? intval( $current['expires_at'] ) : 0;
		if ( $expires <= $now ) {
			self::delete_option_if_current_value_matches( self::OPTION_START_LOCK, $current );
			if ( add_option( self::OPTION_START_LOCK, $payload, '', false ) ) {
				return array(
					'success'       => true,
					'token'         => $token,
					'stale_cleared' => true,
				);
			}
		}

		return array(
			'success' => false,
			'error'   => 'start_lock_active',
		);
	}

	private static function release_start_lock( $lock ) {
		if ( ! is_array( $lock ) || empty( $lock['token'] ) ) {
			return;
		}

		$current = get_option( self::OPTION_START_LOCK, array() );
		if ( is_array( $current ) && isset( $current['token'] ) && hash_equals( (string) $current['token'], (string) $lock['token'] ) ) {
			self::delete_option_if_current_value_matches( self::OPTION_START_LOCK, $current );
		}
	}

	private static function delete_option_if_current_value_matches( $option_key, $expected_value ) {
		global $wpdb;
		if ( ! $wpdb || empty( $wpdb->options ) ) {
			return false;
		}

		$serialized = function_exists( 'maybe_serialize' ) ? maybe_serialize( $expected_value ) : serialize( $expected_value );
		$deleted = $wpdb->delete(
			$wpdb->options,
			array(
				'option_name'  => (string) $option_key,
				'option_value' => $serialized,
			),
			array( '%s', '%s' )
		);

		if ( false !== $deleted && intval( $deleted ) > 0 ) {
			if ( function_exists( 'wp_cache_delete' ) ) {
				wp_cache_delete( (string) $option_key, 'options' );
				wp_cache_delete( 'alloptions', 'options' );
				wp_cache_delete( 'notoptions', 'options' );
			}
			return true;
		}

		return false;
	}

	private static function state_is_running( $state ) {
		if ( ! is_array( $state ) ) {
			return false;
		}

		$status     = isset( $state['status'] ) ? sanitize_key( (string) $state['status'] ) : '';
		$started_at = intval( isset( $state['started_at'] ) ? $state['started_at'] : 0 );
		if ( 'starting' === $status && $started_at > 0 && ( time() - $started_at ) < 60 ) {
			return true;
		}

		$pid = intval( isset( $state['pid'] ) ? $state['pid'] : 0 );
		if ( $pid <= 0 ) {
			return false;
		}

		if ( ! class_exists( 'Niwa_AI_Admin_Autonomy_Process_Runtime' ) || ! Niwa_AI_Admin_Autonomy_Process_Runtime::background_process_is_alive( $pid ) ) {
			return false;
		}

		$expires_at = intval( isset( $state['expires_at'] ) ? $state['expires_at'] : 0 );
		if ( $expires_at > 0 && time() > ( $expires_at + self::EXPIRY_GRACE ) ) {
			return false;
		}

		$sleep_seconds = intval( isset( $state['sleep_seconds'] ) ? $state['sleep_seconds'] : self::DEFAULT_SLEEP );
		$stale_after   = max( 180, $sleep_seconds * 4 );
		$last_tick_at  = intval( isset( $state['last_tick_at'] ) ? $state['last_tick_at'] : 0 );
		if ( $last_tick_at > 0 && ( time() - $last_tick_at ) > $stale_after ) {
			return false;
		}
		if ( $last_tick_at <= 0 && $started_at > 0 && ( time() - $started_at ) > 120 ) {
			return false;
		}

		$token = isset( $state['token'] ) ? (string) $state['token'] : '';
		return self::pid_matches_clock( $pid, $token );
	}

	private static function pid_matches_clock( $pid, $token = '' ) {
		$pid = intval( $pid );
		if ( $pid <= 0 ) {
			return false;
		}

		$cmdline_path = '/proc/' . $pid . '/cmdline';
		if ( ! is_readable( $cmdline_path ) ) {
			return true;
		}

		$cmdline = @file_get_contents( $cmdline_path );
		if ( ! is_string( $cmdline ) || '' === $cmdline ) {
			return true;
		}

		$cmdline = str_replace( "\0", ' ', $cmdline );
		if ( false === strpos( $cmdline, 'background-runner.php' ) ) {
			return false;
		}

		return '' === $token || false !== strpos( $cmdline, $token );
	}

	private static function loop_lock_is_held() {
		$lock_dir = defined( 'WP_CONTENT_DIR' ) ? trailingslashit( WP_CONTENT_DIR ) . 'uploads/niwa-ai/locks' : sys_get_temp_dir();
		if ( function_exists( 'wp_mkdir_p' ) ) {
			wp_mkdir_p( $lock_dir );
		}
		$handle = @fopen( trailingslashit( $lock_dir ) . 'background-runner.lock', 'c' );
		if ( ! is_resource( $handle ) ) {
			return false;
		}
		if ( @flock( $handle, LOCK_EX | LOCK_NB ) ) {
			@flock( $handle, LOCK_UN );
			@fclose( $handle );
			return false;
		}
		@fclose( $handle );
		return true;
	}

	private static function token_matches_state( $state, $token ) {
		$state_token = is_array( $state ) && isset( $state['token'] ) ? (string) $state['token'] : '';
		$token       = (string) $token;
		return '' !== $state_token && '' !== $token && hash_equals( $state_token, $token );
	}

	private static function runner_args( $token, $limit, $sleep, $max_runtime, $source ) {
		return array(
			'limit'       => max( 1, intval( $limit ) ),
			'loop'        => '1',
			'sleep'       => max( 5, intval( $sleep ) ),
			'max-runtime' => max( 60, intval( $max_runtime ) ),
			'rotate'      => '1',
			'clock-token' => (string) $token,
			'source'      => sanitize_key( (string) $source ),
		);
	}

	private static function sleep_seconds() {
		$value = self::DEFAULT_SLEEP;
		if ( function_exists( 'apply_filters' ) ) {
			$value = apply_filters( 'niwa_ai_admin_autonomy_background_clock_sleep_seconds', $value );
		}
		return max( 15, intval( $value ) );
	}

	private static function limit() {
		$value = self::DEFAULT_LIMIT;
		if ( function_exists( 'apply_filters' ) ) {
			$value = apply_filters( 'niwa_ai_admin_autonomy_background_clock_limit', $value );
		}
		return max( 1, intval( $value ) );
	}

	private static function max_runtime_seconds() {
		$value = self::DEFAULT_MAX_RUNTIME;
		if ( function_exists( 'apply_filters' ) ) {
			$value = apply_filters( 'niwa_ai_admin_autonomy_background_clock_max_runtime_seconds', $value );
		}
		return max( 300, intval( $value ) );
	}

	private static function new_token() {
		if ( function_exists( 'wp_generate_uuid4' ) ) {
			return wp_generate_uuid4();
		}

		return bin2hex( random_bytes( 16 ) );
	}

	private static function get_state() {
		$state = get_option( self::OPTION_STATE, array() );
		return is_array( $state ) ? $state : array();
	}

	private static function save_state( $state ) {
		$state = is_array( $state ) ? $state : array();
		update_option( self::OPTION_STATE, $state, false );
		self::write_status_file( $state );
	}

	private static function write_status_file( $state ) {
		$dir = self::debug_dir();
		if ( '' === $dir ) {
			return;
		}

		@file_put_contents( $dir . '/background-clock.json', self::json_encode( $state ) . "\n", LOCK_EX );
	}

	private static function append_log( $event, $context = array() ) {
		$dir = self::debug_dir();
		if ( '' === $dir ) {
			return;
		}

		$line = self::json_encode(
			array(
				'at'      => gmdate( 'c' ),
				'event'   => sanitize_key( (string) $event ),
				'context' => is_array( $context ) ? $context : array(),
			)
		);

		@file_put_contents( $dir . '/background-clock.log', $line . "\n", FILE_APPEND | LOCK_EX );
	}

	private static function debug_dir() {
		$base = '';
		if ( function_exists( 'wp_upload_dir' ) ) {
			$uploads = wp_upload_dir( null, false );
			if ( is_array( $uploads ) && ! empty( $uploads['basedir'] ) ) {
				$base = (string) $uploads['basedir'];
			}
		}

		if ( '' === $base && defined( 'WP_CONTENT_DIR' ) ) {
			$base = rtrim( WP_CONTENT_DIR, '/\\' ) . '/uploads';
		}

		if ( '' === $base ) {
			return '';
		}

		$dir = rtrim( $base, '/\\' ) . '/niwa-debug';
		if ( function_exists( 'wp_mkdir_p' ) ) {
			wp_mkdir_p( $dir );
		} elseif ( ! is_dir( $dir ) ) {
			@mkdir( $dir, 0755, true );
		}

		return is_dir( $dir ) && is_writable( $dir ) ? $dir : '';
	}

	private static function json_encode( $payload ) {
		return function_exists( 'wp_json_encode' ) ? wp_json_encode( $payload ) : json_encode( $payload );
	}
}
