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

namespace IMATHUZH\Messenger;

require_once dirname(__DIR__) . '/Classes/Constants.php';
require_once dirname(__DIR__) . '/Classes/TokenInterface.php';
require_once dirname(__DIR__) . '/Classes/AuthorizationToken.php';
require_once dirname(__DIR__) . '/Classes/TokenManagerInterface.php';
require_once dirname(__DIR__) . '/Classes/SessionTokenManager.php';


/**
 * A helper function to immediately reject a request
 */
function reject(string $message): bool {
    throw new \Exception($message);    
}


class NchanResponse
{
    public function __construct(
        public array $headers,
        public string $body
    ) { }

    public function asJson(): ?object {
        static $parsedBody;
        if (!isset($parsedBody)) {
            $parsedBody = json_decode($this->body, false);
        }
        return $parsedBody;
    }

    public function getLastMessageId(): string {
        return $this->asJson()->last_message_id ?? '';
    }
}

/**
 * An implementation of the helper request class for nchan hooks
 */
class NchanRequest extends AbstractNchanRequest
{
    const ID_GENERATOR_URL = '/messenger/:group:/generator';


    /**
     * @inheritDoc
     */
    public function send(string $payload, string $url): ?NchanResponse {
        $response = file_get_contents($url, false, stream_context_create(['http' => [
            'method' => 'POST',
            'ignore_errors' => true,
            'header' => ["Accept: text/json", "Content-Type: text/json"],
            'content' => $payload
        ]])) ?: '';
        return $response === false ? null : new NchanResponse($http_response_header ?? [], $response);
    }

    public function generateClientId(): string {
        $response = $this->send(
            $this->generateControllerMessage('user-id'),
            $this->baseServerUrl . strtr(
                self::ID_GENERATOR_URL,
                [':group:' => $this->group]
            )
        );
        return $response ? strtr($response->getLastMessageId(), [':' => '-', ',' => '_', '[' => '', ']' => '']) : '';
    }
}


/**
 * A base class for nchan requests
 */
abstract class AbstractNchanRequest
{

    const SERVER_BASE_URL = 'http://localhost:8080';
    /** The pattern for publisher to client's channel */
    const CLIENT_REPLY_URL = '/messenger/:group:/user/:id:';
    /** The pattern for publisher to client's channel */
    const CONTROLLER_REPLY_URL = '/messenger/:group:/controller/:id:';
    /** The pattern for publisher location */
    const PUBLISHING_URL = '/messenger/:group:/pub/:channels:';
    /** The pattern for controller location (specific channels) */
    const CONTROLLING_URL = '/messenger/:group:/ctl/:channels:';


    
    const HEADER_NCHAN_VERSION   = 'HTTP_X_NCHAN_VERSION';
    const HEADER_ORIGINAL_URI    = 'HTTP_X_ORIGINAL_URI';
    const HEADER_SUBSCRIBER_TYPE = 'HTTP_X_SUBSCRIBER_TYPE';

    const HEADER_MESSENGER_TYPE = 'HTTP_X_MESSENGER_TYPE';
    const HEADER_MESSENGER_GROUP = 'HTTP_X_MESSENGER_GROUP';
    const HEADER_MESSENGER_PUB = 'HTTP_X_MESSENGER_PUB';
    const HEADER_MESSENGER_SUB = 'HTTP_X_MESSENGER_SUB';
    const HEADER_MESSENGER_USER = 'HTTP_X_MESSENGER_USER';
    const HEADER_MESSENGER_SESSION = 'HTTP_X_MESSENGER_SESSION';

    const PARAM_TOKEN = 'auth';

    protected string $requesterType = '';

    protected string $clientId = '';

    protected string $sessionId = '';

    /** The group name of the messenger channel */
    protected string $group = '';

    /** A list of publisher channels */
    protected array $pubChannels = [];

    /** A list of subscriber channels */
    protected array $subChannels = [];

    /** The messenger channel associated with the request */
    public $channelUrl = '';

    /** The location for messages to the requester */
    public $responseUrl = '';

    /** The publisher location in case the message must be forwarded */
    public $publishUrl = '';

    /** Channel controller location for notifications */
    public $controllerUrl = '';

    /**
     * Creates an instance of the class from the current HTTP request
     */
    public static function fromServerRequest(): static {
        return (new static(
            self::SERVER_BASE_URL,
            $_SERVER[self::HEADER_ORIGINAL_URI] ?? '',
            $_SERVER[self::HEADER_SUBSCRIBER_TYPE] ?? '',
            $_SERVER[self::HEADER_NCHAN_VERSION] ?? ''
        ))
            // Set channel properties from request headers
            ->setRequesterType($_SERVER[self::HEADER_MESSENGER_TYPE] ?? '')
            ->setChannelGroup($_SERVER[self::HEADER_MESSENGER_GROUP] ?? '')
            ->setFlattenedPubChannels($_SERVER[self::HEADER_MESSENGER_PUB] ?? '')
            ->setFlattenedSubChannels($_SERVER[self::HEADER_MESSENGER_SUB] ?? '')
            ->setClientId($_SERVER[self::HEADER_MESSENGER_USER] ?? '')
            ->setSessionId($_SERVER[self::HEADER_MESSENGER_SESSION] ?? '');
    }

    /**
     * @param string $serverEndpoint - the base URL for server-side messenger locations
     * @param string $requestUri     - the request URL, read from HTTP headers
     * @param string $subscriberType - the subscriber type, read from HTTP headers
     * @param string $nchanVersion   - the version of Nchan, read from HTTP headers
     */
    public function __construct(
        protected string $baseServerUrl,
        public string $requestUri,
        public string $subscriberType,
        public string $nchanVersion
    ) { }

    /**
     * Returns the meta data of the channel:
     *  + `nchanVersion: string`
     *  + `requestUri: string`
     *  + `subscriberType: string`
     * 
     * @return array the associative array with metadata
     */
    public function getRequestMetaData(): array {
        return [
            'nchanVersion' => $this->nchanVersion,
            'requestUri' => $this->requestUri,
            'subscriberType' => $this->subscriberType
        ];
    }

    /**
     * Returns metadata about the channel associated with this request.
     * 
     * The returned array contains the following keys:
     *  + `group : string` - the group name.
     *  + `pub : array` - the list of publisher channels.
     *  + `sub : array` - the list of subscriber channels.
     *  + `ctl : array` - the list of controller channels.
     * 
     * @return array
     */
    public function getChannelMetaData(): array {
        return [
            'group' => $this->group,
            'pub' => $this->pubChannels,
            'sub' => $this->subChannels,
            'clientId' => $this->clientId,
            'sessionId' => $this->getSessionId()
        ];
    }

    public function getRequesterType(): string
    {
        return $this->requesterType ?: 'unknown';
    }

    public function setRequesterType(string $type): static
    {
        $this->requesterType = $type;
        return $this;
    }

    public function getClientId(): string
    {
        return $this->clientId;
    }

    public function setClientId(string $identifier): static
    {
        $this->clientId = $identifier;
        return $this;
    }

    public function getSessionId(): string
    {
        return $this->sessionId ?: 'anonymous';
    }

    public function setSessionId(string $identifier): static
    {
        $this->sessionId = $identifier;
        return $this;
    }

    public function getChannelGroup(): string
    {
        return $this->group;
    }

    public function setChannelGroup(string $group): static
    {
        $this->group = $group;
        return $this;
    }

    public function getPubChannels(): array
    {
        return $this->pubChannels;
    }

    public function getFlattenedPubChannels(): string
    {
        return implode(",", $this->pubChannels);
    }

    public function setPubChannels(array $channels): static
    {
        $this->pubChannels = $channels;
        return $this;
    }

    public function setFlattenedPubChannels(string $channels): static
    {
        $this->pubChannels = $channels ? explode(",", $channels) : [];
        return $this;
    }

    public function getSubChannels(): array
    {
        return $this->subChannels;
    }

    public function getFlattenedSubChannels(): string
    {
        return implode(",", $this->subChannels);
    }

    public function setSubChannels(array $channels): static
    {
        $this->subChannels = $channels;
        return $this;
    }

    public function setFlattenedSubChannels(string $channels): static
    {
        $this->subChannels = $channels ? explode(",", $channels) : [];
        return $this;
    }

    /**
     * Configures URLs associated with the channel data:
     *  + channelUrl - the location for the channel
     *  + controllerUrl - the controller channel for this location
     *  + responseUrl - the URL for responses
     */
    public function generateUrls(): static {
        if (!$this->clientId && $this->subscriberType !== 'http') {
            $this->clientId = $this->generateClientId();
        }
        $dict = [
            ':group:'  => $this->group ?: '-',
            ':client:' => $this->clientId ?: '-',
            ':channels:'    => $this->getFlattenedPubChannels(),
            ':id:'     => $this->clientId
        ];
        if ($this->requesterType === 'controller') {
            $this->responseUrl = strtr(self::CONTROLLER_REPLY_URL, $dict);
            $this->publishUrl = strtr(self::CONTROLLING_URL, $dict);
            $dict[':channels:'] = '';
            $this->controllerUrl = strtr(self::CONTROLLING_URL, $dict);
        } else {
            $this->responseUrl = strtr(self::CLIENT_REPLY_URL, $dict);
            $this->publishUrl = strtr(self::PUBLISHING_URL, $dict);
            $dict[':channels:'] = implode(",", array_unique(
                [...$this->pubChannels, ...$this->subChannels],
                SORT_STRING
            ));
            $this->controllerUrl = strtr(self::CONTROLLING_URL, $dict);
        }
        return $this;
    }

    /**
     * Closes the request with a given HTTP code and an optional message.
     * 
     * @param int $code The HTTP status code to respond with.
     * @param string $msg An optional message to include in the response body.
     */
    public function close(int $code, string $msg=''): void {
        http_response_code($code);
        if ($msg) echo $msg;
//        fastcgi_finish_request();
    }

    public function authorize(): void {
        http_response_code(200);
        header("X-Messenger-Pub: " . $this->getFlattenedPubChannels());
        header("X-Messenger-Sub: " . $this->getFlattenedSubChannels());
        header("X-Messenger-User: " . $this->clientId);
//        fastcgi_finish_request();
    }


    /**
     * Redirects the request to a given URL using X-Accel-Redirect (for GET requests)
     * or by sending a POST request to the publisher channel (for POST requests).
     */
    public function forward(string $body): void {
        $response = $this->send($body, $this->publishUrl);
        if ($response === null) {
            http_response_code(400);
            echo "Failed to send the request to publisher channel";
        } else {
            // Forward the response headers and body from the publisher channel
            foreach($response->headers ?? [] as $header) {
                header($header, false);
            }
            echo $response->body;
        }
        fastcgi_finish_request();
    }

    /**
     * Sends a response to the requester.
     * 
     * @param array $payload The data to send in the response body.
     */
    public function respond(array $payload): ?NchanResponse {
        return $this->send(\json_encode($payload), $this->baseServerUrl.$this->responseUrl);
    }

    protected function generateControllerMessage(string $event, array $payload=[]): string {
        return \json_encode([
            'type' => 'message',
            'name' => $event,
            'payload' => array_merge($payload, $this->getRequestMetaData())
        ]);
    }

    /**
     * Sends a notification to the controller endpoint.
     * 
     * @param array $payload The data to send in the notification.
     */
    public function notifyController(string $event, array $payload = []): ?NchanResponse {
        return $this->send($this->generateControllerMessage($event, $payload), $this->baseServerUrl.$this->controllerUrl);
    }

    /**
     * Sends a payload to a given URL.
     * 
     * @param array $payload The data to send in the request body.
     * @param string $url The URL to which the payload should be sent.
     * @abstract
     */
    abstract public function send(string $payload, string $url): ?NchanResponse;

    abstract public function generateClientId(): string;
}
