<?php

/*
 * This file is part of the AuthStack project.
 *
 * (c) Buckhill Ltd <development@buckhill.co.uk>
 *
 * This source file is subject to the Buckhill license that is bundled
 * with this source code in the file LICENSE.
 */

namespace Buckhill\License\Model;

use Illuminate\Support\Str;
use Buckhill\License\Exceptions\EncodingException;
use Buckhill\License\Exceptions\LicenseEncryptionException;

class License
{
    /**
     * Representation of License model (default blueprint)
     * These properties get encoded as JSON and persisted to database.
     *
     * When exchanging the license, it's encrypted, signed and encoded.
     * Holder of public key can decrypt the license.
     * Holder of private key can encrypt the license.
     *
     * @var array
     */
    protected $default = [
        /**
         * Information about license id, validity period and allowed number of users
         */
        'license' => [
            'id' => '',
            'startsAt' => 0,
            'endsAt' => 0,
            'gracePeriod' => '',
            'allowedNumberOfUsers' => 0,
            'numberOfProcessingServers' => 1
        ],

        /**
         * List of domains where license will function. If license is used at the domain that's not in this list,
         * the middleware checking the license will return an error, thus disabling the software
         */
        'domain' => ['localhost'],


        /**
         * Information about two factor authentication (MFAStack functionality)
         */
        'twoFactor' => [
            'enabled' => false,
            'numberOfKeys' => [
                'global' => 0,
                'perPerson' => 0
            ]
        ],

        /**
         * Information about product that's being licensed
         */
        'product' => [
            'id' => 0,
            'title' => '',
            'url' => ''
        ],

        /**
         * Information about contact person
         */
        'contact' => [
            'person' => '',
            'email' => ''
        ],

        /**
         * Information about notification E-mail
         */
        'notificationEmail' => '',

        /**
         *
         */
        'encryption' => [
            'alg' => 'AES-256-CTR',
            'key' => '',
            'iv' => ''
        ]
    ];

    /**
     * Actual model, created out of real information. It corresponds to the above blueprint
     *
     * @var array
     */
    protected $model = [];

    /**
     * @var null
     */
    protected $encrypted = null;

    /**
     * Generates a new License using $data as source of licensing information
     * This creates a NEW license, so it auto-generates the symmetric cipher.
     * Use this method when you are issuing completely new license (I.E. customer just bought a license)
     *
     * To load an existing license, refer to static::load() method which doesn't auto-generate symmetric cipher
     *
     * @param array $data
     * @param string $alg
     * @return License
     */
    public static function generate(array $data, $alg = 'AES-256-CTR')
    {
        $me = new static();

        $me->model = array_merge($me->default, $data);

        $me->model['encryption'] = $me->generateEncryptionKey($alg);

        return $me;
    }

    /**
     * This method is intended for use with database or from encrypted string.
     * If you have license information in the database, retrieve it and pass it to this function.
     * Consequently, if the license is shared via encrypted string, pass the string to Decrypter class which will
     * invoke this method and return an instance of License
     *
     *
     * @param array $data
     * @return License
     */
    public static function load(array $data)
    {
        $me = new static();

        $me->model = array_merge($me->default, $data);

        // Create a new IV
        $me->setIV($me->getNewIV());

        return $me;
    }

    /**
     * @param bool $include_encryption_data
     * @return array
     */
    public function getModel($include_encryption_data = false)
    {
        if(!$include_encryption_data)
        {
            $model = $this->model;

            unset($model['encryption']);

            return $model;
        }

        return $this->model;
    }

    /**
     * @param $alg
     * @return $this
     */
    public function setEncryptionAlg($alg)
    {
        $this->model['encryption']['alg'] = $alg;

        return $this;
    }

    /**
     * @param $key
     * @return $this
     */
    public function setEncryptionKey($key)
    {
        $this->model['encryption']['key'] = $key;

        return $this;
    }

    /**
     * @param $iv
     * @return $this
     */
    public function setIV($iv)
    {
        $this->model['encryption']['iv'] = $iv;

        return $this;
    }

    /**
     * @param bool $encode
     * @return EncryptedLicense
     */
    public function encrypt($encode = false)
    {
        $payload    = $this->getDataToEncrypt();
        $encrypted  = $this->encryptPayload($payload, $encode);

        return new EncryptedLicense($encrypted, [
            'alg'   => $this->getAlg(),
            'key'   => $this->getKey(),
            'iv'    => $this->getIV()
        ]);
    }

    /**
     * @param $payload
     * @param bool $encode
     * @return string
     * @throws EncodingException
     * @throws LicenseEncryptionException
     */
    protected function encryptPayload($payload, $encode = false)
    {
        $alg = $this->getAlg();
        $key = $this->getKey();
        $iv = $this->getIV();

        $result = @openssl_encrypt($payload, $alg, $key, true, $iv);

        if(false === $result)
        {
            throw new LicenseEncryptionException(openssl_error_string());
        }

        if($encode)
        {
            $result = base64_encode($result);

            if(false === $result)
            {
                throw new EncodingException("Unable to encode encrypted payload");
            }
        }

        return $result;
    }

    /**
     * @return string
     */
    public function getAlg()
    {
        return $this->model['encryption']['alg'];
    }

    /**
     * @return string
     */
    public function getKey()
    {
        return openssl_digest($this->model['encryption']['key'], 'sha256');
    }

    /**
     * @return string
     */
    public function getIV()
    {
        $iv = $this->model['encryption']['iv'];

        if(empty($iv))
        {
            $this->model['encryption']['iv'] = $this->getNewIV();
        }

        return $iv;
    }

    /**
     * @return string
     */
    protected function getNewIV()
    {
        return Str::random(openssl_cipher_iv_length($this->getAlg()));
    }

    /**
     * @return string
     */
    protected function getDataToEncrypt()
    {
        return json_encode([
            'license'           => $this->model['license'],
            'domain'            => $this->model['domain'],
            'twoFactor'         => $this->model['twoFactor'],
            'product'           => $this->model['product'],
            'contact'           => $this->model['contact'],
            'notificationEmail' => $this->model['notificationEmail']
        ]);
    }

    /**
     * @param $alg
     * @return array
     */
    protected function generateEncryptionKey($alg)
    {
        return [
            'alg'   => $alg,
            'key'   => Str::random(32),
            'iv'    => Str::random(openssl_cipher_iv_length($alg))
        ];
    }
}