/***************************************************************************
                          qgsnetworkaccessmanager.h  -  description
                             -------------------
    begin                : 2010-05-08
    copyright            : (C) 2010 by Juergen E. Fischer
    email                : jef at norbit dot de
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#ifndef QGSNETWORKACCESSMANAGER_H
#define QGSNETWORKACCESSMANAGER_H

#include <QList>
#include "qgsnetworkreply.h"
#include "qgis_sip.h"
#include <QStringList>
#include <QNetworkAccessManager>
#include <QNetworkProxy>
#include <QNetworkRequest>
#include <QMutex>
#include <QWaitCondition>
#include <memory>

#include "qgis_core.h"
#include "qgis_sip.h"

class QgsFeedback;

#ifndef SIP_RUN
#include "qgsconfig.h"
constexpr int sFilePrefixLength = CMAKE_SOURCE_DIR[sizeof( CMAKE_SOURCE_DIR ) - 1] == '/' ? sizeof( CMAKE_SOURCE_DIR ) + 1 : sizeof( CMAKE_SOURCE_DIR );

#define QgsSetRequestInitiatorClass(request, _class) request.setAttribute( static_cast< QNetworkRequest::Attribute >( QgsNetworkRequestParameters::AttributeInitiatorClass ), _class ); request.setAttribute( static_cast< QNetworkRequest::Attribute >( QgsNetworkRequestParameters::AttributeInitiatorRequestId ), QString( __FILE__ ).mid( sFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")" );
#define QgsSetRequestInitiatorId(request, str) request.setAttribute( static_cast< QNetworkRequest::Attribute >( QgsNetworkRequestParameters::AttributeInitiatorRequestId ), QString( __FILE__ ).mid( sFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + "): " + str );
#endif

/**
 * \class QgsNetworkRequestParameters
 * \ingroup core
 * Encapsulates parameters and properties of a network request.
 * \since QGIS 3.6
 */
class CORE_EXPORT QgsNetworkRequestParameters
{
  public:

    //! Custom request attributes
    enum RequestAttributes
    {
      AttributeInitiatorClass = QNetworkRequest::User + 3000, //!< Class name of original object which created the request
      AttributeInitiatorRequestId, //!< Internal ID used by originator object to identify requests
    };

    /**
     * Default constructor.
     */
    QgsNetworkRequestParameters() = default;

    /**
     * Constructor for QgsNetworkRequestParameters, with the specified network
     * \a operation and original \a request.
     */
    QgsNetworkRequestParameters( QNetworkAccessManager::Operation operation,
                                 const QNetworkRequest &request,
                                 int requestId,
                                 const QByteArray &content = QByteArray() );

    /**
     * Returns the request operation, e.g. GET or POST.
     */
    QNetworkAccessManager::Operation operation() const { return mOperation; }

    /**
     * Returns the network request.
     *
     * This is the original network request sent to QgsNetworkAccessManager, but with QGIS specific
     * configuration options such as proxy handling and SSL exceptions applied.
     */
    QNetworkRequest request() const { return mRequest; }

    /**
     * Returns a string identifying the thread which the request originated from.
     */
    QString originatingThreadId() const { return mOriginatingThreadId; }

    /**
     * Returns a unique ID identifying the request.
     */
    int requestId() const { return mRequestId; }

    /**
     * Returns the request's content. This is only used for POST or PUT operation
     * requests.
     */
    QByteArray content() const { return mContent; }

    /**
     * Returns the class name of the object which initiated this request.
     *
     * This is only available for QNetworkRequests which have had the
     * QgsNetworkRequestParameters::AttributeInitiatorClass attribute set.
     *
     * \see initiatorRequestId()
     */
    QString initiatorClassName() const { return mInitiatorClass; }

    /**
     * Returns the internal ID used by the object which initiated this request to identify
     * individual requests.
     *
     * This is only available for QNetworkRequests which have had the
     * QgsNetworkRequestParameters::AttributeInitiatorRequestId attribute set.
     *
     * \see initiatorClassName()
     */
    QVariant initiatorRequestId() const { return mInitiatorRequestId; }

  private:

    QNetworkAccessManager::Operation mOperation;
    QNetworkRequest mRequest;
    QString mOriginatingThreadId;
    int mRequestId = 0;
    QByteArray mContent;
    QString mInitiatorClass;
    QVariant mInitiatorRequestId;
};

class QgsNetworkAccessManager;

#ifndef SIP_RUN

/**
 * \class QgsSslErrorHandler
 * \brief SSL error handler, used for responding to SSL errors encountered during network requests.
 * \ingroup core
 *
 * QgsSslErrorHandler responds to SSL errors encountered during network requests. The
 * base QgsSslErrorHandler class responds to SSL errors only by logging the errors,
 * and uses the default Qt response, which is to abort the request.
 *
 * Subclasses can override this behavior by implementing their own handleSslErrors()
 * method. QgsSslErrorHandlers are ONLY ever called from the main thread, so it
 * is safe to utilize gui widgets and dialogs during handleSslErrors (e.g. to
 * present prompts to users notifying them of the errors and asking them
 * to choose the appropriate response.).
 *
 * If a reply is coming from background thread, that thread is blocked while handleSslErrors()
 * is running.
 *
 * If the errors should be ignored and the request allowed to proceed, the subclasses'
 * handleSslErrors() method MUST call QNetworkReply::ignoreSslErrors() on the specified
 * QNetworkReply object.
 *
 * An application instance can only have a single SSL error handler. The current
 * SSL error handler is set by calling QgsNetworkAccessManager::setSslErrorHandler().
 * By default an instance of the logging-only QgsSslErrorHandler base class is used.
 *
 * \since QGIS 3.6
 */
class CORE_EXPORT QgsSslErrorHandler
{

  public:

    virtual ~QgsSslErrorHandler() = default;

    /**
     * Called whenever SSL \a errors are encountered during a network \a reply.
     *
     * Subclasses should reimplement this method to implement their own logic
     * regarding whether or not these SSL errors should be ignored, and how
     * to present them to users.
     *
     * The base class method just logs errors and leaves the default Qt response
     * to SSL errors, which is to abort the network request on any errors.
     */
    virtual void handleSslErrors( QNetworkReply *reply, const QList<QSslError> &errors );

};

/**
 * \class QgsNetworkAuthenticationHandler
 * \brief Network authentication handler, used for responding to network authentication requests during network requests.
 * \ingroup core
 *
 * QgsNetworkAuthenticationHandler responds to authentication requests encountered during network requests. The
 * base QgsNetworkAuthenticationHandler class responds to requests only by logging the request,
 * but does not provide any username or password to allow the request to proceed.
 *
 * Subclasses can override this behavior by implementing their own handleAuthRequest()
 * method. QgsNetworkAuthenticationHandler are ONLY ever called from the main thread, so it
 * is safe to utilize gui widgets and dialogs during handleAuthRequest (e.g. to
 * present prompts to users requesting the username and password).
 *
 * If a reply is coming from background thread, that thread is blocked while handleAuthRequest()
 * is running.
 *
 * An application instance can only have a single network authentication handler. The current
 * authentication handler is set by calling QgsNetworkAccessManager::setAuthHandler().
 * By default an instance of the logging-only QgsNetworkAuthenticationHandler base class is used.
 *
 * \since QGIS 3.6
 */
class CORE_EXPORT QgsNetworkAuthenticationHandler
{

  public:

    virtual ~QgsNetworkAuthenticationHandler() = default;

    /**
     * Called whenever network authentication requests are encountered during a network \a reply.
     *
     * Subclasses should reimplement this method to implement their own logic
     * regarding how to handle the requests and whether they should be presented to users.
     *
     * The base class method just logs the request but does not provide any username/password resolution.
     */
    virtual void handleAuthRequest( QNetworkReply *reply, QAuthenticator *auth );

};
#endif


/**
 * \class QgsNetworkAccessManager
 * \brief network access manager for QGIS
 * \ingroup core
 *
 * This class implements the QGIS network access manager.  It's a singleton
 * that can be used across QGIS.
 *
 * Plugins can insert proxy factories and thereby redirect requests to
 * individual proxies.
 *
 * If no proxy factories are there or none returns a proxy for an URL a
 * fallback proxy can be set.  There's also a exclude list that defines URLs
 * that the fallback proxy should not be used for, then no proxy will be used.
 *
 * \since 1.5
 */
class CORE_EXPORT QgsNetworkAccessManager : public QNetworkAccessManager
{
    Q_OBJECT

  public:

    /**
     * Returns a pointer to the active QgsNetworkAccessManager
     * for the current thread.
     *
     * With the \a connectionType parameter it is possible to setup the default connection
     * type that is used to handle signals that might require user interaction and therefore
     * need to be handled on the main thread. See in-depth discussion below.
     *
     * \param connectionType In most cases the default of using a ``Qt::BlockingQueuedConnection``
     * is ok, to make a background thread wait for the main thread to answer such a request is
     * fine and anything else is dangerous.
     * However, in case the request was started on the main thread, one should execute a
     * local event loop in a helper thread and freeze the main thread for the duration of the
     * download. In this case, if an authentication request is sent from the background thread
     * network access manager, the background thread should be blocked, the main thread be woken
     * up, processEvents() executed once, the main thread frozen again and the background thread
     * continued.
     */
    static QgsNetworkAccessManager *instance( Qt::ConnectionType connectionType = Qt::BlockingQueuedConnection );

    QgsNetworkAccessManager( QObject *parent = nullptr );

#ifndef SIP_RUN

    /**
     * Sets the application SSL error \a handler, which is used to respond to SSL errors encountered
     * during network requests.
     *
     * Ownership of \a handler is transferred to the main thread QgsNetworkAccessManager instance.
     *
     * This method must ONLY be called on the main thread QgsNetworkAccessManager. It is not
     * necessary to set handlers for background threads -- the main thread QgsSslErrorHandler will
     * automatically be used in a thread-safe manner for any SSL errors encountered on background threads.
     *
     * The default QgsSslErrorHandler responds to SSL errors only by logging the errors,
     * and uses the default Qt response, which is to abort the request.
     *
     * \note Not available in Python bindings.
     * \since QGIS 3.6
     */
    void setSslErrorHandler( std::unique_ptr< QgsSslErrorHandler > handler );

    /**
     * Sets the application network authentication \a handler, which is used to respond to network
     * authentication prompts during network requests.
     *
     * Ownership of \a handler is transferred to the main thread QgsNetworkAccessManager instance.
     *
     * This method must ONLY be called on the main thread QgsNetworkAccessManager. It is not
     * necessary to set handlers for background threads -- the main thread QgsNetworkAuthenticationHandler will
     * automatically be used in a thread-safe manner for any authentication requests encountered on background threads.
     *
     * The default QgsNetworkAuthenticationHandler responds to request only by logging the request,
     * but does not provide any username or password resolution.
     *
     * \note Not available in Python bindings.
     * \since QGIS 3.6
     */
    void setAuthHandler( std::unique_ptr< QgsNetworkAuthenticationHandler > handler );
#endif

    /**
     * Inserts a \a factory into the proxy factories list.
     *
     * Ownership of \a factory is transferred to the manager.
     *
     * \see removeProxyFactory()
     * \see proxyFactories()
     */
    void insertProxyFactory( QNetworkProxyFactory *factory SIP_TRANSFER );

    /**
     * Removes a \a factory from the proxy factories list.
     *
     * \see insertProxyFactory()
     * \see proxyFactories()
     */
    void removeProxyFactory( QNetworkProxyFactory *factory SIP_TRANSFERBACK );

    /**
     * Returns a list of proxy factories used by the manager.
     *
     * \see insertProxyFactory()
     * \see proxyFactories()
     */
    const QList<QNetworkProxyFactory *> proxyFactories() const;

    /**
     * Returns the fallback proxy used by the manager.
     *
     * The fallback proxy is used for URLs which no other proxy factory returned proxies for.
     *
     * \see proxyFactories()
     * \see setFallbackProxyAndExcludes()
     */
    const QNetworkProxy &fallbackProxy() const;

    /**
     * Returns the proxy exclude list.
     *
     * This list consists of the beginning of URL strings which will not use the fallback proxy.
     *
     * \see noProxyList()
     * \see fallbackProxy()
     * \see setFallbackProxyAndExcludes()
     */
    QStringList excludeList() const;

    /**
     * Returns the no proxy list.
     *
     * This list consists of the beginning of URL strings which will not use any proxy at all
     *
     * \see excludeList()
     * \see fallbackProxy()
     * \see setFallbackProxyAndExcludes()
     */
    QStringList noProxyList() const;

    /**
     * Sets the fallback \a proxy and URLs which shouldn't use it.
     *
     * The fallback proxy is used for URLs which no other proxy factory returned proxies for.
     * The \a excludes list specifies the beginning of URL strings which will not use this fallback proxy.
     * The \a noProxyURLs list specifies the beginning of URL strings which will not use any proxy at all
     *
     * \see fallbackProxy()
     * \see excludeList()
     * \see noProxyList()
     */
    void setFallbackProxyAndExcludes( const QNetworkProxy &proxy, const QStringList &excludes, const QStringList &noProxyURLs );

    /**
     * Returns the name for QNetworkRequest::CacheLoadControl.
     *
     * \see cacheLoadControlFromName()
     */
    static QString cacheLoadControlName( QNetworkRequest::CacheLoadControl control );

    /**
     * Returns QNetworkRequest::CacheLoadControl from a \a name.
     *
     * \see cacheLoadControlName()
     */
    static QNetworkRequest::CacheLoadControl cacheLoadControlFromName( const QString &name );

    /**
     * Setup the QgsNetworkAccessManager (NAM) according to the user's settings.
     * The \a connectionType sets up the default connection type that is used to
     * handle signals that might require user interaction and therefore
     * need to be handled on the main thread. See in-depth discussion in the documentation
     * for the constructor of this class.
     */
    void setupDefaultProxyAndCache( Qt::ConnectionType connectionType = Qt::BlockingQueuedConnection );

    /**
     * Returns whether the system proxy should be used.
     */
    bool useSystemProxy() const { return mUseSystemProxy; }

    /**
     * Returns the network timeout length, in milliseconds.
     *
     * \see setTimeout()
     * \since QGIS 3.6
     */
    static int timeout();

    /**
     * Sets the maximum timeout \a time for network requests, in milliseconds.
     *
     * \see timeout()
     * \since QGIS 3.6
     */
    static void setTimeout( int time );

    /**
     * Posts a GET request to obtain the contents of the target request and returns a new QgsNetworkReplyContent object for reading.
     * The current thread will be blocked until the request is returned.
     *
     * This method is safe to call in either the main thread or a worker thread.
     *
     * If \a forceRefresh is FALSE then previously cached replies may be used for the request. If
     * it is set to TRUE then a new query is always performed.
     *
     * If an \a authCfg has been specified, then that authentication configuration required will automatically be applied to
     * \a request. There is no need to manually apply the authentication to the request prior to calling
     * this method.
     *
     * The optional \a feedback argument can be used to abort ongoing requests.
     *
     * The contents of the reply will be returned after the request is completed or an error occurs.
     *
     * \see blockingPost()
     * \since QGIS 3.6
     */
    static QgsNetworkReplyContent blockingGet( QNetworkRequest &request, const QString &authCfg = QString(), bool forceRefresh = false, QgsFeedback *feedback = nullptr );

    /**
     * Posts a POST request to obtain the contents of the target \a request, using the given \a data, and returns a new
     * QgsNetworkReplyContent object for reading. The current thread will be blocked until the request is returned.
     *
     * This method is safe to call in either the main thread or a worker thread.
     *
     * If \a forceRefresh is FALSE then previously cached replies may be used for the request. If
     * it is set to TRUE then a new query is always performed.
     *
     * If an \a authCfg has been specified, then that authentication configuration required will automatically be applied to
     * \a request. There is no need to manually apply the authentication to the request prior to calling
     * this method.
     *
     * The optional \a feedback argument can be used to abort ongoing requests.
     *
     * The contents of the reply will be returned after the request is completed or an error occurs.
     *
     * \see blockingGet()
     * \since QGIS 3.6
     */
    static QgsNetworkReplyContent blockingPost( QNetworkRequest &request, const QByteArray &data, const QString &authCfg = QString(), bool forceRefresh = false, QgsFeedback *feedback = nullptr );

  signals:

    /**
     * \deprecated Use the thread-safe requestAboutToBeCreated( QgsNetworkRequestParameters ) signal instead.
     */
    Q_DECL_DEPRECATED void requestAboutToBeCreated( QNetworkAccessManager::Operation, const QNetworkRequest &, QIODevice * ) SIP_DEPRECATED;

    /**
     * Emitted when a network request is about to be created.
     *
     * This signal is propagated to the main thread QgsNetworkAccessManager instance, so it is necessary
     * only to connect to the main thread's signal in order to receive notifications about requests
     * created in any thread.
     *
     * \see finished( QgsNetworkReplyContent )
     * \see requestTimedOut( QgsNetworkRequestParameters )
     * \since QGIS 3.6
     */
    void requestAboutToBeCreated( QgsNetworkRequestParameters request );

    /**
     * Emitted whenever a pending network reply is finished.
     *
     * The \a reply parameter will contain a QgsNetworkReplyContent object, containing all the useful
     * information relating to the reply, including headers and reply content.
     *
     * This signal is propagated to the main thread QgsNetworkAccessManager instance, so it is necessary
     * only to connect to the main thread's signal in order to receive notifications about requests
     * created in any thread.
     *
     * \see requestAboutToBeCreated( QgsNetworkRequestParameters )
     * \see requestTimedOut( QgsNetworkRequestParameters )
     * \since QGIS 3.6
     */
    void finished( QgsNetworkReplyContent reply );

    /**
     * Emitted when a network request has timed out.
     *
     * This signal is propagated to the main thread QgsNetworkAccessManager instance, so it is necessary
     * only to connect to the main thread's signal in order to receive notifications about requests
     * created in any thread.
     *
     * \see requestAboutToBeCreated( QgsNetworkRequestParameters )
     * \see finished( QgsNetworkReplyContent )
     * \since QGIS 3.6
     */
    void requestTimedOut( QgsNetworkRequestParameters request );

    /**
     * Emitted when a network reply receives a progress report.
     *
     * The \a requestId argument reflects the unique ID identifying the original request which the progress report relates to.
     *
     * The \a bytesReceived parameter indicates the number of bytes received, while \a bytesTotal indicates the total number
     * of bytes expected to be downloaded. If the number of bytes to be downloaded is not known, \a bytesTotal will be -1.
     *
     * This signal is propagated to the main thread QgsNetworkAccessManager instance, so it is necessary
     * only to connect to the main thread's signal in order to receive notifications about requests
     * created in any thread.
     *
     * \since QGIS 3.6
     */
    void downloadProgress( int requestId, qint64 bytesReceived, qint64 bytesTotal );

    /**
     * Emitted when a network request prompts an authentication request.
     *
     * The \a requestId argument reflects the unique ID identifying the original request which the authentication relates to.
     *
     * This signal is propagated to the main thread QgsNetworkAccessManager instance, so it is necessary
     * only to connect to the main thread's signal in order to receive notifications about authentication requests
     * from any thread.
     *
     * This signal is for debugging and logging purposes only, and cannot be used to respond to the
     * requests. See QgsNetworkAuthenticationHandler for details on how to handle authentication requests.
     *
     * \see requestAuthDetailsAdded()
     * \since QGIS 3.6
     */
    void requestRequiresAuth( int requestId, const QString &realm );

    /**
     * Emitted when network authentication details have been added to a request.
     *
     * The \a requestId argument reflects the unique ID identifying the original request which the authentication relates to.
     *
     * This signal is always sent from the main thread QgsNetworkAccessManager instance, so it is necessary
     * only to connect to the main thread's signal in order to receive notifications about authentication requests
     * from any thread.
     *
     * This signal is for debugging and logging purposes only, and should not be used to respond to the
     * requests. See QgsNetworkAuthenticationHandler for details on how to handle authentication requests.
     *
     * \see requestRequiresAuth()
     * \since QGIS 3.6
     */
    void requestAuthDetailsAdded( int requestId, const QString &realm, const QString &user, const QString &password );

#ifndef QT_NO_SSL

    /**
     * Emitted when a network request encounters SSL \a errors.
     *
     * The \a requestId argument reflects the unique ID identifying the original request which the SSL error relates to.
     *
     * This signal is propagated to the main thread QgsNetworkAccessManager instance, so it is necessary
     * only to connect to the main thread's signal in order to receive notifications about SSL errors
     * from any thread.
     *
     * This signal is for debugging and logging purposes only, and cannot be used to respond to the errors.
     * See QgsSslErrorHandler for details on how to handle SSL errors and potentially ignore them.
     *
     * \since QGIS 3.6
     */
    void requestEncounteredSslErrors( int requestId, const QList<QSslError> &errors );

#ifndef SIP_RUN
///@cond PRIVATE
    // these signals are for internal use only - it's not safe to connect by external code
    void sslErrorsOccurred( QNetworkReply *, const QList<QSslError> &errors );
    void sslErrorsHandled( QNetworkReply *reply );
///@endcond
#endif

#endif

    /**
     * \deprecated Use the thread-safe requestAboutToBeCreated( QgsNetworkRequestParameters ) signal instead.
     */
    Q_DECL_DEPRECATED void requestCreated( QNetworkReply * ) SIP_DEPRECATED;

    void requestTimedOut( QNetworkReply * );

#ifndef SIP_RUN
///@cond PRIVATE
    // these signals are for internal use only - it's not safe to connect by external code
    void authRequestOccurred( QNetworkReply *, QAuthenticator *auth );
    void authRequestHandled( QNetworkReply *reply );
///@endcond
#endif


  private slots:
    void abortRequest();

    void onReplyFinished( QNetworkReply *reply );

    void onReplyDownloadProgress( qint64 bytesReceived, qint64 bytesTotal );
#ifndef QT_NO_SSL
    void onReplySslErrors( const QList<QSslError> &errors );

    void handleSslErrors( QNetworkReply *reply, const QList<QSslError> &errors );
#endif

    void onAuthRequired( QNetworkReply *reply, QAuthenticator *auth );
    void handleAuthRequest( QNetworkReply *reply, QAuthenticator *auth );

  protected:
    QNetworkReply *createRequest( QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *outgoingData = nullptr ) override;

  private:
#ifndef QT_NO_SSL
    void unlockAfterSslErrorHandled();
    void afterSslErrorHandled( QNetworkReply *reply );
#endif

    void unlockAfterAuthRequestHandled();
    void afterAuthRequestHandled( QNetworkReply *reply );

    void pauseTimeout( QNetworkReply *reply );
    void restartTimeout( QNetworkReply *reply );
    static int getRequestId( QNetworkReply *reply );

    QList<QNetworkProxyFactory *> mProxyFactories;
    QNetworkProxy mFallbackProxy;
    QStringList mExcludedURLs;
    QStringList mNoProxyURLs;
    bool mUseSystemProxy = false;
    bool mInitialized = false;
    static QgsNetworkAccessManager *sMainNAM;
    // ssl error handler, will be set for main thread ONLY
    std::unique_ptr< QgsSslErrorHandler > mSslErrorHandler;
    // only in use by worker threads, unused in main thread
    QMutex mSslErrorHandlerMutex;
    // only in use by worker threads, unused in main thread
    QWaitCondition mSslErrorWaitCondition;

    // auth request handler, will be set for main thread ONLY
    std::unique_ptr< QgsNetworkAuthenticationHandler > mAuthHandler;
    // only in use by worker threads, unused in main thread
    QMutex mAuthRequestHandlerMutex;
    // only in use by worker threads, unused in main thread
    QWaitCondition mAuthRequestWaitCondition;

};

#endif // QGSNETWORKACCESSMANAGER_H
