/***************************************************************************
 * SPDX-FileCopyrightText: 2024 S. MANKOWSKI stephane@mankowski.fr
 * SPDX-FileCopyrightText: 2024 G. DE BURE support@mankowski.fr
 * SPDX-License-Identifier: GPL-3.0-or-later
 ***************************************************************************/
/** @file
 * This file is Skrooge plugin for ledger import / export.
 *
 * @author Stephane MANKOWSKI / Guillaume DE BURE
 */
#include "skgimportpluginledger.h"

#include <klocalizedstring.h>
#include <kpluginfactory.h>

#include <qdir.h>
#include <qfile.h>
#include <qprocess.h>
#include <qsavefile.h>
#include <quuid.h>

#include "skgbankincludes.h"
#include "skgdocumentbank.h"
#include "skgservices.h"
#include "skgtraces.h"
#include <utility>

/**
 * This plugin factory.
 */
K_PLUGIN_CLASS_WITH_JSON(SKGImportPluginLedger, "metadata.json")

SKGImportPluginLedger::SKGImportPluginLedger(QObject *iImporter, const QVariantList &iArg)
    : SKGImportPlugin(iImporter)
{
    SKGTRACEINFUNC(10)
    Q_UNUSED(iArg)

    m_importParameters[QStringLiteral("ledger_account_identification")] = QStringLiteral("COMPTE,COMPTES,CAPITAUX,ASSETS,LIABILITIES,SAVING");
}

SKGImportPluginLedger::~SKGImportPluginLedger() = default;

bool SKGImportPluginLedger::isExportPossible()
{
    SKGTRACEINFUNC(10)
    return (m_importer->getDocument() == nullptr ? true : m_importer->getFileNameExtension() == QStringLiteral("LEDGER"));
}

SKGError SKGImportPluginLedger::exportFile()
{
    SKGError err;
    QSaveFile file(m_importer->getLocalFileName(false));
    if (!file.open(QIODevice::WriteOnly)) {
        err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Save file '%1' failed", m_importer->getFileName().toDisplayString()));
    } else {
        auto listUUIDs = SKGServices::splitCSVLine(m_exportParameters.value(QStringLiteral("uuid_of_selected_accounts_or_operations")));

        QString wc;
        for (const auto &uuid : std::as_const(listUUIDs)) {
            auto items = SKGServices::splitCSVLine(uuid, '-');
            if (items.at(1) == QStringLiteral("operation")) {
                if (!wc.isEmpty()) {
                    wc += QStringLiteral(" AND ");
                }
                wc += " i_OPID=" + items.at(0);
            } else if (items.at(1) == QStringLiteral("account")) {
                if (!wc.isEmpty()) {
                    wc += QStringLiteral(" AND ");
                }
                wc += " rd_account_id=" + items.at(0);
            }
        }
        if (wc.isEmpty()) {
            wc = QStringLiteral("1=1");
        } else {
            IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Only selected accounts and transactions have been exported")))
        }

        QLocale en(QLocale::C);
        QTextStream stream(&file);
        if (!m_importer->getCodec().isEmpty()) {
#ifdef SKG_QT6
            stream.setEncoding(QStringConverter::encodingForName(m_importer->getCodec().toLatin1().constData()).value_or(QStringConverter::Utf8));
#else
            stream.setCodec(m_importer->getCodec().toLatin1().constData());
#endif
        }
        stream << "; -*- ledger file generated by Skrooge -*-" << Qt::endl;
        err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export %1 file", "ledger"), 2);
        IFOK(err)
        {
            auto punit = m_importer->getDocument()->getPrimaryUnit();
            SKGObjectBase::SKGListSKGObjectBase units;
            err = m_importer->getDocument()->getObjects(QStringLiteral("v_unit"), QStringLiteral("t_type NOT IN ('C', '1', '2')"), units);
            int nb = units.count();
            IFOK(err)
            {
                err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export units"), nb);
                for (int i = 0; !err && i < nb; ++i) {
                    SKGUnitObject unit(units.at(i));
                    QString qs =
                        en.toCurrencyString(SKGServices::stringToDouble(unit.getAttribute(QStringLiteral("f_CURRENTAMOUNT"))), punit.Symbol, punit.NbDecimal);
                    stream << "P " << SKGServices::dateToSqlString(QDate::currentDate()).replace('-', '/') << " \"" << unit.getSymbol() << '"' << " " << qs
                           << Qt::endl;
                    stream << Qt::endl;

                    IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
                }

                SKGENDTRANSACTION(m_importer->getDocument(), err)
            }
        }

        IFOKDO(err, m_importer->getDocument()->stepForward(1))

        IFOK(err)
        {
            SKGObjectBase::SKGListSKGObjectBase transactions;
            err =
                m_importer->getDocument()->getObjects(QStringLiteral("v_operation"), wc % QStringLiteral(" AND t_template='N' ORDER BY d_date"), transactions);
            int nb = transactions.count();
            IFOK(err)
            {
                err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Export step", "Export transactions"), nb);
                for (int i = 0; !err && i < nb; ++i) {
                    SKGOperationObject op(transactions.at(i));
                    auto status = op.getStatus();
                    auto number = op.getNumber();

                    SKGPayeeObject payee;
                    op.getPayee(payee);

                    SKGUnitObject unit;
                    op.getUnit(unit);
                    bool isCurrency =
                        unit.getType() == SKGUnitObject::CURRENCY || unit.getType() == SKGUnitObject::PRIMARY || unit.getType() == SKGUnitObject::SECONDARY;

                    auto payeeString = payee.getName();
                    if (payeeString.isEmpty()) {
                        payeeString = op.getComment();
                    }

                    auto nbDec = SKGServices::stringToInt(op.getAttribute(QStringLiteral("i_NBDEC")));
                    if (nbDec == 0) {
                        nbDec = 2;
                    }
                    QString symbol = unit.getSymbol();
                    if (symbol.contains(QStringLiteral(" "))) {
                        symbol = '"' + symbol + '"';
                    }
                    QString qs = en.toCurrencyString(SKGServices::stringToDouble(op.getAttribute(QStringLiteral("f_QUANTITY"))), QStringLiteral(" "), nbDec);
                    if (isCurrency) {
                        qs = symbol + qs;
                    } else {
                        qs = qs + ' ' + symbol;
                    }

                    stream << SKGServices::dateToSqlString(op.getDate()).replace('-', '/')
                           << (status == SKGOperationObject::CHECKED      ? " *"
                                   : status == SKGOperationObject::MARKED ? " !"
                                                                          : "")
                           << (!number.isEmpty() ? QStringLiteral(" (") % number % ")" : QString()) << QStringLiteral(" ") << payeeString << Qt::endl;
                    stream << "  ; Skrooge ID: " << op.getID() << Qt::endl;
                    stream << "  ; Import ID: " << op.getImportID() << Qt::endl;
                    auto properties = op.getProperties();
                    for (const auto &p : std::as_const(properties)) {
                        stream << "  ; " << p << ": " << op.getProperty(p) << Qt::endl;
                    }
                    stream << "  " << i18nc("The default category for the accounts for ledger export", "Account") << ':'
                           << op.getAttribute(QStringLiteral("t_ACCOUNT")) << "  " << qs << Qt::endl;

                    SKGObjectBase::SKGListSKGObjectBase subtransactions;
                    IFOKDO(err, op.getSubOperations(subtransactions))
                    int nbsuboperations = subtransactions.count();
                    for (int j = 0; !err && j < nbsuboperations; ++j) {
                        SKGSubOperationObject sop(subtransactions.at(j));
                        SKGCategoryObject cat;
                        sop.getCategory(cat);
                        auto &catString = cat.getFullName().replace(OBJECTSEPARATOR, QStringLiteral(":"));
                        if (catString.isEmpty()) {
                            catString = i18nc("Category not defined", "Not defined");
                        }
                        QString qs = en.toCurrencyString(-sop.getQuantity(), QStringLiteral(" "), nbDec);
                        if (isCurrency) {
                            qs = unit.getSymbol() + qs;
                        } else {
                            qs = qs + ' ' + unit.getSymbol();
                        }

                        stream << "  " << i18nc("The default category for the categories for ledger export", "Category") << ':' << catString << "  " << qs;
                        if (sop.getDate() != op.getDate()) {
                            stream << "  ; [=" << SKGServices::dateToSqlString(sop.getDate()).replace('-', '/') << "]";
                        }

                        auto comment = sop.getComment();
                        if (!comment.isEmpty()) {
                            stream << "  ;comment=" << comment;
                        }
                        stream << "  ; Skrooge ID: " << sop.getID();
                        stream << Qt::endl;
                    }
                    stream << Qt::endl;

                    IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
                }

                SKGENDTRANSACTION(m_importer->getDocument(), err)
            }
        }

        IFOKDO(err, m_importer->getDocument()->stepForward(2))

        SKGENDTRANSACTION(m_importer->getDocument(), err)

        // Close file
        file.commit();
    }
    return err;
}

bool SKGImportPluginLedger::isImportPossible()
{
    SKGTRACEINFUNC(10)
    return (m_importer->getDocument() == nullptr ? true : m_importer->getFileNameExtension() == QStringLiteral("LEDGER"));
}

bool SKGImportPluginLedger::isAccount(const QString &type)
{
    return m_importParameters.value(QStringLiteral("ledger_account_identification")).split(u',').indexOf(type) != -1;
}

SKGError SKGImportPluginLedger::importFile()
{
    if (m_importer->getDocument() == nullptr) {
        return SKGError(ERR_ABORT, i18nc("Error message", "Invalid parameters"));
    }
    SKGError err;
    SKGTRACEINFUNCRC(2, err)

    // Initialisation
    // Generate xml
    QString uniqueId = QUuid::createUuid().toString();
    QString temporaryPath = QDir::tempPath() % "/" % uniqueId % ".xml";
    QString cmd = "ledger -f \"" % m_importer->getLocalFileName() % "\" xml --output " % temporaryPath;
    SKGTRACEL(10) << "Execution of :" << cmd << Qt::endl;
    QProcess p;
    p.start(QStringLiteral("/bin/bash"), QStringList() << QStringLiteral("-c") << cmd);
    if (p.waitForFinished(1000 * 60 * 5) && p.exitCode() == 0) {
        // Open file
        QFile file(temporaryPath);
        if (!file.open(QIODevice::ReadOnly)) {
            err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Open file '%1' failed", m_importer->getFileName().toDisplayString()));
        } else {
            QDomDocument doc;

            // Set the file
            QString errorMsg;
            int errorLine = 0;
            int errorCol = 0;
            bool contentOK = false;
#ifdef SKG_QT6
            const auto &content = QString::fromUtf8(file.readAll());
            const auto &result = doc.setContent(content);

            contentOK = !!result;
            errorLine = result.errorLine;
            errorCol = result.errorColumn;
            errorMsg = result.errorMessage;
#else
            contentOK = doc.setContent(file.readAll(), &errorMsg, &errorLine, &errorCol);
#endif
            file.close();

            // Get root
            QDomElement docElem = doc.documentElement();
            if (!contentOK) {
                err.setReturnCode(ERR_ABORT).setMessage(i18nc("Error message", "%1-%2: '%3'", errorLine, errorCol, errorMsg));
                err.addError(ERR_INVALIDARG, i18nc("Error message", "Invalid XML content in file '%1'", m_importer->getFileName().toDisplayString()));
            } else {
                err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import %1 file", "LEDGER"), 2);

                QMap<QString, SKGUnitObject> mapIdUnit;
                QMap<QString, SKGAccountObject> mapIdAccount;
                QMap<QString, SKGCategoryObject> mapIdCategory;
                QMap<QString, SKGPayeeObject> mapIdPayee;

                // Step 1-Create units
                IFOK(err)
                {
                    auto commodityL = docElem.elementsByTagName(QStringLiteral("commodity"));
                    int nb = commodityL.count();
                    err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import units"), nb);
                    for (int i = 0; !err && i < nb; ++i) {
                        // Get unit object
                        auto commodity = commodityL.at(i).toElement();
                        auto symbol = commodity.firstChildElement(QStringLiteral("symbol")).toElement().text();

                        // Creation of the units
                        SKGUnitObject unitObj(m_importer->getDocument());
                        IFOKDO(err, unitObj.setName(symbol))
                        IFOKDO(err, unitObj.setSymbol(symbol))
                        IFOKDO(err, unitObj.setNumberDecimal(2))
                        IFOKDO(err, unitObj.save())

                        // Creation of the unit values
                        auto annotation = commodity.firstChildElement(QStringLiteral("annotation")).toElement();
                        auto price = annotation.firstChildElement(QStringLiteral("price")).toElement();
                        auto commodity2 = price.firstChildElement(QStringLiteral("commodity")).toElement();
                        auto quantity = price.firstChildElement(QStringLiteral("quantity")).toElement().text().trimmed();
                        auto symbol2 = commodity2.firstChildElement(QStringLiteral("symbol")).toElement();
                        auto date = annotation.firstChildElement(QStringLiteral("date")).toElement().text().trimmed();
                        if (!date.isNull() && !symbol2.isNull() && !quantity.isNull()) {
                            SKGUnitValueObject unitValueObj;
                            IFOKDO(err, unitObj.addUnitValue(unitValueObj))
                            IFOKDO(err, unitValueObj.setDate(QDate::fromString(date, QStringLiteral("yyyy/MM/dd"))))
                            IFOKDO(err, unitValueObj.setQuantity(1.0 / SKGServices::stringToDouble(quantity)))
                            IFOKDO(err, unitValueObj.save())
                        }

                        mapIdUnit[symbol] = unitObj;

                        IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
                    }

                    SKGENDTRANSACTION(m_importer->getDocument(), err)
                }
                IFOKDO(err, m_importer->getDocument()->stepForward(1))

                // Step 2-Create transaction
                IFOK(err)
                {
                    auto transactionL = docElem.elementsByTagName(QStringLiteral("transaction"));
                    int nb = transactionL.count();
                    err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import transactions"), nb);
                    for (int i = 0; !err && i < nb; ++i) {
                        // Get account object
                        auto transaction = transactionL.at(i).toElement();

                        auto date = transaction.firstChildElement(QStringLiteral("date")).toElement().text().trimmed();
                        auto payee = transaction.firstChildElement(QStringLiteral("payee")).toElement().text().trimmed();
                        auto note = transaction.firstChildElement(QStringLiteral("note")).toElement().text().trimmed();
                        auto status = getAttribute(transaction, QStringLiteral("state"));

                        // Create transaction and suboperation
                        SKGOperationObject opObj;
                        SKGSubOperationObject subObj;
                        auto postingL = transaction.elementsByTagName(QStringLiteral("posting"));

                        auto symbol = postingL.at(0).toElement().elementsByTagName(QStringLiteral("symbol")).at(0).toElement().text().trimmed();
                        int nb2 = postingL.count();

                        // Put account in the first place
                        QList<QDomElement> list_porting;
                        for (int i2 = 0; !err && i2 < nb2; ++i2) {
                            auto posting = postingL.at(i2).toElement();
                            auto account = posting.firstChildElement(QStringLiteral("account")).toElement();
                            auto type = account.firstChildElement(QStringLiteral("name")).toElement().text().split(QStringLiteral(":"))[0].toUpper().trimmed();

                            if (isAccount(type)) {
                                list_porting.insert(0, posting);
                            } else {
                                list_porting.append(posting);
                            }
                        }
                        SKGOperationObject opPreviousObj;
                        for (int i2 = 0; !err && i2 < nb2; ++i2) {
                            auto posting = list_porting.at(i2);
                            auto account = posting.firstChildElement(QStringLiteral("account")).toElement();
                            auto name = account.firstChildElement(QStringLiteral("name")).toElement().text().trimmed();
                            auto snote = posting.firstChildElement(QStringLiteral("note")).toElement().text().trimmed();

                            auto postamount = posting.firstChildElement(QStringLiteral("post-amount")).toElement();

                            auto amount = postamount.firstChildElement(QStringLiteral("amount")).toElement();
                            auto quantity = amount.firstChildElement(QStringLiteral("quantity")).toElement().text().trimmed();

                            auto names = name.split(QStringLiteral(":"));
                            QString type;
                            if (names.length() > 1) {
                                type = names[0].toUpper();
                                name = name.right(name.length() - type.length() - 1);
                            }
                            SKGTRACEL(2) << "Sub transaction : " << name << ": " << date << ": " << payee << " | " << quantity << Qt::endl;

                            auto isaccount = isAccount(type);
                            if (i2 > 0) {
                                if (isaccount) {
                                    // Save the subtransaction
                                    IFOKDO(err, subObj.save())
                                } else {
                                    // Forget the subtransaction
                                    IFOKDO(err, opObj.load())
                                }
                            }
                            if (isaccount || !opObj.exist()) {
                                auto account_id = getAttribute(account, QStringLiteral("ref"));

                                SKGAccountObject accountObj;
                                if (!mapIdAccount.contains(account_id)) {
                                    auto err2 = m_importer->getDocument()->getObject(QStringLiteral("v_account"),
                                                                                     "t_name='" % SKGServices::stringToSqlString(name) % u'\'',
                                                                                     accountObj);
                                    if (!!err2) {
                                        SKGBankObject bankDefault(m_importer->getDocument());
                                        IFOKDO(err, bankDefault.setName(QStringLiteral("LEDGER")))
                                        IFOKDO(err, bankDefault.save())
                                        IFOK(err)
                                        {
                                            IFOKDO(err, bankDefault.addAccount(accountObj))
                                            IFOKDO(err, accountObj.setName(name))
                                            IFOKDO(err, accountObj.save())
                                            mapIdAccount[account_id] = accountObj;
                                        }
                                    }
                                } else {
                                    accountObj = mapIdAccount[account_id];
                                }

                                // Creation of the transaction
                                IFOKDO(err, accountObj.addOperation(opObj, true))
                                IFOKDO(err, opObj.setDate(QDate::fromString(date, QStringLiteral("yyyy/MM/dd"))))

                                IFOKDO(err, opObj.setUnit(mapIdUnit[symbol]))
                                if (!payee.isEmpty()) {
                                    SKGPayeeObject payeeObject;
                                    if (!mapIdPayee.contains(payee)) {
                                        IFOKDO(err, SKGPayeeObject::createPayee(m_importer->getDocument(), payee, payeeObject))
                                        mapIdPayee[payee] = payeeObject;
                                    } else {
                                        payeeObject = mapIdPayee[payee];
                                    }

                                    IFOKDO(err, opObj.setPayee(payeeObject))
                                }
                                IFOKDO(err, opObj.setComment(note))
                                IFOKDO(err, opObj.setImported(true))
                                IFOKDO(err, opObj.setImportID(QStringLiteral("LEDGER-")))
                                IFOKDO(err,
                                       opObj.setStatus(status == QStringLiteral("cleared")       ? SKGOperationObject::CHECKED
                                                           : status == QStringLiteral("pending") ? SKGOperationObject::MARKED
                                                                                                 : SKGOperationObject::NONE))
                                IFOKDO(err, opObj.save())

                                if (opPreviousObj.getID()) {
                                    IFOKDO(err, opPreviousObj.setGroupOperation(opObj));
                                    IFOKDO(err, opPreviousObj.save())
                                } else {
                                    opPreviousObj = opObj;
                                }
                            }

                            // Creation of the subtransaction
                            IFOKDO(err, opObj.addSubOperation(subObj))
                            if (!isaccount) {
                                SKGCategoryObject catObj;
                                if (!mapIdCategory.contains(name)) {
                                    IFOKDO(err,
                                           SKGCategoryObject::createPathCategory(m_importer->getDocument(),
                                                                                 name.replace(QStringLiteral(":"), QStringLiteral(" > ")),
                                                                                 catObj))
                                    mapIdCategory[name] = catObj;
                                } else {
                                    catObj = mapIdCategory[name];
                                }
                                IFOKDO(err, subObj.setCategory(catObj))
                            }
                            if (snote.startsWith(QStringLiteral("[=")) && snote.endsWith(QStringLiteral("]"))) {
                                IFOKDO(err, subObj.setDate(QDate::fromString(snote.mid(2, snote.length() - 3), QStringLiteral("yyyy/MM/dd"))))
                            } else {
                                IFOKDO(err, subObj.setComment(snote))
                            }
                            IFOKDO(err, subObj.setQuantity((isaccount ? 1 : -1) * SKGServices::stringToDouble(quantity)))
                            if (!isaccount || i2 == nb2 - 1) {
                                IFOKDO(err, subObj.save())
                            }
                        }

                        if (!err && i % 500 == 0) {
                            err = m_importer->getDocument()->executeSqliteOrder(QStringLiteral("ANALYZE"));
                        }
                        IFOKDO(err, m_importer->getDocument()->stepForward(i + 1))
                    }

                    SKGENDTRANSACTION(m_importer->getDocument(), err)
                }
                IFOKDO(err, m_importer->getDocument()->stepForward(2))
                SKGENDTRANSACTION(m_importer->getDocument(), err)

                IFOKDO(err, m_importer->getDocument()->executeSqliteOrder(QStringLiteral("ANALYZE")))
            }
        }
    } else {
        err.setReturnCode(ERR_FAIL)
            .setMessage(i18nc("Error message", "The execution of '%1' failed", cmd))
            .addError(ERR_FAIL, i18nc("Error message", "The ledger conversion in xml of '%1' failed", m_importer->getFileName().toDisplayString()));
    }
    return err;
}

QString SKGImportPluginLedger::getMimeTypeFilter() const
{
    return "*.ledger|" % i18nc("A file format", "Ledger file");
}

QString SKGImportPluginLedger::getAttribute(const QDomElement &iElement, const QString &iAttribute)
{
    QString val = iElement.attribute(iAttribute);
    if (val == QStringLiteral("(null)")) {
        val = QString();
    }
    return val;
}
#include <skgimportpluginledger.moc>
