<?php

declare(strict_types=1);

namespace MemberPress\GroundLevel\Mothership\Api;

use MemberPress\GroundLevel\Support\Contracts\Arrayable;

/**
 * A response from the Mothership API.
 *
 * @method ?Response first(array $args = []) Retrieves the first page of data for the collection or `null` for non-paginated responses.
 * @method ?Response last(array $args = [])  Retrieves the last page of data for the collection or `null` for non-paginated responses.
 * @method ?Response next(array $args = [])  Retrieves the next page of data for the collection or `null` for non-paginated responses.
 * @method ?Response prev(array $args = [])  Retrieves the previous page of data for the collection or `null` for non-paginated responses.
 * @method ?Response self(array $args = [])  Retrieves the current page of data for the collection or `null` for non-paginated responses.
 *
 * @property string  $error The error message, if any.
 * @property integer $errorCode The error code, if any.
 * @property array   $errors The errors, if any.
 *
 * @package MemberPress\GroundLevel\Mothership\Api
 */
class Response implements Arrayable
{
    /**
     * The data to be stored in the response.
     *
     * @var mixed
     */
    protected $data;

    /**
     * The error message, if any.
     *
     * @var string
     */
    protected $error;

    /**
     * The error code, if any.
     *
     * @var integer
     */
    protected $errorCode;

    /**
     * The Details of the error, if any.
     *
     * @var array
     */
    protected $errors;

    /**
     * Constructor for the Response class.
     *
     * @param mixed $data      The data to be stored in the response.
     * @param mixed $error     The error Message, if any.
     * @param mixed $errorCode The error code, if any.
     * @param array $errors    The errors, if any.
     */
    public function __construct($data = null, $error = null, $errorCode = null, $errors = null)
    {
        $this->data      = $data;
        $this->error     = $error;
        $this->errorCode = $errorCode;
        $this->errors    = $errors;
    }

    /**
     * Magic method to call a link request.
     *
     * Makes a request to a valid link by providing the relation (rel) name.
     *
     * @param  string $rel       The rel of the link to request.
     * @param  array  $arguments The arguments to pass to the request.
     * @return \MemberPress\GroundLevel\Mothership\Api\Response|null The response from the link request.
     * @throws \RuntimeException If the requested link does not exist.
     */
    public function __call(string $rel, array $arguments = []): ?Response
    {
        if ($this->hasLink($rel)) {
            return $this->performLinkRequest($rel, ...$arguments);
        }

        throw new \RuntimeException(
            sprintf(
                'Method %s does not exist on the response object.',
                $rel
            )
        );
    }

    /**
     * Magic getter to access response properties.
     * Returns the property if it exists, otherwise it returns the error.
     *
     * @param  string $name The name of the property to get.
     * @return mixed The value of the property or the error.
     */
    public function __get(string $name)
    {
        return $this->returnDataOrError($name);
    }

    /**
     * Magic isset to check if data or error exists.
     *
     * @param  string $name The name of the property to check.
     * @return boolean
     */
    public function __isset(string $name): bool
    {
        if ($this->isError() && in_array($name, ['error', 'errorCode', 'errors'], true)) {
            return true;
        }
        return isset($this->data->{$name});
    }

    /**
     * Check if the response has a link.
     *
     * @param  string $rel The rel of the link to check for.
     * @return boolean
     */
    public function hasLink(string $rel): bool
    {
        return isset($this->data->_links->{$rel});
    }

    /**
     * Check if there is a next page of data.
     *
     * @return boolean
     */
    public function hasNext(): bool
    {
        return $this->hasLink('next');
    }

    /**
     * Check if the response has pagination.
     *
     * @return boolean
     */
    public function hasPagination(): bool
    {
        return $this->hasNext() || $this->hasPrevious();
    }

    /**
     * Check if there is a previous page of data.
     *
     * @return boolean
     */
    public function hasPrevious(): bool
    {
        return $this->hasLink('prev');
    }

    /**
     * Check if the response is an error.
     *
     * @return boolean True if the response is an error, false otherwise.
     */
    public function isError(): bool
    {
        return !empty($this->error);
    }

    /**
     * Check if the response is successful.
     *
     * @return boolean True if the response is successful, false otherwise.
     */
    public function isSuccess(): bool
    {
        return empty($this->error);
    }

    /**
     * Perform a request to a link.
     *
     * @param  string $rel  The rel of the link to request.
     * @param  array  $args The arguments to pass to the request.
     * @return \MemberPress\GroundLevel\Mothership\Api\Response|null The response from the link request.
     */
    protected function performLinkRequest(string $rel, array $args = []): ?Response
    {
        $link     = $this->data->_links->{$rel};
        $endpoint = basename(wp_parse_url($link->href, PHP_URL_PATH));
        $method   = strtolower($link->method ?? 'GET');
        $qs       = wp_parse_url($link->href, PHP_URL_QUERY);

        if ($qs) {
            parse_str($qs, $query);
            $args = array_merge($query, $args);
        }

        return Request::getContainer()->get(RequestFactory::class)->{$method}($endpoint, $args);
    }

    /**
     * Return the data or the error.
     *
     * @param  string $name The name of the property to get.
     * @return mixed The value of the property or the error.
     */
    private function returnDataOrError(string $name)
    {
        if ($this->isError()) {
            if ($name === 'errorCode') {
                return $this->errorCode;
            }
            if ($name === 'errors') {
                return $this->errors;
            }
            return $this->error;
        }

        if (!isset($this->data->{$name})) {
            return sprintf(
                // Translators: %s The name of the property that does not exist on the response object.
                esc_html__(
                    'Property %s does not exist on the response object.',
                    'memberpress'
                ),
                $name
            );
        }
        return $this->data->{$name};
    }

    /**
     * Convert the response to an array.
     *
     * @return array The response as an array.
     */
    public function toArray(): array
    {
        if ($this->isError()) {
            return [
                'error'     => $this->error,
                'errorCode' => $this->errorCode,
                'errors'    => $this->errors,
            ];
        }

        return (array) $this->data;
    }
}
