/*
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/session_private.h"

#include "mtproto/details/mtproto_bound_key_creator.h"
#include "mtproto/details/mtproto_dcenter.h"
#include "mtproto/details/mtproto_dump_to_text.h"
#include "mtproto/details/mtproto_rsa_public_key.h"
#include "mtproto/session.h"
#include "mtproto/mtproto_response.h"
#include "mtproto/mtproto_dc_options.h"
#include "mtproto/connection_abstract.h"
#include "platform/platform_specific.h"
#include "base/random.h"
#include "base/qthelp_url.h"
#include "base/openssl_help.h"
#include "base/unixtime.h"
#include "base/platform/base_platform_info.h"
#include "zlib.h"

namespace MTP {
namespace details {
namespace {

constexpr auto kIntSize = static_cast<int>(sizeof(mtpPrime));
constexpr auto kWaitForBetterTimeout = crl::time(2000);
constexpr auto kMinConnectedTimeout = crl::time(1000);
constexpr auto kMaxConnectedTimeout = crl::time(8000);
constexpr auto kMinReceiveTimeout = crl::time(4000);
constexpr auto kMaxReceiveTimeout = crl::time(64000);
constexpr auto kMarkConnectionOldTimeout = crl::time(192000);
constexpr auto kPingDelayDisconnect = 60;
constexpr auto kPingSendAfter = 30 * crl::time(1000);
constexpr auto kPingSendAfterForce = 45 * crl::time(1000);
constexpr auto kTemporaryExpiresIn = TimeId(86400);
constexpr auto kBindKeyAdditionalExpiresTimeout = TimeId(30);
constexpr auto kKeyOldEnoughForDestroy = 60 * crl::time(1000);
constexpr auto kSentContainerLives = 600 * crl::time(1000);
constexpr auto kFastRequestDuration = crl::time(500);

// If we can't connect for this time we will ask _instance to update config.
constexpr auto kRequestConfigTimeout = 8 * crl::time(1000);

// Don't try to handle messages larger than this size.
constexpr auto kMaxMessageLength = 16 * 1024 * 1024;

// How much time passed from send till we resend request or check its state.
constexpr auto kCheckSentRequestTimeout = 10 * crl::time(1000);

// How much time to wait for some more requests,
// when resending request or checking its state.
constexpr auto kSendStateRequestWaiting = crl::time(1000);

// How much time to wait for some more requests, when sending msg acks.
constexpr auto kAckSendWaiting = 10 * crl::time(1000);

auto SyncTimeRequestDuration = kFastRequestDuration;

using namespace details;

[[nodiscard]] QString LogIdsVector(const QVector<MTPlong> &ids) {
	if (!ids.size()) return "[]";
	auto idsStr = QString("[%1").arg(ids.cbegin()->v);
	for (const auto &id : ids) {
		idsStr += QString(", %2").arg(id.v);
	}
	return idsStr + "]";
}

[[nodiscard]] QString ComputeAppVersion() {
#if defined Q_OS_WIN && defined Q_PROCESSOR_X86_64
	const auto arch = u" x64"_q;
#elif (defined Q_OS_WIN && defined Q_PROCESSOR_X86_32) || defined Q_PROCESSOR_X86_64
	const auto arch = QString();
#else
	const auto arch = ' ' + QSysInfo::buildCpuArchitecture();
#endif
	return QString::fromLatin1(AppVersionStr) + arch + ([] {
#if defined OS_MAC_STORE
		return u" Mac App Store"_q;
#elif defined OS_WIN_STORE // OS_MAC_STORE
		return u" Microsoft Store"_q;
#elif defined Q_OS_UNIX && !defined Q_OS_MAC // OS_MAC_STORE || OS_WIN_STORE
		return Platform::InFlatpak()
			? u" Flatpak"_q
			: Platform::InSnap()
			? u" Snap"_q
			: QString();
#else // OS_MAC_STORE || OS_WIN_STORE || (defined Q_OS_UNIX && !defined Q_OS_MAC)
		return QString();
#endif // OS_MAC_STORE || OS_WIN_STORE || (defined Q_OS_UNIX && !defined Q_OS_MAC)
	})();
}

void WrapInvokeAfter(
		SerializedRequest &to,
		const SerializedRequest &from,
		const base::flat_map<mtpMsgId, SerializedRequest> &haveSent,
		int32 skipBeforeRequest = 0) {
	const auto afterId = *(mtpMsgId*)(from->after->data() + 4);
	const auto i = afterId ? haveSent.find(afterId) : haveSent.end();
	int32 size = to->size(), lenInInts = (tl::count_length(from) >> 2), headlen = 4, fulllen = headlen + lenInInts;
	if (i == haveSent.end()) { // no invoke after or such msg was not sent or was completed recently
		to->resize(size + fulllen + skipBeforeRequest);
		if (skipBeforeRequest) {
			memcpy(to->data() + size, from->constData() + 4, headlen * sizeof(mtpPrime));
			memcpy(to->data() + size + headlen + skipBeforeRequest, from->constData() + 4 + headlen, lenInInts * sizeof(mtpPrime));
		} else {
			memcpy(to->data() + size, from->constData() + 4, fulllen * sizeof(mtpPrime));
		}
	} else {
		to->resize(size + fulllen + skipBeforeRequest + 3);
		memcpy(to->data() + size, from->constData() + 4, headlen * sizeof(mtpPrime));
		(*to)[size + 3] += 3 * sizeof(mtpPrime);
		*((mtpTypeId*)&((*to)[size + headlen + skipBeforeRequest])) = mtpc_invokeAfterMsg;
		memcpy(to->data() + size + headlen + skipBeforeRequest + 1, &afterId, 2 * sizeof(mtpPrime));
		memcpy(to->data() + size + headlen + skipBeforeRequest + 3, from->constData() + 4 + headlen, lenInInts * sizeof(mtpPrime));
		if (size + 3 != 7) (*to)[7] += 3 * sizeof(mtpPrime);
	}
}

[[nodiscard]] bool ConstTimeIsDifferent(
		const void *a,
		const void *b,
		size_t size) {
	auto ca = reinterpret_cast<const char*>(a);
	auto cb = reinterpret_cast<const char*>(b);
	volatile auto different = false;
	for (const auto ce = ca + size; ca != ce; ++ca, ++cb) {
		different = different | (*ca != *cb);
	}
	return different;
}

} // namespace

SessionPrivate::SessionPrivate(
	not_null<Instance*> instance,
	not_null<QThread*> thread,
	std::shared_ptr<SessionData> data,
	ShiftedDcId shiftedDcId)
: QObject(nullptr)
, _instance(instance)
, _shiftedDcId(shiftedDcId)
, _realDcType(_instance->dcOptions().dcType(_shiftedDcId))
, _currentDcType(_realDcType)
, _state(DisconnectedState)
, _retryTimer(thread, [=] { retryByTimer(); })
, _oldConnectionTimer(thread, [=] { markConnectionOld(); })
, _waitForConnectedTimer(thread, [=] { waitConnectedFailed(); })
, _waitForReceivedTimer(thread, [=] { waitReceivedFailed(); })
, _waitForBetterTimer(thread, [=] { waitBetterFailed(); })
, _waitForReceived(kMinReceiveTimeout)
, _waitForConnected(kMinConnectedTimeout)
, _pingSender(thread, [=] { sendPingByTimer(); })
, _checkSentRequestsTimer(thread, [=] { checkSentRequests(); })
, _clearOldContainersTimer(thread, [=] { clearOldContainers(); })
, _sessionData(std::move(data)) {
	Expects(_shiftedDcId != 0);

	moveToThread(thread);

	InvokeQueued(this, [=] {
		_clearOldContainersTimer.callEach(kSentContainerLives);
		connectToServer();
	});
}

SessionPrivate::~SessionPrivate() {
	releaseKeyCreationOnFail();
	doDisconnect();

	Expects(!_connection);
	Expects(_testConnections.empty());
}

void SessionPrivate::appendTestConnection(
		DcOptions::Variants::Protocol protocol,
		const QString &ip,
		int port,
		const bytes::vector &protocolSecret) {
	QWriteLocker lock(&_stateMutex);

	const auto priority = (qthelp::is_ipv6(ip) ? 0 : 1)
		+ (protocol == DcOptions::Variants::Tcp ? 1 : 0)
		+ (protocolSecret.empty() ? 0 : 1);
	_testConnections.push_back({
		AbstractConnection::Create(
			_instance,
			protocol,
			thread(),
			protocolSecret,
			_options->proxy),
		priority
	});
	const auto weak = _testConnections.back().data.get();
	connect(weak, &AbstractConnection::error, [=](int errorCode) {
		onError(weak, errorCode);
	});
	connect(weak, &AbstractConnection::receivedSome, [=] {
		onReceivedSome();
	});
	_firstSentAt = 0;
	if (_oldConnection) {
		_oldConnection = false;
		DEBUG_LOG(("This connection marked as not old!"));
	}
	_oldConnectionTimer.callOnce(kMarkConnectionOldTimeout);
	connect(weak, &AbstractConnection::connected, [=] {
		onConnected(weak);
	});
	connect(weak, &AbstractConnection::disconnected, [=] {
		onDisconnected(weak);
	});
	connect(weak, &AbstractConnection::syncTimeRequest, [=] {
		InvokeQueued(_instance, [instance = _instance] {
			instance->syncHttpUnixtime();
		});
	});

	const auto protocolForFiles = isDownloadDcId(_shiftedDcId)
		//|| isUploadDcId(_shiftedDcId)
		|| (_realDcType == DcType::Cdn);
	const auto protocolDcId = getProtocolDcId();
	InvokeQueued(_testConnections.back().data, [=] {
		weak->connectToServer(
			ip,
			port,
			protocolSecret,
			protocolDcId,
			protocolForFiles);
	});
}

int16 SessionPrivate::getProtocolDcId() const {
	const auto dcId = BareDcId(_shiftedDcId);
	const auto simpleDcId = isTemporaryDcId(dcId)
		? getRealIdFromTemporaryDcId(dcId)
		: dcId;
	const auto testedDcId = _instance->isTestMode()
		? (kTestModeDcIdShift + simpleDcId)
		: simpleDcId;
	return (_currentDcType == DcType::MediaCluster)
		? -testedDcId
		: testedDcId;
}

void SessionPrivate::checkSentRequests() {
	const auto now = crl::now();
	const auto checkTime = now - kCheckSentRequestTimeout;
	if (_bindMsgId && _bindMessageSent < checkTime) {
		DEBUG_LOG(("MTP Info: "
			"Request state while key is not bound, restarting."));
		restart();
		_checkSentRequestsTimer.callOnce(kCheckSentRequestTimeout);
		return;
	}
	auto requesting = false;
	auto nextTimeout = kCheckSentRequestTimeout;
	{
		QReadLocker locker(_sessionData->haveSentMutex());
		auto &haveSent = _sessionData->haveSentMap();
		for (const auto &[msgId, request] : haveSent) {
			if (request->lastSentTime <= checkTime) {
				// Need to check state.
				request->lastSentTime = now;
				if (_stateRequestData.emplace(msgId).second) {
					requesting = true;
				}
			} else {
				nextTimeout = std::min(request->lastSentTime - checkTime, nextTimeout);
			}
		}
	}
	if (requesting) {
		_sessionData->queueSendAnything(kSendStateRequestWaiting);
	}
	if (nextTimeout < kCheckSentRequestTimeout) {
		_checkSentRequestsTimer.callOnce(nextTimeout);
	}
}

void SessionPrivate::clearOldContainers() {
	auto resent = false;
	auto nextTimeout = kSentContainerLives;
	const auto now = crl::now();
	const auto checkTime = now - kSentContainerLives;
	for (auto i = _sentContainers.begin(); i != _sentContainers.end();) {
		if (i->second.sent <= checkTime) {
			DEBUG_LOG(("MTP Info: Removing old container with resending %1, "
				"sent: %2, now: %3, current unixtime: %4"
				).arg(i->first
				).arg(i->second.sent
				).arg(now
				).arg(base::unixtime::now()));

			const auto ids = std::move(i->second.messages);
			i = _sentContainers.erase(i);

			resent = resent || !ids.empty();
			for (const auto innerMsgId : ids) {
				resend(innerMsgId, -1);
			}
		} else {
			nextTimeout = std::min(i->second.sent - checkTime, nextTimeout);
			++i;
		}
	}
	if (resent) {
		_sessionData->queueNeedToResumeAndSend();
	}
	if (nextTimeout < kSentContainerLives) {
		_clearOldContainersTimer.callOnce(nextTimeout);
	} else if (!_clearOldContainersTimer.isActive()) {
		_clearOldContainersTimer.callEach(nextTimeout);
	}
}

void SessionPrivate::destroyAllConnections() {
	clearUnboundKeyCreator();
	_waitForBetterTimer.cancel();
	_waitForReceivedTimer.cancel();
	_waitForConnectedTimer.cancel();
	_testConnections.clear();
	_connection = nullptr;
}

void SessionPrivate::cdnConfigChanged() {
	connectToServer(true);
}

int32 SessionPrivate::getShiftedDcId() const {
	return _shiftedDcId;
}

void SessionPrivate::dcOptionsChanged() {
	_retryTimeout = 1;
	connectToServer(true);
}

int32 SessionPrivate::getState() const {
	QReadLocker lock(&_stateMutex);
	int32 result = _state;
	if (_state < 0) {
		if (_retryTimer.isActive()) {
			result = int32(crl::now() - _retryWillFinish);
			if (result >= 0) {
				result = -1;
			}
		}
	}
	return result;
}

QString SessionPrivate::transport() const {
	QReadLocker lock(&_stateMutex);
	if (!_connection || (_state < 0)) {
		return QString();
	}

	Assert(_options != nullptr);
	return _connection->transport();
}

bool SessionPrivate::setState(int state, int ifState) {
	if (ifState != kUpdateStateAlways) {
		QReadLocker lock(&_stateMutex);
		if (_state != ifState) {
			return false;
		}
	}

	QWriteLocker lock(&_stateMutex);
	if (_state == state) {
		return false;
	}
	_state = state;
	if (state < 0) {
		_retryTimeout = -state;
		_retryTimer.callOnce(_retryTimeout);
		_retryWillFinish = crl::now() + _retryTimeout;
	}
	lock.unlock();

	_sessionData->queueConnectionStateChange(state);
	return true;
}

void SessionPrivate::resetSession() {
	MTP_LOG(_shiftedDcId, ("Resetting session!"));
	_needSessionReset = false;

	DEBUG_LOG(("MTP Info: creating new session in resetSession."));
	changeSessionId();

	_sessionData->queueResetDone();
}

void SessionPrivate::changeSessionId() {
	auto sessionId = _sessionId;
	do {
		sessionId = base::RandomValue<uint64>();
	} while (_sessionId == sessionId);

	DEBUG_LOG(("MTP Info: setting server_session: %1").arg(sessionId));

	_sessionId = sessionId;
	_messagesCounter = 0;
	_sessionMarkedAsStarted = false;
	_ackRequestData.clear();
	_resendRequestData.clear();
	_stateRequestData.clear();
	_receivedMessageIds.clear();
}

uint32 SessionPrivate::nextRequestSeqNumber(bool needAck) {
	const auto result = _messagesCounter;
	_messagesCounter += (needAck ? 1 : 0);
	return result * 2 + (needAck ? 1 : 0);
}

bool SessionPrivate::realDcTypeChanged() {
	const auto now = _instance->dcOptions().dcType(_shiftedDcId);
	if (_realDcType == now) {
		return false;
	}
	_realDcType = now;
	return true;
}

bool SessionPrivate::markSessionAsStarted() {
	if (_sessionMarkedAsStarted) {
		return false;
	}
	_sessionMarkedAsStarted = true;
	return true;
}

mtpMsgId SessionPrivate::prepareToSend(
		SerializedRequest &request,
		mtpMsgId currentLastId,
		bool forceNewMsgId) {
	Expects(request->size() > 8);

	if (const auto msgId = request.getMsgId()) {
		// resending this request
		const auto i = _resendingIds.find(msgId);
		if (i != _resendingIds.cend()) {
			_resendingIds.erase(i);
		}

		return (forceNewMsgId || msgId > currentLastId)
			? replaceMsgId(request, currentLastId)
			: msgId;
	}
	request.setMsgId(currentLastId);
	request.setSeqNo(nextRequestSeqNumber(request.needAck()));
	if (request->requestId) {
		MTP_LOG(_shiftedDcId, ("[r%1] msg_id 0 -> %2").arg(request->requestId).arg(currentLastId));
	}
	return currentLastId;
}

mtpMsgId SessionPrivate::replaceMsgId(SerializedRequest &request, mtpMsgId newId) {
	Expects(request->size() > 8);

	const auto oldMsgId = request.getMsgId();
	if (oldMsgId == newId) {
		return newId;
	}
	// haveSentMutex() was locked in tryToSend()
	auto &haveSent = _sessionData->haveSentMap();

	while (_resendingIds.contains(newId)
		|| _ackedIds.contains(newId)
		|| haveSent.contains(newId)) {
		newId = base::unixtime::mtproto_msg_id();
	}

	MTP_LOG(_shiftedDcId, ("[r%1] msg_id %2 -> %3"
		).arg(request->requestId
		).arg(oldMsgId
		).arg(newId));

	const auto i = _resendingIds.find(oldMsgId);
	if (i != _resendingIds.end()) {
		const auto requestId = i->second;
		_resendingIds.erase(i);
		_resendingIds.emplace(newId, requestId);
	}

	const auto j = _ackedIds.find(oldMsgId);
	if (j != _ackedIds.end()) {
		const auto requestId = j->second;
		_ackedIds.erase(j);
		_ackedIds.emplace(newId, requestId);
	}

	const auto k = haveSent.find(oldMsgId);
	if (k != haveSent.end()) {
		const auto request = k->second;
		haveSent.erase(k);
		haveSent.emplace(newId, request);
	}
	for (auto &[msgId, container] : _sentContainers) {
		for (auto &innerMsgId : container.messages) {
			if (innerMsgId == oldMsgId) {
				innerMsgId = newId;
			}
		}
	}
	request.setMsgId(newId);
	request.setSeqNo(nextRequestSeqNumber(request.needAck()));
	return newId;
}

mtpMsgId SessionPrivate::placeToContainer(
		SerializedRequest &toSendRequest,
		mtpMsgId &bigMsgId,
		bool forceNewMsgId,
		SerializedRequest &req) {
	const auto msgId = prepareToSend(req, bigMsgId, forceNewMsgId);
	if (msgId >= bigMsgId) {
		bigMsgId = base::unixtime::mtproto_msg_id();
	}

	uint32 from = toSendRequest->size(), len = req.messageSize();
	toSendRequest->resize(from + len);
	memcpy(toSendRequest->data() + from, req->constData() + 4, len * sizeof(mtpPrime));

	return msgId;
}

MTPVector<MTPJSONObjectValue> SessionPrivate::prepareInitParams() {
	const auto local = QDateTime::currentDateTime();
	const auto utc = QDateTime(local.date(), local.time(), Qt::UTC);
	const auto shift = base::unixtime::now() - (TimeId)::time(nullptr);
	const auto delta = int(utc.toSecsSinceEpoch()) - int(local.toSecsSinceEpoch()) - shift;
	auto sliced = delta;
	while (sliced < -12 * 3600) {
		sliced += 24 * 3600;
	}
	while (sliced > 14 * 3600) {
		sliced -= 24 * 3600;
	}
	const auto sign = (sliced < 0) ? -1 : 1;
	const auto rounded = base::SafeRound(std::abs(sliced) / 900.)
		* 900
		* sign;
	return MTP_vector<MTPJSONObjectValue>(
		1,
		MTP_jsonObjectValue(
			MTP_string("tz_offset"),
			MTP_jsonNumber(MTP_double(rounded))));
}

void SessionPrivate::tryToSend() {
	DEBUG_LOG(("MTP Info: tryToSend for dc %1.").arg(_shiftedDcId));
	if (!_connection) {
		DEBUG_LOG(("MTP Info: not yet connected in dc %1.").arg(_shiftedDcId));
		return;
	} else if (!_keyId) {
		DEBUG_LOG(("MTP Info: not yet with auth key in dc %1.").arg(_shiftedDcId));
		return;
	}

	const auto needsLayer = !_sessionData->connectionInited();
	const auto state = getState();
	const auto sendOnlyFirstPing = (state != ConnectedState);
	const auto sendAll = !sendOnlyFirstPing && !_keyCreator;
	const auto isMainSession = (GetDcIdShift(_shiftedDcId) == 0);
	if (sendOnlyFirstPing && !_pingIdToSend) {
		DEBUG_LOG(("MTP Info: dc %1 not sending, waiting for Connected state, state: %2").arg(_shiftedDcId).arg(state));
		return; // just do nothing, if is not connected yet
	} else if (isMainSession
		&& !sendOnlyFirstPing
		&& !_pingIdToSend
		&& !_pingId
		&& _pingSendAt <= crl::now()) {
		_pingIdToSend = base::RandomValue<mtpPingId>();
	}
	const auto forceNewMsgId = sendAll && markSessionAsStarted();
	if (forceNewMsgId && _keyCreator) {
		_keyCreator->restartBinder();
	}

	auto pingRequest = SerializedRequest();
	auto ackRequest = SerializedRequest();
	auto resendRequest = SerializedRequest();
	auto stateRequest = SerializedRequest();
	auto httpWaitRequest = SerializedRequest();
	auto bindDcKeyRequest = SerializedRequest();
	if (_pingIdToSend) {
		if (sendOnlyFirstPing || !isMainSession) {
			DEBUG_LOG(("MTP Info: sending ping, ping_id: %1"
				).arg(_pingIdToSend));
			pingRequest = SerializedRequest::Serialize(MTPPing(
				MTP_long(_pingIdToSend)
			));
		} else {
			DEBUG_LOG(("MTP Info: sending ping_delay_disconnect, "
				"ping_id: %1").arg(_pingIdToSend));
			pingRequest = SerializedRequest::Serialize(MTPPing_delay_disconnect(
				MTP_long(_pingIdToSend),
				MTP_int(kPingDelayDisconnect)));
			_pingSender.callOnce(kPingSendAfterForce);
		}
		_pingSendAt = pingRequest->lastSentTime + kPingSendAfter;
		_pingId = base::take(_pingIdToSend);
	} else if (!sendAll) {
		DEBUG_LOG(("MTP Info: dc %1 sending only service or bind."
			).arg(_shiftedDcId));
	} else {
		DEBUG_LOG(("MTP Info: dc %1 trying to send after ping, state: %2"
			).arg(_shiftedDcId
			).arg(state));
	}

	if (!sendOnlyFirstPing) {
		if (!_ackRequestData.isEmpty()) {
			ackRequest = SerializedRequest::Serialize(MTPMsgsAck(
				MTP_msgs_ack(MTP_vector<MTPlong>(
					base::take(_ackRequestData)))));
		}
		if (!_resendRequestData.isEmpty()) {
			resendRequest = SerializedRequest::Serialize(MTPMsgResendReq(
				MTP_msg_resend_req(MTP_vector<MTPlong>(
					base::take(_resendRequestData)))));
		}
		if (!_stateRequestData.empty()) {
			auto ids = QVector<MTPlong>();
			ids.reserve(_stateRequestData.size());
			for (const auto id : base::take(_stateRequestData)) {
				ids.push_back(MTP_long(id));
			}
			stateRequest = SerializedRequest::Serialize(MTPMsgsStateReq(
				MTP_msgs_state_req(MTP_vector<MTPlong>(ids))));
		}
		if (_connection->usingHttpWait()) {
			httpWaitRequest = SerializedRequest::Serialize(MTPHttpWait(
				MTP_http_wait(MTP_int(100), MTP_int(30), MTP_int(25000))));
		}
		if (!_bindMsgId && _keyCreator && _keyCreator->readyToBind()) {
			bindDcKeyRequest = _keyCreator->prepareBindRequest(
				_encryptionKey,
				_sessionId);

			// This is a special request with msgId used inside the message
			// body, so it is prepared already with a msgId and we place
			// seqNo for it manually here.
			bindDcKeyRequest.setSeqNo(
				nextRequestSeqNumber(bindDcKeyRequest.needAck()));
		}
	}

	MTPInitConnection<SerializedRequest> initWrapper;
	int32 initSize = 0, initSizeInInts = 0;
	if (needsLayer) {
		Assert(_options != nullptr);
		const auto systemLangCode = _options->systemLangCode;
		const auto cloudLangCode = _options->cloudLangCode;
		const auto langPackName = _options->langPackName;
		const auto deviceModel = (_currentDcType == DcType::Cdn)
			? "n/a"
			: _instance->deviceModel();
		const auto systemVersion = (_currentDcType == DcType::Cdn)
			? "n/a"
			: _instance->systemVersion();
		const auto appVersion = ComputeAppVersion();
		const auto proxyType = _options->proxy.type;
		const auto mtprotoProxy = (proxyType == ProxyData::Type::Mtproto);
		const auto clientProxyFields = mtprotoProxy
			? MTP_inputClientProxy(
				MTP_string(_options->proxy.host),
				MTP_int(_options->proxy.port))
			: MTPInputClientProxy();
		using Flag = MTPInitConnection<SerializedRequest>::Flag;
		initWrapper = MTPInitConnection<SerializedRequest>(
			MTP_flags(Flag::f_params
				| (mtprotoProxy ? Flag::f_proxy : Flag(0))),
			MTP_int(ApiId),
			MTP_string(deviceModel),
			MTP_string(systemVersion),
			MTP_string(appVersion),
			MTP_string(systemLangCode),
			MTP_string(langPackName),
			MTP_string(cloudLangCode),
			clientProxyFields,
			MTP_jsonObject(prepareInitParams()),
			SerializedRequest());
		initSizeInInts = (tl::count_length(initWrapper) >> 2) + 2;
		initSize = initSizeInInts * sizeof(mtpPrime);
	}

	bool needAnyResponse = false;
	SerializedRequest toSendRequest;
	{
		QWriteLocker locker1(_sessionData->toSendMutex());

		auto scheduleCheckSentRequests = false;

		auto toSendDummy = base::flat_map<mtpRequestId, SerializedRequest>();
		auto &toSend = sendAll
			? _sessionData->toSendMap()
			: toSendDummy;
		if (!sendAll) {
			locker1.unlock();
		}

		uint32 toSendCount = toSend.size();
		if (pingRequest) ++toSendCount;
		if (ackRequest) ++toSendCount;
		if (resendRequest) ++toSendCount;
		if (stateRequest) ++toSendCount;
		if (httpWaitRequest) ++toSendCount;
		if (bindDcKeyRequest) ++toSendCount;

		if (!toSendCount) {
			return; // nothing to send
		}

		const auto first = pingRequest
			? pingRequest
			: ackRequest
			? ackRequest
			: resendRequest
			? resendRequest
			: stateRequest
			? stateRequest
			: httpWaitRequest
			? httpWaitRequest
			: bindDcKeyRequest
			? bindDcKeyRequest
			: toSend.begin()->second;
		if (toSendCount == 1 && !first->forceSendInContainer) {
			toSendRequest = first;
			if (sendAll) {
				toSend.clear();
				locker1.unlock();
			}

			const auto msgId = prepareToSend(
				toSendRequest,
				base::unixtime::mtproto_msg_id(),
				forceNewMsgId && !bindDcKeyRequest);
			if (bindDcKeyRequest) {
				_bindMsgId = msgId;
				_bindMessageSent = crl::now();
				needAnyResponse = true;
			} else if (pingRequest) {
				_pingMsgId = msgId;
				needAnyResponse = true;
			} else if (stateRequest || resendRequest) {
				_stateAndResendRequests.emplace(
					msgId,
					stateRequest ? stateRequest : resendRequest);
				needAnyResponse = true;
			}

			if (toSendRequest->requestId) {
				if (toSendRequest.needAck()) {
					toSendRequest->lastSentTime = crl::now();

					QWriteLocker locker2(_sessionData->haveSentMutex());
					auto &haveSent = _sessionData->haveSentMap();
					haveSent.emplace(msgId, toSendRequest);
					scheduleCheckSentRequests = true;

					const auto wrapLayer = needsLayer && toSendRequest->needsLayer;
					if (toSendRequest->after) {
						const auto toSendSize = tl::count_length(toSendRequest) >> 2;
						auto wrappedRequest = SerializedRequest::Prepare(
							toSendSize,
							toSendSize + 3);
						wrappedRequest->resize(4);
						memcpy(wrappedRequest->data(), toSendRequest->constData(), 4 * sizeof(mtpPrime));
						WrapInvokeAfter(wrappedRequest, toSendRequest, haveSent);
						toSendRequest = std::move(wrappedRequest);
					}
					if (wrapLayer) {
						const auto noWrapSize = (tl::count_length(toSendRequest) >> 2);
						const auto toSendSize = noWrapSize + initSizeInInts;
						auto wrappedRequest = SerializedRequest::Prepare(toSendSize);
						memcpy(wrappedRequest->data(), toSendRequest->constData(), 7 * sizeof(mtpPrime)); // all except length
						wrappedRequest->push_back(mtpc_invokeWithLayer);
						wrappedRequest->push_back(kCurrentLayer);
						initWrapper.write<mtpBuffer>(*wrappedRequest);
						wrappedRequest->resize(wrappedRequest->size() + noWrapSize);
						memcpy(wrappedRequest->data() + wrappedRequest->size() - noWrapSize, toSendRequest->constData() + 8, noWrapSize * sizeof(mtpPrime));
						toSendRequest = std::move(wrappedRequest);
					}

					needAnyResponse = true;
				} else {
					_ackedIds.emplace(msgId, toSendRequest->requestId);
				}
			}
		} else { // send in container
			bool willNeedInit = false;
			uint32 containerSize = 1 + 1; // cons + vector size
			if (pingRequest) containerSize += pingRequest.messageSize();
			if (ackRequest) containerSize += ackRequest.messageSize();
			if (resendRequest) containerSize += resendRequest.messageSize();
			if (stateRequest) containerSize += stateRequest.messageSize();
			if (httpWaitRequest) containerSize += httpWaitRequest.messageSize();
			if (bindDcKeyRequest) containerSize += bindDcKeyRequest.messageSize();
			for (const auto &[requestId, request] : toSend) {
				containerSize += request.messageSize();
				if (needsLayer && request->needsLayer) {
					containerSize += initSizeInInts;
					willNeedInit = true;
				}
			}
			mtpBuffer initSerialized;
			if (willNeedInit) {
				initSerialized.reserve(initSizeInInts);
				initSerialized.push_back(mtpc_invokeWithLayer);
				initSerialized.push_back(kCurrentLayer);
				initWrapper.write<mtpBuffer>(initSerialized);
			}
			// prepare container + each in invoke after
			toSendRequest = SerializedRequest::Prepare(
				containerSize,
				containerSize + 3 * toSend.size());
			toSendRequest->push_back(mtpc_msg_container);
			toSendRequest->push_back(toSendCount);

			// check for a valid container
			auto bigMsgId = base::unixtime::mtproto_msg_id();

			// the fact of this lock is used in replaceMsgId()
			QWriteLocker locker2(_sessionData->haveSentMutex());
			auto &haveSent = _sessionData->haveSentMap();

			// prepare sent container
			auto sentIdsWrap = SentContainer();
			sentIdsWrap.sent = crl::now();
			sentIdsWrap.messages.reserve(toSendCount);

			if (bindDcKeyRequest) {
				_bindMsgId = placeToContainer(
					toSendRequest,
					bigMsgId,
					false,
					bindDcKeyRequest);
				_bindMessageSent = crl::now();
				needAnyResponse = true;
			}
			if (pingRequest) {
				_pingMsgId = placeToContainer(
					toSendRequest,
					bigMsgId,
					forceNewMsgId,
					pingRequest);
				needAnyResponse = true;
			}

			for (auto &[requestId, request] : toSend) {
				const auto msgId = prepareToSend(
					request,
					bigMsgId,
					forceNewMsgId);
				if (msgId >= bigMsgId) {
					bigMsgId = base::unixtime::mtproto_msg_id();
				}
				bool added = false;
				if (request->requestId) {
					if (request.needAck()) {
						request->lastSentTime = crl::now();
						int32 reqNeedsLayer = (needsLayer && request->needsLayer) ? toSendRequest->size() : 0;
						if (request->after) {
							WrapInvokeAfter(toSendRequest, request, haveSent, reqNeedsLayer ? initSizeInInts : 0);
							if (reqNeedsLayer) {
								memcpy(toSendRequest->data() + reqNeedsLayer + 4, initSerialized.constData(), initSize);
								*(toSendRequest->data() + reqNeedsLayer + 3) += initSize;
							}
							added = true;
						} else if (reqNeedsLayer) {
							toSendRequest->resize(reqNeedsLayer + initSizeInInts + request.messageSize());
							memcpy(toSendRequest->data() + reqNeedsLayer, request->constData() + 4, 4 * sizeof(mtpPrime));
							memcpy(toSendRequest->data() + reqNeedsLayer + 4, initSerialized.constData(), initSize);
							memcpy(toSendRequest->data() + reqNeedsLayer + 4 + initSizeInInts, request->constData() + 8, tl::count_length(request));
							*(toSendRequest->data() + reqNeedsLayer + 3) += initSize;
							added = true;
						}

						// #TODO rewrite so that it will always hold.
						//Assert(!haveSent.contains(msgId));
						haveSent.emplace(msgId, request);
						sentIdsWrap.messages.push_back(msgId);
						scheduleCheckSentRequests = true;
						needAnyResponse = true;
					} else {
						_ackedIds.emplace(msgId, request->requestId);
					}
				}
				if (!added) {
					uint32 from = toSendRequest->size(), len = request.messageSize();
					toSendRequest->resize(from + len);
					memcpy(toSendRequest->data() + from, request->constData() + 4, len * sizeof(mtpPrime));
				}
			}
			toSend.clear();

			if (stateRequest) {
				const auto msgId = placeToContainer(
					toSendRequest,
					bigMsgId,
					forceNewMsgId,
					stateRequest);
				_stateAndResendRequests.emplace(msgId, stateRequest);
				needAnyResponse = true;
			}
			if (resendRequest) {
				const auto msgId = placeToContainer(
					toSendRequest,
					bigMsgId,
					forceNewMsgId,
					resendRequest);
				_stateAndResendRequests.emplace(msgId, resendRequest);
				needAnyResponse = true;
			}
			if (ackRequest) {
				placeToContainer(
					toSendRequest,
					bigMsgId,
					forceNewMsgId,
					ackRequest);
			}
			if (httpWaitRequest) {
				placeToContainer(
					toSendRequest,
					bigMsgId,
					forceNewMsgId,
					httpWaitRequest);
			}

			const auto containerMsgId = prepareToSend(
				toSendRequest,
				bigMsgId,
				forceNewMsgId);
			_sentContainers.emplace(containerMsgId, std::move(sentIdsWrap));

			if (scheduleCheckSentRequests && !_checkSentRequestsTimer.isActive()) {
				_checkSentRequestsTimer.callOnce(kCheckSentRequestTimeout);
			}
		}
	}
	sendSecureRequest(std::move(toSendRequest), needAnyResponse);
}

void SessionPrivate::retryByTimer() {
	if (_retryTimeout < 3) {
		++_retryTimeout;
	} else if (_retryTimeout == 3) {
		_retryTimeout = 1000;
	} else if (_retryTimeout < 64000) {
		_retryTimeout *= 2;
	}
	connectToServer();
}

void SessionPrivate::restartNow() {
	_retryTimeout = 1;
	_retryTimer.cancel();
	restart();
}

void SessionPrivate::connectToServer(bool afterConfig) {
	if (afterConfig && (!_testConnections.empty() || _connection)) {
		return;
	}

	destroyAllConnections();

	if (realDcTypeChanged() && _keyCreator) {
		destroyTemporaryKey();
		return;
	}

	_options = std::make_unique<SessionOptions>(_sessionData->options());

	const auto bareDc = BareDcId(_shiftedDcId);

	_currentDcType = tryAcquireKeyCreation();
	if (_currentDcType == DcType::Cdn && !_instance->isKeysDestroyer()) {
		if (!_instance->dcOptions().hasCDNKeysForDc(bareDc)) {
			requestCDNConfig();
			return;
		}
	}
	if (_options->proxy.type == ProxyData::Type::Mtproto) {
		// host, port, secret for mtproto proxy are taken from proxy.
		appendTestConnection(DcOptions::Variants::Tcp, {}, 0, {});
	} else {
		using Variants = DcOptions::Variants;
		const auto special = (_currentDcType == DcType::Temporary);
		const auto variants = _instance->dcOptions().lookup(
			bareDc,
			_currentDcType,
			_options->proxy.type != ProxyData::Type::None);
		const auto useIPv4 = special ? true : _options->useIPv4;
		const auto useIPv6 = special ? false : _options->useIPv6;
		const auto useTcp = special ? true : _options->useTcp;
		const auto useHttp = special ? false : _options->useHttp;
		const auto skipAddress = !useIPv4
			? Variants::IPv4
			: !useIPv6
			? Variants::IPv6
			: Variants::AddressTypeCount;
		const auto skipProtocol = !useTcp
			? Variants::Tcp
			: !useHttp
			? Variants::Http
			: Variants::ProtocolCount;
		for (auto address = 0; address != Variants::AddressTypeCount; ++address) {
			if (address == skipAddress) {
				continue;
			}
			for (auto protocol = 0; protocol != Variants::ProtocolCount; ++protocol) {
				if (protocol == skipProtocol) {
					continue;
				}
				for (const auto &endpoint : variants.data[address][protocol]) {
					appendTestConnection(
						static_cast<Variants::Protocol>(protocol),
						QString::fromStdString(endpoint.ip),
						endpoint.port,
						endpoint.secret);
				}
			}
		}
	}
	if (_testConnections.empty()) {
		if (_instance->isKeysDestroyer()) {
			LOG(("MTP Error: DC %1 options for not found for auth key destruction!").arg(_shiftedDcId));
			_instance->keyWasPossiblyDestroyed(_shiftedDcId);
			return;
		} else if (afterConfig) {
			LOG(("MTP Error: DC %1 options for not found right after config load!").arg(_shiftedDcId));
			return restart();
		}
		DEBUG_LOG(("MTP Info: DC %1 options not found, waiting for config").arg(_shiftedDcId));
		InvokeQueued(_instance, [instance = _instance] {
			instance->requestConfig();
		});
		return;
	}
	DEBUG_LOG(("Connection Info: Connecting to %1 with %2 test connections."
		).arg(_shiftedDcId
		).arg(_testConnections.size()));

	if (!_startedConnectingAt) {
		_startedConnectingAt = crl::now();
	} else if (crl::now() - _startedConnectingAt > kRequestConfigTimeout) {
		InvokeQueued(_instance, [instance = _instance] {
			instance->requestConfigIfOld();
		});
	}

	_retryTimer.cancel();
	_waitForConnectedTimer.cancel();

	setState(ConnectingState);

	_bindMsgId = 0;
	_pingId = _pingMsgId = _pingIdToSend = _pingSendAt = 0;
	_pingSender.cancel();

	_waitForConnectedTimer.callOnce(_waitForConnected);
}

void SessionPrivate::restart() {
	DEBUG_LOG(("MTP Info: restarting Connection"));

	_waitForReceivedTimer.cancel();
	_waitForConnectedTimer.cancel();

	doDisconnect();

	if (_needSessionReset) {
		resetSession();
	}
	if (_retryTimer.isActive()) {
		return;
	}

	DEBUG_LOG(("MTP Info: restart timeout: %1ms").arg(_retryTimeout));

	setState(-_retryTimeout);
}

void SessionPrivate::onSentSome(uint64 size) {
	if (!_waitForReceivedTimer.isActive()) {
		auto remain = static_cast<uint64>(_waitForReceived);
		if (!_oldConnection) {
			// 8kb / sec, so 512 kb give 64 sec
			auto remainBySize = size * _waitForReceived / 8192;
			remain = std::clamp(
				remainBySize,
				remain,
				uint64(kMaxReceiveTimeout));
			if (remain != _waitForReceived) {
				DEBUG_LOG(("Checking connect for request with size %1 bytes, delay will be %2").arg(size).arg(remain));
			}
		}
		if (isUploadDcId(_shiftedDcId)) {
			remain *= kUploadSessionsCount;
		}
		_waitForReceivedTimer.callOnce(remain);
	}
	if (!_firstSentAt) {
		_firstSentAt = crl::now();
	}
}

void SessionPrivate::onReceivedSome() {
	if (_oldConnection) {
		_oldConnection = false;
		DEBUG_LOG(("This connection marked as not old!"));
	}
	_oldConnectionTimer.callOnce(kMarkConnectionOldTimeout);
	_waitForReceivedTimer.cancel();
	if (_firstSentAt > 0) {
		const auto ms = crl::now() - _firstSentAt;
		DEBUG_LOG(("MTP Info: response in %1ms, _waitForReceived: %2ms"
			).arg(ms
			).arg(_waitForReceived));

		if (ms > 0 && ms * 2 < _waitForReceived) {
			_waitForReceived = qMax(ms * 2, kMinReceiveTimeout);
		}
		_firstSentAt = -1;
	}
}

void SessionPrivate::markConnectionOld() {
	_oldConnection = true;
	_waitForReceived = kMinReceiveTimeout;
	DEBUG_LOG(("This connection marked as old! _waitForReceived now %1ms"
		).arg(_waitForReceived));
}

void SessionPrivate::sendPingByTimer() {
	if (_pingId) {
		// _pingSendAt: when to send next ping (lastPingAt + kPingSendAfter)
		// could be equal to zero.
		const auto now = crl::now();
		const auto mustSendTill = _pingSendAt
			+ kPingSendAfterForce
			- kPingSendAfter;
		if (mustSendTill < now + 1000) {
			LOG(("Could not send ping for some seconds, restarting..."));
			return restart();
		} else {
			_pingSender.callOnce(mustSendTill - now);
		}
	} else {
		_sessionData->queueNeedToResumeAndSend();
	}
}

void SessionPrivate::sendPingForce() {
	DEBUG_LOG(("MTP Info: send ping force for dcWithShift %1.").arg(_shiftedDcId));
	if (!_pingId) {
		_pingSendAt = 0;
		DEBUG_LOG(("Will send ping!"));
		tryToSend();
	}
}

void SessionPrivate::waitReceivedFailed() {
	Expects(_options != nullptr);

	DEBUG_LOG(("MTP Info: bad connection, _waitForReceived: %1ms").arg(_waitForReceived));
	if (_waitForReceived < kMaxReceiveTimeout) {
		_waitForReceived *= 2;
	}
	doDisconnect();
	if (_retryTimer.isActive()) {
		return;
	}

	DEBUG_LOG(("MTP Info: immediate restart!"));
	InvokeQueued(this, [=] { connectToServer(); });

	const auto instance = _instance;
	const auto shiftedDcId = _shiftedDcId;
	InvokeQueued(instance, [=] {
		instance->restartedByTimeout(shiftedDcId);
	});
}

void SessionPrivate::waitConnectedFailed() {
	DEBUG_LOG(("MTP Info: can't connect in %1ms").arg(_waitForConnected));
	auto maxTimeout = kMaxConnectedTimeout;
	for (const auto &connection : _testConnections) {
		accumulate_max(maxTimeout, connection.data->fullConnectTimeout());
	}
	if (_waitForConnected < maxTimeout) {
		_waitForConnected = std::min(maxTimeout, 2 * _waitForConnected);
	}

	connectingTimedOut();

	DEBUG_LOG(("MTP Info: immediate restart!"));
	InvokeQueued(this, [=] { connectToServer(); });
}

void SessionPrivate::waitBetterFailed() {
	confirmBestConnection();
}

void SessionPrivate::connectingTimedOut() {
	for (const auto &connection : _testConnections) {
		connection.data->timedOut();
	}
	doDisconnect();
}

void SessionPrivate::doDisconnect() {
	destroyAllConnections();
	setState(DisconnectedState);
}

void SessionPrivate::requestCDNConfig() {
	InvokeQueued(_instance, [instance = _instance] {
		instance->requestCDNConfig();
	});
}

void SessionPrivate::handleReceived() {
	Expects(_encryptionKey != nullptr);

	onReceivedSome();

	while (!_connection->received().empty()) {
		auto intsBuffer = std::move(_connection->received().front());
		_connection->received().pop_front();

		constexpr auto kExternalHeaderIntsCount = 6U; // 2 auth_key_id, 4 msg_key
		constexpr auto kEncryptedHeaderIntsCount = 8U; // 2 salt, 2 session, 2 msg_id, 1 seq_no, 1 length
		constexpr auto kMinimalEncryptedIntsCount = kEncryptedHeaderIntsCount + 4U; // + 1 data + 3 padding
		constexpr auto kMinimalIntsCount = kExternalHeaderIntsCount + kMinimalEncryptedIntsCount;
		auto intsCount = uint32(intsBuffer.size());
		auto ints = intsBuffer.constData();
		if ((intsCount < kMinimalIntsCount) || (intsCount > kMaxMessageLength / kIntSize)) {
			LOG(("TCP Error: bad message received, len %1").arg(intsCount * kIntSize));
			return restart();
		}
		if (_keyId != *(uint64*)ints) {
			LOG(("TCP Error: bad auth_key_id %1 instead of %2 received").arg(_keyId).arg(*(uint64*)ints));
			return restart();
		}

		constexpr auto kMinPaddingSize = 12U;
		constexpr auto kMaxPaddingSize = 1024U;

		auto encryptedInts = ints + kExternalHeaderIntsCount;
		auto encryptedIntsCount = (intsCount - kExternalHeaderIntsCount) & ~0x03U;
		auto encryptedBytesCount = encryptedIntsCount * kIntSize;
		auto decryptedBuffer = QByteArray(encryptedBytesCount, Qt::Uninitialized);
		auto msgKey = *(MTPint128*)(ints + 2);

		aesIgeDecrypt(encryptedInts, decryptedBuffer.data(), encryptedBytesCount, _encryptionKey, msgKey);

		auto decryptedInts = reinterpret_cast<const mtpPrime*>(decryptedBuffer.constData());
		auto serverSalt = *(uint64*)&decryptedInts[0];
		auto session = *(uint64*)&decryptedInts[2];
		auto msgId = *(uint64*)&decryptedInts[4];
		auto seqNo = *(uint32*)&decryptedInts[6];
		auto needAck = ((seqNo & 0x01) != 0);
		auto messageLength = *(uint32*)&decryptedInts[7];
		auto fullDataLength = kEncryptedHeaderIntsCount * kIntSize + messageLength; // Without padding.

		// Can underflow, but it is an unsigned type, so we just check the range later.
		auto paddingSize = static_cast<uint32>(encryptedBytesCount) - static_cast<uint32>(fullDataLength);

		std::array<uchar, 32> sha256Buffer = { { 0 } };

		SHA256_CTX msgKeyLargeContext;
		SHA256_Init(&msgKeyLargeContext);
		SHA256_Update(&msgKeyLargeContext, _encryptionKey->partForMsgKey(false), 32);
		SHA256_Update(&msgKeyLargeContext, decryptedInts, encryptedBytesCount);
		SHA256_Final(sha256Buffer.data(), &msgKeyLargeContext);

		constexpr auto kMsgKeyShift = 8U;
		if (ConstTimeIsDifferent(&msgKey, sha256Buffer.data() + kMsgKeyShift, sizeof(msgKey))) {
			LOG(("TCP Error: bad SHA256 hash after aesDecrypt in message"));
			return restart();
		}

		if ((messageLength > kMaxMessageLength)
			|| (messageLength & 0x03)
			|| (paddingSize < kMinPaddingSize)
			|| (paddingSize > kMaxPaddingSize)) {
			LOG(("TCP Error: bad msg_len received %1, data size: %2").arg(messageLength).arg(encryptedBytesCount));
			return restart();
		}

		if (Logs::DebugEnabled()) {
			_connection->logInfo(u"Decrypted message %1,%2,%3 is %4 len"_q
				.arg(msgId)
				.arg(seqNo)
				.arg(Logs::b(needAck))
				.arg(fullDataLength));
		}

		if (session != _sessionId) {
			LOG(("MTP Error: bad server session received"));
			return restart();
		}

		const auto serverTime = int32(msgId >> 32);
		const auto isReply = ((msgId & 0x03) == 1);
		if (!isReply && ((msgId & 0x03) != 3)) {
			LOG(("MTP Error: bad msg_id %1 in message received").arg(msgId));

			return restart();
		}

		const auto clientTime = base::unixtime::now();
		const auto badTime = (serverTime > clientTime + 60)
			|| (serverTime + 300 < clientTime);
		if (badTime) {
			DEBUG_LOG(("MTP Info: bad server time from msg_id: %1, my time: %2").arg(serverTime).arg(clientTime));
		}

		bool wasConnected = (getState() == ConnectedState);
		if (serverSalt != _sessionSalt) {
			if (!badTime) {
				DEBUG_LOG(("MTP Info: other salt received... received: %1, my salt: %2, updating...").arg(serverSalt).arg(_sessionSalt));
				_sessionSalt = serverSalt;

				if (setState(ConnectedState, ConnectingState)) {
					resendAll();
				}
			} else {
				DEBUG_LOG(("MTP Info: other salt received... received: %1, my salt: %2").arg(serverSalt).arg(_sessionSalt));
			}
		} else {
			serverSalt = 0; // dont pass to handle method, so not to lock in setSalt()
		}

		if (needAck) _ackRequestData.push_back(MTP_long(msgId));

		auto res = HandleResult::Success; // if no need to handle, then succeed
		auto from = decryptedInts + kEncryptedHeaderIntsCount;
		auto end = from + (messageLength / kIntSize);
		auto sfrom = decryptedInts + 4U; // msg_id + seq_no + length + message
		MTP_LOG(_shiftedDcId, ("Recv: ")
			+ DumpToText(sfrom, end)
			+ QString(" (dc:%1,key:%2)"
			).arg(AbstractConnection::ProtocolDcDebugId(getProtocolDcId())
			).arg(_encryptionKey->keyId()));

		const auto registered = _receivedMessageIds.registerMsgId(
			msgId,
			needAck);
		if (registered == ReceivedIdsManager::Result::Success) {
			res = handleOneReceived(from, end, msgId, {
				.outerMsgId = msgId,
				.serverSalt = serverSalt,
				.serverTime = serverTime,
				.badTime = badTime,
			});
		} else if (registered == ReceivedIdsManager::Result::TooOld) {
			res = HandleResult::ResetSession;
		}
		_receivedMessageIds.shrink();

		// send acks
		if (const auto toAckSize = _ackRequestData.size()) {
			DEBUG_LOG(("MTP Info: will send %1 acks, ids: %2").arg(toAckSize).arg(LogIdsVector(_ackRequestData)));
			_sessionData->queueSendAnything(kAckSendWaiting);
		}

		auto lock = QReadLocker(_sessionData->haveReceivedMutex());
		const auto tryToReceive = !_sessionData->haveReceivedMessages().empty();
		lock.unlock();

		if (tryToReceive) {
			DEBUG_LOG(("MTP Info: queueTryToReceive() - need to parse in another thread, %1 messages.").arg(_sessionData->haveReceivedMessages().size()));
			_sessionData->queueTryToReceive();
		}

		if (res != HandleResult::Success && res != HandleResult::Ignored) {
			if (res == HandleResult::DestroyTemporaryKey) {
				destroyTemporaryKey();
			} else if (res == HandleResult::ResetSession) {
				_needSessionReset = true;
			}
			return restart();
		}
		_retryTimeout = 1; // reset restart() timer

		_startedConnectingAt = crl::time(0);

		if (!wasConnected) {
			if (getState() == ConnectedState) {
				_sessionData->queueNeedToResumeAndSend();
			}
		}
	}
	if (_connection->needHttpWait()) {
		_sessionData->queueSendAnything();
	}
}

SessionPrivate::HandleResult SessionPrivate::handleOneReceived(
		const mtpPrime *from,
		const mtpPrime *end,
		uint64 msgId,
		OuterInfo info) {
	Expects(from < end);

	switch (mtpTypeId(*from)) {

	case mtpc_gzip_packed: {
		DEBUG_LOG(("Message Info: gzip container"));
		mtpBuffer response = ungzip(++from, end);
		if (response.empty()) {
			return HandleResult::RestartConnection;
		}
		return handleOneReceived(response.data(), response.data() + response.size(), msgId, info);
	}

	case mtpc_msg_container: {
		if (++from >= end) {
			return HandleResult::ParseError;
		}

		const mtpPrime *otherEnd;
		const auto msgsCount = (uint32)*(from++);
		DEBUG_LOG(("Message Info: container received, count: %1").arg(msgsCount));
		for (uint32 i = 0; i < msgsCount; ++i) {
			if (from + 4 >= end) {
				return HandleResult::ParseError;
			}
			otherEnd = from + 4;

			MTPlong inMsgId;
			if (!inMsgId.read(from, otherEnd)) {
				return HandleResult::ParseError;
			}
			bool isReply = ((inMsgId.v & 0x03) == 1);
			if (!isReply && ((inMsgId.v & 0x03) != 3)) {
				LOG(("Message Error: bad msg_id %1 in contained message received").arg(inMsgId.v));
				return HandleResult::RestartConnection;
			}

			MTPint inSeqNo;
			if (!inSeqNo.read(from, otherEnd)) {
				return HandleResult::ParseError;
			}
			MTPint bytes;
			if (!bytes.read(from, otherEnd)) {
				return HandleResult::ParseError;
			}
			if ((bytes.v & 0x03) || bytes.v < 4) {
				LOG(("Message Error: bad length %1 of contained message received").arg(bytes.v));
				return HandleResult::RestartConnection;
			}

			bool needAck = (inSeqNo.v & 0x01);
			if (needAck) _ackRequestData.push_back(inMsgId);

			DEBUG_LOG(("Message Info: message from container, msg_id: %1, needAck: %2").arg(inMsgId.v).arg(Logs::b(needAck)));

			otherEnd = from + (bytes.v >> 2);
			if (otherEnd > end) {
				return HandleResult::ParseError;
			}

			auto res = HandleResult::Success; // if no need to handle, then succeed
			const auto registered = _receivedMessageIds.registerMsgId(
				inMsgId.v,
				needAck);
			if (registered == ReceivedIdsManager::Result::Success) {
				res = handleOneReceived(from, otherEnd, inMsgId.v, info);
				info.badTime = false;
			} else if (registered == ReceivedIdsManager::Result::TooOld) {
				res = HandleResult::ResetSession;
			}
			if (res != HandleResult::Success) {
				return res;
			}

			from = otherEnd;
		}
	} return HandleResult::Success;

	case mtpc_msgs_ack: {
		MTPMsgsAck msg;
		if (!msg.read(from, end)) {
			return HandleResult::ParseError;
		}
		const auto &ids = msg.c_msgs_ack().vmsg_ids().v;
		DEBUG_LOG(("Message Info: acks received, ids: %1"
			).arg(LogIdsVector(ids)));
		if (ids.isEmpty()) {
			return info.badTime ? HandleResult::Ignored : HandleResult::Success;
		}

		if (info.badTime) {
			if (!requestsFixTimeSalt(ids, info)) {
				return HandleResult::Ignored;
			}
		} else {
			correctUnixtimeByFastRequest(ids, info.serverTime);
		}
		requestsAcked(ids);
	} return HandleResult::Success;

	case mtpc_bad_msg_notification: {
		MTPBadMsgNotification msg;
		if (!msg.read(from, end)) {
			return HandleResult::ParseError;
		}
		const auto &data(msg.c_bad_msg_notification());
		LOG(("Message Info: bad message notification received (error_code %3) for msg_id = %1, seq_no = %2").arg(data.vbad_msg_id().v).arg(data.vbad_msg_seqno().v).arg(data.verror_code().v));

		const auto resendId = data.vbad_msg_id().v;
		const auto errorCode = data.verror_code().v;
		if (false
			|| errorCode == 16
			|| errorCode == 17
			|| errorCode == 32
			|| errorCode == 33
			|| errorCode == 64) { // can handle
			const auto needResend = false
				|| (errorCode == 16) // bad msg_id
				|| (errorCode == 17) // bad msg_id
				|| (errorCode == 64); // bad container
			if (errorCode == 64) { // bad container!
				if (Logs::DebugEnabled()) {
					const auto i = _sentContainers.find(resendId);
					if (i == _sentContainers.end()) {
						LOG(("Message Error: Container not found!"));
					} else {
						auto idsList = QStringList();
						for (const auto innerMsgId : i->second.messages) {
							idsList.push_back(QString::number(innerMsgId));
						}
						LOG(("Message Info: bad container received! messages: %1").arg(idsList.join(',')));
					}
				}
			}

			if (!wasSent(resendId)) {
				DEBUG_LOG(("Message Error: "
					"such message was not sent recently %1").arg(resendId));
				return info.badTime
					? HandleResult::Ignored
					: HandleResult::Success;
			}

			if (needResend) { // bad msg_id or bad container
				if (info.serverSalt) {
					_sessionSalt = info.serverSalt;
				}

				correctUnixtimeWithBadLocal(info.serverTime);

				DEBUG_LOG(("Message Info: unixtime updated, now %1, resending in container...").arg(info.serverTime));

				resend(resendId);
			} else { // must create new session, because msg_id and msg_seqno are inconsistent
				if (info.badTime) {
					if (info.serverSalt) {
						_sessionSalt = info.serverSalt;
					}
					correctUnixtimeWithBadLocal(info.serverTime);
					info.badTime = false;
				}
				LOG(("Message Info: bad message notification received, msgId %1, error_code %2").arg(data.vbad_msg_id().v).arg(errorCode));
				return HandleResult::ResetSession;
			}
		} else { // fatal (except 48, but it must not get here)
			const auto badMsgId = mtpMsgId(data.vbad_msg_id().v);
			const auto requestId = wasSent(resendId);
			if (requestId) {
				LOG(("Message Error: "
					"fatal bad message notification received, "
					"msgId %1, error_code %2, requestId: %3"
					).arg(badMsgId
					).arg(errorCode
					).arg(requestId));
				auto reply = mtpBuffer();
				MTPRpcError(MTP_rpc_error(
					MTP_int(500),
					MTP_string("PROTOCOL_ERROR")
				)).write(reply);

				// Save rpc_error for processing in the main thread.
				QWriteLocker locker(_sessionData->haveReceivedMutex());
				_sessionData->haveReceivedMessages().push_back({
					.reply = std::move(reply),
					.outerMsgId = info.outerMsgId,
					.requestId = requestId,
				});
			} else {
				DEBUG_LOG(("Message Error: "
					"such message was not sent recently %1").arg(badMsgId));
			}
			return info.badTime
				? HandleResult::Ignored
				: HandleResult::Success;
		}
	} return HandleResult::Success;

	case mtpc_bad_server_salt: {
		MTPBadMsgNotification msg;
		if (!msg.read(from, end)) {
			return HandleResult::ParseError;
		}
		const auto &data = msg.c_bad_server_salt();
		DEBUG_LOG(("Message Info: bad server salt received (error_code %4) for msg_id = %1, seq_no = %2, new salt: %3").arg(data.vbad_msg_id().v).arg(data.vbad_msg_seqno().v).arg(data.vnew_server_salt().v).arg(data.verror_code().v));

		const auto resendId = data.vbad_msg_id().v;
		if (!wasSent(resendId)) {
			DEBUG_LOG(("Message Error: such message was not sent recently %1").arg(resendId));
			return (info.badTime ? HandleResult::Ignored : HandleResult::Success);
		}

		_sessionSalt = data.vnew_server_salt().v;
		correctUnixtimeWithBadLocal(info.serverTime);

		if (setState(ConnectedState, ConnectingState)) {
			resendAll();
		}

		info.badTime = false;

		DEBUG_LOG(("Message Info: unixtime updated, now %1, server_salt updated, now %2, resending...").arg(info.serverTime).arg(info.serverSalt));
		resend(resendId);
	} return HandleResult::Success;

	case mtpc_msgs_state_info: {
		MTPMsgsStateInfo msg;
		if (!msg.read(from, end)) {
			return HandleResult::ParseError;
		}
		auto &data = msg.c_msgs_state_info();

		auto reqMsgId = data.vreq_msg_id().v;
		auto &states = data.vinfo().v;

		DEBUG_LOG(("Message Info: msg state received, msgId %1, reqMsgId: %2, HEX states %3").arg(msgId).arg(reqMsgId).arg(Logs::mb(states.data(), states.length()).str()));
		const auto i = _stateAndResendRequests.find(reqMsgId);
		if (i == _stateAndResendRequests.end()) {
			DEBUG_LOG(("Message Error: such message was not sent recently %1").arg(reqMsgId));
			return info.badTime
				? HandleResult::Ignored
				: HandleResult::Success;
		}
		if (info.badTime) {
			if (info.serverSalt) {
				_sessionSalt = info.serverSalt; // requestsFixTimeSalt with no lookup
			}
			correctUnixtimeWithBadLocal(info.serverTime);

			DEBUG_LOG(("Message Info: unixtime updated from mtpc_msgs_state_info, now %1").arg(info.serverTime));

			info.badTime = false;
		}
		const auto originalRequest = i->second;
		Assert(originalRequest->size() > 8);

		requestsAcked(QVector<MTPlong>(1, MTP_long(reqMsgId)), true);

		auto rFrom = originalRequest->constData() + 8;
		const auto rEnd = originalRequest->constData() + originalRequest->size();
		if (mtpTypeId(*rFrom) == mtpc_msgs_state_req) {
			MTPMsgsStateReq request;
			if (!request.read(rFrom, rEnd)) {
				LOG(("Message Error: could not parse sent msgs_state_req"));
				return HandleResult::ParseError;
			}
			handleMsgsStates(request.c_msgs_state_req().vmsg_ids().v, states);
		} else {
			MTPMsgResendReq request;
			if (!request.read(rFrom, rEnd)) {
				LOG(("Message Error: could not parse sent msgs_resend_req"));
				return HandleResult::ParseError;
			}
			handleMsgsStates(request.c_msg_resend_req().vmsg_ids().v, states);
		}
	} return HandleResult::Success;

	case mtpc_msgs_all_info: {
		if (info.badTime) {
			DEBUG_LOG(("Message Info: skipping with bad time..."));
			return HandleResult::Ignored;
		}

		MTPMsgsAllInfo msg;
		if (!msg.read(from, end)) {
			return HandleResult::ParseError;
		}
		auto &data = msg.c_msgs_all_info();
		auto &ids = data.vmsg_ids().v;
		auto &states = data.vinfo().v;

		DEBUG_LOG(("Message Info: msgs all info received, msgId %1, reqMsgIds: %2, states %3").arg(
			QString::number(msgId),
			LogIdsVector(ids),
			Logs::mb(states.data(), states.length()).str()));

		handleMsgsStates(ids, states);
	} return HandleResult::Success;

	case mtpc_msg_detailed_info: {
		MTPMsgDetailedInfo msg;
		if (!msg.read(from, end)) {
			return HandleResult::ParseError;
		}
		const auto &data(msg.c_msg_detailed_info());

		DEBUG_LOG(("Message Info: msg detailed info, sent msgId %1, answerId %2, status %3, bytes %4").arg(data.vmsg_id().v).arg(data.vanswer_msg_id().v).arg(data.vstatus().v).arg(data.vbytes().v));

		QVector<MTPlong> ids(1, data.vmsg_id());
		if (info.badTime) {
			if (requestsFixTimeSalt(ids, info)) {
				info.badTime = false;
			} else {
				DEBUG_LOG(("Message Info: error, such message was not sent recently %1").arg(data.vmsg_id().v));
				return HandleResult::Ignored;
			}
		}
		requestsAcked(ids);

		const auto resMsgId = data.vanswer_msg_id();
		if (_receivedMessageIds.lookup(resMsgId.v) != ReceivedIdsManager::State::NotFound) {
			_ackRequestData.push_back(resMsgId);
		} else {
			DEBUG_LOG(("Message Info: answer message %1 was not received, requesting...").arg(resMsgId.v));
			_resendRequestData.push_back(resMsgId);
		}
	} return HandleResult::Success;

	case mtpc_msg_new_detailed_info: {
		if (info.badTime) {
			DEBUG_LOG(("Message Info: skipping msg_new_detailed_info with bad time..."));
			return HandleResult::Ignored;
		}
		MTPMsgDetailedInfo msg;
		if (!msg.read(from, end)) {
			return HandleResult::ParseError;
		}
		const auto &data(msg.c_msg_new_detailed_info());

		DEBUG_LOG(("Message Info: msg new detailed info, answerId %2, status %3, bytes %4").arg(data.vanswer_msg_id().v).arg(data.vstatus().v).arg(data.vbytes().v));

		const auto resMsgId = data.vanswer_msg_id();
		if (_receivedMessageIds.lookup(resMsgId.v) != ReceivedIdsManager::State::NotFound) {
			_ackRequestData.push_back(resMsgId);
		} else {
			DEBUG_LOG(("Message Info: answer message %1 was not received, requesting...").arg(resMsgId.v));
			_resendRequestData.push_back(resMsgId);
		}
	} return HandleResult::Success;

	case mtpc_rpc_result: {
		if (from + 3 > end) {
			return HandleResult::ParseError;
		}
		auto response = mtpBuffer();

		MTPlong reqMsgId;
		if (!reqMsgId.read(++from, end)) {
			return HandleResult::ParseError;
		}
		const auto requestMsgId = reqMsgId.v;

		DEBUG_LOG(("RPC Info: response received for %1, queueing...").arg(requestMsgId));

		QVector<MTPlong> ids(1, reqMsgId);
		if (info.badTime) {
			if (requestsFixTimeSalt(ids, info)) {
				info.badTime = false;
			} else {
				DEBUG_LOG(("Message Info: error, such message was not sent recently %1").arg(requestMsgId));
				return HandleResult::Ignored;
			}
		}

		mtpTypeId typeId = from[0];
		if (typeId == mtpc_gzip_packed) {
			DEBUG_LOG(("RPC Info: gzip container"));
			response = ungzip(++from, end);
			if (response.empty()) {
				return HandleResult::RestartConnection;
			}
			typeId = response[0];
		} else {
			response.resize(end - from);
			memcpy(response.data(), from, (end - from) * sizeof(mtpPrime));
		}
		if (typeId == mtpc_rpc_error) {
			if (IsDestroyedTemporaryKeyError(response)) {
				return HandleResult::DestroyTemporaryKey;
			}
			// An error could be some RPC_CALL_FAIL or other error inside
			// the initConnection, so we're not sure yet that it was inited.
			// Wait till a good response is received.
		} else {
			_sessionData->notifyConnectionInited(*_options);
		}
		requestsAcked(ids, true);

		const auto bindResult = handleBindResponse(requestMsgId, response);
		if (bindResult != HandleResult::Ignored) {
			return bindResult;
		}
		const auto requestId = wasSent(requestMsgId);
		if (requestId && requestId != mtpRequestId(0xFFFFFFFF)) {
			// Save rpc_result for processing in the main thread.
			QWriteLocker locker(_sessionData->haveReceivedMutex());
			_sessionData->haveReceivedMessages().push_back({
				.reply = std::move(response),
				.outerMsgId = info.outerMsgId,
				.requestId = requestId,
			});
		} else {
			DEBUG_LOG(("RPC Info: requestId not found for msgId %1").arg(requestMsgId));
		}
	} return HandleResult::Success;

	case mtpc_new_session_created: {
		const mtpPrime *start = from;
		MTPNewSession msg;
		if (!msg.read(from, end)) {
			return HandleResult::ParseError;
		}
		const auto &data(msg.c_new_session_created());

		if (info.badTime) {
			if (requestsFixTimeSalt(QVector<MTPlong>(1, data.vfirst_msg_id()), info)) {
				info.badTime = false;
			} else {
				DEBUG_LOG(("Message Info: error, such message was not sent recently %1").arg(data.vfirst_msg_id().v));
				return HandleResult::Ignored;
			}
		}

		DEBUG_LOG(("Message Info: new server session created, unique_id %1, first_msg_id %2, server_salt %3").arg(data.vunique_id().v).arg(data.vfirst_msg_id().v).arg(data.vserver_salt().v));
		_sessionSalt = data.vserver_salt().v;

		mtpMsgId firstMsgId = data.vfirst_msg_id().v;
		QVector<quint64> toResend;
		{
			QReadLocker locker(_sessionData->haveSentMutex());
			const auto &haveSent = _sessionData->haveSentMap();
			toResend.reserve(haveSent.size());
			for (const auto &[msgId, request] : haveSent) {
				if (msgId >= firstMsgId) {
					break;
				} else if (request->requestId) {
					toResend.push_back(msgId);
				}
			}
		}
		for (const auto msgId : toResend) {
			resend(msgId, 10);
		}

		mtpBuffer update(from - start);
		if (from > start) memcpy(update.data(), start, (from - start) * sizeof(mtpPrime));

		// Notify main process about new session - need to get difference.
		QWriteLocker locker(_sessionData->haveReceivedMutex());
		_sessionData->haveReceivedMessages().push_back({
			.reply = update,
			.outerMsgId = info.outerMsgId,
		});
	} return HandleResult::Success;

	case mtpc_pong: {
		MTPPong msg;
		if (!msg.read(from, end)) {
			return HandleResult::ParseError;
		}
		const auto &data(msg.c_pong());
		DEBUG_LOG(("Message Info: pong received, msg_id: %1, ping_id: %2").arg(data.vmsg_id().v).arg(data.vping_id().v));

		if (!wasSent(data.vmsg_id().v)) {
			DEBUG_LOG(("Message Error: such msg_id %1 ping_id %2 was not sent recently").arg(data.vmsg_id().v).arg(data.vping_id().v));
			return HandleResult::Ignored;
		}
		if (data.vping_id().v == _pingId) {
			_pingId = 0;
		} else {
			DEBUG_LOG(("Message Info: just pong..."));
		}

		QVector<MTPlong> ids(1, data.vmsg_id());
		if (info.badTime) {
			if (requestsFixTimeSalt(ids, info)) {
				info.badTime = false;
			} else {
				return HandleResult::Ignored;
			}
		}
		requestsAcked(ids, true);
	} return HandleResult::Success;

	}

	if (info.badTime) {
		DEBUG_LOG(("Message Error: bad time in updates cons, must create new session"));
		return HandleResult::ResetSession;
	}

	if (_currentDcType == DcType::Regular) {
		mtpBuffer update(end - from);
		if (end > from) {
			memcpy(update.data(), from, (end - from) * sizeof(mtpPrime));
		}

		// Notify main process about the new updates.
		QWriteLocker locker(_sessionData->haveReceivedMutex());
		_sessionData->haveReceivedMessages().push_back({
			.reply = update,
			.outerMsgId = info.outerMsgId,
		});
	} else {
		LOG(("Message Error: unexpected updates in dcType: %1"
			).arg(static_cast<int>(_currentDcType)));
	}

	return HandleResult::Success;
}

SessionPrivate::HandleResult SessionPrivate::handleBindResponse(
		mtpMsgId requestMsgId,
		const mtpBuffer &response) {
	if (!_keyCreator || !_bindMsgId || _bindMsgId != requestMsgId) {
		return HandleResult::Ignored;
	}
	_bindMsgId = 0;

	const auto result = _keyCreator->handleBindResponse(response);
	switch (result) {
	case DcKeyBindState::Success:
		if (!_sessionData->releaseKeyCreationOnDone(
			_encryptionKey,
			base::take(_keyCreator)->bindPersistentKey())) {
			return HandleResult::DestroyTemporaryKey;
		}
		_sessionData->queueNeedToResumeAndSend();
		return HandleResult::Success;
	case DcKeyBindState::DefinitelyDestroyed:
		if (destroyOldEnoughPersistentKey()) {
			return HandleResult::DestroyTemporaryKey;
		}
		[[fallthrough]];
	case DcKeyBindState::Failed:
		_sessionData->queueNeedToResumeAndSend();
		return HandleResult::Success;
	}
	Unexpected("Result of BoundKeyCreator::handleBindResponse.");
}

mtpBuffer SessionPrivate::ungzip(const mtpPrime *from, const mtpPrime *end) const {
	mtpBuffer result; // * 4 because of mtpPrime type
	result.resize(0);

	MTPstring packed;
	if (!packed.read(from, end)) { // read packed string as serialized mtp string type
		LOG(("RPC Error: could not read gziped bytes."));
		return result;
	}
	uint32 packedLen = packed.v.size(), unpackedChunk = packedLen;

	z_stream stream;
	stream.zalloc = 0;
	stream.zfree = 0;
	stream.opaque = 0;
	stream.avail_in = 0;
	stream.next_in = 0;
	int res = inflateInit2(&stream, 16 + MAX_WBITS);
	if (res != Z_OK) {
		LOG(("RPC Error: could not init zlib stream, code: %1").arg(res));
		return result;
	}
	stream.avail_in = packedLen;
	stream.next_in = reinterpret_cast<Bytef*>(packed.v.data());

	stream.avail_out = 0;
	while (!stream.avail_out) {
		result.resize(result.size() + unpackedChunk);
		stream.avail_out = unpackedChunk * sizeof(mtpPrime);
		stream.next_out = (Bytef*)&result[result.size() - unpackedChunk];
		int res = inflate(&stream, Z_NO_FLUSH);
		if (res != Z_OK && res != Z_STREAM_END) {
			inflateEnd(&stream);
			LOG(("RPC Error: could not unpack gziped data, code: %1").arg(res));
			DEBUG_LOG(("RPC Error: bad gzip: %1").arg(Logs::mb(packed.v.constData(), packedLen).str()));
			return mtpBuffer();
		}
	}
	if (stream.avail_out & 0x03) {
		uint32 badSize = result.size() * sizeof(mtpPrime) - stream.avail_out;
		LOG(("RPC Error: bad length of unpacked data %1").arg(badSize));
		DEBUG_LOG(("RPC Error: bad unpacked data %1").arg(Logs::mb(result.data(), badSize).str()));
		return mtpBuffer();
	}
	result.resize(result.size() - (stream.avail_out >> 2));
	inflateEnd(&stream);
	if (!result.size()) {
		LOG(("RPC Error: bad length of unpacked data 0"));
	}
	return result;
}

bool SessionPrivate::requestsFixTimeSalt(const QVector<MTPlong> &ids, const OuterInfo &info) {
	for (const auto &id : ids) {
		if (wasSent(id.v)) {
			// Found such msg_id in recent acked or in recent sent requests.
			if (info.serverSalt) {
				_sessionSalt = info.serverSalt;
			}
			correctUnixtimeWithBadLocal(info.serverTime);
			return true;
		}
	}
	return false;
}

void SessionPrivate::correctUnixtimeByFastRequest(
		const QVector<MTPlong> &ids,
		TimeId serverTime) {
	const auto now = crl::now();

	QReadLocker locker(_sessionData->haveSentMutex());
	const auto &haveSent = _sessionData->haveSentMap();
	for (const auto &id : ids) {
		const auto i = haveSent.find(id.v);
		if (i == haveSent.end()) {
			continue;
		}
		const auto duration = (now - i->second->lastSentTime);
		if (duration < 0 || duration > SyncTimeRequestDuration) {
			continue;
		}
		locker.unlock();

		SyncTimeRequestDuration = duration;
		base::unixtime::update(serverTime, true);
		return;
	}
}

void SessionPrivate::correctUnixtimeWithBadLocal(TimeId serverTime) {
	SyncTimeRequestDuration = kFastRequestDuration;
	base::unixtime::update(serverTime, true);
}

void SessionPrivate::requestsAcked(const QVector<MTPlong> &ids, bool byResponse) {
	DEBUG_LOG(("Message Info: requests acked, ids %1").arg(LogIdsVector(ids)));

	QVector<MTPlong> toAckMore;
	{
		QWriteLocker locker2(_sessionData->haveSentMutex());
		auto &haveSent = _sessionData->haveSentMap();

		for (const auto &wrappedMsgId : ids) {
			const auto msgId = wrappedMsgId.v;
			if (const auto i = _sentContainers.find(msgId); i != end(_sentContainers)) {
				DEBUG_LOG(("Message Info: container ack received, msgId %1").arg(msgId));
				const auto &list = i->second.messages;
				toAckMore.reserve(toAckMore.size() + list.size());
				for (const auto msgId : list) {
					toAckMore.push_back(MTP_long(msgId));
				}
				_sentContainers.erase(i);
				continue;
			}
			if (const auto i = _stateAndResendRequests.find(msgId); i != end(_stateAndResendRequests)) {
				_stateAndResendRequests.erase(i);
				continue;
			}
			if (const auto i = haveSent.find(msgId); i != end(haveSent)) {
				const auto requestId = i->second->requestId;

				if (!byResponse && _instance->hasCallback(requestId)) {
					DEBUG_LOG(("Message Info: ignoring ACK for msgId %1 because request %2 requires a response").arg(msgId).arg(requestId));
					continue;
				}
				haveSent.erase(i);

				_ackedIds.emplace(msgId, requestId);
				continue;
			}
			DEBUG_LOG(("Message Info: msgId %1 was not found in recent sent, while acking requests, searching in resend...").arg(msgId));
			if (const auto i = _resendingIds.find(msgId); i != end(_resendingIds)) {
				const auto requestId = i->second;

				if (!byResponse && _instance->hasCallback(requestId)) {
					DEBUG_LOG(("Message Info: ignoring ACK for msgId %1 because request %2 requires a response").arg(msgId).arg(requestId));
					continue;
				}
				_resendingIds.erase(i);

				QWriteLocker locker4(_sessionData->toSendMutex());
				auto &toSend = _sessionData->toSendMap();
				const auto j = toSend.find(requestId);
				if (j == end(toSend)) {
					DEBUG_LOG(("Message Info: msgId %1 was found in recent resent, requestId %2 was not found in prepared to send").arg(msgId).arg(requestId));
					continue;
				}
				if (j->second->requestId != requestId) {
					DEBUG_LOG(("Message Error: for msgId %1 found resent request, requestId %2, contains requestId %3").arg(msgId).arg(requestId).arg(j->second->requestId));
				} else {
					DEBUG_LOG(("Message Info: acked msgId %1 that was prepared to resend, requestId %2").arg(msgId).arg(requestId));
				}

				_ackedIds.emplace(msgId, j->second->requestId);

				toSend.erase(j);
				continue;
			}
			DEBUG_LOG(("Message Info: msgId %1 was not found in recent resent either").arg(msgId));
		}
	}

	auto ackedCount = _ackedIds.size();
	if (ackedCount > kIdsBufferSize) {
		DEBUG_LOG(("Message Info: removing some old acked sent msgIds %1").arg(ackedCount - kIdsBufferSize));
		while (ackedCount-- > kIdsBufferSize) {
			_ackedIds.erase(_ackedIds.begin());
		}
	}

	if (toAckMore.size()) {
		requestsAcked(toAckMore);
	}
}

void SessionPrivate::handleMsgsStates(const QVector<MTPlong> &ids, const QByteArray &states) {
	const auto idsCount = ids.size();
	if (!idsCount) {
		DEBUG_LOG(("Message Info: void ids vector in handleMsgsStates()"));
		return;
	}
	if (states.size() != idsCount) {
		LOG(("Message Error: got less states than required ids count."));
		return;
	}

	auto acked = QVector<MTPlong>();
	acked.reserve(idsCount);
	for (auto i = 0; i != idsCount; ++i) {
		const auto state = states[i];
		const auto requestMsgId = ids[i].v;
		{
			QReadLocker locker(_sessionData->haveSentMutex());
			if (!_sessionData->haveSentMap().contains(requestMsgId)) {
				DEBUG_LOG(("Message Info: state was received for msgId %1, but request is not found, looking in resent requests...").arg(requestMsgId));
				const auto reqIt = _resendingIds.find(requestMsgId);
				if (reqIt != _resendingIds.cend()) {
					if ((state & 0x07) != 0x04) { // was received
						DEBUG_LOG(("Message Info: state was received for msgId %1, state %2, already resending in container").arg(requestMsgId).arg((int32)state));
					} else {
						DEBUG_LOG(("Message Info: state was received for msgId %1, state %2, ack, cancelling resend").arg(requestMsgId).arg((int32)state));
						acked.push_back(MTP_long(requestMsgId)); // will remove from resend in requestsAcked
					}
				} else {
					DEBUG_LOG(("Message Info: msgId %1 was not found in recent resent either").arg(requestMsgId));
				}
				continue;
			}
		}
		if ((state & 0x07) != 0x04) { // was received
			DEBUG_LOG(("Message Info: state was received for msgId %1, state %2, resending in container").arg(requestMsgId).arg((int32)state));
			resend(requestMsgId, 10);
		} else {
			DEBUG_LOG(("Message Info: state was received for msgId %1, state %2, ack").arg(requestMsgId).arg((int32)state));
			acked.push_back(MTP_long(requestMsgId));
		}
	}
	requestsAcked(acked);
}

void SessionPrivate::clearSpecialMsgId(mtpMsgId msgId) {
	if (msgId == _pingMsgId) {
		_pingMsgId = 0;
		_pingId = 0;
	} else if (msgId == _bindMsgId) {
		_bindMsgId = 0;
	}
}

void SessionPrivate::resend(mtpMsgId msgId, crl::time msCanWait) {
	const auto guard = gsl::finally([&] {
		clearSpecialMsgId(msgId);
		if (msCanWait >= 0) {
			_sessionData->queueSendAnything(msCanWait);
		}
	});

	if (const auto i = _sentContainers.find(msgId); i != end(_sentContainers)) {
		DEBUG_LOG(("Message Info: resending container, msgId %1").arg(msgId));
		const auto ids = std::move(i->second.messages);
		_sentContainers.erase(i);

		for (const auto innerMsgId : ids) {
			resend(innerMsgId, -1);
		}
		return;
	}
	auto lock = QWriteLocker(_sessionData->haveSentMutex());
	auto &haveSent = _sessionData->haveSentMap();
	auto i = haveSent.find(msgId);
	if (i == haveSent.end()) {
		return;
	}
	auto request = i->second;
	haveSent.erase(i);
	lock.unlock();

	request->lastSentTime = crl::now();
	request->forceSendInContainer = true;
	_resendingIds.emplace(msgId, request->requestId);
	{
		QWriteLocker locker(_sessionData->toSendMutex());
		_sessionData->toSendMap().emplace(request->requestId, request);
	}
}

void SessionPrivate::resendAll() {
	auto lock = QWriteLocker(_sessionData->haveSentMutex());
	auto haveSent = base::take(_sessionData->haveSentMap());
	lock.unlock();
	{
		auto lock = QWriteLocker(_sessionData->toSendMutex());
		auto &toSend = _sessionData->toSendMap();
		const auto now = crl::now();
		for (auto &[msgId, request] : haveSent) {
			const auto requestId = request->requestId;
			request->lastSentTime = now;
			request->forceSendInContainer = true;
			_resendingIds.emplace(msgId, requestId);
			toSend.emplace(requestId, std::move(request));
		}
	}

	_sessionData->queueSendAnything();
}

void SessionPrivate::onConnected(
		not_null<AbstractConnection*> connection) {
	disconnect(connection, &AbstractConnection::connected, nullptr, nullptr);
	if (!connection->isConnected()) {
		LOG(("Connection Error: not connected in onConnected(), "
			"state: %1").arg(connection->debugState()));
		return restart();
	}

	_waitForConnected = kMinConnectedTimeout;
	_waitForConnectedTimer.cancel();

	const auto i = ranges::find(
		_testConnections,
		connection.get(),
		[](const TestConnection &test) { return test.data.get(); });
	Assert(i != end(_testConnections));
	const auto my = i->priority;
	const auto j = ranges::find_if(
		_testConnections,
		[&](const TestConnection &test) { return test.priority > my; });
	if (j != end(_testConnections)) {
		DEBUG_LOG(("MTP Info: connection %1 succeed, waiting for %2.").arg(
			i->data->tag(),
			j->data->tag()));
		_waitForBetterTimer.callOnce(kWaitForBetterTimeout);
	} else {
		DEBUG_LOG(("MTP Info: connection through IPv4 succeed."));
		_waitForBetterTimer.cancel();
		_connection = std::move(i->data);
		_testConnections.clear();
		checkAuthKey();
	}
}

void SessionPrivate::onDisconnected(
		not_null<AbstractConnection*> connection) {
	removeTestConnection(connection);

	if (_testConnections.empty()) {
		destroyAllConnections();
		restart();
	} else {
		confirmBestConnection();
	}
}

void SessionPrivate::confirmBestConnection() {
	if (_waitForBetterTimer.isActive()) {
		return;
	}
	const auto i = ranges::max_element(
		_testConnections,
		std::less<>(),
		[](const TestConnection &test) {
			return test.data->isConnected() ? test.priority : -1;
		});
	Assert(i != end(_testConnections));
	if (!i->data->isConnected()) {
		return;
	}

	DEBUG_LOG(("MTP Info: can't connect through better, using %1."
		).arg(i->data->tag()));

	_connection = std::move(i->data);
	_testConnections.clear();

	checkAuthKey();
}

void SessionPrivate::removeTestConnection(
		not_null<AbstractConnection*> connection) {
	_testConnections.erase(
		ranges::remove(
			_testConnections,
			connection.get(),
			[](const TestConnection &test) { return test.data.get(); }),
		end(_testConnections));
}

void SessionPrivate::checkAuthKey() {
	if (_keyId) {
		authKeyChecked();
	} else if (_instance->isKeysDestroyer()) {
		applyAuthKey(_sessionData->getPersistentKey());
	} else {
		applyAuthKey(_sessionData->getTemporaryKey(
			TemporaryKeyTypeByDcType(_currentDcType)));
	}
}

void SessionPrivate::updateAuthKey() {
	if (_instance->isKeysDestroyer() || _keyCreator || !_connection) {
		return;
	}

	DEBUG_LOG(("AuthKey Info: Connection updating key from Session, dc %1"
		).arg(_shiftedDcId));
	applyAuthKey(_sessionData->getTemporaryKey(
		TemporaryKeyTypeByDcType(_currentDcType)));
}

void SessionPrivate::setCurrentKeyId(uint64 newKeyId) {
	if (_keyId == newKeyId) {
		return;
	}
	_keyId = newKeyId;

	DEBUG_LOG(("MTP Info: auth key id set to id %1").arg(newKeyId));
	changeSessionId();
}

void SessionPrivate::applyAuthKey(AuthKeyPtr &&encryptionKey) {
	_encryptionKey = std::move(encryptionKey);
	const auto newKeyId = _encryptionKey ? _encryptionKey->keyId() : 0;
	if (_keyId) {
		if (_keyId == newKeyId) {
			return;
		}
		setCurrentKeyId(0);
		DEBUG_LOG(("MTP Info: auth_key id for dc %1 changed, restarting..."
			).arg(_shiftedDcId));
		if (_connection) {
			restart();
		}
		return;
	}
	if (!_connection) {
		return;
	}
	setCurrentKeyId(newKeyId);
	Assert(!_connection->sentEncryptedWithKeyId());

	DEBUG_LOG(("AuthKey Info: Connection update key from Session, "
		"dc %1 result: %2"
		).arg(_shiftedDcId
		).arg(Logs::mb(&_keyId, sizeof(_keyId)).str()));
	if (_keyId) {
		return authKeyChecked();
	}

	if (_instance->isKeysDestroyer()) {
		// We are here to destroy an old key, so we're done.
		LOG(("MTP Error: No key %1 in updateAuthKey() for destroying."
			).arg(_shiftedDcId));
		_instance->keyWasPossiblyDestroyed(_shiftedDcId);
	} else if (noMediaKeyWithExistingRegularKey()) {
		DEBUG_LOG(("AuthKey Info: No key in updateAuthKey() for media, "
			"but someone has created regular, trying to acquire."));
		const auto dcType = tryAcquireKeyCreation();
		if (_keyCreator && dcType != _currentDcType) {
			DEBUG_LOG(("AuthKey Info: "
				"Dc type changed for creation, restarting."));
			restart();
			return;
		}
	}
	if (_keyCreator) {
		DEBUG_LOG(("AuthKey Info: No key in updateAuthKey(), creating."));
		_keyCreator->start(
			BareDcId(_shiftedDcId),
			getProtocolDcId(),
			_connection.get(),
			&_instance->dcOptions());
	} else {
		DEBUG_LOG(("AuthKey Info: No key in updateAuthKey(), "
			"but someone is creating already, waiting."));
	}
}

bool SessionPrivate::noMediaKeyWithExistingRegularKey() const {
	return (TemporaryKeyTypeByDcType(_currentDcType)
			== TemporaryKeyType::MediaCluster)
		&& _sessionData->getTemporaryKey(TemporaryKeyType::Regular);
}

bool SessionPrivate::destroyOldEnoughPersistentKey() {
	Expects(_keyCreator != nullptr);

	const auto key = _keyCreator->bindPersistentKey();
	Assert(key != nullptr);

	const auto created = key->creationTime();
	if (created > 0 && crl::now() - created < kKeyOldEnoughForDestroy) {
		return false;
	}
	const auto instance = _instance;
	const auto shiftedDcId = _shiftedDcId;
	const auto keyId = key->keyId();
	InvokeQueued(instance, [=] {
		instance->keyDestroyedOnServer(shiftedDcId, keyId);
	});
	return true;
}

DcType SessionPrivate::tryAcquireKeyCreation() {
	if (_keyCreator) {
		return _currentDcType;
	} else if (_instance->isKeysDestroyer()) {
		return _realDcType;
	}

	const auto acquired = _sessionData->acquireKeyCreation(_realDcType);
	if (acquired == CreatingKeyType::None) {
		return _realDcType;
	}

	using Result = DcKeyResult;
	using Error = DcKeyError;
	auto delegate = BoundKeyCreator::Delegate();
	delegate.unboundReady = [=](base::expected<Result, Error> result) {
		if (!result) {
			releaseKeyCreationOnFail();
			if (result.error() == Error::UnknownPublicKey) {
				if (_realDcType == DcType::Cdn) {
					LOG(("Warning: CDN public RSA key not found"));
					requestCDNConfig();
					return;
				}
				LOG(("AuthKey Error: could not choose public RSA key"));
			}
			restart();
			return;
		}
		DEBUG_LOG(("AuthKey Info: unbound key creation succeed, "
			"ids: (%1, %2) server salts: (%3, %4)"
			).arg(result->temporaryKey
				? result->temporaryKey->keyId()
				: 0
			).arg(result->persistentKey
				? result->persistentKey->keyId()
				: 0
			).arg(result->temporaryServerSalt
			).arg(result->persistentServerSalt));

		_sessionSalt = result->temporaryServerSalt;
		result->temporaryKey->setExpiresAt(base::unixtime::now()
			+ kTemporaryExpiresIn
			+ kBindKeyAdditionalExpiresTimeout);
		if (_realDcType != DcType::Cdn) {
			auto key = result->persistentKey
				? std::move(result->persistentKey)
				: _sessionData->getPersistentKey();
			if (!key) {
				releaseKeyCreationOnFail();
				restart();
				return;
			}
			_keyCreator->bind(std::move(key));
		}
		applyAuthKey(std::move(result->temporaryKey));
		if (_realDcType == DcType::Cdn) {
			_keyCreator = nullptr;
			if (!_sessionData->releaseCdnKeyCreationOnDone(_encryptionKey)) {
				restart();
			} else {
				_sessionData->queueNeedToResumeAndSend();
			}
		}
	};
	delegate.sentSome = [=](uint64 size) {
		onSentSome(size);
	};
	delegate.receivedSome = [=] {
		onReceivedSome();
	};

	auto request = DcKeyRequest();
	request.persistentNeeded = (acquired == CreatingKeyType::Persistent);
	request.temporaryExpiresIn = kTemporaryExpiresIn;
	_keyCreator = std::make_unique<BoundKeyCreator>(
		request,
		std::move(delegate));
	const auto forceUseRegular = (_realDcType == DcType::MediaCluster)
		&& (acquired != CreatingKeyType::TemporaryMediaCluster);
	return forceUseRegular ? DcType::Regular : _realDcType;
}

void SessionPrivate::authKeyChecked() {
	connect(_connection, &AbstractConnection::receivedData, [=] {
		handleReceived();
	});

	if (_sessionSalt && setState(ConnectedState)) {
		resendAll();
	} // else receive salt in bad_server_salt first, then try to send all the requests

	_pingIdToSend = base::RandomValue<uint64>(); // get server_salt
	_sessionData->queueNeedToResumeAndSend();
}

void SessionPrivate::onError(
		not_null<AbstractConnection*> connection,
		qint32 errorCode) {
	if (errorCode == -429) {
		LOG(("Protocol Error: -429 flood code returned!"));
	} else if (errorCode == -444) {
		LOG(("Protocol Error: -444 bad dc_id code returned!"));
		InvokeQueued(_instance, [instance = _instance] {
			instance->badConfigurationError();
		});
	}
	removeTestConnection(connection);

	if (_testConnections.empty()) {
		handleError(errorCode);
	} else {
		confirmBestConnection();
	}
}

void SessionPrivate::handleError(int errorCode) {
	destroyAllConnections();
	_waitForConnectedTimer.cancel();

	if (errorCode == -404) {
		destroyTemporaryKey();
	} else {
		MTP_LOG(_shiftedDcId, ("Restarting after error in connection, error code: %1...").arg(errorCode));
		return restart();
	}
}

void SessionPrivate::destroyTemporaryKey() {
	if (_instance->isKeysDestroyer()) {
		LOG(("MTP Info: -404 error received in destroyer %1, assuming key was destroyed.").arg(_shiftedDcId));
		_instance->keyWasPossiblyDestroyed(_shiftedDcId);
		return;
	}
	LOG(("MTP Info: -404 error received in %1 with temporary key, assuming it was destroyed.").arg(_shiftedDcId));
	releaseKeyCreationOnFail();
	if (_encryptionKey) {
		_sessionData->destroyTemporaryKey(_encryptionKey->keyId());
	}
	applyAuthKey(nullptr);
	restart();
}

bool SessionPrivate::sendSecureRequest(
		SerializedRequest &&request,
		bool needAnyResponse) {
	request.addPadding(false);

	uint32 fullSize = request->size();
	if (fullSize < 9) {
		return false;
	}

	auto messageSize = request.messageSize();
	if (messageSize < 5 || fullSize < messageSize + 4) {
		return false;
	}

	memcpy(request->data() + 0, &_sessionSalt, 2 * sizeof(mtpPrime));
	memcpy(request->data() + 2, &_sessionId, 2 * sizeof(mtpPrime));

	auto from = request->constData() + 4;
	MTP_LOG(_shiftedDcId, ("Send: ")
		+ DumpToText(from, from + messageSize)
		+ QString(" (dc:%1,key:%2)"
		).arg(AbstractConnection::ProtocolDcDebugId(getProtocolDcId())
		).arg(_encryptionKey->keyId()));

	uchar encryptedSHA256[32];
	MTPint128 &msgKey(*(MTPint128*)(encryptedSHA256 + 8));

	SHA256_CTX msgKeyLargeContext;
	SHA256_Init(&msgKeyLargeContext);
	SHA256_Update(&msgKeyLargeContext, _encryptionKey->partForMsgKey(true), 32);
	SHA256_Update(&msgKeyLargeContext, request->constData(), fullSize * sizeof(mtpPrime));
	SHA256_Final(encryptedSHA256, &msgKeyLargeContext);

	auto packet = _connection->prepareSecurePacket(_keyId, msgKey, fullSize);
	const auto prefix = packet.size();
	packet.resize(prefix + fullSize);

	aesIgeEncrypt(
		request->constData(),
		&packet[prefix],
		fullSize * sizeof(mtpPrime),
		_encryptionKey,
		msgKey);

	DEBUG_LOG(("MTP Info: sending request, size: %1, num: %2, time: %3").arg(fullSize + 6).arg((*request)[4]).arg((*request)[5]));

	_connection->setSentEncryptedWithKeyId(_keyId);
	_connection->sendData(std::move(packet));

	if (needAnyResponse) {
		onSentSome((prefix + fullSize) * sizeof(mtpPrime));
	}

	return true;
}

mtpRequestId SessionPrivate::wasSent(mtpMsgId msgId) const {
	if (msgId == _pingMsgId || msgId == _bindMsgId) {
		return mtpRequestId(0xFFFFFFFF);
	}
	if (const auto i = _resendingIds.find(msgId); i != end(_resendingIds)) {
		return i->second;
	}
	if (const auto i = _ackedIds.find(msgId); i != end(_ackedIds)) {
		return i->second;
	}
	if (const auto i = _sentContainers.find(msgId); i != end(_sentContainers)) {
		return mtpRequestId(0xFFFFFFFF);
	}

	{
		QReadLocker locker(_sessionData->haveSentMutex());
		const auto &haveSent = _sessionData->haveSentMap();
		const auto i = haveSent.find(msgId);
		if (i != haveSent.end()) {
			return i->second->requestId
				? i->second->requestId
				: mtpRequestId(0xFFFFFFFF);
		}
	}
	return 0;
}

void SessionPrivate::clearUnboundKeyCreator() {
	if (_keyCreator) {
		_keyCreator->stop();
	}
}

void SessionPrivate::releaseKeyCreationOnFail() {
	if (!_keyCreator) {
		return;
	}
	_keyCreator = nullptr;
	_sessionData->releaseKeyCreationOnFail();
}

} // namespace details
} // namespace MTP