<?php

namespace VendorDuplicator\Aws\ClientSideMonitoring;

use VendorDuplicator\Aws\CommandInterface;
use VendorDuplicator\Aws\Exception\AwsException;
use VendorDuplicator\Aws\MonitoringEventsInterface;
use VendorDuplicator\Aws\ResponseContainerInterface;
use VendorDuplicator\Aws\ResultInterface;
use VendorDuplicator\GuzzleHttp\Promise;
use VendorDuplicator\Psr\Http\Message\RequestInterface;
use VendorDuplicator\Psr\Http\Message\ResponseInterface;
/**
 * @internal
 */
abstract class AbstractMonitoringMiddleware implements MonitoringMiddlewareInterface
{
    private static $socket;
    private $nextHandler;
    private $options;
    protected $credentialProvider;
    protected $region;
    protected $service;
    protected static function getAwsExceptionHeader(AwsException $e, $headerName)
    {
        $response = $e->getResponse();
        if ($response !== null) {
            $header = $response->getHeader($headerName);
            if (!empty($header[0])) {
                return $header[0];
            }
        }
        return null;
    }
    protected static function getResultHeader(ResultInterface $result, $headerName)
    {
        if (isset($result['@metadata']['headers'][$headerName])) {
            return $result['@metadata']['headers'][$headerName];
        }
        return null;
    }
    protected static function getExceptionHeader(\Exception $e, $headerName)
    {
        if ($e instanceof ResponseContainerInterface) {
            $response = $e->getResponse();
            if ($response instanceof ResponseInterface) {
                $header = $response->getHeader($headerName);
                if (!empty($header[0])) {
                    return $header[0];
                }
            }
        }
        return null;
    }
    /**
     * Constructor stores the passed in handler and options.
     *
     * @param callable $handler
     * @param callable $credentialProvider
     * @param $options
     * @param $region
     * @param $service
     */
    public function __construct(callable $handler, callable $credentialProvider, $options, $region, $service)
    {
        $this->nextHandler = $handler;
        $this->credentialProvider = $credentialProvider;
        $this->options = $options;
        $this->region = $region;
        $this->service = $service;
    }
    /**
     * Standard invoke pattern for middleware execution to be implemented by
     * child classes.
     *
     * @param  CommandInterface $cmd
     * @param  RequestInterface $request
     * @return Promise\PromiseInterface
     */
    public function __invoke(CommandInterface $cmd, RequestInterface $request)
    {
        $handler = $this->nextHandler;
        $eventData = null;
        $enabled = $this->isEnabled();
        if ($enabled) {
            $cmd['@http']['collect_stats'] = \true;
            $eventData = $this->populateRequestEventData($cmd, $request, $this->getNewEvent($cmd, $request));
        }
        $g = function ($value) use ($eventData, $enabled) {
            if ($enabled) {
                $eventData = $this->populateResultEventData($value, $eventData);
                $this->sendEventData($eventData);
                if ($value instanceof MonitoringEventsInterface) {
                    $value->appendMonitoringEvent($eventData);
                }
            }
            if ($value instanceof \Exception || $value instanceof \Throwable) {
                return Promise\Create::rejectionFor($value);
            }
            return $value;
        };
        return Promise\Create::promiseFor($handler($cmd, $request))->then($g, $g);
    }
    private function getClientId()
    {
        return $this->unwrappedOptions()->getClientId();
    }
    private function getNewEvent(CommandInterface $cmd, RequestInterface $request)
    {
        $event = ['Api' => $cmd->getName(), 'ClientId' => $this->getClientId(), 'Region' => $this->getRegion(), 'Service' => $this->getService(), 'Timestamp' => (int) floor(microtime(\true) * 1000), 'UserAgent' => substr($request->getHeaderLine('User-Agent') . ' ' . \VendorDuplicator\Aws\default_user_agent(), 0, 256), 'Version' => 1];
        return $event;
    }
    private function getHost()
    {
        return $this->unwrappedOptions()->getHost();
    }
    private function getPort()
    {
        return $this->unwrappedOptions()->getPort();
    }
    private function getRegion()
    {
        return $this->region;
    }
    private function getService()
    {
        return $this->service;
    }
    /**
     * Returns enabled flag from options, unwrapping options if necessary.
     *
     * @return bool
     */
    private function isEnabled()
    {
        return $this->unwrappedOptions()->isEnabled();
    }
    /**
     * Returns $eventData array with information from the request and command.
     *
     * @param CommandInterface $cmd
     * @param RequestInterface $request
     * @param array $event
     * @return array
     */
    protected function populateRequestEventData(CommandInterface $cmd, RequestInterface $request, array $event)
    {
        $dataFormat = static::getRequestData($request);
        foreach ($dataFormat as $eventKey => $value) {
            if ($value !== null) {
                $event[$eventKey] = $value;
            }
        }
        return $event;
    }
    /**
     * Returns $eventData array with information from the response, including
     * the calculation for attempt latency.
     *
     * @param ResultInterface|\Exception $result
     * @param array $event
     * @return array
     */
    protected function populateResultEventData($result, array $event)
    {
        $dataFormat = static::getResponseData($result);
        foreach ($dataFormat as $eventKey => $value) {
            if ($value !== null) {
                $event[$eventKey] = $value;
            }
        }
        return $event;
    }
    /**
     * Checks if the socket is created. If PHP version is greater or equals to 8 then,
     * it will check if the var is instance of \Socket otherwise it will check if is
     * a resource.
     *
     * @return bool Returns true if the socket is created, false otherwise.
     */
    private function isSocketCreated(): bool
    {
        // Before version 8, sockets are resources
        // After version 8, sockets are instances of Socket
        if (\PHP_MAJOR_VERSION >= 8) {
            $socketClass = '\Socket';
            return self::$socket instanceof $socketClass;
        } else {
            return is_resource(self::$socket);
        }
    }
    /**
     * Creates a UDP socket resource and stores it with the class, or retrieves
     * it if already instantiated and connected. Handles error-checking and
     * re-connecting if necessary. If $forceNewConnection is set to true, a new
     * socket will be created.
     *
     * @param bool $forceNewConnection
     * @return Resource
     */
    private function prepareSocket($forceNewConnection = \false)
    {
        if (!$this->isSocketCreated() || $forceNewConnection || socket_last_error(self::$socket)) {
            self::$socket = socket_create(\AF_INET, \SOCK_DGRAM, \SOL_UDP);
            socket_clear_error(self::$socket);
            socket_connect(self::$socket, $this->getHost(), $this->getPort());
        }
        return self::$socket;
    }
    /**
     * Sends formatted monitoring event data via the UDP socket connection to
     * the CSM agent endpoint.
     *
     * @param array $eventData
     * @return int
     */
    private function sendEventData(array $eventData)
    {
        $socket = $this->prepareSocket();
        $datagram = json_encode($eventData);
        $result = socket_write($socket, $datagram, strlen($datagram));
        if ($result === \false) {
            $this->prepareSocket(\true);
        }
        return $result;
    }
    /**
     * Unwraps options, if needed, and returns them.
     *
     * @return ConfigurationInterface
     */
    private function unwrappedOptions()
    {
        if (!$this->options instanceof ConfigurationInterface) {
            try {
                $this->options = ConfigurationProvider::unwrap($this->options);
            } catch (\Exception $e) {
                // Errors unwrapping CSM config defaults to disabling it
                $this->options = new Configuration(\false, ConfigurationProvider::DEFAULT_HOST, ConfigurationProvider::DEFAULT_PORT);
            }
        }
        return $this->options;
    }
}
