/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.

For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "mtproto/connection_http.h"

#include "base/random.h"
#include "base/qthelp_url.h"

namespace MTP {
namespace details {
namespace {

constexpr auto kForceHttpPort = 80;
constexpr auto kFullConnectionTimeout = crl::time(8000);

} // namespace

HttpConnection::HttpConnection(QThread *thread, const ProxyData &proxy)
: AbstractConnection(thread, proxy)
, _checkNonce(base::RandomValue<MTPint128>()) {
	_manager.moveToThread(thread);
	_manager.setProxy(ToNetworkProxy(proxy));
}

ConnectionPointer HttpConnection::clone(const ProxyData &proxy) {
	return ConnectionPointer::New<HttpConnection>(thread(), proxy);
}

void HttpConnection::sendData(mtpBuffer &&buffer) {
	Expects(buffer.size() > 2);

	if (_status == Status::Finished) {
		return;
	}

	int32 requestSize = (buffer.size() - 2) * sizeof(mtpPrime);

	QNetworkRequest request(url());
	request.setHeader(
		QNetworkRequest::ContentLengthHeader,
		QVariant(requestSize));
	request.setHeader(
		QNetworkRequest::ContentTypeHeader,
		QVariant(u"application/x-www-form-urlencoded"_q));

	CONNECTION_LOG_INFO(u"Sending %1 len request."_q.arg(requestSize));
	_requests.insert(_manager.post(request, QByteArray((const char*)(&buffer[2]), requestSize)));
}

void HttpConnection::disconnectFromServer() {
	if (_status == Status::Finished) return;
	_status = Status::Finished;

	for (const auto request : base::take(_requests)) {
		request->abort();
		request->deleteLater();
	}

	disconnect(
		&_manager,
		&QNetworkAccessManager::finished,
		this,
		&HttpConnection::requestFinished);
}

void HttpConnection::connectToServer(
		const QString &address,
		int port,
		const bytes::vector &protocolSecret,
		int16 protocolDcId,
		bool protocolForFiles) {
	_address = address;
	connect(
		&_manager,
		&QNetworkAccessManager::finished,
		this,
		&HttpConnection::requestFinished);

	auto buffer = preparePQFake(_checkNonce);

	if (Logs::DebugEnabled()) {
		_debugId = u"%1(dc:%2,%3)"_q
			.arg(_debugId.toInt())
			.arg(ProtocolDcDebugId(protocolDcId))
			.arg(url().toDisplayString());
	}

	_pingTime = crl::now();
	sendData(std::move(buffer));
}

mtpBuffer HttpConnection::handleResponse(QNetworkReply *reply) {
	QByteArray response = reply->readAll();
	CONNECTION_LOG_INFO(u"Read %1 bytes."_q.arg(response.size()));

	if (response.isEmpty()) return mtpBuffer();

	if (response.size() & 0x03 || response.size() < 8) {
		CONNECTION_LOG_ERROR(u"Bad response size %1."_q.arg(response.size()));
		return mtpBuffer(1, -500);
	}

	mtpBuffer data(response.size() >> 2);
	memcpy(data.data(), response.constData(), response.size());

	return data;
}

// Returns "maybe bad key".
qint32 HttpConnection::handleError(QNetworkReply *reply) {
	auto result = qint32(kErrorCodeOther);

	QVariant statusCode = reply->attribute(
		QNetworkRequest::HttpStatusCodeAttribute);
	if (statusCode.isValid()) {
		int status = statusCode.toInt();
		result = -status;
	}

	switch (reply->error()) {
	case QNetworkReply::ConnectionRefusedError:
		CONNECTION_LOG_ERROR(u"Connection refused - %1."_q
			.arg(reply->errorString()));
		break;
	case QNetworkReply::RemoteHostClosedError:
		CONNECTION_LOG_ERROR(u"Remote host closed - %1."_q
			.arg(reply->errorString()));
		break;
	case QNetworkReply::HostNotFoundError:
		CONNECTION_LOG_ERROR(u"Host not found - %1."_q
			.arg(reply->errorString()));
		break;
	case QNetworkReply::TimeoutError:
		CONNECTION_LOG_ERROR(u"Timeout - %1."_q
			.arg(reply->errorString()));
		break;
	case QNetworkReply::OperationCanceledError:
		CONNECTION_LOG_ERROR(u"Cancelled - %1."_q
			.arg(reply->errorString()));
		break;
	case QNetworkReply::SslHandshakeFailedError:
	case QNetworkReply::TemporaryNetworkFailureError:
	case QNetworkReply::NetworkSessionFailedError:
	case QNetworkReply::BackgroundRequestNotAllowedError:
	case QNetworkReply::UnknownNetworkError:
		CONNECTION_LOG_ERROR(u"Network error %1 - %2."_q
			.arg(reply->error())
			.arg(reply->errorString()));
		break;

	// proxy errors (101-199):
	case QNetworkReply::ProxyConnectionRefusedError:
	case QNetworkReply::ProxyConnectionClosedError:
	case QNetworkReply::ProxyNotFoundError:
	case QNetworkReply::ProxyTimeoutError:
	case QNetworkReply::ProxyAuthenticationRequiredError:
	case QNetworkReply::UnknownProxyError:
		CONNECTION_LOG_ERROR(u"Proxy error %1 - %2."_q
			.arg(reply->error())
			.arg(reply->errorString()));
		break;

	// content errors (201-299):
	case QNetworkReply::ContentAccessDenied:
	case QNetworkReply::ContentOperationNotPermittedError:
	case QNetworkReply::ContentNotFoundError:
	case QNetworkReply::AuthenticationRequiredError:
	case QNetworkReply::ContentReSendError:
	case QNetworkReply::UnknownContentError:
		CONNECTION_LOG_ERROR(u"Content error %1 - %2."_q
			.arg(reply->error())
			.arg(reply->errorString()));
		break;

	// protocol errors
	case QNetworkReply::ProtocolUnknownError:
	case QNetworkReply::ProtocolInvalidOperationError:
	case QNetworkReply::ProtocolFailure:
		CONNECTION_LOG_ERROR(u"Protocol error %1 - %2."_q
			.arg(reply->error())
			.arg(reply->errorString()));
		break;
	};

	return result;
}

bool HttpConnection::isConnected() const {
	return (_status == Status::Ready);
}

void HttpConnection::requestFinished(QNetworkReply *reply) {
	if (_status == Status::Finished) return;

	reply->deleteLater();
	if (reply->error() == QNetworkReply::NoError) {
		_requests.remove(reply);

		mtpBuffer data = handleResponse(reply);
		if (data.size() == 1) {
			error(data[0]);
		} else if (!data.isEmpty()) {
			if (_status == Status::Ready) {
				_receivedQueue.push_back(data);
				receivedData();
			} else if (const auto res_pq = readPQFakeReply(data)) {
				const auto &data = res_pq->c_resPQ();
				if (data.vnonce() == _checkNonce) {
					CONNECTION_LOG_INFO(
						"HTTP-transport connected by pq-response.");
					_status = Status::Ready;
					_pingTime = crl::now() - _pingTime;
					connected();
				} else {
					CONNECTION_LOG_ERROR(
						"Wrong nonce in HTTP fake pq-response.");
					error(kErrorCodeOther);
				}
			} else {
				CONNECTION_LOG_ERROR(
					"Could not parse HTTP fake pq-response.");
				error(kErrorCodeOther);
			}
		}
	} else {
		if (!_requests.remove(reply)) {
			return;
		}

		error(handleError(reply));
	}
}

crl::time HttpConnection::pingTime() const {
	return isConnected() ? _pingTime : crl::time(0);
}

crl::time HttpConnection::fullConnectTimeout() const {
	return kFullConnectionTimeout;
}

bool HttpConnection::usingHttpWait() {
	return true;
}

bool HttpConnection::needHttpWait() {
	return _requests.isEmpty();
}

int32 HttpConnection::debugState() const {
	return -1;
}

QString HttpConnection::transport() const {
	if (!isConnected()) {
		return QString();
	}
	auto result = u"HTTP"_q;
	if (qthelp::is_ipv6(_address)) {
		result += u"/IPv6"_q;
	}
	return result;
}

QString HttpConnection::tag() const {
	auto result = u"HTTP"_q;
	if (qthelp::is_ipv6(_address)) {
		result += u"/IPv6"_q;
	} else {
		result += u"/IPv4"_q;
	}
	return result;
}

QUrl HttpConnection::url() const {
	const auto pattern = qthelp::is_ipv6(_address)
		? u"http://[%1]:%2/api"_q
		: u"http://%1:%2/api"_q;

	// Not endpoint.port - always 80 port for http transport.
	return QUrl(pattern.arg(_address).arg(kForceHttpPort));
}

} // namespace details
} // namespace MTP
