/*
 * Squawk messenger. 
 * Copyright (C) 2019 Yury Gubich <blue@macaw.me>
 * 
 * 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 <QtWidgets/QApplication>
#include <QtCore/QDir>

#include "networkaccess.h"

Core::NetworkAccess::NetworkAccess(QObject* parent):
    QObject(parent),
    running(false),
    manager(0),
    storage("fileURLStorage"),
    downloads(),
    uploads(),
    currentPath()
{
    QSettings settings;
    currentPath = settings.value("downloadsPath").toString();
}

Core::NetworkAccess::~NetworkAccess() {
    stop();
}

void Core::NetworkAccess::downladFile(const QString& url) {
    std::map<QString, Transfer*>::iterator itr = downloads.find(url);
    if (itr != downloads.end()) {
        qDebug() << "NetworkAccess received a request to download a file" << url << ", but the file is currently downloading, skipping";
    } else {
        try {
            std::pair<QString, std::list<Shared::MessageInfo>> p = storage.getPath(url);
            if (p.first.size() > 0) {
                QFileInfo info(p.first);
                if (info.exists() && info.isFile())
                    emit downloadFileComplete(p.second, p.first);
                else
                    startDownload(p.second, url);
            } else {
                startDownload(p.second, url);
            }
        } catch (const LMDBAL::NotFound& e) {
            qDebug() << "NetworkAccess received a request to download a file" << url << ", but there is now record of which message uses that file, downloading anyway";
            storage.addFile(url);
            startDownload(std::list<Shared::MessageInfo>(), url);
        } catch (const LMDBAL::Unknown& e) {
            qDebug() << "Error requesting file path:" << e.what();
            emit loadFileError(std::list<Shared::MessageInfo>(), QString("Database error: ") + e.what(), false);
        }
    }
}

void Core::NetworkAccess::start() {
    if (!running) {
        manager = new QNetworkAccessManager();
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
        manager->setTransferTimeout();
#endif
        storage.open();
        running = true;
    }
}

void Core::NetworkAccess::stop() {
    if (running) {
        storage.close();
        manager->deleteLater();
        manager = 0;
        running = false;
        
        for (std::map<QString, Transfer*>::const_iterator itr = downloads.begin(), end = downloads.end(); itr != end; ++itr) {
            itr->second->success = false;
            itr->second->reply->abort();        //assuming it's gonna call onRequestFinished slot
        }
    }
}

void Core::NetworkAccess::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
    QNetworkReply* rpl = static_cast<QNetworkReply*>(sender());
    QString url = rpl->url().toString();
    std::map<QString, Transfer*>::const_iterator itr = downloads.find(url);
    if (itr == downloads.end()) {
        qDebug() << "an error downloading" << url << ": the request had some progress but seems like no one is waiting for it, skipping";
    } else {
        Transfer* dwn = itr->second;
        if (dwn->success) {
            qreal received = bytesReceived;
            qreal total = bytesTotal;
            qreal progress = received/total;
            dwn->progress = progress;
            emit loadFileProgress(dwn->messages, progress, false);
        }
    }
}

void Core::NetworkAccess::onDownloadError(QNetworkReply::NetworkError code) {
    qDebug() << "DEBUG: DOWNLOAD ERROR";
    QNetworkReply* rpl = static_cast<QNetworkReply*>(sender());
    qDebug() << rpl->errorString();
    QString url = rpl->url().toString();
    std::map<QString, Transfer*>::const_iterator itr = downloads.find(url);
    if (itr == downloads.end()) {
        qDebug() << "an error downloading" << url << ": the request is reporting an error but seems like no one is waiting for it, skipping";
    } else {
        QString errorText = getErrorText(code);
        //if (errorText.size() > 0) {
            itr->second->success = false;
            Transfer* dwn = itr->second;
            emit loadFileError(dwn->messages, errorText, false);
        //}
    }
}

void Core::NetworkAccess::onDownloadSSLError(const QList<QSslError>& errors) {
    qDebug() << "DEBUG: DOWNLOAD SSL ERRORS";
    for (const QSslError& err : errors) {
        qDebug() << err.errorString();
    }
    QNetworkReply* rpl = static_cast<QNetworkReply*>(sender());
    QString url = rpl->url().toString();
    std::map<QString, Transfer*>::const_iterator itr = downloads.find(url);
    if (itr == downloads.end()) {
        qDebug() << "an SSL error downloading" << url << ": the request is reporting an error but seems like no one is waiting for it, skipping";
    } else {
        //if (errorText.size() > 0) {
        itr->second->success = false;
        Transfer* dwn = itr->second;
        emit loadFileError(dwn->messages, "SSL errors occured", false);
        //}
    }
}

QString Core::NetworkAccess::getErrorText(QNetworkReply::NetworkError code) {
    QString errorText("");
    switch (code) {
        case QNetworkReply::NoError:
            //this never is supposed to happen
            break;

    // network layer errors [relating to the destination server] (1-99):
        case QNetworkReply::ConnectionRefusedError:
            errorText = "Connection refused";
            break;
        case QNetworkReply::RemoteHostClosedError:
            errorText = "Remote server closed the connection";
            break;
        case QNetworkReply::HostNotFoundError:
            errorText = "Remote host is not found";
            break;
        case QNetworkReply::TimeoutError:
            errorText = "Connection was closed because it timed out";
            break;
        case QNetworkReply::OperationCanceledError:
            //this means I closed it myself by abort() or close()
            //I don't call them directory, but this is the error code
            //Qt returns when it can not resume donwload after the network failure
            //or when the download is canceled by the timout; 
            errorText = "Connection lost";
            break;
        case QNetworkReply::SslHandshakeFailedError:
            errorText = "Security error";           //TODO need to handle sslErrors signal to get a better description here
            break;
        case QNetworkReply::TemporaryNetworkFailureError:
            //this means the connection is lost by opened route, but it's going to be resumed, not sure I need to notify
            break;
        case QNetworkReply::NetworkSessionFailedError:
            errorText = "Outgoing connection problem";
            break;
        case QNetworkReply::BackgroundRequestNotAllowedError:
            errorText = "Background request is not allowed";
            break;
        case QNetworkReply::TooManyRedirectsError:
            errorText = "The request was  redirected too many times";
            break;
        case QNetworkReply::InsecureRedirectError:
            errorText = "The request was redirected to insecure connection";
            break;
        case QNetworkReply::UnknownNetworkError:
            errorText = "Unknown network error";
            break;

    // proxy errors (101-199):
        case QNetworkReply::ProxyConnectionRefusedError:
            errorText = "The connection to the proxy server was refused";
            break;
        case QNetworkReply::ProxyConnectionClosedError:
            errorText = "Proxy server closed the connection";
            break;
        case QNetworkReply::ProxyNotFoundError:
            errorText = "Proxy host was not found";
            break;
        case QNetworkReply::ProxyTimeoutError:
            errorText = "Connection to the proxy server was closed because it timed out";
            break;
        case QNetworkReply::ProxyAuthenticationRequiredError:
            errorText = "Couldn't connect to proxy server, authentication is required";
            break;
        case QNetworkReply::UnknownProxyError:
            errorText = "Unknown proxy error";
            break;

    // content errors (201-299):
        case QNetworkReply::ContentAccessDenied:
            errorText = "The access to file is denied";
            break;
        case QNetworkReply::ContentOperationNotPermittedError:
            errorText = "The operation over requesting file is not permitted";
            break;
        case QNetworkReply::ContentNotFoundError:
            errorText = "The file was not found";
            break;
        case QNetworkReply::AuthenticationRequiredError:
            errorText = "Couldn't access the file, authentication is required";
            break;
        case QNetworkReply::ContentReSendError:
            errorText = "Sending error, one more attempt will probably solve this problem";
            break;
        case QNetworkReply::ContentConflictError:
            errorText = "The request could not be completed due to a conflict with the current state of the resource";
            break;
        case QNetworkReply::ContentGoneError:
            errorText = "The requested resource is no longer available at the server";
            break;
        case QNetworkReply::UnknownContentError:
            errorText = "Unknown content error";
            break;

    // protocol errors
        case QNetworkReply::ProtocolUnknownError:
            errorText = "Unknown protocol error";
            break;
        case QNetworkReply::ProtocolInvalidOperationError:
            errorText = "Requested operation is not permitted in this protocol";
            break;
        case QNetworkReply::ProtocolFailure:
            errorText = "Low level protocol error";
            break;

    // Server side errors (401-499)
        case QNetworkReply::InternalServerError:
            errorText = "Internal server error";
            break;
        case QNetworkReply::OperationNotImplementedError:
            errorText = "Server doesn't support requested operation";
            break;
        case QNetworkReply::ServiceUnavailableError:
            errorText = "The server is not available for this operation right now";
            break;
        case QNetworkReply::UnknownServerError:
            errorText = "Unknown server error";
            break;
    }
    return errorText;
}


void Core::NetworkAccess::onDownloadFinished() {
    qDebug() << "DEBUG: DOWNLOAD FINISHED";
    QNetworkReply* rpl = static_cast<QNetworkReply*>(sender());
    QString url = rpl->url().toString();
    std::map<QString, Transfer*>::const_iterator itr = downloads.find(url);
    if (itr == downloads.end()) {
        qDebug() << "an error downloading" << url << ": the request is done but there is no record of it being downloaded, ignoring";
    } else {
        Transfer* dwn = itr->second;
        if (dwn->success) {
            qDebug() << "download success for" << url;
            QString err;
            QStringList hops = url.split("/");
            QString fileName = hops.back();
            QString jid;
            if (dwn->messages.size() > 0)
                jid = dwn->messages.front().jid;
            else
                qDebug() << "An attempt to save the file but it doesn't seem to belong to any message, download is definately going to be broken";

            QString path = prepareDirectory(jid);
            if (path.size() > 0) {
                path = checkFileName(fileName, path);
                
                QFile file(Shared::resolvePath(path));
                if (file.open(QIODevice::WriteOnly)) {
                    file.write(dwn->reply->readAll());
                    file.close();
                    storage.setPath(url, path);
                    qDebug() << "file" << path << "was successfully downloaded";
                } else {
                    qDebug() << "couldn't save file" << path;
                    err = "Error opening file to write:" + file.errorString();
                }
            } else {
                err = "Couldn't prepare a directory for file";
            }
            
            if (path.size() > 0)
                emit downloadFileComplete(dwn->messages, path);
            else
                emit loadFileError(dwn->messages, "Error saving file " + url + "; " + err, false);
        }
        
        dwn->reply->deleteLater();
        delete dwn;
        downloads.erase(itr);
    }
}

void Core::NetworkAccess::startDownload(const std::list<Shared::MessageInfo>& msgs, const QString& url) {
    Transfer* dwn = new Transfer({msgs, 0, 0, true, "", url, 0});
    QNetworkRequest req(url);
    dwn->reply = manager->get(req);
    connect(dwn->reply, &QNetworkReply::downloadProgress, this, &NetworkAccess::onDownloadProgress);
    connect(dwn->reply, &QNetworkReply::sslErrors, this, &NetworkAccess::onDownloadSSLError);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
    connect(dwn->reply, qOverload<QNetworkReply::NetworkError>(&QNetworkReply::errorOccurred), this, &NetworkAccess::onDownloadError);
#else
    connect(dwn->reply, qOverload<QNetworkReply::NetworkError>(&QNetworkReply::error), this, &NetworkAccess::onDownloadError);
#endif
    connect(dwn->reply, &QNetworkReply::finished, this, &NetworkAccess::onDownloadFinished);
    downloads.insert(std::make_pair(url, dwn));
    emit loadFileProgress(dwn->messages, 0, false);
}

void Core::NetworkAccess::onUploadError(QNetworkReply::NetworkError code) {
    QNetworkReply* rpl = static_cast<QNetworkReply*>(sender());
    QString url = rpl->url().toString();
    std::map<QString, Transfer*>::const_iterator itr = uploads.find(url);
    if (itr == uploads.end()) {
        qDebug() << "an error uploading" << url << ": the request is reporting an error but there is no record of it being uploading, ignoring";
    } else {
        QString errorText = getErrorText(code);
        //if (errorText.size() > 0) {
            itr->second->success = false;
            Transfer* upl = itr->second;
            emit loadFileError(upl->messages, errorText, true);
        //}
        
        //TODO deletion?
    }
}

void Core::NetworkAccess::onUploadFinished() {
    QNetworkReply* rpl = static_cast<QNetworkReply*>(sender());
    QString url = rpl->url().toString();
    std::map<QString, Transfer*>::const_iterator itr = uploads.find(url);
    if (itr == downloads.end()) {
        qDebug() << "an error uploading" << url << ": the request is done there is no record of it being uploading, ignoring";
    } else {
        Transfer* upl = itr->second;
        if (upl->success) {
            qDebug() << "upload success for" << url;

            // Copy file to Download folder if it is a temp file. See Conversation::onImagePasted.
            if (upl->path.startsWith(QDir::tempPath() + QDir::separator() + QStringLiteral("squawk_img_attach_")) && upl->path.endsWith(".png")) {
                QString err = "";
                QString downloadDirPath = prepareDirectory(upl->messages.front().jid);
                if (downloadDirPath.size() > 0) {
                    QString newPath = downloadDirPath + QDir::separator() + upl->path.mid(QDir::tempPath().length() + 1);

                    // Copy {TEMPDIR}/squawk_img_attach_XXXXXX.png to Download folder
                    bool copyResult = QFile::copy(upl->path, Shared::resolvePath(newPath));
                    if (copyResult)
                        upl->path = newPath;  // Change storage
                    else
                        err = "copying to " + newPath + " failed";
                } else {
                    err = "Couldn't prepare a directory for file";
                }

                if (err.size() != 0)
                    qDebug() << "failed to copy temporary upload file " << upl->path << " to download folder:" << err;
            }

            storage.addFile(upl->messages, upl->url, upl->path);
            emit uploadFileComplete(upl->messages, upl->url, upl->path);
        }
        
        upl->reply->deleteLater();
        upl->file->close();
        upl->file->deleteLater();
        delete upl;
        uploads.erase(itr);
    }
}

void Core::NetworkAccess::onUploadProgress(qint64 bytesReceived, qint64 bytesTotal) {
    QNetworkReply* rpl = static_cast<QNetworkReply*>(sender());
    QString url = rpl->url().toString();
    std::map<QString, Transfer*>::const_iterator itr = uploads.find(url);
    if (itr == uploads.end()) {
        qDebug() << "an error downloading" << url << ": the request had some progress but seems like no one is waiting for it, skipping";
    } else {
        Transfer* upl = itr->second;
        if (upl->success) {
            qreal received = bytesReceived;
            qreal total = bytesTotal;
            qreal progress = received/total;
            upl->progress = progress;
            emit loadFileProgress(upl->messages, progress, true);
        }
    }
}

QString Core::NetworkAccess::getFileRemoteUrl(const QString& path) {
    QString p = Shared::squawkifyPath(path);
    
    try {
        p = storage.getUrl(p);
    } catch (const LMDBAL::NotFound& err) {
        p = "";
    } catch (...) {
        throw;
    }
    
    return p;
}

void Core::NetworkAccess::uploadFile(
    const Shared::MessageInfo& info,
    const QString& path,
    const QUrl& put,
    const QUrl& get,
    const QMap<QString, QString> headers
) {
    QFile* file = new QFile(path);
    Transfer* upl = new Transfer({{info}, 0, 0, true, path, get.toString(), file});
    QNetworkRequest req(put);
    for (QMap<QString, QString>::const_iterator itr = headers.begin(), end = headers.end(); itr != end; itr++) {
        req.setRawHeader(itr.key().toUtf8(), itr.value().toUtf8());
    }
    if (file->open(QIODevice::ReadOnly)) {
        upl->reply = manager->put(req, file);
        
        connect(upl->reply, &QNetworkReply::uploadProgress, this, &NetworkAccess::onUploadProgress);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
        connect(upl->reply, qOverload<QNetworkReply::NetworkError>(&QNetworkReply::errorOccurred), this, &NetworkAccess::onUploadError);
#else
        connect(upl->reply, qOverload<QNetworkReply::NetworkError>(&QNetworkReply::error), this, &NetworkAccess::onUploadError);
#endif
        connect(upl->reply, &QNetworkReply::finished, this, &NetworkAccess::onUploadFinished);
        uploads.insert(std::make_pair(put.toString(), upl));
        emit loadFileProgress(upl->messages, 0, true);
    } else {
        qDebug() << "couldn't upload file" << path;
        emit loadFileError(upl->messages, "Error opening file", true);
        delete file;
        delete upl;
    }
}

void Core::NetworkAccess::registerFile(const QString& url, const QString& account, const QString& jid, const QString& id) {
    storage.addFile(url, account, jid, id);
    std::map<QString, Transfer*>::iterator itr = downloads.find(url);
    if (itr != downloads.end())
        itr->second->messages.emplace_back(account, jid, id);   //TODO notification is going to happen the next tick, is that okay?
}

void Core::NetworkAccess::registerFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id) {
    storage.addFile(url, path, account, jid, id);
}

bool Core::NetworkAccess::checkAndAddToUploading(const QString& acc, const QString& jid, const QString id, const QString path) {
    for (const std::pair<const QString, Transfer*>& pair : uploads) {
        Transfer* info = pair.second;
        if (pair.second->path == path) {
            std::list<Shared::MessageInfo>& messages = info->messages;
            bool dup = false;
            for (const Shared::MessageInfo& info : messages) {
                if (info.account == acc && info.jid == jid && info.messageId == id) {
                    dup = true;
                    break;
                }
            }
            if (!dup) {
                info->messages.emplace_back(acc, jid, id);   //TODO notification is going to happen the next tick, is that okay?
                return true;
            }
        }
    }
    
    return false;
}

QString Core::NetworkAccess::prepareDirectory(const QString& jid) {
    QString path = currentPath;
    QString addition;
    if (jid.size() > 0) {
        addition = jid;
        path += QDir::separator() + jid;
    }

    QDir location(path);
    
    if (!location.exists()) {
        bool res = location.mkpath(path);
        if (!res)
            return "";
        else
            return "squawk://" + addition;
    }
    return "squawk://" + addition;
}

QString Core::NetworkAccess::checkFileName(const QString& name, const QString& path) {
    QStringList parts = name.split(".");
    QString suffix("");
    QStringList::const_iterator sItr = parts.begin();
    QString realName = *sItr;
    ++sItr;
    for (QStringList::const_iterator sEnd = parts.end(); sItr != sEnd; ++sItr)
        suffix += "." + (*sItr);

    QString postfix("");
    QString resolvedPath = Shared::resolvePath(path);
    QString count("");
    QFileInfo proposedName(resolvedPath + QDir::separator() + realName + count + suffix);

    int counter = 0;
    while (proposedName.exists()) {
        count = QString("(") + std::to_string(++counter).c_str() + ")";
        proposedName = QFileInfo(resolvedPath + QDir::separator() + realName + count + suffix);
    }

    return path + QDir::separator() + realName + count + suffix;
}

QString Core::NetworkAccess::addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id) {
    return storage.addMessageAndCheckForPath(url, account, jid, id);
}

std::list<Shared::MessageInfo> Core::NetworkAccess::reportPathInvalid(const QString& path) {
    return storage.deletedFile(path);
}

void Core::NetworkAccess::moveFilesDirectory(const QString& newPath) {
    QDir dir(currentPath);
    bool success = true;
    qDebug() << "moving" << currentPath << "to" << newPath;
    for (QFileInfo fileInfo : dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) {
        QString fileName = fileInfo.fileName();
        success = dir.rename(fileName, newPath + QDir::separator() + fileName) && success;
    }

    if (!success)
        qDebug() << "couldn't move downloads directory, most probably downloads will be broken";

    currentPath = newPath;
}