<?php

/**
 * Vonage Client Library for PHP
 *
 * @copyright Copyright (c) 2016-2020 Vonage, Inc. (http://vonage.com)
 * @license https://github.com/Vonage/vonage-php-sdk-core/blob/master/LICENSE.txt Apache License 2.0
 */

declare(strict_types=1);

namespace Vonage\Message;

use InvalidArgumentException;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\ResponseInterface;
use ReflectionClass;
use ReflectionException;
use RuntimeException;
use Vonage\Client\APIResource;
use Vonage\Client\ClientAwareInterface;
use Vonage\Client\ClientAwareTrait;
use Vonage\Client\Exception as ClientException;
use Vonage\Client\Exception\ThrottleException;
use Vonage\Entity\Filter\FilterInterface;
use Vonage\Entity\Filter\KeyValueFilter;

use function class_exists;
use function count;
use function get_class;
use function is_array;
use function is_null;
use function is_string;
use function json_decode;
use function preg_match;
use function sleep;
use function sprintf;
use function strpos;
use function strtolower;
use function substr;
use function trigger_error;
use function ucfirst;

/**
 * Class Client
 *
 * @deprecated Use \Vonage\SMS\Client instead
 *
 * @method Text sendText(string $to, string $from, string $text, array $additional = []) Send a Test Message
 */
class Client implements ClientAwareInterface
{
    /**
     * @deprecated This service will no longer be directly ClientAware
     */
    use ClientAwareTrait;

    /**
     * @var APIResource
     */
    protected $api;

    public function __construct(APIResource $api = null)
    {
        $this->api = $api;
    }

    /**
     * Shim to handle older instantiations of this class
     *
     * @deprecated Will remove in v3
     */
    protected function getApiResource(): APIResource
    {
        if (is_null($this->api)) {
            $api = new APIResource();
            $api->setClient($this->getClient())
                ->setBaseUrl($this->getClient()->getRestUrl())
                ->setIsHAL(false)
                ->setErrorsOn200(true);
            $api->setExceptionErrorHandler(
                function (ResponseInterface $response) {
                    //check for valid data, as well as an error response from the API
                    if ((int)$response->getStatusCode() === 429) {
                        throw new ClientException\Request('too many concurrent requests', $response->getStatusCode());
                    }

                    $data = json_decode($response->getBody()->getContents(), true);

                    if (!isset($data['messages'])) {
                        if (isset($data['error-code'], $data['error-code-label'])) {
                            $e = new ClientException\Request($data['error-code-label'], (int)$data['error-code']);
                        } else {
                            $e = new ClientException\Request('unexpected response from API');
                        }

                        $e->setEntity($data);
                        throw $e;
                    }

                    //normalize errors (client vrs server)
                    foreach ($data['messages'] as $part) {
                        switch ($part['status']) {
                            case '0':
                                break; //all okay
                            case '1':
                                $e = new ThrottleException($part['error-text']);
                                $e->setTimeout(1);
                                $e->setEntity($data);

                                if (preg_match('#\[\s+(\d+)\s+]#', $part['error-text'], $match)) {
                                    $e->setTimeout((int)$match[1] + 1);
                                }

                                throw $e;
                            case '5':
                                $e = new ClientException\Server($part['error-text'], (int)$part['status']);
                                $e->setEntity($data);
                                throw $e;
                            default:
                                $e = new ClientException\Request($part['error-text'], (int)$part['status']);
                                $e->setEntity($data);
                                throw $e;
                        }
                    }
                }
            );

            $this->api = $api;
        }

        return clone $this->api;
    }

    /**
     * @param Message|array $message
     *
     * @throws ClientException\Exception
     * @throws ClientException\Request
     * @throws ClientException\Server
     * @throws ClientExceptionInterface
     */
    public function send($message): Message
    {
        if (!($message instanceof MessageInterface)) {
            trigger_error(
                'Passing an array to Vonage\Messages\Client::send() is deprecated, ' .
                'please pass a MessageInterface object instead',
                E_USER_DEPRECATED
            );

            $message = $this->createMessageFromArray($message);
        }

        $params = @$message->getRequestData(false);

        try {
            $api = $this->getApiResource();
            $api->setBaseUri('/sms/json');
            $api->create($params);

            @$message->setRequest($api->getLastRequest());
            @$message->setResponse($api->getLastResponse());
        } catch (ThrottleException $e) {
            sleep($e->getTimeout());

            $this->send($message);
        } catch (ClientException\Request $e) {
            $e->setEntity($message);

            throw $e;
        }

        return $message;
    }

    /**
     * @throws ClientExceptionInterface
     * @throws ClientException\Exception
     * @throws ClientException\Request
     */
    public function sendShortcode($message): array
    {
        if (!($message instanceof Shortcode)) {
            $message = Shortcode::createMessageFromArray($message);
        }

        $params = $message->getRequestData();

        try {
            $api = $this->getApiResource();
            $api->setBaseUri('/sc/us/' . $message->getType() . '/json');

            $body = $api->create($params);
        } catch (ClientException\Request $e) {
            $e->setEntity($message);
            throw $e;
        }

        foreach ($body['messages'] as $m) {
            if ((int)$m['status'] !== 0) {
                $e = new ClientException\Request($m['error-text'], $m['status']);
                $e->setEntity($message);
                throw $e;
            }
        }

        return $body;
    }

    /**
     * @todo Fix all this error detection so it's standard
     *
     * @throws ClientExceptionInterface
     * @throws ClientException\Exception
     * @throws ClientException\Request
     *
     * @deprecated Please use the Reports API instead
     */
    public function get($query): array
    {
        if ($query instanceof Query) {
            $params = $query->getParams();
        } elseif ($query instanceof FilterInterface) {
            $params = $query->getQuery();
        } elseif ($query instanceof MessageInterface) {
            $params = ['ids' => [$query->getMessageId()]];
        } elseif (is_string($query)) {
            $params = ['ids' => [$query]];
        } elseif (is_array($query)) {
            $params = ['ids' => $query];
        } else {
            throw new InvalidArgumentException(
                'query must be an instance of Query, ' .
                'MessageInterface, string ID, or array of IDs.'
            );
        }

        $api = $this->getApiResource();

        try {
            $data = $api->get('/search/messages', (new KeyValueFilter($params))->getQuery());
        } catch (ClientException\Request $e) {
            $response = $api->getLastResponse();

            if (null !== $response) {
                $response->getBody()->rewind();

                if (null !== $api->getLastResponse()) {
                    $body = $api->getLastResponse()->getBody()->getContents();
                }
            }

            if (empty($body)) {
                $e = new ClientException\Request('error status from API', $e->getCode());
                $response->getBody()->rewind();
                $e->setEntity($response);

                throw $e;
            }

            throw $e;
        }

        if (!isset($data['items'])) {
            // Check if we just got a single result instead of a list
            if (isset($data['message-id'])) {
                $newData = [];
                $newData['items'][] = $data;
                $data = $newData;
            } else {
                // Otherwise we got an unexpected response from the API
                $e = new ClientException\Request('unexpected response from API');
                $e->setEntity($data);
                throw $e;
            }
        }

        if (count($data['items']) === 0) {
            return [];
        }

        $collection = [];

        foreach ($data['items'] as $index => $item) {
            switch ($item['type']) {
                case 'MT':
                    $new = new Message($item['message-id']);
                    break;
                case 'MO':
                    $new = new InboundMessage($item['message-id']);
                    break;
                default:
                    $e = new ClientException\Request('unexpected response from API');
                    $e->setEntity($data);
                    throw $e;
            }

            @$new->setResponse($api->getLastResponse());
            $new->setIndex($index);
            $collection[] = $new;
        }

        return $collection;
    }

    /**
     * @todo Fix all this error detection so it's standard
     *
     * @param string|MessageInterface $idOrMessage
     *
     * @throws ClientExceptionInterface
     * @throws ClientException\Exception
     * @throws ClientException\Request
     *
     * @return InboundMessage|Message|MessageInterface|null
     *
     * @deprecated Please use the Reports API instead
     */
    public function search($idOrMessage)
    {
        if ($idOrMessage instanceof MessageInterface) {
            $id = $idOrMessage->getMessageId();
            $message = $idOrMessage;
        } else {
            $id = $idOrMessage;
        }

        $api = $this->getApiResource();

        try {
            $data = $api->get('/search/messages', (new KeyValueFilter(['id' => $id]))->getQuery());
        } catch (ClientException\Request $e) {
            if ($e->getCode() !== 200) {
                // This method had a different error, so switch to the expected error message
                if ($e->getMessage() === 'unexpected response from API') {
                    $entity = $e->getEntity();
                    $e = new ClientException\Request('error status from API', $e->getCode());
                    $e->setEntity($entity);
                    throw $e;
                }

                throw $e;
            }
        }

        $response = $api->getLastResponse();

        if (null !== $response) {
            $status = (int)$response->getStatusCode();

            if ($status !== 200 && isset($data['error-code'])) {
                throw new ClientException\Request($data['error-code-label'], $data['error-code']);
            }

            if ($status === 429) {
                throw new ClientException\Request('too many concurrent requests', $status);
            }

            if ($status !== 200) {
                $e = new ClientException\Request('error status from API', $status);
                $response->getBody()->rewind();
                $e->setEntity($response);

                throw $e;
            }

            if (empty($data)) {
                $e = new ClientException\Request('no message found for `' . $id . '`');
                $response->getBody()->rewind();
                $e->setEntity($response);

                throw $e;
            }

            switch ($data['type']) {
                case 'MT':
                    $new = new Message($data['message-id']);
                    break;
                case 'MO':
                    $new = new InboundMessage($data['message-id']);
                    break;
                default:
                    $e = new ClientException\Request('unexpected response from API');
                    $e->setEntity($data);
                    throw $e;
            }

            if (isset($message) && !($message instanceof $new)) {
                throw new ClientException\Exception(
                    sprintf(
                        'searched for message with type `%s` but message of type `%s`',
                        get_class($message),
                        get_class($new)
                    )
                );
            }

            if (!isset($message)) {
                $message = $new;
            }

            @$message->setResponse($response);

            return $message;
        }

        return null;
    }

    /**
     * @todo Fix all this error detection so it's standard
     *
     * @throws ClientExceptionInterface
     * @throws ClientException\Exception
     * @throws ClientException\Request
     *
     * @deprecated Please use the Reports API instead
     */
    public function searchRejections(Query $query): array
    {
        $params = $query->getParams();
        $api = $this->getApiResource();

        try {
            $data = $api->get('/search/rejections', (new KeyValueFilter($params))->getQuery());
        } catch (ClientException\Request $e) {
            if ($e->getMessage() === 'unexpected response from API') {
                $entity = $e->getEntity();
                $e = new ClientException\Request('error status from API', $e->getCode());
                $e->setEntity($entity);
                throw $e;
            }

            throw $e;
        }

        $response = $api->getLastResponse();

        if (null !== $response) {
            $status = $response->getStatusCode();

            if ($status !== 200 && isset($data['error-code'])) {
                throw new ClientException\Request($data['error-code-label'], $data['error-code']);
            }

            if ($status !== 200) {
                $e = new ClientException\Request('error status from API', $status);
                $response->getBody()->rewind();
                $e->setEntity($response);

                throw $e;
            }

            if (!isset($data['items'])) {
                $e = new ClientException\Request('unexpected response from API');
                $e->setEntity($data);

                throw $e;
            }

            if (count($data['items']) === 0) {
                return [];
            }

            $collection = [];

            foreach ($data['items'] as $index => $item) {
                switch ($item['type']) {
                    case 'MT':
                        $new = new Message($item['message-id']);
                        break;
                    case 'MO':
                        $new = new InboundMessage($item['message-id']);
                        break;
                    default:
                        $e = new ClientException\Request('unexpected response from API');
                        $e->setEntity($data);
                        throw $e;
                }

                @$new->setResponse($response);
                $new->setIndex($index);

                $collection[] = $new;
            }

            return $collection;
        }

        return [];
    }

    /**
     * @param MessageInterface|array $message
     */
    protected function createMessageFromArray($message): Message
    {
        if (!is_array($message)) {
            throw new RuntimeException('message must implement `' . MessageInterface::class . '` or be an array`');
        }

        foreach (['to', 'from'] as $param) {
            if (!isset($message[$param])) {
                throw new InvalidArgumentException('missing expected key `' . $param . '`');
            }
        }

        $to = $message['to'];
        $from = $message['from'];

        unset($message['to'], $message['from']);

        return new Message($to, $from, $message);
    }

    /**
     * Convenience feature allowing messages to be sent without creating a message object first.
     *
     * @throws ClientExceptionInterface
     * @throws ClientException\Exception
     * @throws ClientException\Request
     * @throws ClientException\Server
     * @throws ReflectionException
     *
     * @return array|Message|MessageInterface
     */
    public function __call(string $name, $arguments)
    {
        if (strpos($name, "send") !== 0) {
            throw new RuntimeException(
                sprintf(
                    '`%s` is not a valid method on `%s`',
                    $name,
                    get_class($this)
                )
            );
        }

        $class = substr($name, 4);
        $class = 'Vonage\\Message\\' . ucfirst(strtolower($class));

        if (!class_exists($class)) {
            throw new RuntimeException(
                sprintf(
                    '`%s` is not a valid method on `%s`',
                    $name,
                    get_class($this)
                )
            );
        }

        $reflection = new ReflectionClass($class);
        /** @var Message $message */
        $message = $reflection->newInstanceArgs($arguments);

        return $this->send($message);
    }
}
