/***************************************************************************
 * SPDX-FileCopyrightText: 2026 S. MANKOWSKI stephane@mankowski.fr
 * SPDX-License-Identifier: GPL-3.0-or-later
 ***************************************************************************/
#include "skgmcontroler.h"

#include "skgobjectmodelbase.h"
#include "skgreportbank.h"
#include "skgservices.h"
#include "skgsortfilterproxymodel.h"

#include <klocalizedstring.h>

#include <QDate>
#include <QSettings>
#include <QVariantMap>
#include <QtGlobal>
#include <qfileinfo.h>
#include <qlocale.h>
#include <qstringbuilder.h>

#include "skgtraces.h"
#include <algorithm>
#include <utility>

SKGMControler::SKGMControler(QObject *iParent)
    : QObject(iParent)
{
    SKGTRACEINFUNC(1)
    m_accountsObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_account_display"), QStringLiteral("1=0"), nullptr);
    m_transactionsObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_operation_display"), QStringLiteral("1=0"), nullptr);
    m_subTransactionsObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_suboperation_consolidated"), QStringLiteral("1=0"), nullptr);
    m_trackersObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_refund_display"), QStringLiteral("1=0"), nullptr);
    m_payeesObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_payee_display"), QStringLiteral("1=0"), nullptr);
    m_categoriesObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_category_display"), QStringLiteral("1=0"), nullptr);
    m_unitsObjectModel = new SKGObjectModelBase(&m_document, QStringLiteral("v_unit_display"), QStringLiteral("1=0"), nullptr);
    m_transactionsProxyModel = new SKGSortFilterProxyModel(this);
    m_transactionsProxyModel->setSourceModel(m_transactionsObjectModel);
    m_transactionsProxyModel->setProperty("filter", m_globalFilterText);
    m_subTransactionsProxyModel = new SKGSortFilterProxyModel(this);
    m_subTransactionsProxyModel->setSourceModel(m_subTransactionsObjectModel);
    m_subTransactionsProxyModel->setProperty("filter", m_globalFilterText);
    m_trackersProxyModel = new SKGSortFilterProxyModel(this);
    m_trackersProxyModel->setSourceModel(m_trackersObjectModel);
    m_trackersProxyModel->setProperty("filter", m_globalFilterText);
    m_payeesProxyModel = new SKGSortFilterProxyModel(this);
    m_payeesProxyModel->setSourceModel(m_payeesObjectModel);
    m_payeesProxyModel->setProperty("filter", m_globalFilterText);
    m_categoriesProxyModel = new SKGSortFilterProxyModel(this);
    m_categoriesProxyModel->setSourceModel(m_categoriesObjectModel);
    m_categoriesProxyModel->setProperty("filter", m_globalFilterText);
    m_unitsProxyModel = new SKGSortFilterProxyModel(this);
    m_unitsProxyModel->setSourceModel(m_unitsObjectModel);
    m_unitsProxyModel->setProperty("filter", m_globalFilterText);

    m_trackersModel.setKind(SKGMReferenceProxyModel::TrackerKind);
    m_trackersModel.setObjectModel(m_trackersObjectModel);
    m_trackersModel.setSourceModel(m_trackersProxyModel);
    m_payeesModel.setKind(SKGMReferenceProxyModel::PayeeKind);
    m_payeesModel.setObjectModel(m_payeesObjectModel);
    m_payeesModel.setSourceModel(m_payeesProxyModel);
    m_categoriesModel.setKind(SKGMReferenceProxyModel::CategoryKind);
    m_categoriesModel.setObjectModel(m_categoriesObjectModel);
    m_categoriesModel.setSourceModel(m_categoriesProxyModel);
    m_unitsModel.setKind(SKGMReferenceProxyModel::UnitKind);
    m_unitsModel.setObjectModel(m_unitsObjectModel);
    m_unitsModel.setSourceModel(m_unitsProxyModel);
    loadPersistedState();
}

SKGMControler::~SKGMControler()
{
    SKGTRACEINFUNC(1)
    delete m_accountsObjectModel;
    m_accountsObjectModel = nullptr;
    delete m_transactionsObjectModel;
    m_transactionsObjectModel = nullptr;
    delete m_subTransactionsObjectModel;
    m_subTransactionsObjectModel = nullptr;
    delete m_trackersObjectModel;
    m_trackersObjectModel = nullptr;
    delete m_payeesObjectModel;
    m_payeesObjectModel = nullptr;
    delete m_categoriesObjectModel;
    m_categoriesObjectModel = nullptr;
    delete m_unitsObjectModel;
    m_unitsObjectModel = nullptr;
    m_transactionsProxyModel = nullptr;
    m_subTransactionsProxyModel = nullptr;
    m_trackersProxyModel = nullptr;
    m_payeesProxyModel = nullptr;
    m_categoriesProxyModel = nullptr;
    m_unitsProxyModel = nullptr;
}

QString SKGMControler::filePath() const
{
    return m_filePath;
}

void SKGMControler::setFilePath(const QString &iFilePath)
{
    if (m_filePath == iFilePath) {
        return;
    }
    m_filePath = iFilePath;
    Q_EMIT filePathChanged();
}

QString SKGMControler::password() const
{
    return m_password;
}

void SKGMControler::setPassword(const QString &iPassword)
{
    if (m_password == iPassword) {
        return;
    }
    m_password = iPassword;
    Q_EMIT passwordChanged();
}

bool SKGMControler::showClosedAccounts() const
{
    return m_showClosedAccounts;
}

void SKGMControler::setShowClosedAccounts(bool iShowClosedAccounts)
{
    if (m_showClosedAccounts == iShowClosedAccounts) {
        return;
    }
    m_showClosedAccounts = iShowClosedAccounts;
    Q_EMIT showClosedAccountsChanged();
    if (m_documentLoaded && m_accountsDataLoaded) {
        reloadAccountsFromCurrentDocument();
    }
}

bool SKGMControler::showClosedTrackers() const
{
    return m_showClosedTrackers;
}

void SKGMControler::setShowClosedTrackers(bool iShowClosedTrackers)
{
    if (m_showClosedTrackers == iShowClosedTrackers) {
        return;
    }
    m_showClosedTrackers = iShowClosedTrackers;
    Q_EMIT showClosedTrackersChanged();
    if (m_documentLoaded && m_trackersDataLoaded) {
        reloadTrackersFromCurrentDocument();
    }
}

QString SKGMControler::globalFilterText() const
{
    return m_globalFilterText;
}

void SKGMControler::setGlobalFilterText(const QString &iFilterText)
{
    SKGTRACEINFUNC(1)
    if (m_globalFilterText == iFilterText) {
        return;
    }
    m_globalFilterText = iFilterText;
    if (m_transactionsProxyModel != nullptr) {
        m_transactionsProxyModel->setProperty("filter", m_globalFilterText);
        if (m_transactionsDataLoaded) {
            m_transactionsProxyModel->invalidate();
            rebuildTransactionsFromProxy();
        }
    }
    if (m_subTransactionsProxyModel != nullptr) {
        m_subTransactionsProxyModel->setProperty("filter", m_globalFilterText);
        if (m_subTransactionsDataLoaded) {
            m_subTransactionsProxyModel->invalidate();
            rebuildSubTransactionsFromProxy();
        }
    }
    if (m_documentLoaded) {
        if (m_accountsDataLoaded) {
            reloadAccountsFromCurrentDocument();
        }
        if (m_trackersDataLoaded || m_payeesDataLoaded || m_categoriesDataLoaded || m_unitsDataLoaded) {
            refreshReferenceProxyFilters();
        }
    }
    Q_EMIT globalFilterTextChanged();
}

QString SKGMControler::errorMessage() const
{
    return m_errorMessage;
}

SKGMAccountListModel *SKGMControler::accountsModel()
{
    return &m_accountsModel;
}

QString SKGMControler::globalAccountsTotalText() const
{
    return m_globalAccountsTotalText;
}

QStringList SKGMControler::openAccountNames() const
{
    return m_openAccountNames;
}

int SKGMControler::selectedOpenAccountIndex() const
{
    return m_selectedOpenAccountIndex;
}

void SKGMControler::setSelectedOpenAccountIndex(int iIndex)
{
    SKGTRACEINFUNC(1)
    const int boundedIndex = (iIndex >= 0 && iIndex < m_openAccountIds.count()) ? iIndex : -1;
    if (m_selectedOpenAccountIndex == boundedIndex) {
        return;
    }
    m_selectedOpenAccountIndex = boundedIndex;
    Q_EMIT selectedOpenAccountIndexChanged();
    updateSelectedAccountBalanceText();
    if (m_transactionsDataLoaded) {
        reloadTransactionsForSelectedAccount();
    }
}

QString SKGMControler::selectedAccountBalanceText() const
{
    return m_selectedAccountBalanceText;
}

SKGMTransactionListModel *SKGMControler::transactionsModel()
{
    return &m_transactionsModel;
}

SKGMTransactionListModel *SKGMControler::subTransactionsModel()
{
    return &m_subTransactionsModel;
}

QString SKGMControler::selectedTrackerName() const
{
    return m_selectedTrackerName;
}

QString SKGMControler::transactionFilterText() const
{
    return m_globalFilterText;
}

void SKGMControler::setTransactionFilterText(const QString &iFilterText)
{
    SKGTRACEINFUNC(1)
    if (m_globalFilterText == iFilterText) {
        return;
    }
    m_transactionFilterText = iFilterText;
    setGlobalFilterText(iFilterText);
    Q_EMIT transactionFilterTextChanged();
}

int SKGMControler::transactionStatusFilter() const
{
    return m_transactionStatusFilter;
}

bool SKGMControler::passwordRequired() const
{
    return m_passwordRequired;
}

void SKGMControler::setTransactionStatusFilter(int iStatusFilter)
{
    SKGTRACEINFUNC(1)
    const int boundedFilter = qBound(0, iStatusFilter, 3);
    if (m_transactionStatusFilter == boundedFilter) {
        return;
    }
    m_transactionStatusFilter = boundedFilter;
    Q_EMIT transactionStatusFilterChanged();
    if (m_transactionsDataLoaded) {
        reloadTransactionsForSelectedAccount();
    }
}

QVariantList SKGMControler::dashboardBankTotals() const
{
    return m_dashboardBankTotals;
}

QString SKGMControler::dashboardBankGrandTotalText() const
{
    return m_dashboardBankGrandTotalText;
}

QString SKGMControler::dashboardCurrentPeriodLabel() const
{
    return m_dashboardCurrentPeriodLabel;
}

QString SKGMControler::dashboardPreviousPeriodLabel() const
{
    return m_dashboardPreviousPeriodLabel;
}

QString SKGMControler::dashboardCurrentIncomeText() const
{
    return m_dashboardCurrentIncomeText;
}

QString SKGMControler::dashboardCurrentExpenseText() const
{
    return m_dashboardCurrentExpenseText;
}

QString SKGMControler::dashboardPreviousIncomeText() const
{
    return m_dashboardPreviousIncomeText;
}

QString SKGMControler::dashboardPreviousExpenseText() const
{
    return m_dashboardPreviousExpenseText;
}

QString SKGMControler::dashboardCurrentSavingsText() const
{
    return m_dashboardCurrentSavingsText;
}

QString SKGMControler::dashboardPreviousSavingsText() const
{
    return m_dashboardPreviousSavingsText;
}

double SKGMControler::dashboardCurrentSavings() const
{
    return m_dashboardCurrentSavings;
}

double SKGMControler::dashboardPreviousSavings() const
{
    return m_dashboardPreviousSavings;
}

QString SKGMControler::dashboardPersonalFinanceScoreText() const
{
    return m_dashboardPersonalFinanceScoreText;
}

QString SKGMControler::dashboardPersonalFinanceScoreMessage() const
{
    return m_dashboardPersonalFinanceScoreMessage;
}

QString SKGMControler::dashboardPersonalFinanceScoreColor() const
{
    return m_dashboardPersonalFinanceScoreColor;
}

QString SKGMControler::dashboardNetWorthText() const
{
    return m_dashboardNetWorthText;
}

QString SKGMControler::dashboardAnnualSpendingText() const
{
    return m_dashboardAnnualSpendingText;
}

QAbstractItemModel *SKGMControler::trackersModel()
{
    return &m_trackersModel;
}

QAbstractItemModel *SKGMControler::payeesModel()
{
    return &m_payeesModel;
}

QAbstractItemModel *SKGMControler::categoriesModel()
{
    return &m_categoriesModel;
}

QAbstractItemModel *SKGMControler::unitsModel()
{
    return &m_unitsModel;
}

bool SKGMControler::openFile()
{
    return openFile(m_filePath, m_password);
}

bool SKGMControler::openFile(const QString &iFilePath, const QString &iPassword)
{
    SKGTRACEINFUNC(1)
    QString filePath = iFilePath;
    setPasswordRequired(false);
    if (filePath.isEmpty()) {
        setErrorMessage(i18nc("Error message", "Missing file path."));
        clearLoadedDocumentState();
        m_documentLoaded = false;
        return false;
    }

    QFileInfo fileInfo(filePath);
    if (!fileInfo.exists() || !fileInfo.isFile()) {
        setErrorMessage(i18nc("Error message", "File not found: %1", filePath));
        clearLoadedDocumentState();
        clearPersistedLastFilePath();
        m_documentLoaded = false;
        return false;
    }
    filePath = fileInfo.absoluteFilePath();

    SKGError err = m_document.load(filePath, iPassword);
    if (err.isFailed()) {
        setErrorMessage(err.getFullMessageWithHistorical());
        if (err.getReturnCode() == ERR_ENCRYPTION) {
            setPasswordRequired(true);
        }
        clearLoadedDocumentState();
        m_documentLoaded = false;
        return false;
    }

    m_documentLoaded = true;
    setFilePath(filePath);
    setPassword(iPassword);
    persistLastFilePath(filePath);
    clearLoadedDocumentState();
    setErrorMessage(QString());
    return true;
}

bool SKGMControler::reloadAccountsFromCurrentDocument()
{
    SKGTRACEINFUNC(1)
    m_accountsObjectModel->setTable(QStringLiteral("v_account_display"));
    m_accountsObjectModel->setFilter(QStringLiteral("1=1 ORDER BY t_TYPENLS, t_BANK, t_name"));
    m_accountsObjectModel->refresh();

    struct OpenAccountEntry {
        int id;
        QString bank;
        QString name;
        QString balanceText;
        bool bookmarked;
    };
    QVector<OpenAccountEntry> openAccountEntries;
    QVector<SKGMAccountListModel::AccountItem> accounts;
    const int nbObjects = m_accountsObjectModel->rowCount();
    accounts.reserve((nbObjects * 3) / 2);
    QString currentType;
    QString currentUnit;
    double currentTypeAmount = 0.0;
    double globalAmount = 0.0;
    QString globalUnit;
    bool hasType = false;
    int currentHeaderIndex = -1;

    auto flushCurrentTypeTotal = [&]() {
        if (!hasType || currentHeaderIndex < 0 || currentHeaderIndex >= accounts.count()) {
            return;
        }
        SKGMAccountListModel::AccountItem &headerItem = accounts[currentHeaderIndex];
        headerItem.amount = currentTypeAmount;
        headerItem.amountText = QLocale().toString(headerItem.amount, 'f', 2);
        if (!currentUnit.isEmpty()) {
            headerItem.amountText = headerItem.amountText % u' ' % currentUnit;
        }
        hasType = false;
    };

    const QString globalFilter = m_globalFilterText.trimmed().toLower();
    const bool hasGlobalFilter = !globalFilter.isEmpty();

    for (int row = 0; row < nbObjects; ++row) {
        const SKGObjectBase obj = m_accountsObjectModel->getObject(m_accountsObjectModel->index(row, 0));
        SKGMAccountListModel::AccountItem item;
        item.name = obj.getAttribute(QStringLiteral("t_name"));
        item.bank = obj.getAttribute(QStringLiteral("t_BANK"));
        item.type = obj.getAttribute(QStringLiteral("t_TYPENLS"));
        item.unit = obj.getAttribute(QStringLiteral("t_UNIT"));
        item.amount = SKGServices::stringToDouble(obj.getAttribute(QStringLiteral("f_CURRENTAMOUNT")));
        item.amountText = QLocale().toString(item.amount, 'f', 2);
        item.closed = obj.getAttribute(QStringLiteral("t_close")) == QStringLiteral("Y");
        if (!item.closed) {
            OpenAccountEntry entry;
            entry.id = SKGServices::stringToInt(obj.getAttribute(QStringLiteral("id")));
            entry.bank = item.bank;
            entry.name = item.name;
            entry.balanceText = item.amountText;
            if (!item.unit.isEmpty()) {
                entry.balanceText = entry.balanceText % u' ' % item.unit;
            }
            entry.bookmarked = obj.getAttribute(QStringLiteral("t_bookmarked")).toUpper() == QStringLiteral("Y");
            openAccountEntries.push_back(std::move(entry));
        }
        if (!m_showClosedAccounts && item.closed) {
            continue;
        }
        if (hasGlobalFilter) {
            const QString haystack = (item.name % u' ' % item.bank % u' ' % item.type).toLower();
            if (!haystack.contains(globalFilter)) {
                continue;
            }
        }

        globalAmount += item.amount;
        if (globalUnit.isEmpty() && !item.unit.isEmpty()) {
            globalUnit = item.unit;
        }

        if (!hasType || item.type != currentType) {
            flushCurrentTypeTotal();
            currentType = item.type;
            currentUnit = item.unit;
            currentTypeAmount = 0.0;
            hasType = true;

            SKGMAccountListModel::AccountItem headerItem;
            headerItem.kind = SKGMAccountListModel::TypeHeaderRow;
            headerItem.type = currentType;
            headerItem.name = currentType;
            accounts.push_back(std::move(headerItem));
            currentHeaderIndex = accounts.count() - 1;
        }

        currentTypeAmount += item.amount;
        if (!item.unit.isEmpty()) {
            item.amountText = item.amountText % u' ' % item.unit;
        }
        accounts.push_back(std::move(item));
    }
    flushCurrentTypeTotal();

    std::sort(openAccountEntries.begin(), openAccountEntries.end(), [](const OpenAccountEntry &iLeft, const OpenAccountEntry &iRight) {
        if (iLeft.bookmarked != iRight.bookmarked) {
            return iLeft.bookmarked && !iRight.bookmarked;
        }
        const int bankCmp = QString::localeAwareCompare(iLeft.bank, iRight.bank);
        if (bankCmp != 0) {
            return bankCmp < 0;
        }
        const int accountCmp = QString::localeAwareCompare(iLeft.name, iRight.name);
        if (accountCmp != 0) {
            return accountCmp < 0;
        }
        return iLeft.id < iRight.id;
    });

    QVector<int> openAccountIds;
    QStringList openAccountNames;
    QStringList openAccountBalanceTexts;
    QStringList openAccountBanks;
    QStringList openAccountAccountNames;
    openAccountIds.reserve(openAccountEntries.count());
    openAccountNames.reserve(openAccountEntries.count());
    openAccountBalanceTexts.reserve(openAccountEntries.count());
    openAccountBanks.reserve(openAccountEntries.count());
    openAccountAccountNames.reserve(openAccountEntries.count());
    for (const OpenAccountEntry &entry : std::as_const(openAccountEntries)) {
        openAccountIds.push_back(entry.id);
        openAccountNames.push_back(entry.bank.isEmpty() ? entry.name : entry.bank % QStringLiteral(" - ") % entry.name);
        openAccountBalanceTexts.push_back(entry.balanceText);
        openAccountBanks.push_back(entry.bank);
        openAccountAccountNames.push_back(entry.name);
    }

    m_accountsModel.setAccounts(std::move(accounts));
    QString globalTotalText = QLocale().toString(globalAmount, 'f', 2);
    if (!globalUnit.isEmpty()) {
        globalTotalText = globalTotalText % u' ' % globalUnit;
    }
    if (m_globalAccountsTotalText != globalTotalText) {
        m_globalAccountsTotalText = std::move(globalTotalText);
        Q_EMIT globalAccountsTotalTextChanged();
    }
    m_openAccountIds = std::move(openAccountIds);
    m_openAccountNames = std::move(openAccountNames);
    m_openAccountBalanceTexts = std::move(openAccountBalanceTexts);
    m_openAccountBanks = std::move(openAccountBanks);
    m_openAccountAccountNames = std::move(openAccountAccountNames);
    updateSelectedAccountBalanceText();
    Q_EMIT openAccountsChanged();
    if (m_openAccountIds.isEmpty()) {
        setSelectedOpenAccountIndex(-1);
    } else if (m_selectedOpenAccountIndex < 0 || m_selectedOpenAccountIndex >= m_openAccountIds.count()) {
        setSelectedOpenAccountIndex(0);
    } else if (m_transactionsDataLoaded) {
        reloadTransactionsForSelectedAccount();
    }
    m_accountsDataLoaded = true;
    m_openAccountsDataLoaded = true;
    setErrorMessage(QString());
    return true;
}

bool SKGMControler::reloadOpenAccountsFromCurrentDocument()
{
    SKGTRACEINFUNC(1)
    m_accountsObjectModel->setTable(QStringLiteral("v_account_display"));
    m_accountsObjectModel->setFilter(QStringLiteral("1=1 ORDER BY t_TYPENLS, t_BANK, t_name"));
    m_accountsObjectModel->refresh();

    struct OpenAccountEntry {
        int id;
        QString bank;
        QString name;
        QString balanceText;
        bool bookmarked;
    };
    QVector<OpenAccountEntry> openAccountEntries;
    const int nbObjects = m_accountsObjectModel->rowCount();
    openAccountEntries.reserve(nbObjects);

    for (int row = 0; row < nbObjects; ++row) {
        const SKGObjectBase obj = m_accountsObjectModel->getObject(m_accountsObjectModel->index(row, 0));
        const bool closed = obj.getAttribute(QStringLiteral("t_close")) == QStringLiteral("Y");
        if (closed) {
            continue;
        }

        OpenAccountEntry entry;
        entry.id = SKGServices::stringToInt(obj.getAttribute(QStringLiteral("id")));
        entry.bank = obj.getAttribute(QStringLiteral("t_BANK"));
        entry.name = obj.getAttribute(QStringLiteral("t_name"));
        entry.balanceText = QLocale().toString(SKGServices::stringToDouble(obj.getAttribute(QStringLiteral("f_CURRENTAMOUNT"))), 'f', 2);
        const QString unit = obj.getAttribute(QStringLiteral("t_UNIT"));
        if (!unit.isEmpty()) {
            entry.balanceText = entry.balanceText % u' ' % unit;
        }
        entry.bookmarked = obj.getAttribute(QStringLiteral("t_bookmarked")).toUpper() == QStringLiteral("Y");
        openAccountEntries.push_back(std::move(entry));
    }

    std::sort(openAccountEntries.begin(), openAccountEntries.end(), [](const OpenAccountEntry &iLeft, const OpenAccountEntry &iRight) {
        if (iLeft.bookmarked != iRight.bookmarked) {
            return iLeft.bookmarked && !iRight.bookmarked;
        }
        const int bankCmp = QString::localeAwareCompare(iLeft.bank, iRight.bank);
        if (bankCmp != 0) {
            return bankCmp < 0;
        }
        const int accountCmp = QString::localeAwareCompare(iLeft.name, iRight.name);
        if (accountCmp != 0) {
            return accountCmp < 0;
        }
        return iLeft.id < iRight.id;
    });

    QVector<int> openAccountIds;
    QStringList openAccountNames;
    QStringList openAccountBalanceTexts;
    QStringList openAccountBanks;
    QStringList openAccountAccountNames;
    openAccountIds.reserve(openAccountEntries.count());
    openAccountNames.reserve(openAccountEntries.count());
    openAccountBalanceTexts.reserve(openAccountEntries.count());
    openAccountBanks.reserve(openAccountEntries.count());
    openAccountAccountNames.reserve(openAccountEntries.count());
    for (const OpenAccountEntry &entry : std::as_const(openAccountEntries)) {
        openAccountIds.push_back(entry.id);
        openAccountNames.push_back(entry.bank.isEmpty() ? entry.name : entry.bank % QStringLiteral(" - ") % entry.name);
        openAccountBalanceTexts.push_back(entry.balanceText);
        openAccountBanks.push_back(entry.bank);
        openAccountAccountNames.push_back(entry.name);
    }

    m_openAccountIds = std::move(openAccountIds);
    m_openAccountNames = std::move(openAccountNames);
    m_openAccountBalanceTexts = std::move(openAccountBalanceTexts);
    m_openAccountBanks = std::move(openAccountBanks);
    m_openAccountAccountNames = std::move(openAccountAccountNames);
    updateSelectedAccountBalanceText();
    Q_EMIT openAccountsChanged();
    if (m_openAccountIds.isEmpty()) {
        setSelectedOpenAccountIndex(-1);
    } else if (m_selectedOpenAccountIndex < 0 || m_selectedOpenAccountIndex >= m_openAccountIds.count()) {
        setSelectedOpenAccountIndex(0);
    } else if (m_transactionsDataLoaded) {
        reloadTransactionsForSelectedAccount();
    }
    m_openAccountsDataLoaded = true;
    setErrorMessage(QString());
    return true;
}

void SKGMControler::reloadDashboardFromCurrentDocument()
{
    SKGTRACEINFUNC(1)
    if (!m_documentLoaded) {
        clearDashboardData();
        return;
    }

    SKGReportBank report(&m_document);
    const SKGServices::SKGUnitInfo primaryUnit = m_document.getPrimaryUnit();
    auto formatAmount = [&](double iAmount) {
        QString text = QLocale().toString(iAmount, 'f', 2);
        if (!primaryUnit.Symbol.isEmpty()) {
            text = text % u' ' % primaryUnit.Symbol;
        }
        return text;
    };
    auto formatPeriodLabel = [](const QString &iPeriod) {
        const QDate date = SKGServices::periodToDate(iPeriod);
        return date.isValid() ? QLocale().toString(date, QStringLiteral("MMMM yyyy")) : iPeriod;
    };

    QVariantList bankTotals;
    QString bankGrandTotalText;
    const QVariantList bankTable = report.getBankTable();
    for (int row = 1; row < bankTable.count(); ++row) {
        const QVariantList line = bankTable.at(row).toList();
        if (line.count() < 4) {
            continue;
        }
        const bool isTotal = line.at(0).toBool();
        const double amount = line.at(3).toDouble();
        QVariantMap item;
        item.insert(QStringLiteral("bank"), line.at(1).toString());
        item.insert(QStringLiteral("amount"), amount);
        item.insert(QStringLiteral("amountText"), formatAmount(amount));
        item.insert(QStringLiteral("isTotal"), isTotal);
        if (isTotal) {
            bankGrandTotalText = item.value(QStringLiteral("amountText")).toString();
        } else {
            bankTotals.push_back(item);
        }
    }
    if (bankGrandTotalText.isEmpty() && !bankTotals.isEmpty()) {
        double total = 0.0;
        for (const QVariant &entry : std::as_const(bankTotals)) {
            total += entry.toMap().value(QStringLiteral("amount")).toDouble();
        }
        bankGrandTotalText = formatAmount(total);
    }

    QString currentPeriod = report.getPeriod();
    QString previousPeriod = report.getPreviousPeriod();
    double currentIncome = 0.0;
    double currentExpense = 0.0;
    double previousIncome = 0.0;
    double previousExpense = 0.0;
    double currentSavings = 0.0;
    double previousSavings = 0.0;
    const QVariantList incomeVsExpenditure = report.getIncomeVsExpenditure(true, true, false, true);
    if (!incomeVsExpenditure.isEmpty()) {
        const QVariantList header = incomeVsExpenditure.at(0).toList();
        if (header.count() >= 4) {
            if (!header.at(2).toString().isEmpty()) {
                previousPeriod = header.at(2).toString();
            }
            if (!header.at(3).toString().isEmpty()) {
                currentPeriod = header.at(3).toString();
            }
        }
    }
    if (incomeVsExpenditure.count() > 1) {
        const QVariantList incomes = incomeVsExpenditure.at(1).toList();
        if (incomes.count() >= 4) {
            previousIncome = incomes.at(2).toDouble();
            currentIncome = incomes.at(3).toDouble();
        }
    }
    if (incomeVsExpenditure.count() > 2) {
        const QVariantList expenses = incomeVsExpenditure.at(2).toList();
        if (expenses.count() >= 4) {
            previousExpense = expenses.at(2).toDouble();
            currentExpense = expenses.at(3).toDouble();
        }
    }
    if (incomeVsExpenditure.count() > 3) {
        const QVariantList savings = incomeVsExpenditure.at(3).toList();
        if (savings.count() >= 4) {
            previousSavings = savings.at(2).toDouble();
            currentSavings = savings.at(3).toDouble();
        }
    }

    const QVariantMap pfsDetails = report.getPersonalFinanceScoreDetails(false, true);
    QString scoreColor = pfsDetails.value(QStringLiteral("color")).toString();
    if (!scoreColor.isEmpty() && !scoreColor.startsWith(QLatin1Char('#'))) {
        scoreColor.prepend(QLatin1Char('#'));
    }

    m_dashboardBankTotals = std::move(bankTotals);
    m_dashboardBankGrandTotalText = std::move(bankGrandTotalText);
    m_dashboardCurrentPeriodLabel = formatPeriodLabel(currentPeriod);
    m_dashboardPreviousPeriodLabel = formatPeriodLabel(previousPeriod);
    m_dashboardCurrentIncomeText = formatAmount(currentIncome);
    m_dashboardCurrentExpenseText = formatAmount(currentExpense);
    m_dashboardPreviousIncomeText = formatAmount(previousIncome);
    m_dashboardPreviousExpenseText = formatAmount(previousExpense);
    m_dashboardCurrentSavingsText = formatAmount(currentSavings);
    m_dashboardPreviousSavingsText = formatAmount(previousSavings);
    m_dashboardCurrentSavings = currentSavings;
    m_dashboardPreviousSavings = previousSavings;
    m_dashboardPersonalFinanceScoreText = QLocale().toString(pfsDetails.value(QStringLiteral("value")).toDouble(), 'f', 2);
    m_dashboardPersonalFinanceScoreMessage = pfsDetails.value(QStringLiteral("message")).toString();
    m_dashboardPersonalFinanceScoreColor = scoreColor.isEmpty() ? QStringLiteral("#666666") : std::move(scoreColor);
    m_dashboardNetWorthText = formatAmount(report.getNetWorth(false, true));
    m_dashboardAnnualSpendingText = formatAmount(report.getAnnualSpending(false, true));
    m_dashboardDataLoaded = true;
    Q_EMIT dashboardDataChanged();
}

void SKGMControler::clearDashboardData()
{
    m_dashboardBankTotals.clear();
    m_dashboardBankGrandTotalText.clear();
    m_dashboardCurrentPeriodLabel.clear();
    m_dashboardPreviousPeriodLabel.clear();
    m_dashboardCurrentIncomeText.clear();
    m_dashboardCurrentExpenseText.clear();
    m_dashboardPreviousIncomeText.clear();
    m_dashboardPreviousExpenseText.clear();
    m_dashboardCurrentSavingsText.clear();
    m_dashboardPreviousSavingsText.clear();
    m_dashboardCurrentSavings = 0.0;
    m_dashboardPreviousSavings = 0.0;
    m_dashboardPersonalFinanceScoreText.clear();
    m_dashboardPersonalFinanceScoreMessage.clear();
    m_dashboardPersonalFinanceScoreColor = QStringLiteral("#666666");
    m_dashboardNetWorthText.clear();
    m_dashboardAnnualSpendingText.clear();
    m_dashboardDataLoaded = false;
    Q_EMIT dashboardDataChanged();
}

void SKGMControler::reloadTrackersFromCurrentDocument()
{
    SKGTRACEINFUNC(1)
    if (!m_documentLoaded) {
        clearReferenceData();
        return;
    }

    m_trackersObjectModel->setTable(QStringLiteral("v_refund_display"));
    if (m_showClosedTrackers) {
        m_trackersObjectModel->setFilter(QStringLiteral("1=1 ORDER BY t_name"));
    } else {
        m_trackersObjectModel->setFilter(QStringLiteral("t_close='N' ORDER BY t_name"));
    }
    m_trackersObjectModel->refresh();
    m_trackersModel.setAmountUnitSymbol(m_document.getPrimaryUnit().Symbol);
    m_trackersProxyModel->setProperty("filter", m_globalFilterText);
    m_trackersProxyModel->invalidate();
    m_trackersDataLoaded = true;
    Q_EMIT referenceDataChanged();
}

void SKGMControler::reloadPayeesFromCurrentDocument()
{
    SKGTRACEINFUNC(1)
    if (!m_documentLoaded) {
        clearReferenceData();
        return;
    }

    m_payeesObjectModel->setTable(QStringLiteral("v_payee_display"));
    m_payeesObjectModel->setFilter(QStringLiteral("1=1 ORDER BY t_name"));
    m_payeesObjectModel->refresh();
    m_payeesModel.setAmountUnitSymbol(m_document.getPrimaryUnit().Symbol);
    m_payeesProxyModel->setProperty("filter", m_globalFilterText);
    m_payeesProxyModel->invalidate();
    m_payeesDataLoaded = true;
    Q_EMIT referenceDataChanged();
}

void SKGMControler::reloadCategoriesFromCurrentDocument()
{
    SKGTRACEINFUNC(1)
    if (!m_documentLoaded) {
        clearReferenceData();
        return;
    }

    m_categoriesObjectModel->setTable(QStringLiteral("v_category_display"));
    m_categoriesObjectModel->setFilter(QStringLiteral("1=1 ORDER BY t_fullname"));
    m_categoriesObjectModel->refresh();
    m_categoriesModel.setAmountUnitSymbol(m_document.getPrimaryUnit().Symbol);
    m_categoriesProxyModel->setProperty("filter", m_globalFilterText);
    m_categoriesProxyModel->invalidate();
    m_categoriesDataLoaded = true;
    Q_EMIT referenceDataChanged();
}

void SKGMControler::reloadUnitsFromCurrentDocument()
{
    SKGTRACEINFUNC(1)
    if (!m_documentLoaded) {
        clearReferenceData();
        return;
    }

    m_unitsObjectModel->setTable(QStringLiteral("v_unit_display"));
    m_unitsObjectModel->setFilter(QStringLiteral("1=1 ORDER BY t_TYPENLS, t_name"));
    m_unitsObjectModel->refresh();
    m_unitsModel.setAmountUnitSymbol(m_document.getPrimaryUnit().Symbol);
    m_unitsProxyModel->setProperty("filter", m_globalFilterText);
    m_unitsProxyModel->invalidate();
    m_unitsDataLoaded = true;
    Q_EMIT referenceDataChanged();
}

void SKGMControler::clearReferenceData()
{
    m_trackersObjectModel->setFilter(QStringLiteral("1=0"));
    m_trackersObjectModel->refresh();
    m_payeesObjectModel->setFilter(QStringLiteral("1=0"));
    m_payeesObjectModel->refresh();
    m_categoriesObjectModel->setFilter(QStringLiteral("1=0"));
    m_categoriesObjectModel->refresh();
    m_unitsObjectModel->setFilter(QStringLiteral("1=0"));
    m_unitsObjectModel->refresh();

    m_trackersProxyModel->invalidate();
    m_payeesProxyModel->invalidate();
    m_categoriesProxyModel->invalidate();
    m_unitsProxyModel->invalidate();

    m_trackersDataLoaded = false;
    m_payeesDataLoaded = false;
    m_categoriesDataLoaded = false;
    m_unitsDataLoaded = false;
    Q_EMIT referenceDataChanged();
}

bool SKGMControler::selectOpenAccount(const QString &iBank, const QString &iAccountName)
{
    SKGTRACEINFUNC(1)
    const int nb = m_openAccountIds.count();
    for (int i = 0; i < nb; ++i) {
        if (m_openAccountBanks.at(i) == iBank && m_openAccountAccountNames.at(i) == iAccountName) {
            if (!m_selectedTrackerName.isEmpty()) {
                m_selectedTrackerName.clear();
                Q_EMIT selectedTrackerNameChanged();
            }
            setSelectedOpenAccountIndex(i);
            return true;
        }
    }
    return false;
}

bool SKGMControler::selectTracker(const QString &iTrackerName)
{
    SKGTRACEINFUNC(1)
    if (!m_documentLoaded || iTrackerName.trimmed().isEmpty()) {
        return false;
    }

    const QString trackerName = iTrackerName.trimmed();
    const QString whereClause =
        QStringLiteral("t_REALREFUND='") % SKGServices::stringToSqlString(trackerName) % QStringLiteral("' ORDER BY date(d_date) DESC, id DESC");
    return loadSubTransactionsForSelection(trackerName, whereClause);
}

bool SKGMControler::selectCategory(const QString &iCategoryFullName)
{
    SKGTRACEINFUNC(1)
    if (!m_documentLoaded || iCategoryFullName.trimmed().isEmpty()) {
        return false;
    }

    const QString categoryName = iCategoryFullName.trimmed();
    const QString whereClause =
        QStringLiteral("t_REALCATEGORY='") % SKGServices::stringToSqlString(categoryName) % QStringLiteral("' ORDER BY date(d_date) DESC, id DESC");
    return loadSubTransactionsForSelection(categoryName, whereClause);
}

bool SKGMControler::selectTransaction(int iOperationId)
{
    SKGTRACEINFUNC(1)
    if (!m_documentLoaded || iOperationId <= 0) {
        return false;
    }

    const QString selectionLabel = i18nc("@info", "Transaction #%1", iOperationId);
    const QString whereClause = QStringLiteral("i_OPID=") % SKGServices::intToString(iOperationId) % QStringLiteral(" ORDER BY date(d_date) DESC, id DESC");
    return loadSubTransactionsForSelection(selectionLabel, whereClause);
}

void SKGMControler::reloadTransactionsForSelectedAccount()
{
    SKGTRACEINFUNC(1)
    if (!m_selectedTrackerName.isEmpty()) {
        m_selectedTrackerName.clear();
        Q_EMIT selectedTrackerNameChanged();
    }

    if (!m_documentLoaded) {
        m_transactionsModel.clear();
        m_transactionsDataLoaded = false;
        return;
    }
    if (m_selectedOpenAccountIndex < 0 || m_selectedOpenAccountIndex >= m_openAccountIds.count()) {
        m_transactionsModel.clear();
        m_transactionsDataLoaded = true;
        return;
    }

    const int accountId = m_openAccountIds.at(m_selectedOpenAccountIndex);
    QString whereClause = QStringLiteral("rd_account_id=") % SKGServices::intToString(accountId) % QStringLiteral(" AND t_template='N'");
    if (m_transactionStatusFilter == 1) {
        whereClause += QStringLiteral(" AND t_status='P'");
    } else if (m_transactionStatusFilter == 2) {
        whereClause += QStringLiteral(" AND t_status='N'");
    } else if (m_transactionStatusFilter == 3) {
        whereClause += QStringLiteral(" AND t_status='Y'");
    }
    whereClause += QStringLiteral(" ORDER BY date(d_date) DESC, id DESC");

    m_transactionsObjectModel->setTable(QStringLiteral("v_operation_display"));
    m_transactionsObjectModel->setFilter(whereClause);
    m_transactionsObjectModel->refresh();

    applyFilterAndDateSort(m_transactionsProxyModel, m_transactionsObjectModel);
    rebuildTransactionsFromProxy();
}

void SKGMControler::rebuildTransactionsFromProxy()
{
    SKGTRACEINFUNC(1)
    QVector<SKGMTransactionListModel::TransactionItem> transactions;
    const int nbRows = m_transactionsProxyModel->rowCount();
    transactions.reserve(nbRows);
    for (int row = 0; row < nbRows; ++row) {
        const QModelIndex proxyIndex = m_transactionsProxyModel->index(row, 0);
        const QModelIndex sourceIndex = m_transactionsProxyModel->mapToSource(proxyIndex);
        const SKGObjectBase obj = m_transactionsObjectModel->getObject(sourceIndex);
        SKGMTransactionListModel::TransactionItem item;
        item.operationId = SKGServices::stringToInt(obj.getAttribute(QStringLiteral("id")));
        item.date = obj.getAttribute(QStringLiteral("d_date"));
        item.payee = obj.getAttribute(QStringLiteral("t_PAYEE"));
        item.category = obj.getAttribute(QStringLiteral("t_CATEGORY"));
        item.comment = obj.getAttribute(QStringLiteral("t_comment"));
        item.mode = obj.getAttribute(QStringLiteral("t_mode"));
        item.number = obj.getAttribute(QStringLiteral("t_number"));
        item.amount = SKGServices::stringToDouble(obj.getAttribute(QStringLiteral("f_CURRENTAMOUNT")));
        QString unit = obj.getAttribute(QStringLiteral("t_UNIT"));
        item.amountText = QLocale().toString(item.amount, 'f', 2);
        if (!unit.isEmpty()) {
            item.amountText = item.amountText % u' ' % unit;
        }
        transactions.push_back(std::move(item));
    }
    m_transactionsModel.setTransactions(std::move(transactions));
    m_transactionsDataLoaded = true;
}

void SKGMControler::rebuildSubTransactionsFromProxy()
{
    SKGTRACEINFUNC(1)
    QVector<SKGMTransactionListModel::TransactionItem> transactions;
    const int nbRows = m_subTransactionsProxyModel->rowCount();
    transactions.reserve(nbRows);
    for (int row = 0; row < nbRows; ++row) {
        const QModelIndex proxyIndex = m_subTransactionsProxyModel->index(row, 0);
        const QModelIndex sourceIndex = m_subTransactionsProxyModel->mapToSource(proxyIndex);
        const SKGObjectBase obj = m_subTransactionsObjectModel->getObject(sourceIndex);
        SKGMTransactionListModel::TransactionItem item;
        item.date = obj.getAttribute(QStringLiteral("d_date"));
        item.payee = obj.getAttribute(QStringLiteral("t_PAYEE"));
        item.category = obj.getAttribute(QStringLiteral("t_REALCATEGORY"));
        item.comment = obj.getAttribute(QStringLiteral("t_REALCOMMENT"));
        item.mode = obj.getAttribute(QStringLiteral("t_mode"));
        item.number = obj.getAttribute(QStringLiteral("t_number"));
        item.amount = SKGServices::stringToDouble(obj.getAttribute(QStringLiteral("f_REALCURRENTAMOUNT")));
        QString unit = obj.getAttribute(QStringLiteral("t_UNIT"));
        item.amountText = QLocale().toString(item.amount, 'f', 2);
        if (!unit.isEmpty()) {
            item.amountText = item.amountText % u' ' % unit;
        }
        transactions.push_back(std::move(item));
    }
    m_subTransactionsModel.setTransactions(std::move(transactions));
    m_subTransactionsDataLoaded = true;
}

void SKGMControler::ensureDataForPage(int iPageIndex)
{
    SKGTRACEINFUNC(1)
    if (!m_documentLoaded) {
        return;
    }

    if (iPageIndex == 0) {
        if (!m_dashboardDataLoaded) {
            reloadDashboardFromCurrentDocument();
        }
    } else if (iPageIndex == 1) {
        if (!m_accountsDataLoaded) {
            reloadAccountsFromCurrentDocument();
        }
    } else if (iPageIndex == 2) {
        if (!m_openAccountsDataLoaded) {
            reloadOpenAccountsFromCurrentDocument();
        }
        if (!m_transactionsDataLoaded) {
            reloadTransactionsForSelectedAccount();
        }
    } else if (iPageIndex == 3) {
        if (!m_trackersDataLoaded) {
            reloadTrackersFromCurrentDocument();
        }
    } else if (iPageIndex == 4) {
        if (!m_payeesDataLoaded) {
            reloadPayeesFromCurrentDocument();
        }
    } else if (iPageIndex == 5) {
        if (!m_categoriesDataLoaded) {
            reloadCategoriesFromCurrentDocument();
        }
    } else if (iPageIndex == 6) {
        if (!m_unitsDataLoaded) {
            reloadUnitsFromCurrentDocument();
        }
    }
}

void SKGMControler::updateSelectedAccountBalanceText()
{
    SKGTRACEINFUNC(1)
    QString balanceText;
    if (m_selectedOpenAccountIndex >= 0 && m_selectedOpenAccountIndex < m_openAccountBalanceTexts.count()) {
        balanceText = m_openAccountBalanceTexts.at(m_selectedOpenAccountIndex);
    }
    if (m_selectedAccountBalanceText == balanceText) {
        return;
    }
    m_selectedAccountBalanceText = std::move(balanceText);
    Q_EMIT selectedAccountBalanceTextChanged();
}

void SKGMControler::setPasswordRequired(bool iRequired)
{
    SKGTRACEINFUNC(1)
    if (m_passwordRequired == iRequired) {
        return;
    }
    m_passwordRequired = iRequired;
    Q_EMIT passwordRequiredChanged();
}

void SKGMControler::clearLoadedDocumentState()
{
    SKGTRACEINFUNC(1)
    m_accountsModel.clear();
    if (!m_globalAccountsTotalText.isEmpty()) {
        m_globalAccountsTotalText.clear();
        Q_EMIT globalAccountsTotalTextChanged();
    }
    m_transactionsModel.clear();
    m_subTransactionsModel.clear();
    if (!m_selectedTrackerName.isEmpty()) {
        m_selectedTrackerName.clear();
        Q_EMIT selectedTrackerNameChanged();
    }
    m_openAccountIds.clear();
    m_openAccountNames.clear();
    m_openAccountBalanceTexts.clear();
    m_openAccountBanks.clear();
    m_openAccountAccountNames.clear();
    m_accountsDataLoaded = false;
    m_openAccountsDataLoaded = false;
    m_transactionsDataLoaded = false;
    m_subTransactionsDataLoaded = false;
    setSelectedOpenAccountIndex(-1);
    updateSelectedAccountBalanceText();
    Q_EMIT openAccountsChanged();
    clearDashboardData();
    clearReferenceData();
}

void SKGMControler::refreshReferenceProxyFilters()
{
    SKGTRACEINFUNC(1)
    if (m_trackersDataLoaded) {
        m_trackersProxyModel->setProperty("filter", m_globalFilterText);
        m_trackersProxyModel->invalidate();
    }
    if (m_payeesDataLoaded) {
        m_payeesProxyModel->setProperty("filter", m_globalFilterText);
        m_payeesProxyModel->invalidate();
    }
    if (m_categoriesDataLoaded) {
        m_categoriesProxyModel->setProperty("filter", m_globalFilterText);
        m_categoriesProxyModel->invalidate();
    }
    if (m_unitsDataLoaded) {
        m_unitsProxyModel->setProperty("filter", m_globalFilterText);
        m_unitsProxyModel->invalidate();
    }
}

void SKGMControler::applyFilterAndDateSort(SKGSortFilterProxyModel *iProxyModel, SKGObjectModelBase *iObjectModel)
{
    SKGTRACEINFUNC(1)
    iProxyModel->setProperty("filter", m_globalFilterText);
    const int dateCol = iObjectModel->getIndexAttribute(QStringLiteral("d_date"));
    if (dateCol >= 0) {
        iProxyModel->sort(dateCol, Qt::DescendingOrder);
    } else {
        iProxyModel->sort(0, Qt::DescendingOrder);
    }
}

bool SKGMControler::loadSubTransactionsForSelection(const QString &iSelectionLabel, const QString &iWhereClause)
{
    SKGTRACEINFUNC(1)
    if (m_selectedTrackerName != iSelectionLabel) {
        m_selectedTrackerName = iSelectionLabel;
        Q_EMIT selectedTrackerNameChanged();
    }

    m_subTransactionsObjectModel->setTable(QStringLiteral("v_suboperation_consolidated"));
    m_subTransactionsObjectModel->setFilter(iWhereClause);
    m_subTransactionsObjectModel->refresh();
    applyFilterAndDateSort(m_subTransactionsProxyModel, m_subTransactionsObjectModel);
    rebuildSubTransactionsFromProxy();
    return true;
}

void SKGMControler::loadPersistedState()
{
    SKGTRACEINFUNC(1)
    QSettings settings(QStringLiteral("kde.org"), QStringLiteral("skroogecompanion"));
    const QString persistedPath = settings.value(QStringLiteral("lastFilePath")).toString();
    if (!persistedPath.isEmpty()) {
        setFilePath(persistedPath);
    }
}

void SKGMControler::persistLastFilePath(const QString &iFilePath)
{
    SKGTRACEINFUNC(1)
    QSettings settings(QStringLiteral("kde.org"), QStringLiteral("skroogecompanion"));
    settings.setValue(QStringLiteral("lastFilePath"), iFilePath);
}

void SKGMControler::clearPersistedLastFilePath()
{
    SKGTRACEINFUNC(1)
    QSettings settings(QStringLiteral("kde.org"), QStringLiteral("skroogecompanion"));
    settings.remove(QStringLiteral("lastFilePath"));
}

void SKGMControler::setErrorMessage(const QString &iMessage)
{
    SKGTRACEINFUNC(1)
    if (m_errorMessage == iMessage) {
        return;
    }
    m_errorMessage = iMessage;
    Q_EMIT errorMessageChanged();
}
