<?php

namespace CookieSession;


use Hashids\Hashids;
use Illuminate\Http\Request;
use CookieSession\Storage\Storage;
use CookieSession\Encrypter\Encrypter;
use CookieSession\Storage\Serialization\Collection;
use Symfony\Component\HttpFoundation\Cookie;

abstract class AbstractSession
{
    protected $session = [];
    protected $expiry;

    protected $cookie_name;
    protected $iv_cookie_name;
    protected $domain;
    protected $expires_at;
    protected $encryption;
    protected $pki;
    protected $hashids_config;
    protected $encrypter;
    protected $hashids;
    protected $storage;

    protected $last_error  ;

    protected $is_started = false;


    /**
     * @param array $config
     *
     * @return static
     * @throws \Exception
     */
    public static function initialize(array $config)
    {
        static::check($config);

        /** @var $me AbstractSession */
        $me = new static();

        $me
            ->setCookieName($config['cookie_name'])
            ->setIVCookieName($config['iv_cookie_name'])
            ->setDomain($config['domain'])
            ->setExpiresAt($config['expiry'])
            ->setEncryption($config['encryption'])
            ->setPKI($config['pki'])
            ->setHashids($config['hashids'])
            ->setStorage(new Storage());

        $me->getStorage()->setHashids($me->getHashids());

        return $me;
    }


    /**
     * @return array
     * @throws \Exception
     */
    public function toCookies()
    {
        $serialized = $this->serialize();

        $metadata   = $serialized->toCookie($this->getCookieName(), $this->getExpiry(), $this->getDomain());
        $iv_cookie  = $this->getIVCookie();
        $data       = $serialized->toCookies($this->getExpiry(), $this->getDomain());

        return array_merge([$metadata, $iv_cookie], $data);
    }


    /**
     * @return Cookie
     * @throws \Exception
     */
    public function getIVCookie()
    {
        return new Cookie($this->getIVCookieName(), $this->getEncrypter()->getIVEncoded(), $this->getExpiry(), '/', $this->getDomain(), true, true);
    }

    /**
     * @return Collection
     */
    public function serialize()
    {
        return $this->getStorage()->serialize($this->jsonEncode($this->session));
    }


    /**
     * @param Request $request
     *
     * @return $this
     * @throws \Exception
     */
    public function unserializeFromRequest(Request $request, $debug = false)
    {
        try
        {
            $iv_cookie = $this->getIVCookieName();

            if(!$request->hasCookie($iv_cookie))
            {
                throw new \Exception("No initialization vector cookie present.");
            }

            $iv = $request->cookie($iv_cookie);
            $this->getStorage()->getEncrypter()->setIV(base64_decode($iv));

            $data = $this->getStorage()->unserializeFromRequest($request, $this->getCookieName());

            $this->session = $data;

            $this->is_started = true;
        }
        catch(\Exception $e)
        {
            $this->is_started = true;

            $this->last_error = $e->getMessage();

            $this->getStorage()->getEncrypter()->autoSetIV();

            $this->session = [];
        }

        return $this;
    }


    /**
     * @return bool
     */
    public function isStarted()
    {
        return $this->is_started;
    }


    /**
     * @return mixed
     */
    public function getLastError()
    {
        return $this->last_error;
    }

    /**
     * @param \DateTime $expiry
     *
     * @return $this
     */
    public function setExpiry(\DateTime $expiry)
    {
        $this->expiry = $expiry;

        return $this;
    }


    /**
     * @return \DateTime
     * @throws \Exception
     */
    public function getExpiry()
    {
        if(!$this->expiry instanceof \DateTime)
        {
            throw new \Exception("No expiry has been set.");
        }

        return $this->expiry;
    }

    /**
     * @param $cookie_name
     *
     * @return $this
     */
    protected function setCookieName($cookie_name)
    {
        $this->cookie_name = $cookie_name;

        return $this;
    }


    /**
     * @return string
     */
    public function getCookieName()
    {
        return $this->cookie_name;
    }


    /**
     * @param $cookie_name
     *
     * @return $this
     */
    public function setIVCookieName($cookie_name)
    {
        $this->iv_cookie_name = $cookie_name;

        return $this;
    }


    /**
     * @return mixed
     */
    public function getIVCookieName()
    {
        return $this->iv_cookie_name;
    }

    /**
     * @param $domain
     *
     * @return $this
     */
    protected function setDomain($domain)
    {
        $this->domain = $domain;

        return $this;
    }


    /**
     * @return string
     */
    protected function getDomain()
    {
        return $this->domain;
    }


    /**
     * @param $expires_at
     *
     * @return AbstractSession
     */
    protected function setExpiresAt($expires_at)
    {
        $this->expires_at = $expires_at;

        $tz = new \DateTimeZone("UTC");

        try
        {
            $expiry = new \DateTime($this->expires_at, $tz);

            if(!$expiry instanceof \DateTime)
            {
                throw new \Exception("");
            }
        }
        catch(\Exception $e)
        {
            $expiry = new \DateTime("now", $tz);
        }

        return $this->setExpiry($expiry);
    }


    /**
     * @param array $encryption
     *
     * @return $this
     */
    protected function setEncryption(array $encryption)
    {
        $this->encryption = $encryption;

        if(true === $this->encryption['enabled'])
        {
            $this->createEncrypter();
        }

        return $this;
    }


    /**
     * @param array $pki
     *
     * @return $this
     */
    protected function setPKI(array $pki)
    {
        $this->pki = $pki;

        if($this->hasEncrypter())
        {
            $this
                ->getEncrypter()
                ->setPublicKey($this->pki['public'])
                ->setPrivateKey($this->pki['private']['value'], $this->pki['private']['passphrase'])
                ->setSignatureAlgo($this->pki['signature_algo']);
        }

        return $this;
    }


    /**
     * @param array $hashids
     *
     * @return $this
     */
    protected function setHashids(array $hashids)
    {
        $this->hashids_config = $hashids;

        $this->createHashids();

        return $this;
    }


    /**
     * @return Hashids
     * @throws \Exception
     */
    public function getHashids()
    {
        if(!$this->hashids instanceof Hashids)
        {
            throw new \Exception("Unable to retrieve an instance of Hashids. Instance of Hashids has not been set.");
        }

        return $this->hashids;
    }


    /**
     * @param Storage $storage
     *
     * @return $this
     * @throws \Exception
     */
    protected function setStorage(Storage $storage)
    {
        $this->storage = $storage;

        if($this->hasEncrypter())
        {
            $this->storage->setEncrypter($this->getEncrypter());
        }

        return $this;
    }


    /**
     * @return Storage
     * @throws \Exception
     */
    protected function getStorage()
    {
        if(!$this->storage instanceof Storage)
        {
            throw new \Exception("Unable to retrieve Storage object. No storage object has been set.");
        }

        return $this->storage;
    }

    /**
     * @return $this
     */
    protected function createEncrypter()
    {
        $this->encrypter = new Encrypter();

        $this->encrypter->setMethod($this->encryption['method']);
        $this->encrypter->setSecret($this->encryption['secret']);

        if(isset($this->encryption['iv']))
        {
            $this->encrypter->setIV($this->encryption['iv']);
        }
        else
        {
            $this->encrypter->autoSetIV();
        }

        return $this;
    }


    /**
     * @return $this
     */
    protected function createHashids()
    {
        $this->hashids = new Hashids($this->hashids_config['salt'], $this->hashids['min_length'], $this->hashids_config['alphabet']);

        return $this;
    }


    /**
     * @return bool
     */
    protected function hasEncrypter()
    {
        return ($this->encrypter instanceof Encrypter);
    }


    /**
     * @return Encrypter
     * @throws \Exception
     */
    protected function getEncrypter()
    {
        if(!$this->hasEncrypter())
        {
            throw new \Exception("There is no instance of Encrypter set. Unable to retrieve Encrypter object.");
        }

        return $this->encrypter;
    }

    /**
     * @param array $config
     *
     * @throws \Exception
     */
    protected static function check(array $config)
    {
        if(!isset($config['cookie_name']))
        {
            throw new \Exception("Required value 'cookie_name' is not set.");
        }

        if(!isset($config['iv_cookie_name']))
        {
            throw new \Exception("Required value 'iv_cookie_name' is not set.");
        }

        if(!isset($config['domain']))
        {
            throw new \Exception("Cookie domain is not set.");
        }

        if(!isset($config['expiry']))
        {
            throw new \Exception("Expiry time is not set.");
        }

        if(!isset($config['encryption']))
        {
            throw new \Exception("Required value 'encryption' is not set.");
        }

        if(!isset($config['pki']))
        {
            throw new \Exception("Required value 'pki' is not set.");
        }

        if(!isset($config['hashids']))
        {
            throw new \Exception("Required value 'hashids' is not set.");
        }
    }


    /**
     * @param     $value
     * @param int $options
     * @param int $depth
     *
     * @return string
     * @throws \Exception
     */
    protected function jsonEncode($value, $options = 0, $depth = 512)
    {
        $encoded = json_encode($value, $options, $depth);

        if(false === $encoded)
        {
            throw new \Exception("An error occurred while encoding to JSON. Error reported: " . json_last_error_msg());
        }

        return $encoded;
    }
}