/*******************************************************************
* bugzillalib.cpp
* Copyright  2009, 2011  Dario Andres Rodriguez <andresbajotierra@gmail.com>
* Copyright  2012  George Kiagiadakis <kiagiadakis.george@gmail.com>
*
* 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.
*
* 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 "bugzillalib.h"

#include <QtGlobal>
#include <QTextStream>
#include <QByteArray>
#include <QString>

#include <QDomNode>
#include <QDomNodeList>
#include <QDomElement>
#include <QDomNamedNodeMap>

#include <KIO/Job>
#include <KLocalizedString>
#include "drkonqi_debug.h"

#define MAKE_BUGZILLA_VERSION(a,b,c) (((a) << 16) | ((b) << 8) | (c))

static const char columns[] = "bug_severity,priority,bug_status,product,short_desc,resolution";

//Bugzilla URLs
static const char searchUrl[] =
        "buglist.cgi?query_format=advanced&order=Importance&ctype=csv"
        "&product=%1"
        "&longdesc_type=allwordssubstr&longdesc=%2"
        "&chfieldfrom=%3&chfieldto=%4&chfield=[Bug+creation]"
        "&bug_severity=%5"
        "&columnlist=%6";
// short_desc, product, long_desc(possible backtraces lines), searchFrom, searchTo, severity, columnList
static const char showBugUrl[] = "show_bug.cgi?id=%1";
static const char fetchBugUrl[] = "show_bug.cgi?id=%1&ctype=xml";

static inline Component buildComponent(const QVariantMap& map);
static inline Version buildVersion(const QVariantMap& map);
static inline Product buildProduct(const QVariantMap& map);

//BEGIN BugzillaManager

BugzillaManager::BugzillaManager(const QString &bugTrackerUrl, QObject *parent)
        : QObject(parent)
        , m_bugTrackerUrl(bugTrackerUrl)
        , m_logged(false)
        , m_searchJob(nullptr)
{
    m_xmlRpcClient = new KXmlRpc::Client(QUrl(m_bugTrackerUrl + QStringLiteral("xmlrpc.cgi")), this);
    m_xmlRpcClient->setUserAgent(QStringLiteral("DrKonqi"));
    // Allow constructors for ReportInterface and assistant dialogs to finish.
    // We do not want them to be racing the remote Bugzilla database.
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
    QMetaObject::invokeMethod (this, &BugzillaManager::lookupVersion, Qt::QueuedConnection);
#else
    QMetaObject::invokeMethod (this, "lookupVersion", Qt::QueuedConnection);
#endif
}

// BEGIN Checks of Bugzilla software versions.
void BugzillaManager::lookupVersion()
{
    QMap<QString, QVariant> args;
    callBugzilla("Bugzilla.version", "version", args, SecurityDisabled);
}

void BugzillaManager::setFeaturesForVersion(const QString& version)
{
    // A procedure to change Dr Konqi behaviour automatically when Bugzilla
    // software versions change.
    //
    // Changes should be added to Dr Konqi AHEAD of when the corresponding
    // Bugzilla software changes are released into bugs.kde.org, so that
    // Dr Konqi can continue to operate smoothly, without bug reports and a
    // reactive KDE software release.
    //
    // If Bugzilla announces a change to its software that affects Dr Konqi,
    // add executable code to implement the change automatically when the
    // Bugzilla software version changes. It goes at the end of this procedure
    // and elsewhere in this class (BugzillaManager) and/or other classes where
    // the change should actually be implemented.

    const int nVersionParts = 3;
    QString seps = QLatin1String("[._-]");
    QStringList digits = version.split(QRegExp(seps), QString::SkipEmptyParts);
    while (digits.count() < nVersionParts) {
        digits << QLatin1String("0");
    }
    if (digits.count() > nVersionParts) {
        qCWarning(DRKONQI_LOG) << QStringLiteral("Current Bugzilla version %1 has more than %2 parts. Check that this is not a problem.").arg(version).arg(nVersionParts);
    }
    int currentVersion = MAKE_BUGZILLA_VERSION(digits.at(0).toUInt(),
                             digits.at(1).toUInt(), digits.at(2).toUInt());

    // Set the code(s) for historical versions of Bugzilla - before any change.
    m_security = UseCookies;    // Used to have cookies for update-security.

    if (currentVersion >= MAKE_BUGZILLA_VERSION(4, 4, 3)) {
        // Security method changes from cookies to tokens in Bugzilla 4.4.3.
	// BUT, tokens fail when kio_http sends any cookies found in KCookieJar,
	// so go directly to passwords-only security (supported since Bugzilla
	// 3.6 and will be enforced in Bugzilla 4.5.x).
        m_security = UsePasswords;
    }

    qCDebug(DRKONQI_LOG) << "VERSION" << version << "SECURITY" << m_security;
}
// END Checks of Bugzilla software versions.

// BEGIN Generic remote-procedure (RPC) call to Bugzilla
void BugzillaManager::callBugzilla(const char* method, const char* id,
                                   QMap<QString, QVariant>& args,
                                   SecurityStatus security)
{
    if (security == SecurityEnabled) {
        switch (m_security) {
        case UseTokens:
            qCDebug(DRKONQI_LOG) << method << id << "using token";
            args.insert(QLatin1String("Bugzilla_token"), m_token);
            break;
        case UsePasswords:
            qCDebug(DRKONQI_LOG) << method << id << "using username" << m_username;
            args.insert(QLatin1String("Bugzilla_login"), m_username);
            args.insert(QLatin1String("Bugzilla_password"), m_password);
            break;
        case UseCookies:
            qCDebug(DRKONQI_LOG) << method << id << "using cookies";
            // Some KDE process other than Dr Konqi should provide cookies.
            break;
        }
    }

    m_xmlRpcClient->call(QLatin1String(method), args,
            this, SLOT(callMessage(QList<QVariant>,QVariant)),
            this, SLOT(callFault(int,QString,QVariant)),
            QLatin1String(id));
}
// END Generic call to Bugzilla

//BEGIN Login methods
void BugzillaManager::tryLogin(const QString& username, const QString& password)
{
    m_username = username;
    if (m_security == UsePasswords) {
        m_password = password;
    }
    m_logged = false;

    QMap<QString, QVariant> args;
    args.insert(QLatin1String("login"), username);
    args.insert(QLatin1String("password"), password);
    if (m_security == UseCookies) {
        // Removed in Bugzilla 4.4.3 software, which no longer issues cookies.
        args.insert(QLatin1String("remember"), false);
    }

    callBugzilla("User.login", "login", args, SecurityDisabled);
}

bool BugzillaManager::getLogged() const
{
    return m_logged;
}

QString BugzillaManager::getUsername() const
{
    return m_username;
}
//END Login methods

//BEGIN Bugzilla Action methods
void BugzillaManager::fetchBugReport(int bugnumber, QObject * jobOwner)
{
    QUrl url(m_bugTrackerUrl + QString::fromLatin1(fetchBugUrl).arg(bugnumber));

    if (!jobOwner) {
        jobOwner = this;
    }

    KIO::Job * fetchBugJob = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo);
    fetchBugJob->setParent(jobOwner);
    connect(fetchBugJob, &KIO::Job::finished, this, &BugzillaManager::fetchBugJobFinished);
}


void BugzillaManager::searchBugs(const QStringList & products,
                                const QString & severity, const QString & date_start,
                                const QString & date_end, QString comment)
{
    QString product;
    if (products.size() > 0) {
        if (products.size() == 1) {
            product = products.at(0);
        } else  {
            Q_FOREACH(const QString & p, products) {
                product += p + QStringLiteral("&product=");
            }
            product = product.mid(0,product.size()-9);
        }
    }

    QString url = QString(m_bugTrackerUrl) +
                  QString::fromLatin1(searchUrl).arg(product, comment.replace(QLatin1Char(' ') , QLatin1Char('+')), date_start,
                                         date_end, severity, QString::fromLatin1(columns));

    stopCurrentSearch();

    m_searchJob = KIO::storedGet(QUrl(url) , KIO::Reload, KIO::HideProgressInfo);
    connect(m_searchJob, &KIO::Job::finished, this, &BugzillaManager::searchBugsJobFinished);
}

void BugzillaManager::sendReport(const BugReport & report)
{
    QMap<QString, QVariant> args;
    args.insert(QLatin1String("product"), report.product());
    args.insert(QLatin1String("component"), report.component());
    args.insert(QLatin1String("version"), report.version());
    args.insert(QLatin1String("summary"), report.shortDescription());
    args.insert(QLatin1String("description"), report.description());
    args.insert(QLatin1String("op_sys"), report.operatingSystem());
    args.insert(QLatin1String("platform"), report.platform());
    args.insert(QLatin1String("keywords"), report.keywords());
    args.insert(QLatin1String("priority"), report.priority());
    args.insert(QLatin1String("severity"), report.bugSeverity());

    callBugzilla("Bug.create", "Bug.create", args, SecurityEnabled);
}

void BugzillaManager::attachTextToReport(const QString & text, const QString & filename,
    const QString & summary, int bugId, const QString & comment)
{
    QMap<QString, QVariant> args;
    args.insert(QLatin1String("ids"), QVariantList() << bugId);
    args.insert(QLatin1String("file_name"), filename);
    args.insert(QLatin1String("summary"), summary);
    args.insert(QLatin1String("comment"), comment);
    args.insert(QLatin1String("content_type"), QLatin1String("text/plain"));

    //data needs to be a QByteArray so that it is encoded in base64 (query.cpp:246)
    args.insert(QLatin1String("data"), text.toUtf8());

    callBugzilla("Bug.add_attachment", "Bug.add_attachment", args,
                 SecurityEnabled);
}

void BugzillaManager::addMeToCC(int bugId)
{
    QMap<QString, QVariant> args;
    args.insert(QLatin1String("ids"), QVariantList() << bugId);

    QMap<QString, QVariant> ccChanges;
    ccChanges.insert(QLatin1String("add"), QVariantList() << m_username);
    args.insert(QLatin1String("cc"), ccChanges);

    callBugzilla("Bug.update", "Bug.update.cc", args, SecurityEnabled);
}

void BugzillaManager::fetchProductInfo(const QString & product)
{
    QMap<QString, QVariant> args;

    args.insert(QStringLiteral("names"), (QStringList() << product) ) ;

    QStringList includeFields;
    // currently we only need these informations
    includeFields << QStringLiteral("name") << QStringLiteral("is_active") << QStringLiteral("components") << QStringLiteral("versions");

    args.insert(QStringLiteral("include_fields"), includeFields) ;

    callBugzilla("Product.get", "Product.get.versions", args, SecurityDisabled);
}


//END Bugzilla Action methods

//BEGIN Misc methods
QString BugzillaManager::urlForBug(int bug_number) const
{
    return QString(m_bugTrackerUrl) + QString::fromLatin1(showBugUrl).arg(bug_number);
}

void BugzillaManager::stopCurrentSearch()
{
    if (m_searchJob) { //Stop previous searchJob
        m_searchJob->disconnect();
        m_searchJob->kill();
        m_searchJob = nullptr;
    }
}
//END Misc methods

//BEGIN Slots to handle KJob::finished

void BugzillaManager::fetchBugJobFinished(KJob* job)
{
    if (!job->error()) {
        KIO::StoredTransferJob * fetchBugJob = static_cast<KIO::StoredTransferJob*>(job);

        BugReportXMLParser * parser = new BugReportXMLParser(fetchBugJob->data());
        BugReport report = parser->parse();

        if (parser->isValid()) {
            emit bugReportFetched(report, job->parent());
        } else {
            emit bugReportError(i18nc("@info","Invalid report information (malformed data). This "
                                      "could mean that the bug report does not exist, or the "
                                      "bug tracking site is experiencing a problem."), job->parent());
        }

        delete parser;
    } else {
        emit bugReportError(job->errorString(), job->parent());
    }
}

void BugzillaManager::searchBugsJobFinished(KJob * job)
{
    if (!job->error()) {
        KIO::StoredTransferJob * searchBugsJob = static_cast<KIO::StoredTransferJob*>(job);

        BugListCSVParser * parser = new BugListCSVParser(searchBugsJob->data());
        BugMapList list = parser->parse();

        if (parser->isValid()) {
            emit searchFinished(list);
        } else {
            emit searchError(i18nc("@info","Invalid bug list: corrupted data"));
        }

        delete parser;
    } else {
        emit searchError(job->errorString());
    }

    m_searchJob = nullptr;
}

static inline Component buildComponent(const QVariantMap& map)
{
    QString name = map.value(QStringLiteral("name")).toString();
    bool active = map.value(QStringLiteral("is_active")).toBool();

    return Component(name, active);
}

static inline Version buildVersion(const QVariantMap& map)
{
    QString name = map.value(QStringLiteral("name")).toString();
    bool active = map.value(QStringLiteral("is_active")).toBool();

    return Version(name, active);
}

static inline Product buildProduct(const QVariantMap& map)
{
    QString name = map.value(QStringLiteral("name")).toString();
    bool active = map.value(QStringLiteral("is_active")).toBool();

    Product product(name, active);

    QVariantList components = map.value(QStringLiteral("components")).toList();
    foreach (const QVariant& c, components) {
        Component component = buildComponent(c.toMap());
        product.addComponent(component);

    }

    QVariantList versions = map.value(QStringLiteral("versions")).toList();
    foreach (const QVariant& v, versions) {
        Version version = buildVersion(v.toMap());
        product.addVersion(version);
    }

    return product;
}

void BugzillaManager::fetchProductInfoFinished(const QVariantMap & map)
{
    QList<Product> products;

    QVariantList plist = map.value(QStringLiteral("products")).toList();
    foreach (const QVariant& p, plist) {
        Product product = buildProduct(p.toMap());
        products.append(product);
    }

    if ( products.size() > 0 ) {
        emit productInfoFetched(products.at(0));
    } else {
        emit productInfoError();
    }
}

//END Slots to handle KJob::finished

void BugzillaManager::callMessage(const QList<QVariant> & result, const QVariant & id)
{
    qCDebug(DRKONQI_LOG) << id << result;

    if (id.toString() == QLatin1String("login")) {
        if ((m_security == UseTokens) && (result.count() > 0)) {
            QVariantMap map = result.at(0).toMap();
            m_token = map.value(QLatin1String("token")).toString();
        }
        m_logged = true;
        Q_EMIT loginFinished(true);
    } else if (id.toString() == QLatin1String("Product.get.versions")) {
        QVariantMap map = result.at(0).toMap();
        fetchProductInfoFinished(map);
    } else if (id.toString() == QLatin1String("Bug.create")) {
        QVariantMap map = result.at(0).toMap();
        int bug_id = map.value(QLatin1String("id")).toInt();
        Q_ASSERT(bug_id != 0);
        Q_EMIT reportSent(bug_id);
    } else if (id.toString() == QLatin1String("Bug.add_attachment")) {
        QVariantMap map = result.at(0).toMap();
        if (map.contains(QLatin1String("attachments"))){  // for bugzilla 4.2
            map = map.value(QLatin1String("attachments")).toMap();
            map = map.constBegin()->toMap();
            const int attachment_id = map.value(QLatin1String("id")).toInt();
            Q_EMIT attachToReportSent(attachment_id);
        } else if (map.contains(QLatin1String("ids"))) {  // for bugzilla 4.4
            const int attachment_id = map.value(QLatin1String("ids")).toList().at(0).toInt();
            Q_EMIT attachToReportSent(attachment_id);
        }
    } else if (id.toString() == QLatin1String("Bug.update.cc")) {
        QVariantMap map = result.at(0).toMap().value(QLatin1String("bugs")).toList().at(0).toMap();
        int bug_id = map.value(QLatin1String("id")).toInt();
        Q_ASSERT(bug_id != 0);
        Q_EMIT addMeToCCFinished(bug_id);
    } else if (id.toString() == QLatin1String("version")) {
        QVariantMap map = result.at(0).toMap();
        QString bugzillaVersion = map.value(QLatin1String("version")).toString();
        setFeaturesForVersion(bugzillaVersion);
        Q_EMIT bugzillaVersionFound();
    }
}

void BugzillaManager::callFault(int errorCode, const QString & errorString, const QVariant & id)
{
    qCDebug(DRKONQI_LOG) << id << errorCode << errorString;

    QString genericError = i18nc("@info", "Received unexpected error code %1 from bugzilla. "
                                 "Error message was: %2", errorCode, errorString);

    if (id.toString() == QLatin1String("login")) {
        switch(errorCode) {
        case 300: //invalid username or password
            Q_EMIT loginFinished(false); //TODO replace with loginError
            break;
        default:
            Q_EMIT loginError(genericError);
            break;
        }
    } else if (id.toString() == QLatin1String("Bug.create")) {
        switch (errorCode) {
        case 51:  //invalid object (one example is invalid platform value)
        case 105: //invalid component
        case 106: //invalid product
            Q_EMIT sendReportErrorInvalidValues();
            break;
        default:
            Q_EMIT sendReportError(genericError);
            break;
        }
    } else if (id.toString() == QLatin1String("Bug.add_attachment")) {
        switch (errorCode) {
        default:
            Q_EMIT attachToReportError(genericError);
            break;
        }
    } else if (id.toString() == QLatin1String("Bug.update.cc")) {
        switch (errorCode) {
        default:
            Q_EMIT addMeToCCError(genericError);
            break;
        }
    }
}

//END BugzillaManager

//BEGIN BugzillaCSVParser

BugListCSVParser::BugListCSVParser(const QByteArray& data)
{
    m_data = data;
    m_isValid = false;
}

BugMapList BugListCSVParser::parse()
{
    BugMapList list;

    if (!m_data.isEmpty()) {
        //Parse buglist CSV
        QTextStream ts(&m_data);
        QString headersLine = ts.readLine().remove(QLatin1Char('\"')) ;   //Discard headers
        QString expectedHeadersLine = QString::fromLatin1(columns);

        if (headersLine == (QStringLiteral("bug_id,") + expectedHeadersLine)) {
            QStringList headers = expectedHeadersLine.split(QLatin1Char(','), QString::KeepEmptyParts);
            int headersCount = headers.count();

            while (!ts.atEnd()) {
                BugMap bug; //bug report data map

                QString line = ts.readLine();

                //Get bug_id (always at first column)
                int bug_id_index = line.indexOf(QLatin1Char(','));
                QString bug_id = line.left(bug_id_index);
                bug.insert(QStringLiteral("bug_id"), bug_id);

                line = line.mid(bug_id_index + 2);

                QStringList fields = line.split(QStringLiteral(",\""));

                for (int i = 0; i < headersCount && i < fields.count(); i++) {
                    QString field = fields.at(i);
                    field = field.left(field.size() - 1) ;   //Remove trailing "
                    bug.insert(headers.at(i), field);
                }

                list.append(bug);
            }

            m_isValid = true;
        }
    }

    return list;
}

//END BugzillaCSVParser

//BEGIN BugzillaXMLParser

BugReportXMLParser::BugReportXMLParser(const QByteArray & data)
{
    m_valid = m_xml.setContent(data, true);
}

BugReport BugReportXMLParser::parse()
{
    BugReport report; //creates an invalid and empty report object

    if (m_valid) {
        //Check bug notfound
        QDomNodeList bug_number = m_xml.elementsByTagName(QStringLiteral("bug"));
        QDomNode d = bug_number.at(0);
        QDomNamedNodeMap a = d.attributes();
        QDomNode d2 = a.namedItem(QStringLiteral("error"));
        m_valid = d2.isNull();

        if (m_valid) {
            report.setValid(true);

            //Get basic fields
            report.setBugNumber(getSimpleValue(QStringLiteral("bug_id")));
            report.setShortDescription(getSimpleValue(QStringLiteral("short_desc")));
            report.setProduct(getSimpleValue(QStringLiteral("product")));
            report.setComponent(getSimpleValue(QStringLiteral("component")));
            report.setVersion(getSimpleValue(QStringLiteral("version")));
            report.setOperatingSystem(getSimpleValue(QStringLiteral("op_sys")));
            report.setBugStatus(getSimpleValue(QStringLiteral("bug_status")));
            report.setResolution(getSimpleValue(QStringLiteral("resolution")));
            report.setPriority(getSimpleValue(QStringLiteral("priority")));
            report.setBugSeverity(getSimpleValue(QStringLiteral("bug_severity")));
            report.setMarkedAsDuplicateOf(getSimpleValue(QStringLiteral("dup_id")));
            report.setVersionFixedIn(getSimpleValue(QStringLiteral("cf_versionfixedin")));

            //Parse full content + comments
            QStringList m_commentList;
            QDomNodeList comments = m_xml.elementsByTagName(QStringLiteral("long_desc"));
            for (int i = 0; i < comments.count(); i++) {
                QDomElement element = comments.at(i).firstChildElement(QStringLiteral("thetext"));
                m_commentList << element.text();
            }

            report.setComments(m_commentList);

        } //isValid
    } //isValid

    return report;
}

QString BugReportXMLParser::getSimpleValue(const QString & name)   //Extract an unique tag from XML
{
    QString ret;

    QDomNodeList bug_number = m_xml.elementsByTagName(name);
    if (bug_number.count() == 1) {
        QDomNode node = bug_number.at(0);
        ret = node.toElement().text();
    }
    return ret;
}

//END BugzillaXMLParser

void BugReport::setBugStatus(const QString &stat)
{
    setData(QStringLiteral("bug_status"), stat);

    m_status = parseStatus(stat);
}

void BugReport::setResolution(const QString &res)
{
    setData(QStringLiteral("resolution"), res);

    m_resolution = parseResolution(res);
}

BugReport::Status BugReport::parseStatus(const QString &stat)
{
    if (stat == QLatin1String("UNCONFIRMED")) {
        return Unconfirmed;
    } else if (stat == QLatin1String("CONFIRMED")) {
        return New;
    } else if (stat == QLatin1String("ASSIGNED")) {
        return Assigned;
    } else if (stat == QLatin1String("REOPENED")) {
        return Reopened;
    } else if (stat == QLatin1String("RESOLVED")) {
        return Resolved;
    } else if (stat == QLatin1String("NEEDSINFO")) {
        return NeedsInfo;
    } else if (stat == QLatin1String("VERIFIED")) {
        return Verified;
    } else if (stat == QLatin1String("CLOSED")) {
        return Closed;
    } else {
        return UnknownStatus;
    }
}

BugReport::Resolution BugReport::parseResolution(const QString &res)
{
    if (res.isEmpty()) {
        return NotResolved;
    } else if (res == QLatin1String("FIXED")) {
        return Fixed;
    } else if (res == QLatin1String("INVALID")) {
        return Invalid;
    } else if (res == QLatin1String("WONTFIX")) {
        return WontFix;
    } else if (res == QLatin1String("LATER")) {
        return Later;
    } else if (res == QLatin1String("REMIND")) {
        return Remind;
    } else if (res == QLatin1String("DUPLICATE")) {
        return Duplicate;
    } else if (res == QLatin1String("WORKSFORME")) {
        return WorksForMe;
    } else if (res == QLatin1String("MOVED")) {
        return Moved;
    } else if (res == QLatin1String("UPSTREAM")) {
        return Upstream;
    } else if (res == QLatin1String("DOWNSTREAM")) {
        return Downstream;
    } else if (res == QLatin1String("WAITINGFORINFO")) {
        return WaitingForInfo;
    } else if (res == QLatin1String("BACKTRACE")) {
        return Backtrace;
    } else if (res == QLatin1String("UNMAINTAINED")) {
        return Unmaintained;
    } else {
        return UnknownResolution;
    }
}
