#include "wallet/lite_wallet_state_apply_plan.h"

#include <algorithm>
#include <cmath>
#include <limits>
#include <sstream>
#include <utility>

namespace dragonx::wallet {
namespace {

constexpr double kZatoshisPerCoin = 100000000.0;

std::string stringValue(const std::string& value)
{
    return value;
}

std::string intValue(std::int64_t value)
{
    return std::to_string(value);
}

std::string uintValue(std::uint64_t value)
{
    return std::to_string(value);
}

std::string doubleValue(double value)
{
    std::ostringstream output;
    output << value;
    return output.str();
}

std::string boolValue(bool value)
{
    return value ? "true" : "false";
}

std::uint64_t coinsToZatoshis(double coins)
{
    if (coins <= 0.0) return 0;
    const double zatoshis = std::round(coins * kZatoshisPerCoin);
    const double maxValue = static_cast<double>(std::numeric_limits<std::uint64_t>::max());
    if (zatoshis >= maxValue) return std::numeric_limits<std::uint64_t>::max();
    return static_cast<std::uint64_t>(zatoshis);
}

bool modelHasAnySection(const LiteWalletAppRefreshModel& model)
{
    return model.hasChainInfo || model.hasHeight || model.hasBalance || model.hasAddresses ||
           model.hasSpendableOutputs || model.hasTransactions || model.hasSyncStatus;
}

void addIssue(LiteWalletStateApplyPlan& plan,
              LiteWalletStateApplyIssue issue,
              std::string message)
{
    plan.issues.push_back(LiteWalletStateApplyIssueInfo{issue, std::move(message)});
}

void addFieldPlan(LiteWalletStateApplyPlan& plan,
                  LiteWalletStateApplySection section,
                  const std::string& fieldName,
                  std::string currentValue,
                  std::string proposedValue)
{
    LiteWalletStateApplyFieldPlan fieldPlan;
    fieldPlan.section = section;
    fieldPlan.fieldName = fieldName;
    fieldPlan.currentValue = std::move(currentValue);
    fieldPlan.proposedValue = std::move(proposedValue);
    fieldPlan.wouldChange = fieldPlan.currentValue != fieldPlan.proposedValue;
    fieldPlan.action = fieldPlan.wouldChange
        ? LiteWalletStateApplyAction::SetField
        : LiteWalletStateApplyAction::Noop;
    if (fieldPlan.wouldChange) plan.wouldChangeWalletState = true;
    plan.fieldPlans.push_back(std::move(fieldPlan));
}

void addCollectionPlan(LiteWalletStateApplyPlan& plan,
                       LiteWalletStateApplySection section,
                       std::string collectionName,
                       std::size_t currentCount,
                       std::size_t proposedCount,
                       bool hasWalletStateTarget,
                       bool wouldReplace)
{
    LiteWalletStateApplyCollectionPlan collectionPlan;
    collectionPlan.section = section;
    collectionPlan.collectionName = std::move(collectionName);
    collectionPlan.currentCount = currentCount;
    collectionPlan.proposedCount = proposedCount;
    collectionPlan.hasWalletStateTarget = hasWalletStateTarget;
    collectionPlan.wouldReplace = hasWalletStateTarget && wouldReplace;
    if (!hasWalletStateTarget) {
        collectionPlan.action = LiteWalletStateApplyAction::InspectOnly;
    } else {
        collectionPlan.action = collectionPlan.wouldReplace
            ? LiteWalletStateApplyAction::ReplaceCollection
            : LiteWalletStateApplyAction::Noop;
    }
    if (collectionPlan.wouldReplace) plan.wouldChangeWalletState = true;
    plan.collectionPlans.push_back(std::move(collectionPlan));
}

std::vector<std::string> mappedAddresses(const LiteWalletAppRefreshModel& model,
                                         LiteWalletAppAddressKind kind)
{
    std::vector<std::string> addresses;
    for (const auto& address : model.addresses) {
        if (address.kind == kind) addresses.push_back(address.address);
    }
    return addresses;
}

std::vector<std::string> mappedAddressStrings(const LiteWalletAppRefreshModel& model)
{
    std::vector<std::string> addresses;
    addresses.reserve(model.addresses.size());
    for (const auto& address : model.addresses) addresses.push_back(address.address);
    return addresses;
}

std::vector<std::string> stateAddressStrings(const std::vector<dragonx::AddressInfo>& addresses)
{
    std::vector<std::string> values;
    values.reserve(addresses.size());
    for (const auto& address : addresses) values.push_back(address.address);
    return values;
}

std::vector<std::string> mappedTransactionIds(const LiteWalletAppRefreshModel& model)
{
    std::vector<std::string> txids;
    txids.reserve(model.transactions.size());
    for (const auto& transaction : model.transactions) txids.push_back(transaction.txid);
    return txids;
}

std::vector<std::string> stateTransactionIds(const std::vector<dragonx::TransactionInfo>& transactions)
{
    std::vector<std::string> txids;
    txids.reserve(transactions.size());
    for (const auto& transaction : transactions) txids.push_back(transaction.txid);
    return txids;
}

void addChainInfoPlans(LiteWalletStateApplyPlan& plan,
                       const LiteWalletAppRefreshModel& model,
                       const dragonx::WalletState& state)
{
    if (!model.hasChainInfo) return;
    plan.hasChainInfo = true;

    if (model.chain.longestChain) {
        addFieldPlan(plan,
                     LiteWalletStateApplySection::ChainInfo,
                     "longestchain",
                     intValue(state.longestchain),
                     intValue(*model.chain.longestChain));
    }
    if (model.chain.notarized) {
        addFieldPlan(plan,
                     LiteWalletStateApplySection::ChainInfo,
                     "notarized",
                     intValue(state.notarized),
                     intValue(*model.chain.notarized));
    }
    if (model.chain.chainName) {
        addFieldPlan(plan,
                     LiteWalletStateApplySection::ChainInfo,
                     "chain_name",
                     stringValue(state.mining.chain),
                     stringValue(*model.chain.chainName));
    }
}

void addHeightPlans(LiteWalletStateApplyPlan& plan,
                    const LiteWalletAppRefreshModel& model,
                    const dragonx::WalletState& state)
{
    if (!model.hasHeight) return;
    plan.hasHeight = true;
    addFieldPlan(plan,
                 LiteWalletStateApplySection::Height,
                 "sync.blocks",
                 intValue(state.sync.blocks),
                 intValue(model.height.height));
}

void addBalancePlans(LiteWalletStateApplyPlan& plan,
                     const LiteWalletAppRefreshModel& model,
                     const dragonx::WalletState& state)
{
    if (!model.hasBalance) return;
    plan.hasBalance = true;
    addFieldPlan(plan,
                 LiteWalletStateApplySection::Balance,
                 "privateBalanceZatoshis",
                 uintValue(coinsToZatoshis(state.privateBalance)),
                 uintValue(model.balance.shieldedZatoshis));
    addFieldPlan(plan,
                 LiteWalletStateApplySection::Balance,
                 "transparentBalanceZatoshis",
                 uintValue(coinsToZatoshis(state.transparentBalance)),
                 uintValue(model.balance.transparentZatoshis));
    addFieldPlan(plan,
                 LiteWalletStateApplySection::Balance,
                 "totalBalanceZatoshis",
                 uintValue(coinsToZatoshis(state.totalBalance)),
                 uintValue(model.balance.totalZatoshis));
    addFieldPlan(plan,
                 LiteWalletStateApplySection::Balance,
                 "unconfirmedBalanceZatoshis",
                 uintValue(coinsToZatoshis(state.unconfirmedBalance)),
                 uintValue(model.balance.unconfirmedZatoshis));
}

void addAddressPlans(LiteWalletStateApplyPlan& plan,
                     const LiteWalletAppRefreshModel& model,
                     const dragonx::WalletState& state)
{
    if (!model.hasAddresses) return;
    plan.hasAddresses = true;

    const auto proposedShielded = mappedAddresses(model, LiteWalletAppAddressKind::Shielded);
    const auto proposedTransparent = mappedAddresses(model, LiteWalletAppAddressKind::Transparent);
    const auto proposedCombined = mappedAddressStrings(model);
    const auto currentShielded = stateAddressStrings(state.z_addresses);
    const auto currentTransparent = stateAddressStrings(state.t_addresses);
    const auto currentCombined = stateAddressStrings(state.addresses);

    addCollectionPlan(plan,
                      LiteWalletStateApplySection::Addresses,
                      "z_addresses",
                      currentShielded.size(),
                      proposedShielded.size(),
                      true,
                      currentShielded != proposedShielded);
    addCollectionPlan(plan,
                      LiteWalletStateApplySection::Addresses,
                      "t_addresses",
                      currentTransparent.size(),
                      proposedTransparent.size(),
                      true,
                      currentTransparent != proposedTransparent);
    addCollectionPlan(plan,
                      LiteWalletStateApplySection::Addresses,
                      "addresses",
                      currentCombined.size(),
                      proposedCombined.size(),
                      true,
                      currentCombined != proposedCombined);

    const auto unknownSpendability = std::any_of(model.addresses.begin(), model.addresses.end(),
        [](const LiteWalletAppAddressModel& address) { return !address.spendabilityKnown; });
    if (unknownSpendability) {
        addIssue(plan,
                 LiteWalletStateApplyIssue::AddressSpendabilityUnknown,
                 "lite address rows do not yet carry wallet spendability policy");
    }
}

void addSpendableOutputPlans(LiteWalletStateApplyPlan& plan,
                             const LiteWalletAppRefreshModel& model)
{
    if (!model.hasSpendableOutputs) return;
    plan.hasSpendableOutputs = true;
    addCollectionPlan(plan,
                      LiteWalletStateApplySection::SpendableOutputs,
                      "spendableOutputs",
                      0,
                      model.spendableOutputs.size(),
                      false,
                      false);
    addIssue(plan,
             LiteWalletStateApplyIssue::SpendableOutputsHaveNoWalletStateTarget,
             "lite spendable outputs remain source-only until an app apply target is designed");
}

void addTransactionPlans(LiteWalletStateApplyPlan& plan,
                         const LiteWalletAppRefreshModel& model,
                         const dragonx::WalletState& state)
{
    if (!model.hasTransactions) return;
    plan.hasTransactions = true;
    const auto proposedTxids = mappedTransactionIds(model);
    const auto currentTxids = stateTransactionIds(state.transactions);
    addCollectionPlan(plan,
                      LiteWalletStateApplySection::Transactions,
                      "transactions",
                      currentTxids.size(),
                      proposedTxids.size(),
                      true,
                      currentTxids != proposedTxids);
}

void addSyncPlans(LiteWalletStateApplyPlan& plan,
                  const LiteWalletAppRefreshModel& model,
                  const dragonx::WalletState& state)
{
    if (!model.hasSyncStatus) return;
    plan.hasSyncStatus = true;
    addFieldPlan(plan,
                 LiteWalletStateApplySection::SyncStatus,
                 "sync.blocks",
                 intValue(state.sync.blocks),
                 uintValue(model.sync.walletHeight));
    addFieldPlan(plan,
                 LiteWalletStateApplySection::SyncStatus,
                 "sync.headers",
                 intValue(state.sync.headers),
                 uintValue(model.sync.chainHeight));
    addFieldPlan(plan,
                 LiteWalletStateApplySection::SyncStatus,
                 "sync.verification_progress",
                 doubleValue(state.sync.verification_progress),
                 doubleValue(model.sync.progress));
    addFieldPlan(plan,
                 LiteWalletStateApplySection::SyncStatus,
                 "sync.syncing",
                 boolValue(state.sync.syncing),
                 boolValue(!model.sync.complete));
}

} // namespace

const char* liteWalletStateApplySectionName(LiteWalletStateApplySection section)
{
    switch (section) {
        case LiteWalletStateApplySection::ChainInfo: return "ChainInfo";
        case LiteWalletStateApplySection::Height: return "Height";
        case LiteWalletStateApplySection::Balance: return "Balance";
        case LiteWalletStateApplySection::Addresses: return "Addresses";
        case LiteWalletStateApplySection::SpendableOutputs: return "SpendableOutputs";
        case LiteWalletStateApplySection::Transactions: return "Transactions";
        case LiteWalletStateApplySection::SyncStatus: return "SyncStatus";
    }
    return "Unknown";
}

const char* liteWalletStateApplyActionName(LiteWalletStateApplyAction action)
{
    switch (action) {
        case LiteWalletStateApplyAction::Noop: return "Noop";
        case LiteWalletStateApplyAction::SetField: return "SetField";
        case LiteWalletStateApplyAction::ReplaceCollection: return "ReplaceCollection";
        case LiteWalletStateApplyAction::InspectOnly: return "InspectOnly";
    }
    return "Unknown";
}

const char* liteWalletStateApplyIssueName(LiteWalletStateApplyIssue issue)
{
    switch (issue) {
        case LiteWalletStateApplyIssue::EmptyModel: return "EmptyModel";
        case LiteWalletStateApplyIssue::IncompleteModel: return "IncompleteModel";
        case LiteWalletStateApplyIssue::AddressSpendabilityUnknown: return "AddressSpendabilityUnknown";
        case LiteWalletStateApplyIssue::SpendableOutputsHaveNoWalletStateTarget: return "SpendableOutputsHaveNoWalletStateTarget";
    }
    return "Unknown";
}

LiteWalletStateApplyPlan planLiteWalletStateApply(const LiteWalletAppRefreshModel& model,
                                                  const dragonx::WalletState& state)
{
    LiteWalletStateApplyPlan plan;
    plan.sourceComplete = model.complete;

    if (!modelHasAnySection(model)) {
        addIssue(plan,
                 LiteWalletStateApplyIssue::EmptyModel,
                 "lite app refresh model has no sections to compare");
        plan.error = plan.issues.back().message;
        return plan;
    }

    if (!model.complete) {
        addIssue(plan,
                 LiteWalletStateApplyIssue::IncompleteModel,
                 "lite app refresh model is partial; dry-run apply planning only");
    }

    addChainInfoPlans(plan, model, state);
    addHeightPlans(plan, model, state);
    addBalancePlans(plan, model, state);
    addAddressPlans(plan, model, state);
    addSpendableOutputPlans(plan, model);
    addTransactionPlans(plan, model, state);
    addSyncPlans(plan, model, state);

    plan.ok = true;
    return plan;
}

LiteWalletStateApplyPlan LiteWalletStateApplyPlanner::buildPlan(
    const LiteWalletAppRefreshModel& model,
    const dragonx::WalletState& state) const
{
    return planLiteWalletStateApply(model, state);
}

} // namespace dragonx::wallet