<?php

namespace Moip\Resource;

use JsonSerializable;
use Moip\Exceptions;
use Moip\Helper\Filters;
use Moip\Helper\Links;
use Moip\Helper\Pagination;
use Moip\Moip;
use Requests;
use Requests_Exception;
use stdClass;

/**
 * Class MoipResource.
 */
abstract class MoipResource implements JsonSerializable
{
    /**
     * Version of API.
     *
     * @const string
     */
    const VERSION = 'v2';

    /**
     * Api version content type.
     *
     * @cont string
     */
    const ACCEPT_VERSION = 'application/json;version=2.1';

    /**
     * @var \Moip\Moip
     */
    protected $moip;

    /**
     * @var \stdClass
     */
    protected $data;

    /**
     * Initialize a new instance.
     */
    abstract protected function initialize();

    /**
     * Mount information of a determined object.
     *
     * @param \stdClass $response
     *
     * @return mixed
     */
    abstract protected function populate(stdClass $response);

    /**
     * Create a new instance.
     *
     * @param \Moip\Moip $moip
     */
    public function __construct(Moip $moip)
    {
        $this->moip = $moip;
        $this->data = new stdClass();
        $this->initialize();
    }

    /**
     * Get a key of an object if it exists.
     *
     * @param string         $key
     * @param \stdClass|null $data
     *
     * @return mixed
     */
    protected function getIfSet($key, stdClass $data = null)
    {
        if (empty($data)) {
            $data = $this->data;
        }

        if (isset($data->$key)) {
            return $data->$key;
        }
    }

    /**
     * @return \Moip\Helper\Links
     */
    public function getLinks()
    {
        $links = $this->getIfSet('_links');

        if ($links !== null) {
            return new Links($links);
        }
    }

    /**
     * @param $key
     * @param $fmt
     * @param stdClass|null $data
     *
     * @return bool|\DateTime|null
     */
    protected function getIfSetDateFmt($key, $fmt, stdClass $data = null)
    {
        $val = $this->getIfSet($key, $data);
        if (!empty($val)) {
            $dt = \DateTime::createFromFormat($fmt, $val);

            return $dt ? $dt : null;
        }
    }

    /**
     * Get a key, representing a date (Y-m-d), of an object if it exists.
     *
     * @param string        $key
     * @param stdClass|null $data
     *
     * @return \DateTime|null
     */
    protected function getIfSetDate($key, stdClass $data = null)
    {
        return $this->getIfSetDateFmt($key, 'Y-m-d', $data);
    }

    /**
     * Get a key representing a datetime (\Datetime::ATOM), of an object if it exists.
     *
     * @param string        $key
     * @param stdClass|null $data
     *
     * @return \DateTime|null
     */
    protected function getIfSetDateTime($key, stdClass $data = null)
    {
        $rawDateTime = $this->getIfSet($key, $data);

        $dateTime = null;
        if (!empty($rawDateTime)) {
            $dateTime = new \DateTime($rawDateTime);
        }

        return $dateTime;
    }

    /**
     * Specify data which should be serialized to JSON.
     *
     * @return \stdClass
     */
    public function jsonSerialize()
    {
        return $this->data;
    }

    /**
     * Generate URL to request.
     *
     * @param $action
     * @param $id
     *
     * @return string
     */
    public function generatePath($action, $id = null)
    {
        if (!is_null($id)) {
            return sprintf('%s/%s/%s/%s', self::VERSION, static::PATH, $action, $id);
        }

        return sprintf('%s/%s/%s', self::VERSION, static::PATH, $action);
    }

    /**
     * Generate URL to request a get list.
     *
     * @param Pagination $pagination
     * @param Filters    $filters
     * @param array      $params
     *
     * @return string
     */
    public function generateListPath(Pagination $pagination = null, Filters $filters = null, $params = [])
    {
        $queryParams = [];

        if (!is_null($pagination)) {
            if ($pagination->getLimit() != 0) {
                $queryParams['limit'] = $pagination->getLimit();
            }

            if ($pagination->getOffset() >= 0) {
                $queryParams['offset'] = $pagination->getOffset();
            }
        }

        if (!is_null($filters)) {
            $queryParams['filters'] = $filters->__toString();
        }

        if (!empty($params)) {
            $queryParams = array_merge($queryParams, $params);
        }

        if (!empty($queryParams)) {
            return sprintf('/%s/%s?%s', self::VERSION, static::PATH, http_build_query($queryParams));
        }

        return sprintf('/%s/%s', self::VERSION, static::PATH);
    }

    /**
     * Execute a http request. If payload == null no body will be sent. Empty body ('{}') is supported by sending a
     * empty stdClass.
     *
     * @param string     $path    request path
     * @param string     $method  http method
     * @param mixed|null $payload request body
     * @param array      $headers request headers
     *
     * @throws Exceptions\ValidationException  if the API returns a 4xx http status code. Usually means invalid data was sent.
     * @throws Exceptions\UnautorizedException if the API returns a 401 http status code. Check API token and key.
     * @throws Exceptions\UnexpectedException  if the API returns a 500 http status code or something unexpected happens (ie.: Network error).
     *
     * @return stdClass
     */
    protected function httpRequest($path, $method, $payload = null, $headers = [])
    {
        $http_sess = $this->moip->getSession();
        $body = null;
        if ($payload !== null) {
            $body = json_encode($payload, JSON_UNESCAPED_SLASHES);
            if ($body) {    // if it's json serializable
                $headers['Content-Type'] = 'application/json';
            } else {
                $body = null;
            }
        }

        try {
            $http_response = $http_sess->request($path, $headers, $body, $method);
        } catch (Requests_Exception $e) {
            throw new Exceptions\UnexpectedException($e);
        }

        $code = $http_response->status_code;
        $response_body = $http_response->body;
        if ($code >= 200 && $code < 300) {
            return json_decode($response_body);
        } elseif ($code == 401) {
            throw new Exceptions\UnautorizedException();
        } elseif ($code >= 400 && $code <= 499) {
            $errors = Exceptions\Error::parseErrors($response_body);

            throw new Exceptions\ValidationException($code, $errors);
        }

        throw new Exceptions\UnexpectedException();
    }

    /**
     * Find by path.
     *
     * @param string $path    resource path
     * @param array  $headers request headers
     *
     * @return stdClass
     */
    public function getByPath($path, $headers = [])
    {
        $response = $this->httpRequest($path, Requests::GET, null, $headers);

        if (is_array($response)) {
            $response = (object) $response;
        }

        return $this->populate($response);
    }

    /**
     * Find by path with no populate method.
     *
     * @param string $path
     *
     * @return stdClass
     */
    public function getByPathNoPopulate($path)
    {
        return $this->httpRequest($path, Requests::GET);
    }

    /**
     * Create a new item in Moip.
     *
     * @param string $path
     *
     * @return stdClass
     */
    public function createResource($path)
    {
        $response = $this->httpRequest($path, Requests::POST, $this);

        return $this->populate($response);
    }

    /**
     * Update an item in Moip.
     *
     * @param string $path
     *
     * @return stdClass
     */
    public function updateByPath($path)
    {
        $response = $this->httpRequest($path, Requests::PUT, $this);

        return $this->populate($response);
    }

    /**
     * Delete a new item in Moip.
     *
     * @param $path
     *
     * @return mixed
     */
    public function deleteByPath($path)
    {
        return $this->httpRequest($path, Requests::DELETE);
    }
}
