#include "restconnection_pimpl.h"

#include <QRegularExpression>
#include <QEventLoop>
#include <QTimer>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkProxy>

namespace Nxt {
namespace Internal {

RestConnection::pimpl::pimpl(RestConnection *parent) : m_parent(parent)
  , m_socket(new QTcpSocket(this))
  , m_state(SocketUnconnected)
  , m_error(RestNoError)
{
    m_socket->setProxy(QNetworkProxy::DefaultProxy);
    m_timeoutTimer.setSingleShot(true);
    m_timeoutTimer.setInterval(m_parent->connectionParameters().timeout() * 1000);
}

RestConnection::pimpl::~pimpl()
{
    disconnect();
}

void RestConnection::pimpl::authenticate()
{
    QStringList lines;
    lines.append(headerGet("/users"));
    lines.append(headerHost());
    lines.append(headerAuth());

    sendData(toRequest(lines));
}

void RestConnection::pimpl::connectToHost()
{
    m_incomingData.clear();
    m_errorString.clear();
    m_error = RestNoError;

    connect(m_socket, &QTcpSocket::stateChanged, this, &RestConnection::pimpl::handleStateChanged);
    connect(m_socket, &QAbstractSocket::connected, this, &RestConnection::pimpl::handleSocketConnected);
    connect(m_socket, &QIODevice::readyRead, this, &RestConnection::pimpl::handleIncomingData);
    connect(m_socket, static_cast<void (QAbstractSocket::*)(QAbstractSocket::SocketError)>(&QAbstractSocket::error), this, &RestConnection::pimpl::handleSocketError);
    connect(m_socket, &QAbstractSocket::disconnected, this, &RestConnection::pimpl::handleSocketDisconnected);

    connect(&m_timeoutTimer, &QTimer::timeout, this, &RestConnection::pimpl::handleTimeout);

    m_state = SocketConnecting;
    m_timeoutTimer.start();

    m_socket->connectToHost(m_parent->connectionParameters().host(), static_cast<quint16>(m_parent->connectionParameters().port()));
}

void RestConnection::pimpl::closeConnection(RestError restError, RestError userError, const QByteArray &serverErrorString, const QString &userErrorString)
{
    Q_UNUSED(restError);
    Q_UNUSED(serverErrorString);

    if (m_state == SocketUnconnected || m_error != RestNoError)
        return;

    m_error = userError;
    m_errorString = userErrorString;
    m_timeoutTimer.stop();

    disconnect(m_socket, nullptr, this, nullptr);
    disconnect(&m_timeoutTimer, nullptr, this, nullptr);

    if (m_error != RestNoError)
        emit error(userError);
    if (m_state == ConnectionEstablished)
        emit disconnected();
    if (canUseSocket())
        m_socket->disconnectFromHost();

    m_state = SocketUnconnected;
}

void RestConnection::pimpl::handleSocketConnected()
{
    m_timeoutTimer.stop();
    m_state = SocketConnected;
    emit connected();
}

void RestConnection::pimpl::handleSocketError()
{
    if (m_error == RestNoError) {
        closeConnection(RestError::RestSocketError, RestError::RestSocketError, "Network error", m_socket->errorString());
    }
}

void RestConnection::pimpl::handleSocketDisconnected()
{
    closeConnection(RestError::RestSocketError, RestError::RestSocketError, "Connection closed unexpectedly.", tr("Connection closed unexpectedly."));
}

void RestConnection::pimpl::handleStateChanged()
{
#if IDS_NXT_DEBUG
    qDebug() << m_socket->state();
#endif /* IDS_NXT_DEBUG */
}

void RestConnection::pimpl::handleIncomingData()
{
    if (m_state == SocketUnconnected)
        return;

    if (!canUseSocket())
        return;

    m_incomingData.clear();
    m_incomingData += m_socket->readAll();

    const QString response = QString::fromLocal8Bit(m_incomingData);
    if (validHttpVersion(response))
    {
        int httpStatusCode = getHttpStatusCode(response);
        if (httpStatusCode >= 200 && httpStatusCode < 400) {
            emit authenticated(httpStatusCode);
        }
    }

    handleStateChanged();
}

void RestConnection::pimpl::handleTimeout()
{
    closeConnection(RestError::RestTimeoutError, RestError::RestTimeoutError, "", tr("Timeout"));
}

void RestConnection::pimpl::sendData(const QByteArray &data)
{
    if (canUseSocket())
        m_socket->write(data);
}

bool RestConnection::pimpl::canUseSocket() const
{
    return m_socket->isValid() && m_socket->state() == QAbstractSocket::ConnectedState;
}

bool RestConnection::pimpl::validHttpVersion(const QString &response)
{
    return response.startsWith(http_version);
}

int RestConnection::pimpl::getHttpStatusCode(const QString &response)
{
    QRegularExpressionMatch match = QRegularExpression("^(HTTP/1.1) ([0-9]+).*").match(response);
    if (match.hasMatch() && match.capturedTexts().size() > 2) {
        return match.captured(2).toInt();
    }
    return 500;
}

const QString RestConnection::pimpl::token() const
{
    return QString("%1:%2").arg(m_parent->connectionParameters().userName(), m_parent->connectionParameters().password());
}

const QString RestConnection::pimpl::tokenBase64() const
{
    return QString::fromStdString(token().toLocal8Bit().toBase64().toStdString());
}

const QString RestConnection::pimpl::headerGet(const QString &path) const
{
    return QString("GET %1 %2").arg(path, http_version);
}

const QString RestConnection::pimpl::headerHost() const
{
    return QString("Host: %1").arg(m_parent->connectionParameters().host());
}

const QString RestConnection::pimpl::headerAuth() const
{
    return QString("Authorization: Basic %1").arg(tokenBase64());
}

const QByteArray RestConnection::pimpl::toRequest(const QStringList &lines)
{
    if (lines.size() > 0)
        return lines.join("\r\n").append("\r\n\r\n").toLocal8Bit().data();
    return QByteArray();
}

} // namespace Internal
} // namespace Nxt
