/*
    SPDX-FileCopyrightText: 2009-2020 Thomas Baumgart <tbaumgart@kde.org>
    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include <config-kmymoney.h>

#include "ofxpartner.h"
#include "kmymoneysettings.h"

// ----------------------------------------------------------------------------
// QT Includes

#include <QApplication>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QDomDocument>
#include <QEventLoop>
#include <QFile>
#include <QFileInfo>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QTextStream>

// ----------------------------------------------------------------------------
// KDE Includes

#include <KIO/Job>
#include <KIO/TransferJob>
#include <KIO/CopyJob>
#include <KJobUiDelegate>
#include <KLocalizedString>
#include <KMessageBox>

// ----------------------------------------------------------------------------
// Some standard defined stuff collides with libofx.h
#ifdef Q_CC_MSVC
#undef ERROR
#undef DELETE
#endif

// ----------------------------------------------------------------------------
// Project Includes

namespace OfxPartner
{
bool post(const QString& request, const QMap<QString, QString>& attr, const QUrl &url, const QUrl& filename);
bool get(const QString& request, const QMap<QString, QString>& attr, const QUrl &url, const QUrl& filename);

Q_GLOBAL_STATIC_WITH_ARGS(QString, kBankFilename, ("ofx-bank-index.xml"));

#define VER "9"

static QString directory;

void setDirectory(const QString& dir)
{
    directory = dir;
}

bool needReload(const QFileInfo& i)
{
    return ((!i.isReadable()) //
            || (i.lastModified().addDays(7) < QDateTime::currentDateTime()) //
            || (i.size() < 1024));
}

void ValidateIndexCache()
{
    // TODO (Ace) Check whether these files exist and are recent enough before getting them again

    QUrl fname;

    QMap<QString, QString> attr;

    fname = QUrl(QStringLiteral("file://") + directory + *kBankFilename);
    QDir dir;
    dir.mkpath(directory);

    QFileInfo i(fname.toLocalFile());
    if (needReload(i))
        get("", attr, QUrl(QStringLiteral("https://www.ofxhome.com/api.php?all=yes")), fname);
}

static void ParseFile(QMap<QString, QString>& result, const QString& fileName, const QString& bankName)
{
    QFile f(fileName);
    if (f.open(QIODevice::ReadOnly)) {
        QTextStream stream(&f);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        stream.setCodec("UTF-8");
#endif
        QString msg;
        int errl, errc;
        QDomDocument doc;
        if (doc.setContent(stream.readAll(), &msg, &errl, &errc)) {
            QDomNodeList olist = doc.elementsByTagName("institutionid");
            for (int i = 0; i < olist.count(); ++i) {
                QDomNode onode = olist.item(i);
                if (onode.isElement()) {
                    QDomElement elo = onode.toElement();
                    QString name = elo.attribute("name");

                    if (bankName.isEmpty())
                        result[name].clear();

                    else if (name == bankName) {
                        result[elo.attribute("id")].clear();
                    }
                }
            }
        }
        f.close();
    }
}

QStringList BankNames()
{
    QMap<QString, QString> result;

    // Make sure the index files are up to date
    ValidateIndexCache();

    ParseFile(result, directory + *kBankFilename, QString());

    // Add Innovision
    result["Innovision"].clear();

    return QStringList() << result.keys();
}

QStringList FipidForBank(const QString& bank)
{
    QMap<QString, QString> result;

    ParseFile(result, directory + *kBankFilename, bank);

    // the fipid for Innovision is 1.
    if (bank == "Innovision")
        result["1"].clear();

    return QStringList() << result.keys();
}

QString extractNodeText(QDomElement& node, const QString& name)
{
    QString res;
    static const QRegularExpression exp(QLatin1String("([^/]+)/?([^/].*)?"));
    const auto match(exp.match(name));
    if (match.hasMatch()) {
        QDomNodeList olist = node.elementsByTagName(match.captured(1));
        if (olist.count()) {
            QDomNode onode = olist.item(0);
            if (onode.isElement()) {
                QDomElement elo = onode.toElement();
                if (match.captured(2).isEmpty()) {
                    res = elo.text();
                } else {
                    res = extractNodeText(elo, match.captured(2));
                }
            }
        }
    }
    return res;
}

QString extractNodeText(QDomDocument& doc, const QString& name)
{
    QString res;
    static const QRegularExpression exp(QLatin1String("([^/]+)/?([^/].*)?"));
    const auto match(exp.match(name));
    if (match.hasMatch()) {
        QDomNodeList olist = doc.elementsByTagName(match.captured(1));
        if (olist.count()) {
            QDomNode onode = olist.item(0);
            if (onode.isElement()) {
                QDomElement elo = onode.toElement();
                if (match.captured(2).isEmpty()) {
                    res = elo.text();
                } else {
                    res = extractNodeText(elo, match.captured(2));
                }
            }
        }
    }
    return res;
}

OfxHomeServiceInfo ServiceInfo(const QString& fipid)
{
    OfxHomeServiceInfo result;
    memset(&result.ofxInfo, 0, sizeof(result.ofxInfo));
    result.ofxValidated = true;
    result.sslValidated = true;
    result.lastOfxValidated = QDate::currentDate().toString();
    result.lastSslValidated = result.lastOfxValidated;

    // Hard-coded values for Innovision test server
    if (fipid == "1") {
        strncpy(result.ofxInfo.fid, "00000", OFX_FID_LENGTH - 1);
        strncpy(result.ofxInfo.org, "ReferenceFI", OFX_ORG_LENGTH - 1);
        strncpy(result.ofxInfo.url, "https://ofx.innovision.com", OFX_URL_LENGTH - 1);
        result.ofxInfo.accountlist = 1;
        result.ofxInfo.statements = 1;
        result.ofxInfo.billpay = 1;
        result.ofxInfo.investments = 1;

        return result;
    }

    QMap<QString, QString> attr;

    QUrl guidFile(QString("file://%1fipid-%2.xml").arg(directory, fipid));

    QFileInfo i(guidFile.toLocalFile());

    if (!i.isReadable() || i.lastModified().addDays(7) < QDateTime::currentDateTime())
        get("", attr, QUrl(QString("https://www.ofxhome.com/api.php?lookup=%1").arg(fipid)), guidFile);

    QFile f(guidFile.toLocalFile());
    if (f.open(QIODevice::ReadOnly)) {
        QTextStream stream(&f);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        stream.setCodec("UTF-8");
#endif
        QString msg;
        int errl, errc;
        QDomDocument doc;
        if (doc.setContent(stream.readAll(), &msg, &errl, &errc)) {
            const auto fid = extractNodeText(doc, "institution/fid");
            const auto org = extractNodeText(doc, "institution/org");
            const auto url = extractNodeText(doc, "institution/url");
            result.ofxValidated = (extractNodeText(doc, "institution/ofxfail").toUInt() == 0);
            result.sslValidated = (extractNodeText(doc, "institution/sslfail").toUInt() == 0);
            result.lastOfxValidated = extractNodeText(doc, "institution/lastofxvalidation");
            result.lastSslValidated = extractNodeText(doc, "institution/lastsslvalidation");

            strncpy(result.ofxInfo.fid, fid.toLatin1(), OFX_FID_LENGTH - 1);
            strncpy(result.ofxInfo.org, org.toLatin1(), OFX_ORG_LENGTH - 1);
            strncpy(result.ofxInfo.url, url.toLatin1(), OFX_URL_LENGTH - 1);

            result.ofxInfo.accountlist = true;
            result.ofxInfo.statements = true;
            result.ofxInfo.billpay = false;
            result.ofxInfo.investments = true;
        }
    }
    else
    {
        memset(&result.ofxInfo, 0, sizeof(result.ofxInfo));
        result.ofxValidated = false;
        result.sslValidated = false;
        result.lastOfxValidated.clear();
        result.lastSslValidated.clear();
        qDebug() << "OFX ServiceInfo:" << f.errorString();
    }
    return result;
}

bool get(const QString& request, const QMap<QString, QString>& attr, const QUrl &url, const QUrl& filename)
{
    Q_UNUSED(request);
    QByteArray req;
    OfxHttpRequest job("GET", url, req, attr, filename, false);

    return job.error() == 0;
}

bool post(const QString& request, const QMap<QString, QString>& attr, const QUrl &url, const QUrl& filename)
{
    QByteArray req(request.toUtf8());

    OfxHttpRequest job("POST", url, req, attr, filename, false);
    return job.error() == 0;
}

} // namespace OfxPartner

class OfxHttpRequest::Private
{
public:
    QFile  m_fpTrace;
};

OfxHttpRequest::OfxHttpRequest(const QString& method,
                               const QUrl& url,
                               const QByteArray& postData,
                               const QMap<QString, QString>& metaData,
                               const QUrl& dst,
                               bool showProgressInfo)
    : d(new Private)
    , m_dst(dst.toLocalFile())
    , m_error(-1)
    , m_postJob(nullptr)
    , m_getJob(nullptr)
{
#if defined(Q_OS_WIN)
    // on MS windows, the local file could be presented as
    //
    //   "//<drive-letter>/<path-name>"
    //
    // which needs to be converted to
    //
    //   "<drive-letter>:/<path-name>"
    //
    // see https://bugs.kde.org/show_bug.cgi?id=396286 for details of the analysis.
    QRegularExpression re(QStringLiteral("^//(?<drive>[a-z])/(?<path>.+)$"), QRegularExpression::CaseInsensitiveOption);
    const auto match = re.match(m_dst);
    if (match.hasMatch()) {
        m_dst = QString("/%1:/%2").arg(match.captured(QStringLiteral("drive")), match.captured(QStringLiteral("path")));
        qDebug() << "destination changed to" << m_dst;
    }
#endif

    m_eventLoop = new QEventLoop(qApp->activeWindow());

    if (KMyMoneySettings::logOfxTransactions()) {
        QString logPath = KMyMoneySettings::logPath();
        d->m_fpTrace.setFileName(QString("%1/ofxlog.txt").arg(logPath));
        if (!d->m_fpTrace.open(QIODevice::WriteOnly | QIODevice::Append)) {
            qDebug() << "Could not open" << d->m_fpTrace.fileName();
        }
    }

    auto jobFlags = KIO::DefaultFlags | KIO::Overwrite;
    if (!showProgressInfo)
        jobFlags = KIO::HideProgressInfo | KIO::Overwrite;

    KIO::Job* job;
    if (method.toLower() == QStringLiteral("get")) {
        job = m_getJob = KIO::copy(url, QUrl(QString("file://%1").arg(m_dst)), jobFlags);
    } else {
        job = m_postJob = KIO::http_post(url, postData, jobFlags);
        m_postJob->addMetaData("content-type", "Content-type: application/x-ofx");
        m_postJob->addMetaData(metaData);
        if (metaData.contains(QLatin1String("UserAgent"))) {
            m_postJob->addMetaData(QLatin1String("SendUserAgent"), QLatin1String("true"));
        }
        connect(m_postJob, &KIO::TransferJob::data, this, &OfxHttpRequest::slotOfxData);
        connect(m_postJob, &KIO::Job::connected, this, &OfxHttpRequest::slotOfxConnected);
    }

    if (d->m_fpTrace.isOpen()) {
        QTextStream ts(&d->m_fpTrace);
        ts << "url: " << url.toDisplayString() << "\n";
        ts << "request:\n" << QString(postData) << "\n" << "response:\n";
    }

    connect(job, &KIO::Job::result, this, &OfxHttpRequest::slotOfxFinished);

    job->start();

    qDebug("Starting eventloop");
    if (m_eventLoop)
        m_eventLoop->exec();
    qDebug("Ending eventloop");
}

OfxHttpRequest::~OfxHttpRequest()
{
    delete m_eventLoop;

    if (d->m_fpTrace.isOpen()) {
        d->m_fpTrace.close();
    }
    delete d;
}

void OfxHttpRequest::slotOfxConnected(KIO::Job*)
{
    qDebug() << "OfxHttpRequest::slotOfxConnected" << m_dst;
    m_file.setFileName(m_dst);
    if (m_file.open(QIODevice::WriteOnly)) {
        ; // nothing to do here, just keeping the compiler happy
    }
}

void OfxHttpRequest::slotOfxData(KIO::Job*, const QByteArray& _ba)
{
    if (m_file.isOpen()) {
        m_file.write(_ba);

        if (d->m_fpTrace.isOpen()) {
            d->m_fpTrace.write(_ba);
        }
    }
}

void OfxHttpRequest::slotOfxFinished(KJob* /* e */)
{
    if (m_file.isOpen()) {
        m_file.close();
        if (d->m_fpTrace.isOpen()) {
            d->m_fpTrace.write("\nCompleted\n\n\n\n", 14);
        }
    }

    if(m_postJob) {
        m_error = m_postJob->error();
        if (m_error) {
            m_postJob->uiDelegate()->showErrorMessage();
            QFile::remove(m_dst);
#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
        } else if (m_postJob->isErrorPage()) {
            QString details;
            QFile f(m_dst);
            if (f.open(QIODevice::ReadOnly)) {
                QTextStream stream(&f);
                while (!stream.atEnd()) {
                    details += stream.readLine(); // line of text excluding '\n'
                }
                f.close();
            }
            KMessageBox::detailedError(nullptr, i18n("The HTTP request failed."), details, i18nc("The HTTP request failed", "Failed"));
            QFile::remove(m_dst);
#endif
        }
    } else if(m_getJob) {
        m_error = m_getJob->error();
        if (m_error) {
            m_getJob->uiDelegate()->showErrorMessage();
            QFile::remove(m_dst);
        }
    }

    qDebug("Finishing eventloop");
    if (m_eventLoop)
        m_eventLoop->exit();
}
