// license:MIT
// copyright-holders:Ole Christian Eidheim, Miodrag Milanovic
#ifndef CLIENT_HTTPS_HPP
#define CLIENT_HTTPS_HPP

#include "client_http.hpp"
#include <asio/ssl.hpp>

namespace webpp {
	using HTTPS = asio::ssl::stream<asio::ip::tcp::socket>;

	template<>
	class Client<HTTPS> : public ClientBase<HTTPS> {
	public:
		explicit Client(const std::string& server_port_path, bool verify_certificate=true,
				const std::string& cert_file=std::string(), const std::string& private_key_file=std::string(),
				const std::string& verify_file=std::string()) :
				ClientBase(server_port_path, 443), m_context(asio::ssl::context::tlsv12) {
			if(cert_file.size()>0 && private_key_file.size()>0) {
				m_context.use_certificate_chain_file(cert_file);
				m_context.use_private_key_file(private_key_file, asio::ssl::context::pem);
			}

			if(verify_certificate)
				m_context.set_verify_callback(asio::ssl::rfc2818_verification(host));

			if(verify_file.size()>0)
				m_context.load_verify_file(verify_file);
			else
				m_context.set_default_verify_paths();

			if(verify_file.size()>0 || verify_certificate)
				m_context.set_verify_mode(asio::ssl::verify_peer);
			else
				m_context.set_verify_mode(asio::ssl::verify_none);
		}

	protected:
		asio::ssl::context m_context;

		void connect() override {
			if(!socket || !socket->lowest_layer().is_open()) {
				std::unique_ptr<asio::ip::tcp::resolver::query> query;
				if (config.proxy_server.empty()) {
					query = std::make_unique<asio::ip::tcp::resolver::query>(host, std::to_string(port));
				}
				else {
					auto proxy_host_port = parse_host_port(config.proxy_server, 8080);
					query = std::make_unique<asio::ip::tcp::resolver::query>(proxy_host_port.first, std::to_string(proxy_host_port.second));
				}
				resolver.async_resolve(*query, [this]
							(const std::error_code &ec, asio::ip::tcp::resolver::iterator it) {
					if (!ec) {
						{
							std::lock_guard<std::mutex> lock(socket_mutex);
							socket = std::make_unique<HTTPS>(io_context, m_context);
						}

						auto timer = get_timeout_timer(config.timeout_connect);
						asio::async_connect(socket->lowest_layer(), it, [this, timer]
								(const std::error_code &ec, asio::ip::tcp::resolver::iterator /*it*/) {
							if (timer)
								timer->cancel();
							if (!ec) {
								asio::ip::tcp::no_delay option(true);
								socket->lowest_layer().set_option(option);
							}
							else {
								std::lock_guard<std::mutex> lock(socket_mutex);
								socket = nullptr;
								throw std::system_error(ec);
							}
						});
					}
					else {
						std::lock_guard<std::mutex> lock(socket_mutex);
						socket = nullptr;
						throw std::system_error(ec);
					}
				});
				io_context.reset();
				io_context.run();

				if (!config.proxy_server.empty()) {
					asio::streambuf write_buffer;
					std::ostream write_stream(&write_buffer);
					auto host_port = host + ':' + std::to_string(port);
					write_stream << "CONNECT " + host_port + " HTTP/1.1\r\n" << "Host: " << host_port << "\r\n\r\n";
					auto timer = get_timeout_timer();
					asio::async_write(socket->next_layer(), write_buffer,
						[this, timer](const std::error_code &ec, size_t /*bytes_transferred*/) {
						if (timer)
							timer->cancel();
						if (ec) {
							std::lock_guard<std::mutex> lock(socket_mutex);
							socket = nullptr;
							throw std::system_error(ec);
						}
					});
					io_context.reset();
					io_context.run();

					std::shared_ptr<Response> response(new Response());
					timer=get_timeout_timer();
					asio::async_read_until(socket->next_layer(), response->content_buffer, "\r\n\r\n",
												[this, timer](const std::error_code& ec, size_t /*bytes_transferred*/) {
						if(timer)
							timer->cancel();
						if(ec) {
							std::lock_guard<std::mutex> lock(socket_mutex);
							socket=nullptr;
							throw std::system_error(ec);
						}
					});
					io_context.reset();
					io_context.run();
					parse_response_header(response);

					if (response->status_code.size()>0 && response->status_code.compare(0, 3, "200") != 0) {
						std::lock_guard<std::mutex> lock(socket_mutex);
						socket = nullptr;
						throw std::system_error(std::error_code(int(std::errc::permission_denied), std::generic_category()));
					}
				}

				auto timer = get_timeout_timer();
				this->socket->async_handshake(asio::ssl::stream_base::client,
					[this, timer](const std::error_code& ec) {
					if (timer)
						timer->cancel();
					if (ec) {
						std::lock_guard<std::mutex> lock(socket_mutex);
						socket = nullptr;
						throw std::system_error(ec);
					}
				});
				io_context.reset();
				io_context.run();
			}
		}
	};

	class https_client : public Client<HTTPS> {
	public:
		explicit https_client(const std::string& server_port_path, bool verify_certificate = true,
			const std::string& cert_file = std::string(), const std::string& private_key_file = std::string(),
			const std::string& verify_file = std::string()) : Client<HTTPS>::Client(server_port_path, verify_certificate, cert_file, private_key_file, verify_file) {}
	};

}

#endif  /* CLIENT_HTTPS_HPP */
