// Copyright (c) 2021-2025 The Dash Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <qt/forms/ui_proposallist.h>

#include <chainparams.h>
#include <governance/common.h>
#include <governance/governance.h>
#include <governance/vote.h>
#include <interfaces/node.h>
#include <interfaces/wallet.h>
#include <script/standard.h>
#include <util/strencodings.h>
#include <util/underlying.h>

#include <qt/clientfeeds.h>
#include <qt/descriptiondialog.h>
#include <qt/masternodemodel.h>
#include <qt/guiutil_font.h>
#include <qt/proposalcreate.h>
#include <qt/proposallist.h>
#include <qt/proposalmodel.h>
#include <qt/proposalresume.h>

#include <qt/bitcoinunits.h>
#include <qt/clientmodel.h>
#include <qt/guiutil.h>
#include <qt/optionsmodel.h>
#include <qt/walletmodel.h>

#include <QAbstractItemView>
#include <QApplication>
#include <QClipboard>
#include <QDesktopServices>
#include <QEvent>
#include <QMessageBox>
#include <QMetaObject>
#include <QResizeEvent>
#include <QShowEvent>
#include <QUrl>

#include <univalue.h>

#include <algorithm>

namespace {
constexpr int TITLE_MIN_WIDTH{220};
} // anonymous namespace

ProposalList::ProposalList(QWidget* parent) :
    QWidget(parent),
    ui{new Ui::ProposalList},
    proposalModel{new ProposalModel(this)},
    proposalContextMenu{new QMenu(this)},
    proposalModelProxy{new QSortFilterProxyModel(this)}
{
    ui->setupUi(this);

    GUIUtil::setFont({ui->label_count_2, ui->countLabel},
                     {GUIUtil::FontWeight::Bold, 14});

    ui->govTableView->setContextMenuPolicy(Qt::CustomContextMenu);
    ui->govTableView->setModel(proposalModelProxy);
    ui->govTableView->setSelectionBehavior(QAbstractItemView::SelectRows);
    ui->govTableView->setSortingEnabled(true);
    ui->govTableView->sortByColumn(ProposalModel::Column::TITLE, Qt::AscendingOrder);
    ui->govTableView->verticalHeader()->setVisible(false);
    connect(ui->govTableView, &QTableView::customContextMenuRequested, this, &ProposalList::showProposalContextMenu);
    connect(ui->govTableView, &QTableView::doubleClicked, this, &ProposalList::showAdditionalInfo);
    connect(ui->govTableView->horizontalHeader(), &QHeaderView::sectionResized, this, &ProposalList::refreshColumnWidths);

    ui->emptyPage->setAutoFillBackground(true);
    updateEmptyPagePalette();

    ui->proposalSourceCombo->addItem(tr("Active Proposals"), ToUnderlying(ProposalSource::Active));
    ui->proposalSourceCombo->setMinimumWidth(250);
    connect(ui->proposalSourceCombo, qOverload<int>(&QComboBox::activated), this, &ProposalList::setProposalSource);

    // Set up filtering.
    proposalModelProxy->setSourceModel(proposalModel);
    proposalModelProxy->setSortRole(Qt::EditRole);
    proposalModelProxy->setFilterKeyColumn(ProposalModel::Column::TITLE); // filter by title column...
    connect(ui->filterLineEdit, &QLineEdit::textChanged, proposalModelProxy, &QSortFilterProxyModel::setFilterFixedString);

    // Changes to number of rows should update proposal count display.
    connect(proposalModelProxy, &QSortFilterProxyModel::rowsInserted, this, &ProposalList::updateProposalCount);
    connect(proposalModelProxy, &QSortFilterProxyModel::rowsRemoved, this, &ProposalList::updateProposalCount);
    connect(proposalModelProxy, &QSortFilterProxyModel::layoutChanged, this, &ProposalList::updateProposalCount);

    // Connect buttons
    connect(ui->btnCreateProposal, &QPushButton::clicked, this, &ProposalList::showCreateProposalDialog);
    connect(ui->btnResumeProposal, &QPushButton::clicked, this, &ProposalList::showResumeProposalDialog);
    updateProposalButtons();

    // Set up info button
    ui->btnInfoView->setIcon(style()->standardIcon(QStyle::SP_MessageBoxInformation));
    ui->btnInfoView->setIconSize(QSize(20, 20));
    ui->btnInfoView->setFixedSize(36, 34);
    connect(ui->btnInfoView, &QPushButton::clicked, this, &ProposalList::showProposalInfo);

    GUIUtil::updateFonts();
}

ProposalList::~ProposalList()
{
    delete ui;
}

void ProposalList::changeEvent(QEvent* event)
{
    QWidget::changeEvent(event);
    if (event->type() == QEvent::StyleChange) {
        QTimer::singleShot(0, proposalModel, &ProposalModel::refreshIcons);
        QTimer::singleShot(0, this, &ProposalList::updateEmptyPagePalette);
    }
}

void ProposalList::updateEmptyPagePalette()
{
    QPalette emptyPalette = ui->emptyPage->palette();
    emptyPalette.setColor(QPalette::Window, ui->govTableView->palette().color(QPalette::Base));
    ui->emptyPage->setPalette(emptyPalette);
}

void ProposalList::setClientModel(ClientModel* model)
{
    this->clientModel = model;
    if (!clientModel) {
        return;
    }
    m_feed_masternode = clientModel->feedMasternode();
    m_feed_proposal = clientModel->feedProposal();
    if (m_feed_masternode && m_feed_proposal) {
        connect(m_feed_proposal, &ProposalFeed::dataReady, this, &ProposalList::updateProposalList);
        updateProposalList();
    }
    connect(clientModel, &ClientModel::additionalDataSyncProgressChanged, this, &ProposalList::updateInfoTooltip);
    connect(clientModel, &ClientModel::additionalDataSyncProgressChanged, this, &ProposalList::updateProposalButtons);
    connect(clientModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &ProposalList::updateDisplayUnit);
    if (walletModel && ui->proposalSourceCombo->findData(ToUnderlying(ProposalSource::Local)) == -1) {
        ui->proposalSourceCombo->addItem(tr("My Proposals"), ToUnderlying(ProposalSource::Local));
    }
}

std::vector<Governance::Object> ProposalList::getWalletProposals(std::optional<bool> pending) const
{
    if (!clientModel || !walletModel) {
        return {};
    }

    const auto wallet_objs = walletModel->wallet().getGovernanceObjects();

    const int64_t now{GetTime()};
    std::vector<Governance::Object> result{};
    for (const auto& obj : wallet_objs) {
        const bool is_broadcast{clientModel->node().gov().existsObj(obj.GetHash())};
        bool is_stale{false};
        try {
            UniValue json_data;
            json_data.read(obj.GetDataAsPlainString());
            if (json_data.exists("type") && json_data["type"].getInt<int>() != 1) {
                // Not a voting proposal
                continue;
            }
            if (json_data.exists("end_epoch")) {
                is_stale = json_data["end_epoch"].getInt<int64_t>() < now;
            }
        } catch (const std::exception&) {
            // Unparsable objects are skipped
            continue;
        }
        if (!pending.has_value() || *pending != (is_broadcast || is_stale)) {
            result.push_back(obj);
        }
    }

    return result;
}

void ProposalList::setWalletModel(WalletModel* model)
{
    this->walletModel = model;
    if (!walletModel || !clientModel) {
        return;
    }
    connect(walletModel, &WalletModel::balanceChanged, this, &ProposalList::updateProposalButtons);
    if (clientModel && ui->proposalSourceCombo->findData(ToUnderlying(ProposalSource::Local)) == -1) {
        ui->proposalSourceCombo->addItem(tr("My Proposals"), ToUnderlying(ProposalSource::Local));
    }
}

void ProposalList::updateDisplayUnit()
{
    if (this->clientModel) {
        proposalModel->setDisplayUnit(this->clientModel->getOptionsModel()->getDisplayUnit());
        ui->govTableView->update();
    }
}

int ProposalList::queryCollateralDepth(const uint256& collateralHash) const
{
    if (!walletModel) return 0;
    interfaces::WalletTxStatus tx_status;
    int num_blocks{};
    int64_t block_time{};
    if (walletModel->wallet().tryGetTxStatus(collateralHash, tx_status, num_blocks, block_time)) {
        return tx_status.depth_in_main_chain;
    }
    return 0;
}

void ProposalList::updateProposalList()
{
    if (!clientModel || !m_feed_masternode || !m_feed_proposal) {
        return;
    }

    const auto data_mn = m_feed_masternode->data();
    const auto data_pr = m_feed_proposal->data();
    if (!data_mn || !data_mn->m_valid || !data_pr) {
        return;
    }

    // Assume that wallet-specific knowledge is stale
    votableMasternodes.clear();

    ProposalData ret;
    ret.m_abs_vote_req = data_pr->m_abs_vote_req;
    ret.m_gov_info = data_pr->m_gov_info;
    ret.m_fundable_hashes = data_pr->m_fundable_hashes;
    ret.m_proposals = data_pr->m_proposals;

    // If we don't have a wallet, we can't fetch based on proposal source
    if (!walletModel) {
        setProposalList(std::move(ret));
        return;
    }

    if (m_proposal_source == ProposalSource::Active) {
        // Include unrelayed wallet proposals (0 confs, not yet broadcast)
        for (const auto& obj : getWalletProposals(/*pending=*/true)) {
            CGovernanceObject govObj(obj.hashParent, obj.revision, obj.time, obj.collateralHash, obj.GetDataAsHexString());
            ret.m_proposals.emplace_back(std::make_shared<Proposal>(*clientModel, govObj, ret.m_gov_info, queryCollateralDepth(obj.collateralHash),
                                                                     /*is_broadcast=*/false));
        }
    } else if (m_proposal_source == ProposalSource::Local) {
        // Discard network proposals and only include wallet proposals
        ret.m_proposals.clear();
        for (const auto& obj : getWalletProposals(/*pending=*/std::nullopt)) {
            CGovernanceObject govObj(obj.hashParent, obj.revision, obj.time, obj.collateralHash, obj.GetDataAsHexString());
            ret.m_proposals.emplace_back(std::make_shared<Proposal>(*clientModel, govObj, ret.m_gov_info, queryCollateralDepth(obj.collateralHash),
                                                                     /*is_broadcast=*/clientModel->node().gov().existsObj(obj.GetHash())));
        }
    }

    for (const auto& entry : data_mn->m_entries) {
        if (entry->isBanned()) {
            continue;
        }
        const auto script = GetScriptForDestination(PKHash(entry->keyIdVotingRaw()));
        if (walletModel->wallet().isSpendable(script)) {
            votableMasternodes[entry->proTxHashRaw()] = entry->keyIdVotingRaw();
        }
    }
    setProposalList(std::move(ret));
}

void ProposalList::setProposalList(ProposalData&& data)
{
    proposalModel->setVotingParams(data.m_abs_vote_req);
    proposalModel->reconcile(std::move(data.m_proposals), std::move(data.m_fundable_hashes));
    m_gov_info = std::move(data.m_gov_info);
    updateInfoTooltip();
    updateProposalButtons();
}

void ProposalList::updateProposalCount()
{
    ui->countLabel->setText(QString::number(proposalModelProxy->rowCount()));
    refreshColumnWidths();
    updateEmptyState();
}

void ProposalList::updateEmptyState()
{
    const bool hasProposals = proposalModelProxy->rowCount() > 0;
    if (hasProposals) {
        ui->proposalStack->setCurrentWidget(ui->govTableView);
    } else {
        switch (m_proposal_source) {
        case ProposalSource::Active:
            ui->emptyLabel->setText(tr("No active proposals on the network."));
            break;
        case ProposalSource::Local:
            ui->emptyLabel->setText(tr("No proposals recorded in wallet file."));
            break;
        } // no default case, so the compiler can warn about missing cases
        ui->proposalStack->setCurrentWidget(ui->emptyPage);
    }
}

void ProposalList::showCreateProposalDialog()
{
    if (!this->clientModel || !this->walletModel) {
        QMessageBox::warning(this, tr("Unavailable"), tr("A synced node and an unlocked wallet are required."));
        return;
    }
    ProposalCreate* proposalCreate = new ProposalCreate(this->walletModel, this);
    // Ensure closing the dialog actually destroys it so a fresh flow starts next time
    proposalCreate->setAttribute(Qt::WA_DeleteOnClose, true);
    // Modeless window that does not block the parent
    proposalCreate->setWindowModality(Qt::NonModal);
    proposalCreate->setModal(false);
    proposalCreate->setWindowFlag(Qt::Window, true);
    // Auto-open Resume dialog after successful creation and refresh the proposal list
    connect(proposalCreate, &QDialog::accepted, this, &ProposalList::requestForceRefresh);
    connect(proposalCreate, &QDialog::accepted, this, &ProposalList::updateProposalButtons);
    connect(proposalCreate, &QDialog::accepted, this, &ProposalList::showResumeProposalDialog);
    proposalCreate->show();
}

void ProposalList::showResumeProposalDialog()
{
    if (!clientModel || !walletModel) {
        QMessageBox::warning(this, tr("Resume proposal"), tr("A synced node and an unlocked wallet are required."));
        return;
    }

    const auto proposals = getWalletProposals(/*pending=*/true);
    ProposalResume* dialog = new ProposalResume(clientModel->node(), *clientModel, walletModel, proposals, this);
    connect(dialog, &ProposalResume::proposalBroadcasted, this, &ProposalList::requestForceRefresh);
    dialog->setAttribute(Qt::WA_DeleteOnClose, true);
    dialog->setWindowModality(Qt::NonModal);
    dialog->setModal(false);
    dialog->setWindowFlag(Qt::Window, true);
    dialog->show();
}

void ProposalList::showProposalContextMenu(const QPoint& pos)
{
    const auto index = ui->govTableView->indexAt(pos);

    if (!index.isValid()) {
        return;
    }

    const auto proposal = proposalModel->getProposalAt(proposalModelProxy->mapToSource(index));
    if (!proposal) {
        return;
    }

    proposalContextMenu->clear();
    proposalContextMenu->addAction(tr("Copy Raw JSON"), this, &ProposalList::copyProposalJson);
    if (!proposal->url().isEmpty()) {
        proposalContextMenu->addAction(tr("Open Proposal URL…"), this, &ProposalList::openProposalUrl);
    }

    // Add voting options if wallet is available and has voting capability
    if (walletModel && canVote()) {
        proposalContextMenu->addSeparator();
        proposalContextMenu->addAction(tr("Vote Yes"), this, &ProposalList::voteYes);
        proposalContextMenu->addAction(tr("Vote No"), this, &ProposalList::voteNo);
        proposalContextMenu->addAction(tr("Vote Abstain"), this, &ProposalList::voteAbstain);
    }

    proposalContextMenu->exec(QCursor::pos());
}

void ProposalList::showAdditionalInfo(const QModelIndex& index)
{
    if (!index.isValid() || !clientModel) {
        return;
    }

    const auto proposal = proposalModel->getProposalAt(proposalModelProxy->mapToSource(index));
    if (!proposal) {
        return;
    }

    DescriptionDialog* dialog = new DescriptionDialog(
        tr("Details for %1").arg(proposal->title()),
        proposal->toHtml(clientModel->getOptionsModel()->getDisplayUnit()),
        /*parent=*/this);
    dialog->resize(800, 380);
    dialog->setAttribute(Qt::WA_DeleteOnClose);
    dialog->show();
}

void ProposalList::updateInfoTooltip()
{
    if (!clientModel || !clientModel->getOptionsModel() || !m_feed_proposal) {
        return;
    }

    const auto data_pr = m_feed_proposal->data();
    if (!data_pr || m_gov_info.superblockcycle <= 0) {
        return;
    }

    if (!clientModel->masternodeSync().isBlockchainSynced()) {
        ui->btnInfoView->setToolTip(tr("Waiting for blockchain sync…"));
        return;
    }

    if (!clientModel->masternodeSync().isGovernanceSynced()) {
        ui->btnInfoView->setToolTip(tr("Waiting for governance sync…"));
        return;
    }

    const int current_height{clientModel->getNumBlocks()};
    const auto remaining_blocks{std::max<int>(0, m_gov_info.nextsuperblock - current_height)};
    const QString remaining_str{GUIUtil::formatBlockDuration(remaining_blocks, m_gov_info.targetSpacing)};
    const bool awaiting_superblock{current_height % m_gov_info.superblockcycle >= m_gov_info.superblockcycle - m_gov_info.superblockmaturitywindow};
    const auto voting_remaining_blocks{std::max<int>(0, remaining_blocks - m_gov_info.superblockmaturitywindow)};
    const QString voting_remaining_str{GUIUtil::formatBlockDuration(voting_remaining_blocks, m_gov_info.targetSpacing)};

    const auto tooltip1{tr("%n masternode(s) available for voting", "", votableMasternodes.size())};
    const auto tooltip2{remaining_blocks == 0
        ? (awaiting_superblock ? tr("Superblock imminent") : tr("Voting period ended"))
        : (awaiting_superblock
            ? tr("~%1 (%2 blocks) left for superblock").arg(remaining_str).arg(remaining_blocks)
            : tr("~%1 (%2 blocks) left for voting").arg(voting_remaining_str).arg(voting_remaining_blocks))};
    const auto tooltip3{[&]() {
        const auto allocated_budget{data_pr->m_allocated};
        const auto budget_pct = m_gov_info.governancebudget > 0
            ? static_cast<int>(static_cast<double>(allocated_budget) / static_cast<double>(m_gov_info.governancebudget) * 100.0)
            : 0;
        const auto unit{clientModel->getOptionsModel()->getDisplayUnit()};
        return tr("~%1% of budget committed (%2 / %3)").arg(budget_pct)
            .arg(GUIUtil::formatAmount(unit, allocated_budget, /*is_signed=*/false, /*truncate=*/2))
            .arg(GUIUtil::formatAmount(unit, m_gov_info.governancebudget, /*is_signed=*/false, /*truncate=*/2));
    }()};
    ui->btnInfoView->setToolTip(QString("<nobr>%1</nobr><br><nobr>%2</nobr><br><nobr>%3</nobr>").arg(tooltip1).arg(tooltip2).arg(tooltip3));
}

void ProposalList::updateProposalButtons()
{
    if (!clientModel || !clientModel->masternodeSync().isGovernanceSynced()) {
        const QString tooltip = tr("Cannot interact with governance before sync completes");
        ui->btnCreateProposal->setEnabled(false);
        ui->btnCreateProposal->setToolTip(tooltip);
        ui->btnResumeProposal->setEnabled(false);
        ui->btnResumeProposal->setToolTip(tooltip);
        return;
    }

    // Using filler tooltips as tooltips once set cannot be disabled
    ui->btnCreateProposal->setEnabled(true);
    ui->btnCreateProposal->setToolTip(tr("Creates a new proposal"));
    ui->btnResumeProposal->setEnabled(true);
    ui->btnResumeProposal->setToolTip(tr("Resumes an existing proposal"));

    // Wallets with insufficient balance cannot create proposals
    if (walletModel && walletModel->getOptionsModel()) {
        const auto proposal_fee = m_gov_info.proposalfee;
        if (walletModel->wallet().getBalance() < proposal_fee) {
            ui->btnCreateProposal->setEnabled(false);
            ui->btnCreateProposal->setToolTip(
                tr("Creating proposals costs %1, insufficient balance")
                    .arg(BitcoinUnits::formatWithUnit(walletModel->getOptionsModel()->getDisplayUnit(), proposal_fee, false,
                                                      BitcoinUnits::SeparatorStyle::ALWAYS)));
        }
    }
}

void ProposalList::setProposalSource(int index)
{
    m_proposal_source = static_cast<ProposalSource>(ui->proposalSourceCombo->itemData(index).toInt());
    ui->govTableView->setColumnHidden(ProposalModel::Column::VOTING_STATUS, m_proposal_source == ProposalSource::Local);
    updateProposalList();
}

void ProposalList::voteYes() { voteForProposal(VOTE_OUTCOME_YES); }

void ProposalList::voteNo() { voteForProposal(VOTE_OUTCOME_NO); }

void ProposalList::voteAbstain() { voteForProposal(VOTE_OUTCOME_ABSTAIN); }

void ProposalList::openProposalUrl()
{
    const auto selection = ui->govTableView->selectionModel()->selectedRows();
    if (selection.isEmpty()) {
        return;
    }

    const auto proposal = proposalModel->getProposalAt(proposalModelProxy->mapToSource(selection.first()));
    if (!proposal || proposal->url().isEmpty()) {
        return;
    }

    const QUrl url{QUrl(proposal->url())};
    if (const QString scheme = url.isValid() ? url.scheme().toLower() : QString();
        scheme != QLatin1String("http") && scheme != QLatin1String("https")) {
        QMessageBox::critical(this, tr("Error"), tr("Cannot validate URL, potentially malformed or unknown protocol."));
        return;
    }
    QMessageBox::StandardButton reply = QMessageBox::warning(
        this,
        tr("External Link Warning"),
        tr("You are about to open the following URL in your default browser\n\n%1\n\n"
           "This content was submitted by a user. It may not match what is described in the title.\n\n"
           "Do you wish to continue?").arg(proposal->url()),
        QMessageBox::Yes | QMessageBox::No,
        QMessageBox::No
    );

    if (reply == QMessageBox::Yes) {
        QDesktopServices::openUrl(url);
    }
}

void ProposalList::copyProposalJson()
{
    const auto selection = ui->govTableView->selectionModel()->selectedRows();
    if (selection.isEmpty()) {
        return;
    }

    const auto proposal = proposalModel->getProposalAt(proposalModelProxy->mapToSource(selection.first()));
    if (!proposal) {
        return;
    }

    QApplication::clipboard()->setText(proposal->toJson());
}

void ProposalList::voteForProposal(vote_outcome_enum_t outcome)
{
    if (!walletModel) {
        QMessageBox::warning(this, tr("Voting Failed"), tr("No wallet available."));
        return;
    }

    if (votableMasternodes.empty()) {
        QMessageBox::warning(this, tr("Voting Failed"), tr("No masternode voting keys found in wallet."));
        return;
    }

    if (!m_feed_masternode) {
        QMessageBox::warning(this, tr("Voting Failed"), tr("Unable to fetch masternode data."));
        return;
    }

    const auto data_mn = m_feed_masternode->data();
    if (!data_mn || !data_mn->m_valid) {
        QMessageBox::warning(this, tr("Voting Failed"), tr("Unable to fetch masternode data."));
        return;
    }

    // Get the selected proposal
    const auto selection = ui->govTableView->selectionModel()->selectedRows();
    if (selection.isEmpty()) {
        QMessageBox::warning(this, tr("Voting Failed"), tr("Please select a proposal to vote on."));
        return;
    }

    const auto index = selection.first();
    const auto proposal = proposalModel->getProposalAt(proposalModelProxy->mapToSource(index));
    if (!proposal) return;

    const uint256 proposalHash(uint256S(proposal->hash().toStdString()));

    // Request unlock if needed and keep context alive for the voting operation
    WalletModel::UnlockContext ctx(walletModel->requestUnlock());
    if (!ctx.isValid()) {
        // Unlock cancelled or failed
        QMessageBox::warning(this, tr("Voting Failed"), tr("Unable to unlock wallet."));
        return;
    }

    int nSuccessful = 0;
    int nFailed = 0;
    QStringList failedMessages;

    // Vote with each masternode
    for (const auto& [proTxHash, votingKeyID] : votableMasternodes) {
        // Find the masternode
        QString protx_hash{QString::fromStdString(proTxHash.ToString())};
        const auto dmn = [&]() -> const std::shared_ptr<MasternodeEntry> {
            for (const auto& mn : data_mn->m_entries) {
                if (mn->proTxHash() == protx_hash) {
                    return mn->isBanned() ? nullptr : mn;
                }
            }
            return nullptr;
        }();

        if (!dmn) {
            nFailed++;
            failedMessages.append(tr("Masternode %1 not found").arg(protx_hash));
            continue;
        }

        // Create vote
        CGovernanceVote vote(dmn->collateralOutpointRaw(), proposalHash, VOTE_SIGNAL_FUNDING, outcome);

        // Sign vote via wallet interface
        if (!walletModel->wallet().signGovernanceVote(votingKeyID, vote)) {
            nFailed++;
            failedMessages.append(tr("Failed to sign vote for masternode %1").arg(protx_hash));
            continue;
        }

        // Submit vote
        std::string strError;
        if (clientModel->node().gov().processVoteAndRelay(vote, strError)) {
            nSuccessful++;
        } else {
            nFailed++;
            failedMessages.append(
                tr("Masternode %1: %2").arg(QString::fromStdString(proTxHash.ToString()), QString::fromStdString(strError)));
        }
    }

    // Show results
    QString message;
    if (nSuccessful > 0) {
        message += tr("Voted successfully %n time(s)", "", nSuccessful);
    }
    if (nFailed > 0) {
        if (nSuccessful > 0) {
            message += QString("\n");
        }
        message += tr("Failed to vote %n time(s)", "", nFailed);
    }
    if (!failedMessages.isEmpty()) {
        message += QString("\n\n") + tr("Errors:") + QString("\n") + failedMessages.join("\n");
    }

    QMessageBox::information(this, tr("Voting Results"), message);

    // Update proposal list to show new vote counts
    requestForceRefresh();
}

void ProposalList::showEvent(QShowEvent* event)
{
    QWidget::showEvent(event);
    refreshColumnWidths();
}

void ProposalList::resizeEvent(QResizeEvent* event)
{
    QWidget::resizeEvent(event);
    refreshColumnWidths();
}

void ProposalList::refreshColumnWidths()
{
    // Bail out if resize in progress or viewport is too small
    const int tableWidth = ui->govTableView->viewport()->width();
    if (m_col_refresh || tableWidth <= 0) {
        return;
    } else {
        m_col_refresh = true;
    }

    auto* header = ui->govTableView->horizontalHeader();
    header->setMinimumSectionSize(0);
    header->setSectionResizeMode(ProposalModel::Column::STATUS, QHeaderView::ResizeToContents);
    header->setSectionResizeMode(ProposalModel::Column::PAYMENT_AMOUNT, QHeaderView::ResizeToContents);
    header->setSectionResizeMode(ProposalModel::Column::START_DATE, QHeaderView::ResizeToContents);
    header->setSectionResizeMode(ProposalModel::Column::END_DATE, QHeaderView::ResizeToContents);
    header->setSectionResizeMode(ProposalModel::Column::VOTING_STATUS, QHeaderView::ResizeToContents);
    header->setSectionResizeMode(ProposalModel::Column::HASH, QHeaderView::ResizeToContents);

    // Calculate width used by ResizeToContents columns
    const int availableWidth = [this, &header, &tableWidth](){
        int fixedWidth = 0;
        for (int idx = 0; idx < ProposalModel::Column::_COUNT; idx++) {
            if (idx != ProposalModel::Column::TITLE && idx != ProposalModel::Column::HASH && !ui->govTableView->isColumnHidden(idx)) {
                fixedWidth += header->sectionSize(idx);
            }
        }
        return std::max(0, tableWidth - fixedWidth);
    }();

    // Hash gets what's left after Title takes its minimum, clamped to [0, hashContentWidth]
    const int hashContentWidth = header->sectionSize(ProposalModel::Column::HASH);
    const int hashWidth = std::clamp<int>(availableWidth - TITLE_MIN_WIDTH, 0, hashContentWidth);
    const int titleWidth = availableWidth - hashWidth;
    header->setSectionResizeMode(ProposalModel::Column::TITLE, QHeaderView::Interactive);
    header->setSectionResizeMode(ProposalModel::Column::HASH, QHeaderView::Interactive);
    header->resizeSection(ProposalModel::Column::TITLE, titleWidth);
    header->resizeSection(ProposalModel::Column::HASH, hashWidth);

    m_col_refresh = false;
}

void ProposalList::requestForceRefresh()
{
    if (!m_feed_proposal) {
        return;
    }
    m_feed_proposal->requestForceRefresh();
}
