/* WebClientImpl.cpp */
/*
 * Copyright (C) 2011-2024 Michael Lugmair
 *
 * This file is part of sayonara player
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include "WebClientImpl.h"

#include "Utils/Logger/Logger.h"
#include "Utils/Utils.h"
#include "Utils/FileUtils.h"
#include "Utils/Macros.h"

#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QImage>
#include <QTimer>
#include <QPointer>

namespace
{
	constexpr const auto UserAgentHeader = "User-Agent";

	bool isStream(const QNetworkReply* reply)
	{
		const auto contentType = reply->header(QNetworkRequest::ContentTypeHeader).toString().toLower();
		return contentType.contains("audio/") ||
		       contentType.contains("video/");
	}

	bool isPlaylistFile(const QUrl& url)
	{
		return Util::File::isPlaylistFile(url.fileName());
	}

	WebClient::Status convertNetworkError(QNetworkReply::NetworkError networkError)
	{
		switch(networkError)
		{
			case QNetworkReply::ContentNotFoundError:
				return WebClient::Status::NotFound;
			case QNetworkReply::OperationCanceledError:
				return WebClient::Status::Timeout;
			default:
				return WebClient::Status::Error;
		}
	}

	QString checkGetUrl(QString url)
	{
		url.replace(QStringLiteral("itpc://"), QStringLiteral("http://"));
		url.replace(QStringLiteral("feed://"), QStringLiteral("http://"));

		return url;
	}

	void setRequestHeader(QNetworkRequest& request, const QMap<QByteArray, QByteArray>& headerData)
	{
		for(auto it = headerData.cbegin(); it != headerData.cend(); ++it)
		{
			request.setRawHeader(it.key(), it.value());
		}
	}

	std::pair<QByteArray, WebClient::Status> checkReplyData(QNetworkReply* reply, const WebClient* webAccess)
	{
		if((reply->bytesAvailable() > 0) && reply->isReadable())
		{
			spLog(Log::Verbose, webAccess) << "Got " << reply->bytesAvailable() << " bytes";
			return std::make_pair(reply->readAll(), WebClient::Status::GotData);
		}

		spLog(Log::Verbose, webAccess) << "Answer contains no data (" << reply->errorString() << ")";
;
		return std::make_pair(QByteArray(), WebClient::Status::NoData);
	}

	std::pair<QByteArray, WebClient::Status> checkReplyError(QNetworkReply* reply, const WebClient* webAccess)
	{
		const auto networkError = reply->error();

		spLog(Log::Warning, webAccess) << QString("Cannot open %1: %2 (%3)")
		                                  .arg(reply->request().url().toString())
		                                  .arg(reply->errorString())
		                                  .arg(networkError);

		if(!reply->isOpen() || !reply->isReadable())
		{
			spLog(Log::Warning, webAccess) << "Cannot read data: Open, readable " << reply->isOpen() << ", " << reply->
				isReadable();
			return {{}, WebClient::Status::Error};
		}

		const auto status = reply->rawHeaderList().isEmpty()
		                    ? WebClient::Status::NoHttp
		                    : convertNetworkError(networkError);

		return std::make_pair(reply->readAll(), status);
	}

	QString getUserAgent(const WebClient::Mode mode)
	{
		switch(mode)
		{
			case WebClient::Mode::AsSayonara:
				return QString("Sayonara/%1").arg(SAYONARA_VERSION);
			case WebClient::Mode::AsBrowser:
				return QStringLiteral("Mozilla/5.0 (Linux; rv:35.0) Gecko/20100101 Firefox/35.0");
			case WebClient::Mode::Random:
			{
				constexpr const auto MinimumStringLength = 8;
				constexpr const auto MaximumStringLength = 16;
				return Util::randomString(Util::randomNumber(MinimumStringLength, MaximumStringLength));
			}

			case WebClient::Mode::None:
			default:
				return {};
		}
	}

	void abortIfActive(const QPointer<QNetworkReply>& reply)
	{
		if(reply)
		{
			reply->abort();
		}
	}
} //namespace

struct WebClientImpl::Private
{
	AbstractWebClientStopper* stopper;
	QNetworkAccessManager networkAccessManager;
	QString url;
	QByteArray data;
	QByteArray errorData;
	QMap<QByteArray, QByteArray> header;
	Mode mode{Mode::AsBrowser};
	Status status{Status::NoError};

	explicit Private(WebClientImpl* parent) :
		stopper{new AbstractWebClientStopper(parent)},
		networkAccessManager{QNetworkAccessManager(parent)} {}
};

WebClientImpl::WebClientImpl(QObject* parent) :
	WebClient(parent),
	m{Pimpl::make<Private>(this)}
{
	connect(parent, &QObject::destroyed, this, &WebClientImpl::stop);
}

WebClientImpl::~WebClientImpl() = default;

void WebClientImpl::run(const QString& url, const int timeout)
{
	reset();
	m->url = checkGetUrl(url);

	auto request = QNetworkRequest(m->url);
	request.setMaximumRedirectsAllowed(2);
	setRequestHeader(request, m->header);
	if(!m->header.contains(UserAgentHeader))
	{
		request.setHeader(QNetworkRequest::UserAgentHeader, getUserAgent(m->mode));
	}

	request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
	                     QNetworkRequest::NoLessSafeRedirectPolicy);

	spLog(Log::Debug, this) << "Call " << request.url().toString();

	auto* reply = m->networkAccessManager.get(request);
	connect(reply, &QNetworkReply::readyRead, this, &WebClientImpl::dataAvailable);
	connect(reply, &QNetworkReply::finished, this, &WebClientImpl::finished);

	connect(m->stopper, &AbstractWebClientStopper::sigStopped, this, [reply = QPointer<QNetworkReply>(reply)] {
		abortIfActive(reply);
	});

	connect(m->stopper, &AbstractWebClientStopper::sigTimeout, this, [this, reply = QPointer<QNetworkReply>(reply)] {
		this->timeout();
		abortIfActive(reply);
	});

	m->stopper->startTimer(timeout);
}

void WebClientImpl::runPost(const QString& url, const QByteArray& postData, const int timeout)
{
	reset();
	m->url = url;

	auto request = QNetworkRequest(m->url);
	setRequestHeader(request, m->header);

	auto* reply = m->networkAccessManager.post(request, postData);
	connect(reply, &QNetworkReply::finished, this, &WebClientImpl::finished);

	connect(m->stopper, &AbstractWebClientStopper::sigStopped, this, [this, reply = QPointer<QNetworkReply>(reply)] {
		m->status = Status::Aborted;
		if(reply && reply->isOpen())
		{
			reply->abort();
		}
	});

	connect(m->stopper, &AbstractWebClientStopper::sigTimeout, reply, &QNetworkReply::abort);
	connect(m->stopper, &AbstractWebClientStopper::sigTimeout, this, &WebClientImpl::timeout);

	m->stopper->startTimer(timeout);
}

void WebClientImpl::setMode(const Mode mode) { m->mode = mode; }

void WebClientImpl::setRawHeader(const QMap<QByteArray, QByteArray>& header) { m->header = header; }

bool WebClientImpl::hasData() const { return (m->status == Status::GotData); }

QByteArray WebClientImpl::data() const { return m->data; }

QByteArray WebClientImpl::errorData() const { return m->errorData; }

QString WebClientImpl::url() const { return m->url; }

WebClientImpl::Status WebClientImpl::status() const { return m->status; }

bool WebClientImpl::hasError() const
{
	return (m->status == Status::Error) ||
	       (m->status == Status::Timeout) ||
	       (m->status == Status::NotFound);
}

void WebClientImpl::stop()
{
	m->stopper->stop();
}

void WebClientImpl::setUserAgent(const QString& userAgent)
{
	m->header[UserAgentHeader] = userAgent.toLocal8Bit();
}

void WebClientImpl::dataAvailable()
{
	const auto* reply = dynamic_cast<QNetworkReply*>(sender());
	if(isStream(reply) && !isPlaylistFile(QUrl(m->url)))
	{
		m->status = Status::AudioStream;
		m->data.clear();
		m->stopper->stop();
	}
}

void WebClientImpl::finished()
{
	m->stopper->stopTimer();
	[[maybe_unused]] const auto result = m->stopper->disconnect(this);

	auto* reply = dynamic_cast<QNetworkReply*>(sender());

	if(reply->error() == QNetworkReply::NoError)
	{
		std::tie(m->data, m->status) = checkReplyData(reply, this);
	}

	else if(m->status == Status::NoError) // regular error (request was not cancelled)
	{
		std::tie(m->errorData, m->status) = checkReplyError(reply, this);
	}

	emit sigFinished(); // NOLINT(readability-misleading-indentation)
	reply->deleteLater();
}

void WebClientImpl::timeout()
{
	spLog(Log::Debug, this) << "Timeout reached " << m->url;

	m->status = Status::Timeout;
	m->data.clear();
	m->errorData.clear();
}

void WebClientImpl::reset()
{
	m->status = Status::NoError;
	m->data.clear();
	m->errorData.clear();
	m->networkAccessManager.clearAccessCache();
}

struct AbstractWebClientStopper::Private
{
	QTimer* timer;

	explicit Private(QObject* parent) :
		timer{new QTimer(parent)} {}
};

AbstractWebClientStopper::AbstractWebClientStopper(QObject* parent) :
	QObject{parent},
	m{Pimpl::make<Private>(this)}
{
	m->timer->setSingleShot(true);
	connect(m->timer, &QTimer::timeout, this, &AbstractWebClientStopper::timeout);
}

AbstractWebClientStopper::~AbstractWebClientStopper() noexcept = default;

void AbstractWebClientStopper::startTimer(const int timeout)
{
	if(timeout > 0)
	{
		m->timer->start(timeout);
	}
}

void AbstractWebClientStopper::stopTimer()
{
	m->timer->stop();
}

void AbstractWebClientStopper::stop()
{
	stopTimer();
	emit sigStopped();
}

void AbstractWebClientStopper::timeout()
{
	stopTimer();
	emit sigTimeout();
}
