Current File : /home/digitaw/www/wp-content/plugins/simple-history/inc/channels/class-channels-manager.php
<?php

namespace Simple_History\Channels;

use Simple_History\Services\Service;
use Simple_History\Channels\Interfaces\Channel_Interface;

/**
 * Manages all log forwarding channels.
 *
 * This service coordinates the registration and processing of channels
 * that forward Simple History events to external systems.
 *
 * @since 4.4.0
 */
class Channels_Manager extends Service {
	/**
	 * Array of registered channels.
	 *
	 * @var Channel_Interface[]
	 */
	private array $channels = [];

	/**
	 * Alert rules engine instance.
	 *
	 * @var Alert_Rules_Engine|null
	 */
	private ?Alert_Rules_Engine $rules_engine = null;

	/**
	 * Called when service is loaded.
	 */
	public function loaded() {
		// Register core channels.
		$this->register_core_channels();

		// Hook for premium channels to register themselves.
		do_action( 'simple_history/channels/register', $this );

		// Hook into the logging system to process events.
		add_action( 'simple_history/log/inserted', [ $this, 'process_logged_event' ], 10, 3 );
	}

	/**
	 * Register core channels that come with the free plugin.
	 */
	private function register_core_channels() {
		$this->register_channel( new File_Channel() );
	}

	/**
	 * Register an channel.
	 *
	 * @param Channel_Interface $channel The channel to register.
	 * @return bool True on success, false if already registered.
	 */
	public function register_channel( Channel_Interface $channel ) {
		$slug = $channel->get_slug();

		if ( isset( $this->channels[ $slug ] ) ) {
			return false; // Already registered.
		}

		$this->channels[ $slug ] = $channel;

		// Call loaded() to allow channel to register hooks.
		// This is separate from construction to keep instantiation side-effect free.
		$channel->loaded();

		/**
		 * Fired when an channel is registered.
		 *
		 * @param Channel_Interface $channel The registered channel.
		 * @param Channels_Manager $manager This manager instance.
		 */
		do_action( 'simple_history/channels/registered', $channel, $this );

		return true;
	}

	/**
	 * Get a registered channel by slug.
	 *
	 * @param string $slug The channel slug.
	 * @return Channel_Interface|null The channel or null if not found.
	 */
	public function get_channel( $slug ) {
		return $this->channels[ $slug ] ?? null;
	}

	/**
	 * Get all registered channels.
	 *
	 * @return Channel_Interface[] Array of channels.
	 */
	public function get_channels() {
		return $this->channels;
	}

	/**
	 * Get all enabled channels.
	 *
	 * @return Channel_Interface[] Array of enabled channels.
	 */
	public function get_enabled_channels() {
		return array_filter(
			$this->channels,
			function ( $channel ) {
				return $channel->is_enabled();
			}
		);
	}

	/**
	 * Process a logged event and send to enabled channels.
	 *
	 * @param array $context Context data for the event.
	 * @param array $data Event data.
	 * @param mixed $logger Logger instance that created the event.
	 */
	public function process_logged_event( $context, $data, $logger ) {
		$enabled_channels = $this->get_enabled_channels();

		if ( empty( $enabled_channels ) ) {
			return;
		}

		// Prepare event data for channels.
		$event_data = $this->prepare_event_data( $context, $data, $logger );

		foreach ( $enabled_channels as $channel ) {
			$this->send_to_channel( $channel, $event_data );
		}
	}

	/**
	 * Prepare event data for sending to channels.
	 *
	 * @param array $context Context data for the event.
	 * @param array $data Event data.
	 * @param mixed $logger Logger instance that created the event.
	 * @return array Prepared event data.
	 */
	private function prepare_event_data( $context, $data, $logger ) {
		return [
			'id'              => $data['id'] ?? null,
			'date'            => $data['date'] ?? current_time( 'mysql' ),
			'logger'          => $data['logger'] ?? '',
			'level'           => $data['level'] ?? 'info',
			'message'         => $data['message'] ?? '',
			'initiator'       => $data['initiator'] ?? '',
			'context'         => $context,
			'logger_instance' => $logger,
		];
	}

	/**
	 * Send event data to a specific channel.
	 *
	 * @param Channel_Interface $channel The channel to send to.
	 * @param array             $event_data The event data to send.
	 */
	private function send_to_channel( Channel_Interface $channel, $event_data ) {
		// Check if the event should be sent based on alert rules.
		if ( ! $channel->should_send_event( $event_data ) ) {
			return;
		}

		// Format the message for the channel.
		$formatted_message = $this->format_message_for_channel( $channel, $event_data );

		// Determine if we should process async or sync.
		if ( $channel->supports_async() && $this->should_process_async( $channel ) ) {
			$this->queue_for_async_processing( $channel, $event_data, $formatted_message );
		} else {
			$this->send_sync( $channel, $event_data, $formatted_message );
		}
	}

	/**
	 * Format a message for a specific channel.
	 *
	 * @param Channel_Interface $channel The channel.
	 * @param array             $event_data The event data.
	 * @return string The formatted message.
	 */
	private function format_message_for_channel( Channel_Interface $channel, $event_data ) {
		// For now, use a simple format. This will be enhanced later.
		$message = $event_data['message'];

		// Interpolate context variables into the message.
		if ( ! empty( $event_data['context'] ) ) {
			foreach ( $event_data['context'] as $key => $value ) {
				if ( is_string( $value ) || is_numeric( $value ) ) {
					$message = str_replace( '{' . $key . '}', $value, $message );
				}
			}
		}

		return $message;
	}

	/**
	 * Check if a channel should be processed asynchronously.
	 *
	 * @param Channel_Interface $channel The channel.
	 * @return bool True if should process async.
	 */
	private function should_process_async( Channel_Interface $channel ) {
		// For now, always process async if supported.
		// This could be configurable in the future.
		return true;
	}

	/**
	 * Queue an event for asynchronous processing.
	 *
	 * @param Channel_Interface $channel The channel.
	 * @param array             $event_data The event data.
	 * @param string            $formatted_message The formatted message.
	 */
	private function queue_for_async_processing( Channel_Interface $channel, $event_data, $formatted_message ) {
		// TODO: Implement async queue system using WordPress cron.
		// For now, fall back to synchronous processing.
		$this->send_sync( $channel, $event_data, $formatted_message );
	}

	/**
	 * Send an event synchronously to an channel.
	 *
	 * @param Channel_Interface $channel The channel.
	 * @param array             $event_data The event data.
	 * @param string            $formatted_message The formatted message.
	 */
	private function send_sync( Channel_Interface $channel, $event_data, $formatted_message ) {
		try {
			$channel->send_event( $event_data, $formatted_message );
		} catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
			// Errors are tracked by individual channels via their error handling.
		}
	}
}