<?php

namespace React\Dns\Query;

use React\Cache\CacheInterface;
use React\Dns\Model\Message;
use React\Dns\Model\Record;
use React\Promise;
use React\Promise\PromiseInterface;

/**
 * Wraps an underlying cache interface and exposes only cached DNS data
 */
class RecordCache
{
    private $cache;
    private $expiredAt;

    public function __construct(CacheInterface $cache)
    {
        $this->cache = $cache;
    }

    /**
     * Looks up the cache if there's a cached answer for the given query
     *
     * @param Query $query
     * @return PromiseInterface Promise<Record[],mixed> resolves with array of Record objects on sucess
     *     or rejects with mixed values when query is not cached already.
     */
    public function lookup(Query $query)
    {
        $id = $this->serializeQueryToIdentity($query);

        $expiredAt = $this->expiredAt;

        return $this->cache
            ->get($id)
            ->then(function ($value) use ($query, $expiredAt) {
                // cache 0.5+ resolves with null on cache miss, return explicit cache miss here
                if ($value === null) {
                    return Promise\reject();
                }

                /* @var $recordBag RecordBag */
                $recordBag = unserialize($value);

                // reject this cache hit if the query was started before the time we expired the cache?
                // todo: this is a legacy left over, this value is never actually set, so this never applies.
                // todo: this should probably validate the cache time instead.
                if (null !== $expiredAt && $expiredAt <= $query->currentTime) {
                    return Promise\reject();
                }

                return $recordBag->all();
            });
    }

    /**
     * Stores all records from this response message in the cache
     *
     * @param int     $currentTime
     * @param Message $message
     * @uses self::storeRecord()
     */
    public function storeResponseMessage($currentTime, Message $message)
    {
        foreach ($message->answers as $record) {
            $this->storeRecord($currentTime, $record);
        }
    }

    /**
     * Stores a single record from a response message in the cache
     *
     * @param int    $currentTime
     * @param Record $record
     */
    public function storeRecord($currentTime, Record $record)
    {
        $id = $this->serializeRecordToIdentity($record);

        $cache = $this->cache;

        $this->cache
            ->get($id)
            ->then(
                function ($value) {
                    if ($value === null) {
                        // cache 0.5+ cache miss resolves with null, return empty bag here
                        return new RecordBag();
                    }

                    // reuse existing bag on cache hit to append new record to it
                    return unserialize($value);
                },
                function ($e) {
                    // legacy cache < 0.5 cache miss rejects promise, return empty bag here
                    return new RecordBag();
                }
            )
            ->then(function (RecordBag $recordBag) use ($id, $currentTime, $record, $cache) {
                // add a record to the existing (possibly empty) record bag and save to cache
                $recordBag->set($currentTime, $record);
                $cache->set($id, serialize($recordBag));
            });
    }

    public function expire($currentTime)
    {
        $this->expiredAt = $currentTime;
    }

    public function serializeQueryToIdentity(Query $query)
    {
        return sprintf('%s:%s:%s', $query->name, $query->type, $query->class);
    }

    public function serializeRecordToIdentity(Record $record)
    {
        return sprintf('%s:%s:%s', $record->name, $record->type, $record->class);
    }
}
