<?php

namespace Kevinrob\GuzzleCache;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class CacheEntry
{
    /**
     * @var RequestInterface
     */
    protected $request;

    /**
     * @var ResponseInterface
     */
    protected $response;

    /**
     * This field is only used for serialize.
     * Response::body is a stream and can't be serialized.
     *
     * @var string
     */
    protected $responseBody;

    /**
     * @var \DateTime
     */
    protected $staleAt;

    /**
     * @var \DateTime
     */
    protected $staleIfErrorTo;

    /**
     * @var \DateTime
     */
    protected $staleWhileRevalidateTo;

    /**
     * @var \DateTime
     */
    protected $dateCreated;

    /**
     * Cached timestamp of staleAt variable.
     *
     * @var int
     */
    protected $timestampStale;

    /**
     * @param RequestInterface $request
     * @param ResponseInterface $response
     * @param \DateTime $staleAt
     * @param \DateTime|null $staleIfErrorTo if null, detected with the headers (RFC 5861)
     * @param \DateTime|null $staleWhileRevalidateTo
     */
    public function __construct(
        RequestInterface $request,
        ResponseInterface $response,
        \DateTime $staleAt,
        \DateTime $staleIfErrorTo = null,
        \DateTime $staleWhileRevalidateTo = null
    ) {
        $this->dateCreated = new \DateTime();

        $this->request = $request;
        $this->response = $response;
        $this->staleAt = $staleAt;

        $values = new KeyValueHttpHeader($response->getHeader('Cache-Control'));

        if ($staleIfErrorTo === null && $values->has('stale-if-error')) {
            $this->staleIfErrorTo = (new \DateTime(
                '@'.($this->staleAt->getTimestamp() + (int) $values->get('stale-if-error'))
            ));
        } else {
            $this->staleIfErrorTo = $staleIfErrorTo;
        }

        if ($staleWhileRevalidateTo === null && $values->has('stale-while-revalidate')) {
            $this->staleWhileRevalidateTo = new \DateTime(
                '@'.($this->staleAt->getTimestamp() + (int) $values->get('stale-while-revalidate'))
            );
        } else {
            $this->staleWhileRevalidateTo = $staleWhileRevalidateTo;
        }
    }

    /**
     * @return ResponseInterface
     */
    public function getResponse()
    {
        return $this->response
            ->withHeader('Age', $this->getAge());
    }

    /**
     * @return ResponseInterface
     */
    public function getOriginalResponse()
    {
        return $this->response;
    }

    /**
     * @return RequestInterface
     */
    public function getOriginalRequest()
    {
        return $this->request;
    }

    /**
     * @param RequestInterface $request
     * @return bool
     */
    public function isVaryEquals(RequestInterface $request)
    {
        if ($this->response->hasHeader('Vary')) {
            if ($this->request === null) {
                return false;
            }

            foreach ($this->getVaryHeaders() as $key => $value) {
                if (!$this->request->hasHeader($key)
                    && !$request->hasHeader($key)
                ) {
                    // Absent from both
                    continue;
                } elseif ($this->request->getHeaderLine($key)
                    == $request->getHeaderLine($key)
                ) {
                    // Same content
                    continue;
                }

                return false;
            }
        }

        return true;
    }

    /**
     * Get the vary headers that should be honoured by the cache.
     *
     * @return KeyValueHttpHeader
     */
    public function getVaryHeaders()
    {
        return new KeyValueHttpHeader($this->response->getHeader('Vary'));
    }

    /**
     * @return \DateTime
     */
    public function getStaleAt()
    {
        return $this->staleAt;
    }

    /**
     * @return bool
     */
    public function isFresh()
    {
        return !$this->isStale();
    }

    /**
     * @return bool
     */
    public function isStale()
    {
        return $this->getStaleAge() > 0;
    }

    /**
     * @return int positive value equal staled
     */
    public function getStaleAge()
    {
        // This object is immutable
        if ($this->timestampStale === null) {
            $this->timestampStale = $this->staleAt->getTimestamp();
        }

        return time() - $this->timestampStale;
    }

    /**
     * @return bool
     */
    public function serveStaleIfError()
    {
        return $this->staleIfErrorTo !== null
            && $this->staleIfErrorTo->getTimestamp() >= (new \DateTime())->getTimestamp();
    }

    /**
     * @return bool
     */
    public function staleWhileValidate()
    {
        return $this->staleWhileRevalidateTo !== null
            && $this->staleWhileRevalidateTo->getTimestamp() >= (new \DateTime())->getTimestamp();
    }

    /**
     * @return bool
     */
    public function hasValidationInformation()
    {
        return $this->response->hasHeader('Etag') || $this->response->hasHeader('Last-Modified');
    }

    /**
     * Time in seconds how long the entry should be kept in the cache
     *
     * This will not give the time (in seconds) that the response will still be fresh for
     * from the HTTP point of view, but an upper bound on how long it is necessary and
     * reasonable to keep the response in a cache (to re-use it or re-validate it later on).
     *
     * @return int TTL in seconds (0 = infinite)
     */
    public function getTTL()
    {
        if ($this->hasValidationInformation()) {
            // No TTL if we have a way to re-validate the cache
            return 0;
        }

        $ttl = 0;

        // Keep it when stale if error
        if ($this->staleIfErrorTo !== null) {
            $ttl = max($ttl, $this->staleIfErrorTo->getTimestamp() - time());
        }

        // Keep it when stale-while-revalidate
        if ($this->staleWhileRevalidateTo !== null) {
            $ttl = max($ttl, $this->staleWhileRevalidateTo->getTimestamp() - time());
        }

        // Keep it until it become stale
        $ttl = max($ttl, $this->staleAt->getTimestamp() - time());

        // Don't return 0, it's reserved for infinite TTL
        return $ttl !== 0 ? (int) $ttl : -1;
    }

    /**
     * @return int Age in seconds
     */
    public function getAge()
    {
        return time() - $this->dateCreated->getTimestamp();
    }

    public function __sleep()
    {
        // Stream/Resource can't be serialized... So we copy the content
        if ($this->response !== null) {
            $this->responseBody = (string) $this->response->getBody();
            $this->response->getBody()->rewind();
        }

        return array_keys(get_object_vars($this));
    }

    public function __wakeup()
    {
        // We re-create the stream of the response
        if ($this->response !== null) {
            $this->response = $this->response
                ->withBody(
                    \GuzzleHttp\Psr7\Utils::streamFor($this->responseBody)
                );
        }
    }
}
