<?php
/**
 * @package messenger
 * @author kputyr
 * @date 17.04.2025
 * @dateState 2025-04-28 10:00:00
 */


namespace IMATHUZH\Qfq\Core\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 base URL for publisher locations */
    protected $basepath;

    /**
     * @param string $basepath  The base URL for Nchan locations.
     *                          It may end with '/', but it is not necessary.
     * @param array $headers    Extra HTTP headers
     * @param array $options    Extra HTTP options, such time timeout
     */
    public function __construct(
        string $basepath,
        array $headers = [],
        array $options = []
    ) {
        $this->basepath = str_ends_with($basepath, '/')
            ? $basepath : $basepath . '/';
        // Prepare the context
        $this->options = array_merge($options, [
            'method' => 'POST',
            'ignore_errors' => true,
            'header' => $headers,
            'user_agent' => MESSENGER_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 '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;
    }

    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($this->basepath . $url);
            if ($error) yield ($this->basepath . $url) => $error;
        }
    }
}
