<?php
/**
 * Represents a cron event.
 *
 * @package northmill-online-shared
 */

declare(strict_types=1);

namespace Northmill_Online_Shared\Modules\Cron\Models;

use Northmill_Online_Shared\Abstract_Shared_Base;
use Northmill_Online_Shared\Modules\Loggers\Log_Level_Enum;

defined( 'ABSPATH' ) || exit;

/**
 * Represents a cron event.
 */
class Cron_Event {

	/**
	 * The name of the event.
	 *
	 * @var string $event
	 */
	private string $event;

	/**
	 * Flag to determine if the cron event should be enabled or disabled.
	 *
	 * @var bool
	 */
	private bool $is_enabled;

	/**
	 * The recurrence pattern for the cron event.
	 *
	 * @var string
	 */
	private string $recurrence;

	/**
	 * The next scheduled timestamp for the cron event. Or false if not scheduled.
	 *
	 * @var int|false
	 */
	private int|false $next_scheduled;

	/**
	 * Constructs a new instance of the class.
	 *
	 * @param string $event      The name of the event.
	 * @param string $recurrence The recurrence of the event.
	 * @param bool   $is_enabled Whether the event is enabled or not.
	 * @param array  $functions  An array of functions to be added to the event.
	 *
	 * @return void
	 */
	public function __construct( string $event, string $recurrence, bool $is_enabled, array $functions = array() ) {
		$this->event      = $event;
		$this->is_enabled = $is_enabled;
		$this->recurrence = $recurrence;

		if ( ! empty( $functions ) ) {
			foreach ( $functions as $function ) {
				if ( is_callable( $function ) ) {
					$this->add_function( $function );
				} else {
					Abstract_Shared_Base::base_instance()->logger->log(
						'cron',
						'Cron event function is not callable',
						array(
							'event'      => $event,
							'function'   => $function,
							'is_enabled' => $is_enabled,
							'recurrence' => $recurrence,
						),
						Log_Level_Enum::ERROR
					);
				}
			}
		}

		$this->add_function( array( $this, 'log_start' ), -999 );
		$this->add_function( array( $this, 'log_end' ), 999 );
	}

	/**
	 * Adds a function to this cron event.
	 *
	 * This method adds a callable function to the event using the `add_action()` function.
	 * The function will be executed when the event is triggered.
	 *
	 * @param callable $cron_function The function to be added to the event. It must be a valid callable.
	 * @param int      $priority      The priority of the function execution. Default is 10.
	 *
	 * @return void
	 */
	public function add_function( callable $cron_function, int $priority = 10 ): void {
		add_action( $this->event, $cron_function, $priority );
	}


	/**
	 * Retrieves the event identifier.
	 *
	 * @return string The identifier of the event.
	 */
	public function get_event(): string {
		return $this->event;
	}

	/**
	 * Retrieves the enabled status.
	 *
	 * This method returns the enabled status of the event.
	 *
	 * @return bool The enabled status. True if the event is enabled, false otherwise.
	 */
	public function get_is_enabled(): bool {
		return $this->is_enabled;
	}

	/**
	 * Retrieves the recurrence pattern.
	 *
	 * @return string
	 */
	public function get_recurrence(): string {
		return $this->recurrence;
	}

	/**
	 * Updates the settings for the cron job.
	 *
	 * @param bool   $is_enabled Determines if the cron job is enabled or disabled.
	 * @param string $recurrence The recurrence pattern for the cron job.
	 *
	 * @return void
	 */
	public function update_settings( bool $is_enabled, string $recurrence ): void {
		$this->is_enabled = $is_enabled;
		$this->recurrence = $recurrence;
		$this->check_status();
	}

	/**
	 * Sets the enabled status of the event.
	 *
	 * This method sets the enabled status of the event.
	 *
	 * @param bool $is_enabled     The new enabled status of the event.
	 * @param bool $check_status   Whether to check the status of the event after setting the new status. Default is true.
	 *
	 * @return void
	 */
	public function set_is_enabled( bool $is_enabled, bool $check_status = true ): void {
		$this->is_enabled = $is_enabled;

		if ( $check_status ) {
			$this->check_status();
		}
	}

	/**
	 * Run the cron job now.
	 *
	 * @return bool Returns true if the cron job was successfully scheduled, or false if it failed.
	 */
	public function run_now(): bool {
		$this->clear();
		return $this->schedule();
	}

	/**
	 * Retrieves the next scheduled timestamp for the event.
	 *
	 * This method checks if the next scheduled timestamp has already been set. If not, it uses the
	 * `wp_next_scheduled()` function to fetch the next scheduled timestamp for the specified event.
	 *
	 * @return false|int The next scheduled timestamp for the event, or false if no timestamp is found.
	 */
	public function get_next_scheduled(): false|int {
		if ( ! isset( $this->next_scheduled ) ) {
			$this->next_scheduled = wp_next_scheduled( $this->event );
		}

		return $this->next_scheduled;
	}

	/**
	 * Retrieves the status of the sync event.
	 *
	 * @return string
	 */
	public function get_status(): string {
		$next_scheduled = $this->get_next_scheduled();

		if ( $next_scheduled && $this->is_enabled ) { // Sync is enabled and scheduled.
			$schedule = wp_get_schedule( $this->event );

			if ( $schedule !== $this->recurrence ) { // Sync is enabled but scheduled with wrong recurrence.
				$status = 'unexpected_recurrence';

			} elseif ( time() > $next_scheduled ) { // Sync is running NOW.
				$status = 'running';

			} else { // Sync is scheduled.
				$status = 'enabled';
			}
		} elseif ( $next_scheduled && ! $this->is_enabled ) { // Sync is disabled but scheduled.
			$status = 'unexpected_running';

		} elseif ( ! $next_scheduled && $this->is_enabled ) { // Sync is enabled but not scheduled.
			$status = 'inactive';

		} else { // Sync is disabled and not running.
			$status = 'disabled';
		}

		return $status;
	}

	/**
	 * Get status texts of this event.
	 */
	public function get_status_texts(): array {
		$texts = array(
			'class'       => 'error',
			'header'      => __( 'Unknown', 'northmill-online-shared' ),
			'description' => __( 'Failed to get sync status.', 'northmill-online-shared' ),
		);

		switch ( $this->get_status() ) {
			case 'disabled':
				$texts['class']       = 'warning';
				$texts['header']      = __( 'Disabled in settings', 'northmill-online-shared' );
				$texts['description'] = '';
				break;
			case 'unexpected_recurrence':
				$texts['class']       = 'warning';
				$texts['header']      = __( 'Unexpected interval', 'northmill-online-shared' );
				$texts['description'] = __( 'Sync is enabled with an unexpected interval. Try updating it in settings.', 'northmill-online-shared' );
				break;
			case 'unexpected_running':
				$texts['class']       = 'warning';
				$texts['header']      = __( 'Running unexpectedly', 'northmill-online-shared' );
				$texts['description'] = __( 'Sync is running despite being disabled. Try the reset button below.', 'northmill-online-shared' );
				break;
			case 'inactive':
				$texts['class']       = 'error';
				$texts['header']      = __( 'Stopped', 'northmill-online-shared' );
				$texts['description'] = __( 'Automatic sync has stopped unexpectedly. Use the reset button below to reactivate it.', 'northmill-online-shared' );
				break;
			case 'running':
				$texts['class']  = 'success';
				$texts['header'] = __( 'Enabled', 'northmill-online-shared' );

				$texts['description'] = sprintf(
				// translators: %s: How many seconds have passed from sync start.
					__( 'Sync is currently in progress. Started %s ago.', 'northmill-online-shared' ),
					$this->get_human_diff()
				);
				break;
			case 'enabled':
				$texts['class']  = 'success';
				$texts['header'] = __( 'Enabled', 'northmill-online-shared' );

				$texts['description'] = sprintf(
				// translators: %s: Time to next sync.
					__( 'Next sync will start in %s', 'northmill-online-shared' ),
					$this->get_human_diff()
				);
				break;
		}

		return $texts;
	}

	/**
	 * Print the status texts of this event.
	 *
	 * @param bool $return_only Whether to only return the HTML string instead of echoing it. Default is false.
	 *
	 * @return string HTML string of the status texts.
	 */
	public function print_status_texts( bool $return_only = false ): string {
		$sync_status_texts = $this->get_status_texts();

		ob_start();
		echo '<h4 class="nm-sync-status ' . esc_attr( $sync_status_texts['class'] ) . '">';
		echo esc_html( $sync_status_texts['header'] );
		echo '</h4>';
		if ( ! empty( $sync_status_texts['description'] ) ) {
			echo '<p>';
			echo esc_html( $sync_status_texts['description'] );
			echo '</p>';
		}

		$output = ob_get_clean();

		if ( ! $return_only ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $output;
		}

		return $output;
	}

	/**
	 * Get the human-readable time difference between the current time and the next scheduled time.
	 *
	 * This method calculates the time difference using the human_time_diff() function.
	 *
	 * @return string Returns the human-readable time difference if the next scheduled time is set, or an empty string if it is not set.
	 */
	public function get_human_diff(): string {
		if ( $this->get_next_scheduled() ) {
			return human_time_diff( time(), $this->get_next_scheduled() );
		} else {
			return '';
		}
	}

	/**
	 * Checks the status of the event and performs any necessary actions.
	 *
	 * @return bool Returns true if the status of this event was fixed, or if it was already valid.
	 */
	public function check_status(): bool {
		$status = $this->get_status();

		if ( 'inactive' === $status ) {
			return $this->schedule();
		} elseif ( 'unexpected_running' === $status ) {
			return $this->clear();
		} elseif ( 'unexpected_recurrence' === $status ) {
			return $this->clear() && $this->schedule();
		}

		Abstract_Shared_Base::base_instance()->logger->log(
			'cron',
			'Checked status for ' . $this->event . ', no change needed.'
		);

		return true;
	}

	/**
	 * Schedule an event.
	 *
	 * This method schedules an event using the wp_schedule_event() function.
	 *
	 * @return bool Returns true if the event is (or already was) successfully scheduled, or false if scheduling failed.
	 */
	public function schedule(): bool {
		if ( ! $this->get_next_scheduled() ) {
			$scheduled = wp_schedule_event( time(), $this->recurrence, $this->event );

			if ( $scheduled && ! is_wp_error( $scheduled ) ) {
				$this->next_scheduled = wp_next_scheduled( $this->event );
				Abstract_Shared_Base::base_instance()->logger->log(
					'cron',
					'Cron job added',
					array(
						'event' => $this->event,
					),
					Log_Level_Enum::NOTICE
				);
			} else {
				Abstract_Shared_Base::base_instance()->logger->log(
					'cron',
					'Adding cron job failed',
					array(
						'event'    => $this,
						'wp_error' => is_wp_error( $scheduled ) ? $scheduled->get_error_message() : null,
					),
					Log_Level_Enum::ERROR
				);
				return false;
			}
		}

		return true;
	}

	/**
	 * Clear a scheduled event.
	 *
	 * This method clears a scheduled event using the wp_clear_scheduled_hook() function.
	 *
	 * @return bool Returns true if the event is successfully cleared (or wasn't scheduled to begin with), or false if clearing failed.
	 */
	public function clear(): bool {

		if ( $this->get_next_scheduled() ) {
			$cleared = wp_clear_scheduled_hook( $this->event );

			if ( $cleared && ! is_wp_error( $cleared ) ) {
				$this->next_scheduled = wp_next_scheduled( $this->event );
				Abstract_Shared_Base::base_instance()->logger->log(
					'cron',
					'Cron job cleared',
					array(
						'event' => $this,
					),
					Log_Level_Enum::NOTICE
				);
			} else {
				Abstract_Shared_Base::base_instance()->logger->log(
					'cron',
					'Clearing cron job failed',
					array(
						'event'    => $this,
						'wp_error' => is_wp_error( $cleared ) ? $cleared->get_error_message() : null,
					),
					Log_Level_Enum::ERROR
				);
				return false;
			}
		}

		return true;
	}

	/**
	 * Log function on cron start.
	 *
	 * @return void
	 */
	public function log_start(): void {
		Abstract_Shared_Base::base_instance()->logger->log(
			'cron',
			'Cron job started for event: ' . $this->get_event()
		);
	}

	/**
	 * Log function on cron end.
	 * Will indicate in log that all cron jobs have been run without crashing.
	 *
	 * @return void
	 */
	public function log_end(): void {
		Abstract_Shared_Base::base_instance()->logger->log(
			'cron',
			'Cron job ended for event: ' . $this->get_event()
		);
	}
}
