<?php declare(strict_types=1);
/**
 * @package messenger
 * @author kputyr
 * @date 17.04.2025
 */

namespace IMATHUZH\Messenger;

use Iterator;

/**
 * Implements a notification system based on an HTTP server.
 * 
 * A list of channels is translated to an URL, to which the message
 * is posted. For example it can be a publisher location served by
 * Nchan that distributes the message further to subscribers of the
 * channels.
 */
class HttpPublisher implements MessagePublisherInterface
{
	/** @var array HTTP options for file_get_contents() */
	protected $options;

	/** @var resource Cached stream context */
	private $context;

	/** @var string The URL pattern for publisher locations */
	protected $urlPattern;

	/**
	 * @param string $urlPattern  The pattern URL for Nchan locations. The substring
	 *                            :pub: is replaced with a list of channels.
	 * @param array $headers    Extra HTTP headers
	 * @param array $options    Extra HTTP options, such time timeout
	 */
	public function __construct(
		string $urlPattern,
		array $headers = [],
		array $options = []
	) {
		$this->urlPattern = $urlPattern;
		// Prepare the context
		$this->options = array_merge($options, [
			'method' => 'POST',
			'ignore_errors' => true,
			'header' => $headers,
			'user_agent' => Constants::USER_AGENT
		]);
	}

	/**
	 * Translates a list of channels to the associated URLs.
	 * 
	 * This method returns an iterator of ULRs despite returning
	 * producing only one, because derived classes may want to
	 * provided more that one URL.
	 * 
	 * @param string[] $channels
	 * @return Iterator
	 */
	protected function pathsFromChannels(array $channels): Iterator
	{
		// Despite returning only one URL, we keep this as an iterator
		// so that it is easy to extent the class to serve several
		// locations.
		if ($channels) yield strtr($this->urlPattern, [':pub:' => implode(",", $channels)]);
	}

	/**
	 * Returns true if the provided HTTP code indicates that
	 * a message has been accepted for publishing.
	 */
	protected function isValidResponseCode(int $httpCode): bool
	{
		return $httpCode >= 200 && $httpCode < 300;
	}

	/**
	 * Performs an HTTP request that posts the message to
	 * the provided URL.
	 * 
	 * Making this a separate method simplifies mocking this
	 * class in unit tests.
	 * 
	 * @param string $url
	 * @return ?string    HTTP response header or null if none
	 */
	protected function action(string $url): ?string
	{
		$result = @file_get_contents($url, false, $this->context);
		return $result === false ? null : $http_response_header[0];
	}

	/**
	 * Preforms an HTTP request that posts the message and
	 * analyses the response for potential problems.
	 * 
	 * @param string $url
	 * @return ?string  a potential error
	 */
	protected function post(string $url): ?string
	{
		// Supress warnings - the result is checked later for errors
		$header = $this->action($url);
		// For for general problems (no connection, wrong url, etc.)
		if ($header === null) {
			return "bad request";
		}
		// Assert that a valid HTTP response is given
		$has_status = preg_match('{HTTP\/\S*\s(\d{3})}', $header, $match);
		if (!$has_status) {
			return "bad response";
		}
		// Check the response HTTP code for errors
		$status = (int)($match[1]);
		if (!$this->isValidResponseCode($status)) {
			return "invalid HTTP status $status";
		}
		// The message has been posted sucecssfully
		return null;
	}
	
	/**
	 * @inheritDoc
	 */
	public function publishMessage(array $channels, string $msg): Iterator
	{
		// Create the HTTP context
		$this->options['content'] = $msg;
		$this->context = stream_context_create(['http' => $this->options]);
		// POST the message to all associated URLs
		foreach ($this->pathsFromChannels($channels) as $url) {
			$error = $this->post($url);
			if ($error) yield $url => $error;
		}
	}
}
