//==============================================================================
//
//  OvenMediaEngine
//
//  Created by Hyunjun Jang
//  Copyright (c) 2018 AirenSoft. All rights reserved.
//
//==============================================================================
#pragma once

#include <base/ovlibrary/ovlibrary.h>
#include <openssl/ec.h>
#include <openssl/ssl.h>
#include <openssl/store.h>

#include "base/common_types.h"
#include "openssl/openssl_error.h"

#define CERT_NAME "OME"

static const int DefaultCertificateLifetimeInSeconds = 60 * 60 * 24 * 30 * 12;	// 1 years
static const int CertificateWindowInSeconds = -60 * 60 * 24;

enum class KeyType : int32_t
{
	Rsa,
	Ecdsa,
	Last,

	Default = static_cast<int>(Ecdsa)
};

class Certificate
{
public:
	Certificate() = default;
	explicit Certificate(X509 *cert);
	~Certificate();

	// Gemerate a certificate
	std::shared_ptr<ov::OpensslError> Generate();
	//
	// Generate a certificate from a PEM file
	//
	// chain_cert_filename is optional
	std::shared_ptr<ov::OpensslError> GenerateFromPem(
		const char *private_key_filename,
		const char *cert_filename,
		const char *chain_cert_filename);

	// If generated by GenerateFromPem(), returns the file name at the time of creation. If generated by Generate(), returns nullptr
	EVP_PKEY *GetPrivateKey() const;
	const char *GetPrivateKeyPath() const
	{
		return (_private_key != nullptr) ? _private_key_filename.CStr() : nullptr;
	}

	// If generated by GenerateFromPem(), returns the file name at the time of creation. If generated by Generate(), returns nullptr
	X509 *GetCertification() const;
	const char *GetCertificationPath() const
	{
		return (_certificate != nullptr) ? _certificate_filename.CStr() : nullptr;
	}

	// If generated by GenerateFromPem(), returns the file name at the time of creation. If generated by Generate(), returns nullptr
	STACK_OF(X509) * GetChainCertification() const;
	const char *GetChainCertificationPath() const
	{
		return (_chain_certificate != nullptr) ? _chain_certificate_filename.CStr() : nullptr;
	}

	ov::String GetFingerprint(const ov::String &algorithm);

	// Print Cert for Test
	void Print();

private:
	enum class LoadType
	{
		None = 0,
		Pkey = OSSL_STORE_INFO_PKEY,
		Cert = OSSL_STORE_INFO_CERT,
	};

private:
	static void X509StackFree(STACK_OF(X509) * instance)
	{
		::sk_X509_pop_free(instance, ::X509_free);
	}

	template <typename T>
	static ov::RaiiPtr<T> LoadPem(
		const LoadType type,
		const char *pem_file,
		T *initial_value,
		const std::function<T *(int type, const OSSL_STORE_INFO *info, T *previous_value)> &handler,
		std::function<void(T *instance)> deleter)
	{
		auto store_ctx = ov::RaiiPtr<OSSL_STORE_CTX>(
			::OSSL_STORE_open(pem_file, nullptr, nullptr, nullptr, nullptr),
			::OSSL_STORE_close);

		if (store_ctx == nullptr)
		{
			return {nullptr, deleter};
		}

		if (type != LoadType::None)
		{
			// Filter by type
			if (::OSSL_STORE_expect(store_ctx, static_cast<int>(type)) == 0)
			{
				return {nullptr, deleter};
			}
		}

		T *instance = initial_value;

		while (::OSSL_STORE_eof(store_ctx) == 0)
		{
			auto info = ov::RaiiPtr<OSSL_STORE_INFO>(
				::OSSL_STORE_load(store_ctx),
				::OSSL_STORE_INFO_free);

			if (info == nullptr)
			{
				// The file includes multiple types of things
				continue;
			}

			instance = handler(::OSSL_STORE_INFO_get_type(info), info, instance);
		}

		return {instance, deleter};
	}

	static ov::RaiiPtr<EVP_PKEY> LoadPrivateKey(const char *pem_file);
	static ov::RaiiPtr<X509> LoadCertificate(const char *pem_file);
	static ov::RaiiPtr<STACK_OF(X509)> LoadChainCertificate(const char *pem_file);

private:
	// Make ECDSA Key
	EVP_PKEY *MakeKey();

	// Make Self-Signed Certificate
	X509 *MakeCertificate(EVP_PKEY *pkey);
	// Make Digest
	bool ComputeDigest(const ov::String &algorithm);

	bool GetDigestEVP(const ov::String &algorithm, const EVP_MD **mdp);

	// Private key
	ov::RaiiPtr<EVP_PKEY> _private_key{nullptr, ::EVP_PKEY_free};
	ov::String _private_key_filename;
	// Certificate
	ov::RaiiPtr<X509> _certificate{nullptr, ::X509_free};
	ov::String _certificate_filename;
	// Chain Certificate(s)
	ov::RaiiPtr<STACK_OF(X509)> _chain_certificate{nullptr, X509StackFree};
	ov::String _chain_certificate_filename;

	ov::Data _digest;
	ov::String _digest_algorithm;
};
