<?php

namespace WeDevs\AppBridge\Core\Notifications;

use InvalidArgumentException;

/**
 * Front door for firing notification events defined in EVENTS.MD.
 *
 * Callers fire by event key; the dispatcher resolves the template from
 * EventRegistry, interpolates context tokens, and hands the resulting
 * Event to NotificationManager for transport.
 *
 * Example:
 *   ( new EventDispatcher() )->dispatch(
 *       'vendor', 'order.new',
 *       [ 'order_id' => 1023, 'amount' => '$49.99' ],
 *       [ 'player_ids' => [ '...' ] ]
 *   );
 */
class EventDispatcher {

	private NotificationManager $manager;
	private string $option_key;

	public function __construct(
		?NotificationManager $manager = null,
		string $option_key = NotificationDriverFactory::DEFAULT_OPTION_KEY
	) {
		$this->manager    = $manager ?? new NotificationManager();
		$this->option_key = $option_key;
	}

	/**
	 * Dispatch a registered event.
	 *
	 * @param string               $app      'customer' or 'vendor'
	 * @param string               $event    event key (e.g. 'order.new')
	 * @param array<string, mixed> $context  values used to interpolate {tokens} in title/body
	 *                                       and to populate the `data` payload
	 * @param array<string, mixed> $audience routing hints (player_ids, segments, fcm_token, fcm_topic)
	 *
	 * @return array<string, bool> driver name => success flag, or empty when the event is filtered out
	 */
	public function dispatch( string $app, string $event, array $context = [], array $audience = [] ): array {
		$definition = EventRegistry::get( $app, $event );

		if ( null === $definition ) {
			throw new InvalidArgumentException(
				sprintf( 'Unknown notification event "%s.%s".', esc_html( $app ), esc_html( $event ) )
			);
		}

		$key = EventRegistry::key( $app, $event );

		// Honor the per-event toggle from the settings UI.
		$enabled = (bool) ( $definition['enabled'] ?? true );

		/**
		 * Allow short-circuiting a specific event.
		 *
		 * Defaults to the saved `enabled` value from settings. Filter callers can
		 * force-enable/disable per request, audience, etc.
		 *
		 * @param bool                $send       Whether to dispatch.
		 * @param string              $key        Fully-qualified event key.
		 * @param array<string,mixed> $context    Runtime values supplied to dispatch().
		 * @param array<string,mixed> $audience   Routing hints supplied to dispatch().
		 * @param array<string,mixed> $definition Registry entry.
		 */
		$should_send = apply_filters( 'app_bridge_should_dispatch_notification', $enabled, $key, $context, $audience, $definition );

		if ( ! $should_send ) {
			return [];
		}

		$notification = $this->build( $key, $definition, $context, $audience );

		/**
		 * Last chance to swap the notification before transport.
		 */
		$notification = apply_filters( 'app_bridge_notification_event', $notification, $key, $context, $audience );

		return $this->manager->send( $notification, $this->option_key );
	}

	/**
	 * @param array<string, mixed> $definition
	 * @param array<string, mixed> $context
	 * @param array<string, mixed> $audience
	 */
	private function build( string $key, array $definition, array $context, array $audience ): Event {
		$title = self::interpolate( (string) $definition['title'], $context );
		$body  = self::interpolate( (string) $definition['body'], $context );

		// Catalog can pin specific drivers per event; otherwise the Event falls
		// back to the provider currently selected in settings via Event::via().
		$via = isset( $definition['via'] ) ? (array) $definition['via'] : [];

		return new Event(
			$key,
			$title,
			$body,
			(string) $definition['notification_channel_id'],
			(string) $definition['screen_route'],
			$context,
			$via,
			$audience,
			$this->option_key
		);
	}

	/**
	 * Replace `{token}` placeholders with values from $context.
	 * Missing tokens are left intact so they're easy to spot in logs.
	 *
	 * @param array<string, mixed> $context
	 */
	private static function interpolate( string $template, array $context ): string {
		if ( '' === $template || empty( $context ) ) {
			return $template;
		}

		return preg_replace_callback(
			'/\{([a-z0-9_]+)\}/i',
			static function ( array $match_item ) use ( $context ) {
				$name = $match_item[1];
				if ( ! array_key_exists( $name, $context ) ) {
					return $match_item[0];
				}
				$value = $context[ $name ];

				return is_scalar( $value ) ? (string) $value : wp_json_encode( $value );
			},
			$template
		);
	}
}
