/*
 * This file is part of Bisq.
 *
 * Bisq is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at
 * your option) any later version.
 *
 * Bisq is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Bisq. If not, see <http://www.gnu.org/licenses/>.
 */

package bisq.desktop.main.dao.bonding;

import bisq.desktop.Navigation;
import bisq.desktop.main.MainView;
import bisq.desktop.main.funds.FundsView;
import bisq.desktop.main.funds.deposit.DepositView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.util.GUIUtil;

import bisq.core.btc.setup.WalletsSetup;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.governance.bond.lockup.LockupReason;
import bisq.core.dao.governance.bond.reputation.MyReputation;
import bisq.core.dao.governance.bond.reputation.MyReputationListService;
import bisq.core.dao.governance.bond.role.BondedRolesRepository;
import bisq.core.dao.state.model.blockchain.TxOutput;
import bisq.core.dao.state.model.governance.Role;
import bisq.core.dao.state.model.governance.RoleProposal;
import bisq.core.locale.Res;
import bisq.core.util.coin.ImmutableCoinFormatter;
import bisq.core.util.coin.BsqFormatter;
import bisq.core.util.coin.CoinFormatter;
import bisq.core.util.coin.CoinUtil;
import bisq.core.util.FormattingUtils;

import bisq.network.p2p.P2PService;

import bisq.common.app.DevEnv;
import bisq.common.util.Tuple2;

import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;

import javax.inject.Inject;
import javax.inject.Singleton;

import java.util.Optional;
import java.util.function.Consumer;

import lombok.extern.slf4j.Slf4j;

import static com.google.common.base.Preconditions.checkArgument;

@Slf4j
@Singleton
public class BondingViewUtils {
    private final P2PService p2PService;
    private final MyReputationListService myReputationListService;
    private final BondedRolesRepository bondedRolesRepository;
    private final WalletsSetup walletsSetup;
    private final DaoFacade daoFacade;
    private final Navigation navigation;
    private final BsqFormatter bsqFormatter;

    @Inject
    public BondingViewUtils(P2PService p2PService,
                            MyReputationListService myReputationListService,
                            BondedRolesRepository bondedRolesRepository,
                            WalletsSetup walletsSetup,
                            DaoFacade daoFacade,
                            Navigation navigation,
                            BsqFormatter bsqFormatter) {
        this.p2PService = p2PService;
        this.myReputationListService = myReputationListService;
        this.bondedRolesRepository = bondedRolesRepository;
        this.walletsSetup = walletsSetup;
        this.daoFacade = daoFacade;
        this.navigation = navigation;
        this.bsqFormatter = bsqFormatter;
    }

    public void lockupBondForBondedRole(Role role, Consumer<String> resultHandler) {
        Optional<RoleProposal> roleProposal = getAcceptedBondedRoleProposal(role);
        checkArgument(roleProposal.isPresent(), "roleProposal must be present");

        long requiredBond = daoFacade.getRequiredBond(roleProposal);
        Coin lockupAmount = Coin.valueOf(requiredBond);
        int lockupTime = roleProposal.get().getUnlockTime();
        if (!bondedRolesRepository.isBondedAssetAlreadyInBond(role)) {
            lockupBond(role.getHash(), lockupAmount, lockupTime, LockupReason.BONDED_ROLE, resultHandler);
        } else {
            handleError(new RuntimeException("The role has been used already for a lockup tx."));
        }
    }

    public void lockupBondForReputation(Coin lockupAmount, int lockupTime, byte[] salt, Consumer<String> resultHandler) {
        MyReputation myReputation = new MyReputation(salt);
        lockupBond(myReputation.getHash(), lockupAmount, lockupTime, LockupReason.REPUTATION, resultHandler);
        myReputationListService.addReputation(myReputation);
    }

    private void lockupBond(byte[] hash, Coin lockupAmount, int lockupTime, LockupReason lockupReason,
                            Consumer<String> resultHandler) {
        if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) {
            if (!DevEnv.isDevMode()) {
                try {
                    Tuple2<Coin, Integer> miningFeeAndTxSize = daoFacade.getLockupTxMiningFeeAndTxSize(lockupAmount, lockupTime, lockupReason, hash);
                    Coin miningFee = miningFeeAndTxSize.first;
                    int txSize = miningFeeAndTxSize.second;
                    String duration = FormattingUtils.formatDurationAsWords(lockupTime * 10 * 60 * 1000L, false, false);
                    new Popup().headLine(Res.get("dao.bond.reputation.lockup.headline"))
                            .confirmation(Res.get("dao.bond.reputation.lockup.details",
                                    bsqFormatter.formatCoinWithCode(lockupAmount),
                                    lockupTime,
                                    duration,
                                    bsqFormatter.formatBTCWithCode(miningFee),
                                    CoinUtil.getFeePerByte(miningFee, txSize),
                                    txSize / 1000d
                            ))
                            .actionButtonText(Res.get("shared.yes"))
                            .onAction(() -> publishLockupTx(lockupAmount, lockupTime, lockupReason, hash, resultHandler))
                            .closeButtonText(Res.get("shared.cancel"))
                            .show();
                } catch (Throwable e) {
                    log.error(e.toString());
                    e.printStackTrace();
                    new Popup().warning(e.getMessage()).show();
                }
            } else {
                publishLockupTx(lockupAmount, lockupTime, lockupReason, hash, resultHandler);
            }
        }
    }

    private void publishLockupTx(Coin lockupAmount, int lockupTime, LockupReason lockupReason, byte[] hash, Consumer<String> resultHandler) {
        daoFacade.publishLockupTx(lockupAmount,
                lockupTime,
                lockupReason,
                hash,
                txId -> {
                    if (!DevEnv.isDevMode())
                        new Popup().feedback(Res.get("dao.tx.published.success")).show();

                    if (resultHandler != null)
                        resultHandler.accept(txId);
                },
                this::handleError
        );
    }

    public Optional<RoleProposal> getAcceptedBondedRoleProposal(Role role) {
        return bondedRolesRepository.getAcceptedBondedRoleProposal(role);
    }

    public void unLock(String lockupTxId, Consumer<String> resultHandler) {
        if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) {
            Optional<TxOutput> lockupTxOutput = daoFacade.getLockupTxOutput(lockupTxId);
            checkArgument(lockupTxOutput.isPresent(), "Lockup output must be present. TxId=" + lockupTxId);
            Coin unlockAmount = Coin.valueOf(lockupTxOutput.get().getValue());
            Optional<Integer> opLockTime = daoFacade.getLockTime(lockupTxId);
            int lockTime = opLockTime.orElse(-1);

            try {
                if (!DevEnv.isDevMode()) {
                    Tuple2<Coin, Integer> miningFeeAndTxSize = daoFacade.getUnlockTxMiningFeeAndTxSize(lockupTxId);
                    Coin miningFee = miningFeeAndTxSize.first;
                    int txSize = miningFeeAndTxSize.second;
                    String duration = FormattingUtils.formatDurationAsWords(lockTime * 10 * 60 * 1000L, false, false);
                    new Popup().headLine(Res.get("dao.bond.reputation.unlock.headline"))
                            .confirmation(Res.get("dao.bond.reputation.unlock.details",
                                    bsqFormatter.formatCoinWithCode(unlockAmount),
                                    lockTime,
                                    duration,
                                    bsqFormatter.formatBTCWithCode(miningFee),
                                    CoinUtil.getFeePerByte(miningFee, txSize),
                                    txSize / 1000d
                            ))
                            .actionButtonText(Res.get("shared.yes"))
                            .onAction(() -> publishUnlockTx(lockupTxId, resultHandler))
                            .closeButtonText(Res.get("shared.cancel"))
                            .show();
                } else {
                    publishUnlockTx(lockupTxId, resultHandler);
                }
            } catch (Throwable t) {
                log.error(t.toString());
                t.printStackTrace();
                new Popup().warning(t.getMessage()).show();
            }
        }
        log.info("unlock tx: {}", lockupTxId);
    }

    private void publishUnlockTx(String lockupTxId, Consumer<String> resultHandler) {
        daoFacade.publishUnlockTx(lockupTxId,
                txId -> {
                    if (!DevEnv.isDevMode())
                        new Popup().confirmation(Res.get("dao.tx.published.success")).show();

                    if (resultHandler != null)
                        resultHandler.accept(txId);
                },
                errorMessage -> new Popup().warning(errorMessage.toString()).show()
        );
    }

    private void handleError(Throwable throwable) {
        if (throwable instanceof InsufficientMoneyException) {
            final Coin missingCoin = ((InsufficientMoneyException) throwable).missing;
            final String missing = missingCoin != null ? missingCoin.toFriendlyString() : "null";
            new Popup().warning(Res.get("popup.warning.insufficientBtcFundsForBsqTx", missing))
                    .actionButtonTextWithGoTo("navigation.funds.depositFunds")
                    .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class))
                    .show();
        } else {
            log.error(throwable.toString());
            throwable.printStackTrace();
            new Popup().warning(throwable.toString()).show();
        }
    }
}