<?php

namespace onespace\tools\activeApi\components\clients\base;

use onespace\tools\activeApi\components\clients\interface\OneSpaceClientInterface;
use onespace\tools\activeApi\components\clients\OneSpaceServiceClient;
use onespace\tools\enum\general\HTTPMethod;
use Yii;
use yii\httpclient\Request;
use yii\httpclient\Response;
use yii\web\MethodNotAllowedHttpException;

/**
 * Extend from this to create consistent service clients used for Active API models.
 *
 * Other types of clients should extend from this.
 *
 * @author  Gareth Palmer <gareth@one-space.co.za>
 */
abstract class BaseOneSpaceClient extends \yii\base\Component implements OneSpaceClientInterface {
    private OneSpaceServiceClient $_client;
    private string $_serverUrl;
    private string $_lastRequest;

    /**
     * Returns the actual API endpoint base URL.
     *
     * For example:
     *
     * ```php
     * public function serverUrl(): string {
     *   return Yii::$app->params['serviceUrls']['definedService'];
     * }
     * ```
     *
     * @return  string
     *
     * @access  public
     */
    abstract public function serverUrl(): string;

    /**
     * Returns the OneSpace Service Client for connecting to APIs.
     *
     * @access  public
     */
    public function getClient(): OneSpaceServiceClient {
        /** @var OneSpaceServiceClient $client */
        $this->_client ??= Yii::$app->onespaceServiceClient;
        return $this->_client;
    }

    /**
     * Sets the server URL. Calls the `serverUrl` method.
     *
     * @access  public
     */
    public function setServerUrl(): void {
        $this->_serverUrl = $this->serverUrl();
    }

    /**
     * Returns the API server URL.
     *
     * @return  string
     *
     * @access  public
     */
    public function getServerUrl(): string {
        if (!isset($this->_serverUrl)) {
            $this->setServerUrl();
        }
        return $this->_serverUrl;
    }

    /**
     * Returns the previously made raw HTTP request.
     *
     * @return  string
     *
     * @access  public
     */
    public function getPreviousRequest(): string {
        return $this->_lastRequest;
    }

    /**
     * Sets the last request property from the `Request` object.
     *
     * @param   \yii\httpclient\Request $request    The API request object.
     *
     * @access  public
     */
    public function setLastRequest(Request $request): void {
        $this->_lastRequest = $request->toString();
    }

    /**
     * Validate that the HTTP method being called is valid.
     *
     * @param   HTTPMethod|string   $method         The requested method.
     * @param   array|null          $allowedMethods What methods are allowed on this endpoint. If null, all are allowed. Default: null
     *
     * @return  string  The validated method.
     *
     * @throws  MethodNotAllowedHttpException   IF the method is invalid.
     *
     * @access  public
     */
    public function validateMethod(HTTPMethod|string $method, ?array $allowedMethods = null): string {
        if ($method instanceof HTTPMethod) {
            $method = $method->value();
        }
        $method = strtolower($method);
        $allowedMethods = [
            'get',
            'post',
            'put',
            'patch',
            'delete',
            'head',
            'options',
        ];

        if (!in_array($method, $allowedMethods)) {
            throw new MethodNotAllowedHttpException("Method {$method} is not valid");
        }

        if (!empty($allowedMethods)) {
            if (!in_array($method, $allowedMethods)) {
                throw new MethodNotAllowedHttpException("You tried to use {$method}, valid methods are " . implode(', ', $allowedMethods));
            }
        }

        return $method;
    }

    /**
     * Performs a generic search request for data from the API.
     *
     * @param   string              $endpoint   The endpoint to query. REQUIRED
     * @param   array               $params     The body / uri params sent to the API. Default: []
     * @param   int                 $offset     Where to offset the returned results. Used with limit for pagination. Default: 0 (the first record).
     * @param   int                 $limit      Limit the number of records returned. Used with offset for pagination. Set to -1 to return all. Default: 25
     * @param   string|null         $sort       Any sorting params sent through, null means no sorting. Default: null.
     * @param   HTTPMethod|string   $method     The HTTP method used. Default: `HTTPMethod::GET`
     *
     * @return  \yii\httpclient\Response
     *
     * @access  public
     */
    public function search(
        string $endpoint,
        array $params = [],
        int $offset = 0,
        int $limit = 25,
        ?string $sort = null,
        HTTPMethod|string $method = HTTPMethod::GET,
        array $with = [],
    ): Response {
        $url = $this->serverUrl . $endpoint;
        $method = $this->validateMethod($method);

        if ($method == 'get') {
            $query = [
                'params' => $params,
                'offset' => $offset,
                'limit' => $limit,
                'sort' => $sort,
                'with' => $with,
            ];
            $url .= '?' . http_build_query($query);
            $request = $this->getClient()->get($url);
        } else {
            $request = $this->getClient()->$method($url, [
                'params' => $params,
                'offset' => $offset,
                'limit' => $limit,
                'sort' => $sort,
                'with' => $with,
            ]);
        }
        $this->setLastRequest($request);
        return $request->send();
    }

    /**
     * Performs a generic API create.
     *
     * @param   array               $params     The params to pass to the create endpoint.  Default: []
     * @param   string              $endpoint   The endpoint to send to. Default: 'create'
     * @param   HTTPMethod|string   $method     The method for sending the request: Default: `HTTPMethod::POST`
     *
     * @return  \yii\httpclient\Response
     *
     * @access  public
     */
    public function create(
        array $params = [],
        string $endpoint = 'create',
        HTTPMethod|string $method = HTTPMethod::POST
    ): Response {
        $url = $this->serverUrl . $endpoint;
        $method = $this->validateMethod($method, ['post', 'put']);
        $request = $this->getClient()->$method($url, $params);
        $this->setLastRequest($request);
        return $request->send();
    }

    /**
     * Performs a generic API update.
     *
     * @param   array   $set        The new data to set. REQUIRED
     * @param   array   $where      The conditions to match for the update. Default: []
     * @param   string  $endpoint   The endpoint to send to. Default: 'update'
     * @param   HTTPMethod|string   $method     The method for sending the request: Default: `HTTPMethod::POST`
     *
     * @return  \yii\httpclient\Response
     *
     * @access  public
     */
    public function update(
        array $set,
        array $where = [],
        string $endpoint = 'update',
        HTTPMethod|string $method = HTTPMethod::POST
    ): Response {
        $url = $this->serverUrl . $endpoint;
        $method = $this->validateMethod($method, ['post']);
        $request = $this->getClient()->$method($url, [
            'set' => $set,
            'where' => $where,
        ]);
        $this->setLastRequest($request);
        return $request->send();
    }

    /**
     * Performs a generic API delete.
     *
     * @param   array               $where      The conditions to match for the delete. Default: []
     * @param   string              $endpoint   The endpoint to send to. Default: 'delete'
     * @param   HTTPMethod|string   $method     The method for sending the request: Default: `HTTPMethod::POST`
     *
     * @return  \yii\httpclient\Response
     *
     * @access  public
     */
    public function delete(
        array $where = [],
        string $endpoint = 'delete',
        HTTPMethod|string $method = HTTPMethod::POST
    ): Response {
        $url = $this->serverUrl . $endpoint;
        $method = $this->validateMethod($method, ['post', 'delete']);
        $request = $this->getClient()->$method($url, $where);
        $this->setLastRequest($request);
        return $request->send();
    }

    public static function goGet(string $uri): array {
        $client = new static();

        $url = $client->serverUrl() . $uri;

        Yii::debug($url, __METHOD__);

        $request = $client->getClient()->get($url);
        $client->setLastRequest($request);
        /** @var \yii\httpclient\Response $response */
        $response = $request->send();
                
        if (!$response->getIsOk()) {
            \Yii::error('Status: ' . $response->getStatusCode());
            \Yii::error('Raw body: ' . $response->getContent());
        }

        return [
            'success' => $response->isOk,
            'data' => $response->data,
            'statusCode' => $response->getStatusCode(),
        ];
    }

    public static function goPost(string $uri, array $params): array {
        $client = new static();

        $url = $client->serverUrl() . $uri;

        Yii::debug($url, __METHOD__);

        $request = $client->getClient()->post($url, $params);
        $client->setLastRequest($request);
        /** @var \yii\httpclient\Response $response */
        $response = $request->send();

        if (!$response->getIsOk()) {
            \Yii::error('Status: ' . $response->getStatusCode());
            \Yii::error('Raw body: ' . $response->getContent());
        }

        return [
            'success' => $response->isOk,
            'data' => $response->data,
            'statusCode' => $response->getStatusCode(),
        ];
    }
}
