// Copyright 2023 Northern.tech AS
//
//    Licensed under the Apache License, Version 2.0 (the "License");
//    you may not use this file except in compliance with the License.
//    You may obtain a copy of the License at
//
//        http://www.apache.org/licenses/LICENSE-2.0
//
//    Unless required by applicable law or agreed to in writing, software
//    distributed under the License is distributed on an "AS IS" BASIS,
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//    See the License for the specific language governing permissions and
//    limitations under the License.

#ifndef MENDER_UPDATE_DEPLOYMENTS_HPP
#define MENDER_UPDATE_DEPLOYMENTS_HPP

#include <common/config.h>

#ifdef MENDER_LOG_BOOST
#include <boost/log/sinks/sync_frontend.hpp>
#include <boost/log/sinks/text_ostream_backend.hpp>
#include <boost/smart_ptr/shared_ptr.hpp>
#endif // MENDER_LOG_BOOST

#include <string>
#include <vector>

#include <api/client.hpp>
#include <common/error.hpp>
#include <common/events.hpp>
#include <common/expected.hpp>
#include <common/http.hpp>
#include <common/io.hpp>
#include <common/json.hpp>
#include <common/optional.hpp>
#include <mender-update/context.hpp>

// For friend declaration below, used in tests.
class DeploymentsTests;
namespace mender {
namespace update {
namespace deployments {

using namespace std;

#ifdef MENDER_LOG_BOOST
namespace sinks = boost::log::sinks;
#endif // MENDER_LOG_BOOST

namespace api = mender::api;
namespace context = mender::update::context;
namespace error = mender::common::error;
namespace events = mender::common::events;
namespace expected = mender::common::expected;
namespace http = mender::common::http;
namespace io = mender::common::io;
namespace json = mender::common::json;

enum DeploymentsErrorCode {
	NoError = 0,
	InvalidDataError,
	BadResponseError,
	DeploymentAbortedError,
	TooManyRequestsError,
	RequestBodyTooLargeError,
};

class DeploymentsErrorCategoryClass : public std::error_category {
public:
	const char *name() const noexcept override;
	string message(int code) const override;
};
extern const DeploymentsErrorCategoryClass DeploymentsErrorCategory;

error::Error MakeError(DeploymentsErrorCode code, const string &msg);

struct APIResponseError {
	optional<unsigned> http_code;
	optional<http::Transaction::HeaderMap> http_headers;
	error::Error error;
};

using CheckUpdatesAPIResponseData = optional<json::Json>;
using CheckUpdatesAPIResponseError = struct APIResponseError;
using CheckUpdatesAPIResponse = expected::expected<CheckUpdatesAPIResponseData, APIResponseError>;
using CheckUpdatesAPIResponseHandler = function<void(CheckUpdatesAPIResponse)>;

enum class DeploymentStatus {
	Installing = 0,
	PauseBeforeInstalling,
	Downloading,
	PauseBeforeRebooting,
	Rebooting,
	PauseBeforeCommitting,
	Success,
	Failure,
	AlreadyInstalled,

	// Not a valid status, just used as an int representing the number of values
	// above
	End_
};

string DeploymentStatusString(DeploymentStatus status);

using StatusAPIResponse = struct APIResponseError; // there's no data, we care only about errors
using StatusAPIResponseHandler = function<void(StatusAPIResponse)>;

using LogsAPIResponse = struct APIResponseError; // there's no data, we care only about errors
using LogsAPIResponseHandler = function<void(LogsAPIResponse)>;

class DeploymentAPI {
public:
	virtual ~DeploymentAPI() {
	}

	virtual error::Error CheckNewDeployments(
		context::MenderContext &ctx,
		api::Client &client,
		CheckUpdatesAPIResponseHandler api_handler) = 0;
	virtual error::Error PushStatus(
		const string &deployment_id,
		DeploymentStatus status,
		const string &substate,
		api::Client &client,
		StatusAPIResponseHandler api_handler) = 0;
	virtual error::Error PushLogs(
		const string &deployment_id,
		const string &log_file_path,
		api::Client &client,
		LogsAPIResponseHandler api_handler) = 0;
};

class DeploymentClient : virtual public DeploymentAPI {
public:
	error::Error CheckNewDeployments(
		context::MenderContext &ctx,
		api::Client &client,
		CheckUpdatesAPIResponseHandler api_handler) override;
	error::Error PushStatus(
		const string &deployment_id,
		DeploymentStatus status,
		const string &substate,
		api::Client &client,
		StatusAPIResponseHandler api_handler) override;
	error::Error PushLogs(
		const string &deployment_id,
		const string &log_file_path,
		api::Client &client,
		LogsAPIResponseHandler api_handler) override;

private:
	friend class ::DeploymentsTests;
	void HeaderHandler(
		shared_ptr<vector<uint8_t>> received_body,
		CheckUpdatesAPIResponseHandler api_handler,
		http::ExpectedIncomingResponsePtr exp_resp);
	void PushStatusHeaderHandler(
		shared_ptr<vector<uint8_t>> received_body,
		StatusAPIResponseHandler api_handler,
		http::ExpectedIncomingResponsePtr exp_resp);
	void PushLogsHeaderHandler(
		shared_ptr<vector<uint8_t>> received_body,
		LogsAPIResponseHandler api_handler,
		http::ExpectedIncomingResponsePtr exp_resp);
};

/**
 * A helper class only declared here because of testing. Not to be used
 * separately outside of PushLogs().
 */
class JsonLogMessagesReader : virtual public io::Reader {
public:
	/**
	 * @see GetLogFileDataSize() for details about #data_size
	 */
	JsonLogMessagesReader(const string &log_fpath) :
		log_fpath_ {log_fpath},
		bad_data_msg_ {common::ByteVectorFromString(bad_data_msg_tmpl_)},
		bad_data_msg_rem_ {bad_data_msg_.size()},
		too_much_data_msg_ {common::ByteVectorFromString(too_much_data_msg_tmpl_)},
		too_much_data_msg_rem_ {too_much_data_msg_.size()} {};

	~JsonLogMessagesReader();

	// Must be called before any other methods/member functions below.
	error::Error SanitizeLogs();

	expected::ExpectedSize Read(
		vector<uint8_t>::iterator start, vector<uint8_t>::iterator end) override;

	error::Error Rewind();

	int64_t TotalDataSize();

private:
	const string log_fpath_;
	string sanitized_fpath_;
	unique_ptr<io::FileReader> reader_;
	int64_t raw_data_size_;
	int64_t rem_raw_data_size_;
	static const vector<uint8_t> header_;
	static const vector<uint8_t> closing_;
	static const string default_tstamp_;
	static const string bad_data_msg_tmpl_;
	static const string too_much_data_msg_tmpl_;
	io::Vsize header_rem_ = header_.size();
	io::Vsize closing_rem_ = closing_.size();
	vector<uint8_t> bad_data_msg_;
	io::Vsize bad_data_msg_rem_;
	vector<uint8_t> too_much_data_msg_;
	io::Vsize too_much_data_msg_rem_;
	bool clean_logs_;
	const int32_t maximum_log_size_ = 1000 * 1000 - 256; // 1MB - some room for request headers etc.
	bool large_logs_ = false;

	friend class ::DeploymentsTests;
};

class DeploymentLog {
public:
	DeploymentLog(const string &data_store_dir, const string &deployment_id) :
		data_store_dir_ {data_store_dir},
		id_ {deployment_id} {};
	error::Error BeginLogging();
	error::Error FinishLogging();
	~DeploymentLog() {
		if (sink_) {
			FinishLogging();
		}
	};

	string LogFileName();
	string LogFilePath();

private:
	const string data_store_dir_;
	const string id_;
#ifdef MENDER_LOG_BOOST
	typedef sinks::synchronous_sink<sinks::text_ostream_backend> text_sink;
	boost::shared_ptr<text_sink> sink_;
#endif // MENDER_LOG_BOOST
	error::Error PrepareLogDirectory();
	error::Error DoPrepareLogDirectory();
};

} // namespace deployments
} // namespace update
} // namespace mender

#endif // MENDER_UPDATE_DEPLOYMENTS_HPP
