<?php

namespace onespace\tools\activeApi\helpers;

use Exception;
use onespace\tools\enum\general\HTTPMethod;
use Yii;
use yii\base\Model;
use yii\httpclient\Response;
use yii\web\HttpException;

/**
 * Basic common methods used by the Active Record API tooling.
 */
trait BaseApiHelperTrait {
    /**
     * Whether or not the model is new, i.e. has not been synced back to the API or not.
     *
     * @var bool    $isNew  Default: true
     *
     * @access  protected
     */

    protected bool $isNew = true;

    /**
     * Sets the endpoint to which the API should call.
     *
     * @var string  $_endpoint
     *
     * @access  protected
     */

    protected string $_endpoint;


    /**
     * The method used by the API. Default should be set by calling the method `$this->prepareMethod($default);`
     *
     * @var \onespace\tools\helpers\enum\HTTPMethod  $_method
     *
     * @access  protected
     */

    protected HTTPMethod $_method;

    /**
     * The specified base endpoint version. 'v1', 'v2' etc.
     *
     * @var string  $_version   Default 'v1'
     *
     * @access  protected
     */

    protected string $_version = 'v1';

    /**
     * Records any error messages returned from a query.
     *
     * @var array   $_errors Default: []
     *
     * @access  protected
     */

    protected array $_errors = [];

    /**
     * Records the raw response from the last query. Data
     * may vary according to your endpoint.
     *
     * @var array   $_lastResponseData  Default: []
     *
     * @access  protected
     */

    protected array $_lastResponseData = [];

    /**
     * Set any available base endpoints. If the endpoint is `v1/my-endpoint/create`, you are setting `['v1' => 'v1/endpoint']`.
     *
     * For example:
     *
     * ```php
     * public function baseEndpoints(): array {
     *  return [
     *     'v1' => 'v1/my-base-endpoint',
     *     'v2' => 'v2/my-base-endpoint',
     *   ];
     * }
     * ```
     *
     * @return  array
     *
     * @access  public
     */
    abstract public function baseEndpoints(): array;

    /**
     * Each modal using this trait is required to define a primary key.
     *
     * In most cases, this method should look like this:
     *
     * ```php
     * public function primaryKey(): string {
     *  return 'id';
     * }
     * ```
     *
     * @return  string  The desired primary key.
     *
     * @access  public
     */
    abstract public function primaryKey(): string;

    /**
     * Allow the user to define the endpoint used.
     *
     * @param   string  $endpoint   The desired endpoint.
     *
     * @return  static
     *
     * @access  public
     */
    public function endpoint(string $endpoint): static {
        $this->_endpoint = $endpoint;
        return $this;
    }

    /**
     * Allow the user to define the method used to send the request.
     *
     * @param   \onespace\tools\helpers\enum\HTTPMethod  $method The desired method: passed from the HTTPMethod enum.
     *
     * @return  static
     *
     * @access  public
     */
    public function method(HTTPMethod $method): static {
        $this->_method = $method;
        return $this;
    }

    /**
     * Allow the user to set a previously defined base endpoint version.
     *
     * @param   string  $version    The version key.
     *
     * @return  static
     *
     * @access  public
     */
    public function version(string $version): static {
        $this->_version = $version;
        return $this;
    }

    /**
     * Prepare the endpoint, including setting the default as required, prior to executing the API request.
     *
     * @param   string  $default    The default endpoint.
     *
     * @throws  Exception   If the previously selected endpoint has not been defined.
     *
     * @access  protected
     */
    protected function prepareEndpoint(string $default): void {
        // Sets the default endpoint, only if not already set.
        $this->_endpoint ??= $default;

        // Determines the available valid endpoints.
        $validEndpoints = array_merge(['v1' => 'api/v1'], $this->baseEndpoints());
        if (!isset($validEndpoints[$this->_version])) {
            throw new Exception("No valid endpoint defined. You requested {$this->_version} and only " . implode(', ', $validEndpoints) . " are available");
        }

        // returns the full endpoint
        $this->_endpoint = implode('/', [
            rtrim($validEndpoints[$this->_version], '/'),
            $this->_endpoint,
        ]);
    }

    /**
     * Ensures the method is correctly set. If not set, the param `$default` will be used. Also checks what
     * is passed. If the method is not a valid HTTP method, the `$default` will be used.
     *
     * @param   HTTPMethod|string  $default    The default HTTP method.
     *
     * @access  protected
     */
    protected function prepareMethod(HTTPMethod|string $default): void {
        // If a string, ensures the value is lower case.
        if (is_string($default)) {
            $default = strtolower($default);
        }

        // Sets the default method if the method isn't set.
        $this->_method ??= $default;

        // If not enum, validate from list of methods.
        if (!$this->_method instanceof HTTPMethod) {
            $this->_method = strtolower($this->_method);
            $validMethods = [
                'get',
                'post',
                'put',
                'patch',
                'delete',
                'head',
                'options',
            ];
            if (!in_array($this->_method, $validMethods)) {
                $this->_method = $default;
            }
        }
    }

    /**
     * Once a request is made and a response is received - this does the initial validation
     * of that response. It also records any error messages if `success == false`.
     *
     * @param   \yii\httpclient\Response    $response
     *
     * @throws  yii\web\HttpException   If the response is not ok.
     *
     * @access  protected
     */
    protected function checkResponse(Response $response): void {
        if (!$response->isOk) {
            // if (YII_DEBUG && class_exists(\Debugger\Debug::class)) {
            //     \Debugger\Debug::data($response->data)->dump(true);
            // }
            Yii::debug($response->content, __METHOD__);
            throw new HttpException($response->data['status'] ?? 500, $response->data['message'] ?? "Server error");
        }
        if (isset($response->data['success']) && !$response->data['success']) {
            //Unsuccessful request

            //Add field errors if present.
            if($this instanceof Model) {
                if(isset($response->data['errors']) && is_array($response->data['errors'])) {
                    $this->addErrors($response->data['errors']);
                }
                //Deprecated method of passing errors via data instead of errors
                elseif(isset($response->data['data']) && is_array($response->data['data'])) {
                    $this->addErrors($response->data['data']);
                }
            }

            if (isset($response->data['data']) && is_string($response->data['data'])) {
                $this->_errors[] = $response->data['data'];
            } elseif (isset($response->data['message'])) {
                if (is_string($response->data['message'])) {
                    $this->_errors[] = $response->data['message'];
                } else {
                    foreach ($response->data['message'] as $message) {
                        $this->errors[] = $message;
                    }
                }
            } elseif (isset($response->data['errorMessage'])) {
                if (is_string($response->data['errorMessage'])) {
                    $this->_errors[] = $response->data['errorMessage'];
                } else {
                    foreach ($response->data['errorMessage'] as $message) {
                        $this->errors[] = $message;
                    }
                }
            }
        }
    }

    /**
     * Returns an array of any listed errors recorded after a CREATE, UPDATE or DELETE query.
     *
     * @return  array
     *
     * @access  public
     */
    public function getApiErrors(): array {
        return $this->_errors;
    }

    /**
     * Returns if the record is considered new or not.
     *
     * @return  bool
     *
     * @access  public
     */
    public function isNewRecord(): bool {
        return $this->isNew;
    }

    /**
     * Set the last reponse data value.
     *
     * @param   array   $data   The data to record.
     *
     * @access  protected
     */
    protected function setLastResponseData(array $data): void {
        $this->_lastResponseData = $data;
    }

    /**
     * Returns the last response data. If no request has
     * been sent yet, it will return null.
     *
     * @return  array|null
     *
     * @access  public
     */
    public function getLastResponseData(): array|null {
        if (empty($this->_lastResponseData)) {
            return null;
        }
        return $this->_lastResponseData;
    }
}
