#include "macro-action-http.hpp"
#include "layout-helpers.hpp"

#include <httplib.h>

#undef DELETE

namespace advss {

const std::string MacroActionHttp::id = "http_v2";

bool MacroActionHttp::_registered = MacroActionFactory::Register(
	MacroActionHttp::id,
	{MacroActionHttp::Create, MacroActionHttpEdit::Create,
	 "AdvSceneSwitcher.action.http"});

static httplib::Headers getHeaders(const StringList &strings)
{
	httplib::Headers headers;
	for (int i = 0; i < strings.size(); i = i + 2) {
		const auto pair =
			i + 1 >= strings.size()
				? std::make_pair(std::string(strings.at(i)), "")
				: std::make_pair(
					  std::string(strings.at(i)),
					  std::string(strings.at(i + 1)));
		headers.emplace(pair);
	}
	return headers;
}

static httplib::Params getParams(const StringList &strings)
{
	httplib::Params params;
	for (int i = 0; i < strings.size(); i = i + 2) {
		const auto pair =
			i + 1 >= strings.size()
				? std::make_pair(std::string(strings.at(i)), "")
				: std::make_pair(
					  std::string(strings.at(i)),
					  std::string(strings.at(i + 1)));
		params.emplace(pair);
	}
	return params;
}

static void setTimeout(httplib::Client &client, const Duration &timeout)
{
	const time_t seconds = timeout.Seconds();
	const time_t usecs = timeout.Milliseconds() - (seconds * 1000);
	client.set_read_timeout(seconds, usecs);
	client.set_write_timeout(seconds, usecs);
}

void MacroActionHttp::SetupTempVars()
{
	MacroAction::SetupTempVars();
	AddTempvar("status",
		   obs_module_text("AdvSceneSwitcher.tempVar.http.status"));
	AddTempvar("body",
		   obs_module_text("AdvSceneSwitcher.tempVar.http.body"));
	AddTempvar("error",
		   obs_module_text("AdvSceneSwitcher.tempVar.http.error"),
		   obs_module_text(
			   "AdvSceneSwitcher.tempVar.http.error.description"));
}

struct URLInfo {
	std::string host;
	std::string path;
};

static URLInfo getURLInfo(const std::string &input, bool keepParams)
{
	if (input.empty()) {
		return {};
	}

	QString urlInput = QString::fromStdString(input);
	if (!urlInput.contains("://")) {
		urlInput = "http://" + urlInput;
	}

	const QUrl url(urlInput);
	auto host =
		url.scheme().toStdString() + "://" + url.host().toStdString();

	const int port = url.port();
	if (port != -1) {
		host += ":" + std::to_string(port);
	}

	auto path = url.path().toStdString();
	if (path.empty()) {
		path = "/";
	}

	const auto query = url.query().toStdString();
	if (!query.empty()) {
		if (keepParams) {
			path += "?" + query;
		} else {
			blog(LOG_WARNING,
			     "ignoring query parameters \"%s\" in URL field. "
			     "Using parameter field values instead.",
			     query.c_str());
		}
	}

	return {host, path};
}

bool MacroActionHttp::PerformAction()
{
	const auto [host, path] = getURLInfo(_url, !_setParams);

	httplib::Client cli(host);
	setTimeout(cli, _timeout);
	const auto params = _setParams ? getParams(_params) : httplib::Params();
	const auto headers = _setHeaders ? getHeaders(_headers)
					 : httplib::Headers();

	httplib::Result response;
	switch (_method) {
	case MacroActionHttp::Method::GET:
		response = cli.Get(path, params, headers);
		break;
	case MacroActionHttp::Method::POST: {
		const auto pathWithParam =
			httplib::append_query_params(path, params);
		response =
			cli.Post(pathWithParam, headers, _body, _contentType);
		break;
	}
	case MacroActionHttp::Method::PUT: {
		const auto pathWithParam =
			httplib::append_query_params(path, params);
		response = cli.Put(pathWithParam, headers, _body, _contentType);
		break;
	}
	case MacroActionHttp::Method::PATCH: {
		const auto pathWithParam =
			httplib::append_query_params(path, params);
		response =
			cli.Patch(pathWithParam, headers, _body, _contentType);
		break;
	}
	case MacroActionHttp::Method::DELETE: {
		const auto pathWithParam =
			httplib::append_query_params(path, params);
		response =
			cli.Delete(pathWithParam, headers, _body, _contentType);
		break;
	}
	default:
		break;
	}

	if (VerboseLoggingEnabled() && !response) {
		blog(LOG_INFO, "HTTP action error: %s",
		     httplib::to_string(response.error()).c_str());
	}

	SetTempVarValue("status",
			response ? std::to_string(response->status) : "");
	SetTempVarValue("body", response ? response->body : "");
	SetTempVarValue("error",
			response ? "" : httplib::to_string(response.error()));

	return true;
}

static constexpr std::string_view methodToString(MacroActionHttp::Method method)
{
	switch (method) {
	case MacroActionHttp::Method::GET:
		return "GET";
	case MacroActionHttp::Method::POST:
		return "POST";
	case MacroActionHttp::Method::PUT:
		return "PUT";
	case MacroActionHttp::Method::PATCH:
		return "PATCH";
	case MacroActionHttp::Method::DELETE:
		return "DELETE";
	default:
		break;
	}
	return "unknown";
}

static std::string stringListToString(const StringList &list)
{
	if (list.empty()) {
		return "[]";
	}
	std::string result = "[";
	for (const auto &string : list) {
		result += std::string(string) + ", ";
	}
	result.pop_back();
	result.pop_back();
	return result + "]";
}

void MacroActionHttp::LogAction() const
{
	ablog(LOG_INFO,
	      "sent HTTP request (%s) "
	      "to URL \"%s\" "
	      "with content type \"%s\" "
	      "with body \"%s\" "
	      "with headers \"%s\" "
	      "with parameters \"%s\" "
	      "with timeout \"%s\"",
	      methodToString(_method).data(), _url.c_str(),
	      _contentType.c_str(), _body.c_str(),
	      _setHeaders ? stringListToString(_headers).c_str() : "-",
	      _setParams ? stringListToString(_params).c_str() : "-",
	      _timeout.ToString().c_str());
}

bool MacroActionHttp::Save(obs_data_t *obj) const
{
	MacroAction::Save(obj);
	_url.Save(obj, "url");
	_contentType.Save(obj, "contentType");
	_body.Save(obj, "body");
	obs_data_set_bool(obj, "setHeaders", _setHeaders);
	_headers.Save(obj, "headers", "header");
	obs_data_set_bool(obj, "setParams", _setParams);
	_params.Save(obj, "params", "param");
	obs_data_set_int(obj, "method", static_cast<int>(_method));
	_timeout.Save(obj);
	return true;
}

bool MacroActionHttp::Load(obs_data_t *obj)
{
	MacroAction::Load(obj);
	_url.Load(obj, "url");
	if (obs_data_has_user_value(obj, "path")) {
		std::string url = _url.UnresolvedValue();
		url += obs_data_get_string(obj, "path");
		_url = url;
	}
	_contentType.Load(obj, "contentType");
	_body.Load(obj, "body");
	_setHeaders = obs_data_get_bool(obj, "setHeaders");
	_headers.Load(obj, "headers", "header");
	_setParams = obs_data_get_bool(obj, "setParams");
	_params.Load(obj, "params", "param");
	_method = static_cast<Method>(obs_data_get_int(obj, "method"));
	_timeout.Load(obj);
	return true;
}

std::string MacroActionHttp::GetShortDesc() const
{
	return _url.UnresolvedValue();
}

std::shared_ptr<MacroAction> MacroActionHttp::Create(Macro *m)
{
	return std::make_shared<MacroActionHttp>(m);
}

std::shared_ptr<MacroAction> MacroActionHttp::Copy() const
{
	return std::make_shared<MacroActionHttp>(*this);
}

void MacroActionHttp::ResolveVariablesToFixedValues()
{
	_url.ResolveVariables();
	_contentType.ResolveVariables();
	_body.ResolveVariables();
	_headers.ResolveVariables();
	_params.ResolveVariables();
	_timeout.ResolveVariables();
}

static inline void populateMethodSelection(QComboBox *list)
{
	const static std::map<MacroActionHttp::Method, std::string> methods = {
		{MacroActionHttp::Method::GET,
		 "AdvSceneSwitcher.action.http.type.get"},
		{MacroActionHttp::Method::POST,
		 "AdvSceneSwitcher.action.http.type.post"},
		{MacroActionHttp::Method::PUT,
		 "AdvSceneSwitcher.action.http.type.put"},
		{MacroActionHttp::Method::PATCH,
		 "AdvSceneSwitcher.action.http.type.patch"},
		{MacroActionHttp::Method::DELETE,
		 "AdvSceneSwitcher.action.http.type.delete"},
	};

	for (const auto &[value, name] : methods) {
		list->addItem(obs_module_text(name.c_str()),
			      static_cast<int>(value));
	}
}

MacroActionHttpEdit::MacroActionHttpEdit(
	QWidget *parent, std::shared_ptr<MacroActionHttp> entryData)
	: QWidget(parent),
	  _url(new VariableLineEdit(this)),
	  _contentType(new VariableLineEdit(this)),
	  _contentTypeLayout(new QHBoxLayout()),
	  _methods(new QComboBox()),
	  _body(new VariableTextEdit(this)),
	  _bodyLayout(new QVBoxLayout()),
	  _setHeaders(new QCheckBox(
		  obs_module_text("AdvSceneSwitcher.action.http.setHeaders"))),
	  _headerList(new KeyValueListEdit(
		  this, obs_module_text("AdvSceneSwitcher.action.http.headers"),
		  obs_module_text("AdvSceneSwitcher.action.http.addHeader.name"),
		  obs_module_text("AdvSceneSwitcher.action.http.headers"),
		  obs_module_text(
			  "AdvSceneSwitcher.action.http.addHeader.value"))),
	  _headerListLayout(new QVBoxLayout()),
	  _setParams(new QCheckBox(
		  obs_module_text("AdvSceneSwitcher.action.http.setParams"))),
	  _paramList(new KeyValueListEdit(
		  this, obs_module_text("AdvSceneSwitcher.action.http.params"),
		  obs_module_text("AdvSceneSwitcher.action.http.addParam.name"),
		  obs_module_text("AdvSceneSwitcher.action.http.params"),
		  obs_module_text(
			  "AdvSceneSwitcher.action.http.addParam.value"))),
	  _paramListLayout(new QVBoxLayout()),
	  _timeout(new DurationSelection(this, false))
{
	populateMethodSelection(_methods);

	SetWidgetSignalConnections();
	SetWidgetLayout();

	_entryData = entryData;
	UpdateEntryData();
	_loading = false;
}

void MacroActionHttpEdit::UpdateEntryData()
{
	if (!_entryData) {
		return;
	}

	_url->setText(_entryData->_url);
	_contentType->setText(_entryData->_contentType);
	_body->setPlainText(_entryData->_body);
	_setHeaders->setChecked(_entryData->_setHeaders);
	_headerList->SetStringList(_entryData->_headers);
	_setParams->setChecked(_entryData->_setParams);
	_paramList->SetStringList(_entryData->_params);
	_methods->setCurrentIndex(
		_methods->findData(static_cast<int>(_entryData->_method)));
	_timeout->SetDuration(_entryData->_timeout);
	SetWidgetVisibility();
}

void MacroActionHttpEdit::URLChanged()
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_url = _url->text().toStdString();
	emit(HeaderInfoChanged(_url->text()));
}

void MacroActionHttpEdit::ContentTypeChanged()
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_contentType = _contentType->text().toStdString();
}

void MacroActionHttpEdit::BodyChanged()
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_body = _body->toPlainText().toUtf8().constData();

	adjustSize();
	updateGeometry();
}

void MacroActionHttpEdit::MethodChanged(int idx)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_method = static_cast<MacroActionHttp::Method>(
		_methods->itemData(idx).toInt());
	SetWidgetVisibility();
}

void MacroActionHttpEdit::TimeoutChanged(const Duration &dur)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_timeout = dur;
}

void MacroActionHttpEdit::SetHeadersChanged(int value)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_setHeaders = value;
	SetWidgetVisibility();
}

void MacroActionHttpEdit::HeadersChanged(const StringList &headers)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_headers = headers;
	adjustSize();
	updateGeometry();
}

void MacroActionHttpEdit::SetParamsChanged(int value)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_setParams = value;
	SetWidgetVisibility();
}

void MacroActionHttpEdit::ParamsChanged(const StringList &params)
{
	GUARD_LOADING_AND_LOCK();
	_entryData->_params = params;
	adjustSize();
	updateGeometry();
}

void MacroActionHttpEdit::SetWidgetSignalConnections()
{
	QWidget::connect(_url, SIGNAL(editingFinished()), this,
			 SLOT(URLChanged()));
	QWidget::connect(_contentType, SIGNAL(editingFinished()), this,
			 SLOT(ContentTypeChanged()));
	QWidget::connect(_body, SIGNAL(textChanged()), this,
			 SLOT(BodyChanged()));
	QWidget::connect(_methods, SIGNAL(currentIndexChanged(int)), this,
			 SLOT(MethodChanged(int)));
	QWidget::connect(_setHeaders, SIGNAL(stateChanged(int)), this,
			 SLOT(SetHeadersChanged(int)));
	QWidget::connect(_headerList,
			 SIGNAL(StringListChanged(const StringList &)), this,
			 SLOT(HeadersChanged(const StringList &)));
	QWidget::connect(_setParams, SIGNAL(stateChanged(int)), this,
			 SLOT(SetParamsChanged(int)));
	QWidget::connect(_paramList,
			 SIGNAL(StringListChanged(const StringList &)), this,
			 SLOT(ParamsChanged(const StringList &)));
	QWidget::connect(_timeout, SIGNAL(DurationChanged(const Duration &)),
			 this, SLOT(TimeoutChanged(const Duration &)));
}

void MacroActionHttpEdit::SetWidgetLayout()
{
	const std::unordered_map<std::string, QWidget *> widgets = {
		{"{{url}}", _url},         {"{{contentType}}", _contentType},
		{"{{method}}", _methods},  {"{{body}}", _body},
		{"{{timeout}}", _timeout},
	};

	auto actionLayout = new QHBoxLayout;
	PlaceWidgets(
		obs_module_text("AdvSceneSwitcher.action.http.layout.method"),
		actionLayout, widgets, false);

	PlaceWidgets(obs_module_text(
			     "AdvSceneSwitcher.action.http.layout.contentType"),
		     _contentTypeLayout, widgets);

	_bodyLayout->addWidget(new QLabel(
		obs_module_text("AdvSceneSwitcher.action.http.body")));
	_bodyLayout->addWidget(_body);

	auto timeoutLayout = new QHBoxLayout;
	PlaceWidgets(
		obs_module_text("AdvSceneSwitcher.action.http.layout.timeout"),
		timeoutLayout, widgets);

	_headerListLayout->addWidget(new QLabel(
		obs_module_text("AdvSceneSwitcher.action.http.headers")));
	_headerListLayout->addWidget(_headerList);

	_paramListLayout->addWidget(new QLabel(
		obs_module_text("AdvSceneSwitcher.action.http.params")));
	_paramListLayout->addWidget(_paramList);

	auto layout = new QVBoxLayout;
	layout->addLayout(actionLayout);
	layout->addWidget(_setHeaders);
	layout->addLayout(_headerListLayout);
	layout->addWidget(_setParams);
	layout->addLayout(_paramListLayout);
	layout->addLayout(_contentTypeLayout);
	layout->addLayout(_bodyLayout);
	layout->addLayout(timeoutLayout);
	setLayout(layout);
}

void MacroActionHttpEdit::SetWidgetVisibility()
{
	SetLayoutVisible(_headerListLayout, _entryData->_setHeaders);
	SetLayoutVisible(_paramListLayout, _entryData->_setParams);
	SetLayoutVisible(_contentTypeLayout,
			 _entryData->_method != MacroActionHttp::Method::GET);
	SetLayoutVisible(_bodyLayout,
			 _entryData->_method != MacroActionHttp::Method::GET);

	adjustSize();
	updateGeometry();
}

} // namespace advss
