import ast, csv, json, os, time, base64, random, requests, logging
from io import StringIO
from werkzeug.wrappers import Response
from datetime import datetime
from numbers import Number
from ..util.tx import decoderawtransaction
from ..util.price_providers import get_price_at

from flask import (
    Flask,
    Blueprint,
    render_template,
    request,
    redirect,
    url_for,
    jsonify,
    flash,
)
from flask_login import login_required, current_user
from ..util.descriptor import AddChecksum, Descriptor
from ..helpers import (
    bcur2base64,
    get_devices_with_keys_by_type,
    get_txid,
    is_testnet,
    parse_wallet_data_import,
)
from ..persistence import delete_file
from ..key import Key
from ..specter import Specter
from ..specter_error import SpecterError
from ..wallet_manager import purposes
from ..rpc import RpcError
from binascii import b2a_base64
from ..util.base43 import b43_decode

from flask import current_app as app

rand = random.randint(0, 1e32)  # to force style refresh

# Setup endpoint blueprint
wallets_endpoint = Blueprint("wallets_endpoint", __name__)


################## Wallet overview #######################


@wallets_endpoint.route("/wallets_overview/")
@login_required
def wallets_overview():
    app.specter.check_blockheight()
    for wallet in app.specter.wallet_manager.wallets.values():
        wallet.get_balance()
        wallet.check_utxo()
    return render_template(
        "wallet/wallets_overview.jinja",
        specter=app.specter,
        rand=rand,
    )


################## New wallet #######################


@wallets_endpoint.route("/new_wallet/")
@login_required
def new_wallet_type():
    err = None
    if app.specter.chain is None:
        err = "Configure Bitcoin Core to create wallets"
        return render_template("base.jinja", error=err, specter=app.specter, rand=rand)
    try:
        # Make sure wallet is enabled on Bitcoin Core
        app.specter.rpc.listwallets()
    except Exception:
        err = '<p><br>Configure Bitcoin Core is running with wallets disabled.<br><br>Please make sure disablewallet is off (set disablewallet=0 in your bitcoin.conf), then restart Bitcoin Core and try again.<br>See <a href="https://github.com/cryptoadvance/specter-desktop/blob/34ca139694ecafb2e7c2bd5ad5c4ac74c6d11501/docs/faq.md#im-not-sure-i-want-the-bitcoin-core-wallet-functionality-to-be-used-is-that-mandatory-if-so-is-it-considered-secure" target="_blank" style="color: white;">here</a> for more information.</p>'
        return render_template("base.jinja", error=err, specter=app.specter, rand=rand)
    return render_template(
        "wallet/new_wallet/new_wallet_type.jinja", specter=app.specter, rand=rand
    )


@wallets_endpoint.route("/new_wallet/<wallet_type>/", methods=["GET", "POST"])
@login_required
def new_wallet(wallet_type):
    wallet_types = ["simple", "multisig", "import_wallet"]
    if wallet_type not in wallet_types:
        flash("Unknown wallet type requested", "error")
        return redirect(url_for("wallets_endpoint.new_wallet_type"))

    err = None
    if request.method == "POST":
        action = request.form["action"]
        if action == "importwallet":
            try:
                wallet_data = json.loads(request.form["wallet_data"].replace("'", "h"))
                (
                    wallet_name,
                    recv_descriptor,
                    cosigners_types,
                ) = parse_wallet_data_import(wallet_data)
            except Exception:
                flash("Unsupported wallet import format", "error")
                return redirect(url_for("wallets_endpoint.new_wallet_type"))
            # get min of the two
            # if the node is still syncing
            # and the first block with tx is not there yet
            startblock = min(
                wallet_data.get("blockheight", app.specter.info.get("blocks", 0)),
                app.specter.info.get("blocks", 0),
            )
            # check if pruned
            if app.specter.info.get("pruned", False):
                newstartblock = max(startblock, app.specter.info.get("pruneheight", 0))
                if newstartblock > startblock:
                    flash(
                        f"Using pruned node - we will only rescan from block {newstartblock}",
                        "error",
                    )
                    startblock = newstartblock
            try:
                descriptor = Descriptor.parse(
                    AddChecksum(recv_descriptor.split("#")[0]),
                    testnet=is_testnet(app.specter.chain),
                )
                if descriptor is None:
                    flash("Invalid wallet descriptor.", "error")
                    return redirect(url_for("wallets_endpoint.new_wallet_type"))
            except:
                flash("Invalid wallet descriptor.", "error")
                return redirect(url_for("wallets_endpoint.new_wallet_type"))
            if wallet_name in app.specter.wallet_manager.wallets_names:
                flash("Wallet with the same name already exists", "error")
                return redirect(url_for("wallets_endpoint.new_wallet_type"))

            sigs_total = descriptor.multisig_N
            sigs_required = descriptor.multisig_M
            if descriptor.wpkh:
                address_type = "wpkh"
            elif descriptor.wsh:
                address_type = "wsh"
            elif descriptor.sh_wpkh:
                address_type = "sh-wpkh"
            elif descriptor.sh_wsh:
                address_type = "sh-wsh"
            elif descriptor.sh:
                address_type = "sh-wsh"
            else:
                address_type = "pkh"
            keys = []
            cosigners = []
            unknown_cosigners = []
            unknown_cosigners_types = []
            if sigs_total == None:
                sigs_total = 1
                sigs_required = 1
                descriptor.origin_fingerprint = [descriptor.origin_fingerprint]
                descriptor.origin_path = [descriptor.origin_path]
                descriptor.base_key = [descriptor.base_key]
            for i in range(sigs_total):
                cosigner_found = False
                for device in app.specter.device_manager.devices:
                    cosigner = app.specter.device_manager.devices[device]
                    if descriptor.origin_fingerprint[i] is None:
                        descriptor.origin_fingerprint[i] = ""
                    if descriptor.origin_path[i] is None:
                        descriptor.origin_path[i] = descriptor.origin_fingerprint[i]
                    for key in cosigner.keys:
                        if key.fingerprint + key.derivation.replace(
                            "m", ""
                        ) == descriptor.origin_fingerprint[i] + descriptor.origin_path[
                            i
                        ].replace(
                            "'", "h"
                        ):
                            keys.append(key)
                            cosigners.append(cosigner)
                            cosigner_found = True
                            break
                    if cosigner_found:
                        break
                if not cosigner_found:
                    desc_key = Key.parse_xpub(
                        "[{}{}]{}".format(
                            descriptor.origin_fingerprint[i],
                            descriptor.origin_path[i],
                            descriptor.base_key[i],
                        )
                    )
                    unknown_cosigners.append(desc_key)
                    if len(unknown_cosigners) > len(cosigners_types):
                        unknown_cosigners_types.append("other")
                    else:
                        unknown_cosigners_types.append(cosigners_types[i])
            wallet_type = "multisig" if sigs_total > 1 else "simple"
            createwallet = "createwallet" in request.form
            if createwallet:
                wallet_name = request.form["wallet_name"]
                for i, unknown_cosigner in enumerate(unknown_cosigners):
                    unknown_cosigner_name = request.form[
                        "unknown_cosigner_{}_name".format(i)
                    ]
                    unknown_cosigner_type = request.form.get(
                        "unknown_cosigner_{}_type".format(i), "other"
                    )
                    device = app.specter.device_manager.add_device(
                        name=unknown_cosigner_name,
                        device_type=unknown_cosigner_type,
                        keys=[unknown_cosigner],
                    )
                    keys.append(unknown_cosigner)
                    cosigners.append(device)
                try:
                    wallet = app.specter.wallet_manager.create_wallet(
                        wallet_name, sigs_required, address_type, keys, cosigners
                    )
                except Exception as e:
                    flash("Failed to create wallet: %r" % e, "error")
                    return redirect(url_for("wallets_endpoint.new_wallet_type"))
                wallet.keypoolrefill(0, wallet.IMPORT_KEYPOOL, change=False)
                wallet.keypoolrefill(0, wallet.IMPORT_KEYPOOL, change=True)
                wallet.import_labels(wallet_data.get("labels", {}))
                flash("Wallet imported successfully", "info")
                try:
                    wallet.rpc.rescanblockchain(startblock, timeout=1)
                    app.logger.info("Rescanning Blockchain ...")
                except requests.exceptions.ReadTimeout:
                    # this is normal behavior in our usecase
                    pass
                except Exception as e:
                    app.logger.error("Exception while rescanning blockchain: %r" % e)
                    flash("Failed to perform rescan for wallet: %r" % e, "error")
                wallet.getdata()
                return redirect(
                    url_for("wallets_endpoint.receive", wallet_alias=wallet.alias)
                    + "?newwallet=true"
                )
            else:
                return render_template(
                    "wallet/new_wallet/import_wallet.jinja",
                    wallet_data=json.dumps(wallet_data),
                    wallet_type=wallet_type,
                    wallet_name=wallet_name,
                    cosigners=cosigners,
                    unknown_cosigners=unknown_cosigners,
                    unknown_cosigners_types=unknown_cosigners_types,
                    sigs_required=sigs_required,
                    sigs_total=sigs_total,
                    specter=app.specter,
                    rand=rand,
                )
        if action == "device":
            cosigners = [
                app.specter.device_manager.get_by_alias(alias)
                for alias in request.form.getlist("devices")
            ]
            devices = get_devices_with_keys_by_type(app, cosigners, wallet_type)
            for device in devices:
                if len(device.keys) == 0:
                    err = (
                        "Device %s doesn't have keys matching this wallet type"
                        % device.name
                    )
                    break

            name = wallet_type.title()
            wallet_name = name
            i = 2
            while wallet_name in app.specter.wallet_manager.wallets_names:
                wallet_name = "%s %d" % (name, i)
                i += 1

            return render_template(
                "wallet/new_wallet/new_wallet_keys.jinja",
                purposes=purposes,
                cosigners=devices,
                wallet_type=wallet_type,
                sigs_total=len(devices),
                sigs_required=max(len(devices) * 2 // 3, 1),
                error=err,
                specter=app.specter,
                rand=rand,
            )
        if action == "key" and err is None:
            wallet_name = request.form["wallet_name"]
            address_type = request.form["type"]
            sigs_total = int(request.form.get("sigs_total", 1))
            sigs_required = int(request.form.get("sigs_required", 1))
            if wallet_name in app.specter.wallet_manager.wallets_names:
                err = "Wallet already exists"
            if err:
                devices = [
                    app.specter.device_manager.get_by_alias(
                        request.form.get("cosigner{}".format(i))
                    )
                    for i in range(0, sigs_total)
                ]
                return render_template(
                    "wallet/new_wallet/new_wallet_keys.jinja",
                    purposes=purposes,
                    cosigners=devices,
                    wallet_type=wallet_type,
                    sigs_total=len(devices),
                    sigs_required=max(len(devices) * 2 // 3, 1),
                    error=err,
                    specter=app.specter,
                    rand=rand,
                )

            keys = []
            cosigners = []
            devices = []
            for i in range(sigs_total):
                try:
                    key = request.form["key%d" % i]
                    cosigner_name = request.form["cosigner%d" % i]
                    cosigner = app.specter.device_manager.get_by_alias(cosigner_name)
                    cosigners.append(cosigner)
                    for k in cosigner.keys:
                        if k.original == key:
                            keys.append(k)
                            break
                except:
                    pass
            if len(keys) != sigs_total or len(cosigners) != sigs_total:
                err = "No keys were selected for device, please try adding keys first"
                devices = [
                    app.specter.device_manager.get_by_alias(
                        request.form.get("cosigner{}".format(i))
                    )
                    for i in range(0, sigs_total)
                ]
                return render_template(
                    "wallet/new_wallet/new_wallet_keys.jinja",
                    purposes=purposes,
                    cosigners=devices,
                    wallet_type=wallet_type,
                    sigs_total=len(devices),
                    sigs_required=max(len(devices) * 2 // 3, 1),
                    error=err,
                    specter=app.specter,
                    rand=rand,
                )

            # create a wallet here
            try:
                wallet = app.specter.wallet_manager.create_wallet(
                    wallet_name, sigs_required, address_type, keys, cosigners
                )
            except Exception:
                err = "Failed to create wallet..."
                return render_template(
                    "wallet/new_wallet/new_wallet_keys.jinja",
                    purposes=purposes,
                    cosigners=cosigners,
                    wallet_type=wallet_type,
                    sigs_total=len(devices),
                    sigs_required=max(len(devices) * 2 // 3, 1),
                    error=err,
                    specter=app.specter,
                    rand=rand,
                )

            app.logger.info("Created Wallet %s" % wallet_name)
            rescan_blockchain = "rescanblockchain" in request.form
            if rescan_blockchain:
                # old wallet - import more addresses
                wallet.keypoolrefill(0, wallet.IMPORT_KEYPOOL, change=False)
                wallet.keypoolrefill(0, wallet.IMPORT_KEYPOOL, change=True)
                if "utxo" in request.form.get("full_rescan_option"):
                    explorer = None
                    if "use_explorer" in request.form:
                        explorer = request.form["explorer_url"]
                    wallet.rescanutxo(explorer, app.specter.requests_session(explorer))
                    app.specter.info["utxorescan"] = 1
                    app.specter.utxorescanwallet = wallet.alias
                else:
                    app.logger.info("Rescanning Blockchain ...")
                    startblock = int(request.form["startblock"])
                    try:
                        wallet.rpc.rescanblockchain(startblock, timeout=1)
                    except requests.exceptions.ReadTimeout:
                        # this is normal behavior in our usecase
                        pass
                    except Exception as e:
                        app.logger.error(
                            "Exception while rescanning blockchain: %e" % e
                        )
                        err = "%r" % e
                    wallet.getdata()
            return redirect(
                url_for("wallets_endpoint.receive", wallet_alias=wallet.alias)
                + "?newwallet=true"
            )
        if action == "preselected_device":
            return render_template(
                "wallet/new_wallet/new_wallet_keys.jinja",
                purposes=purposes,
                cosigners=[
                    app.specter.device_manager.get_by_alias(request.form["device"])
                ],
                wallet_type="simple",
                sigs_total=1,
                sigs_required=1,
                error=err,
                specter=app.specter,
                rand=rand,
            )

    return render_template(
        "wallet/new_wallet/new_wallet.jinja",
        wallet_type=wallet_type,
        error=err,
        specter=app.specter,
        rand=rand,
    )


################## Wallet pages #######################

###### Wallet index page ######
@wallets_endpoint.route("/wallet/<wallet_alias>/")
@login_required
def wallet(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
    except SpecterError as se:
        app.logger.error("SpecterError while wallet: %s" % se)
        return render_template("base.jinja", error=se, specter=app.specter, rand=rand)
    if wallet.fullbalance > 0:
        return redirect(url_for("wallets_endpoint.history", wallet_alias=wallet_alias))
    else:
        return redirect(url_for("wallets_endpoint.receive", wallet_alias=wallet_alias))


###### Wallet transaction history ######
@wallets_endpoint.route("/wallet/<wallet_alias>/history/")
@login_required
def history(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
    except SpecterError as se:
        app.logger.error("SpecterError while wallet_tx: %s" % se)
        return render_template("base.jinja", error=se, specter=app.specter, rand=rand)
    # update balances in the wallet
    app.specter.check_blockheight()
    wallet.get_balance()
    wallet.check_utxo()

    return render_template(
        "wallet/history/wallet_history.jinja",
        wallet_alias=wallet_alias,
        wallet=wallet,
        specter=app.specter,
        rand=rand,
    )


###### Wallet receive ######


@wallets_endpoint.route("/wallet/<wallet_alias>/receive/", methods=["GET", "POST"])
@login_required
def receive(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
    except SpecterError as se:
        app.logger.error("SpecterError while wallet_receive: %s" % se)
        return render_template("base.jinja", error=se, specter=app.specter, rand=rand)
    if request.method == "POST":
        action = request.form["action"]
        if action == "newaddress":
            wallet.getnewaddress()
        elif action == "updatelabel":
            label = request.form["label"]
            wallet.setlabel(wallet.address, label)
    # check that current address is unused
    # and generate new one if it is
    wallet.check_unused()
    history_idx = int(request.args.get("history_idx", default=0))
    past_addresses = wallet.addresses[
        -10 * history_idx - 2 : -10 * (history_idx + 1) - 2 : -1
    ]
    return render_template(
        "wallet/receive/wallet_receive.jinja",
        wallet_alias=wallet_alias,
        wallet=wallet,
        past_addresses=past_addresses,
        past_descriptors=[
            wallet.get_descriptor(address=addr) for addr in past_addresses
        ],
        addresses_count=len(wallet.addresses),
        history_idx=history_idx,
        specter=app.specter,
        rand=rand,
    )


###### Wallet send ######


@wallets_endpoint.route("/wallet/<wallet_alias>/send")
@login_required
def send(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
    except SpecterError as se:
        app.logger.error("SpecterError while wallet_send: %s" % se)
        return render_template("base.jinja", error=se, specter=app.specter, rand=rand)
    if len(wallet.pending_psbts) > 0:
        return redirect(
            url_for("wallets_endpoint.send_pending", wallet_alias=wallet_alias)
        )
    else:
        return redirect(url_for("wallets_endpoint.send_new", wallet_alias=wallet_alias))


@wallets_endpoint.route("/wallet/<wallet_alias>/send/new", methods=["GET", "POST"])
@login_required
def send_new(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
    except SpecterError as se:
        app.logger.error("SpecterError while wallet_send: %s" % se)
        return render_template("base.jinja", error=se, specter=app.specter, rand=rand)
    # update balances in the wallet
    wallet.get_balance()
    # update utxo list for coin selection
    wallet.check_utxo()
    psbt = None
    addresses = [""]
    labels = [""]
    amounts = [0]
    fee_rate = 0.0
    err = None
    ui_option = "ui"
    recipients_txt = ""
    if request.method == "POST":
        action = request.form["action"]
        if action == "createpsbt":
            i = 0
            addresses = []
            labels = []
            amounts = []
            ui_option = request.form.get("ui_option")
            if "ui" in ui_option:
                while "address_{}".format(i) in request.form:
                    addresses.append(request.form["address_{}".format(i)])
                    amounts.append(float(request.form["btc_amount_{}".format(i)]))
                    labels.append(request.form["label_{}".format(i)])
                    if request.form["label_{}".format(i)] != "":
                        wallet.setlabel(addresses[i], labels[i])
                    i += 1
            else:
                recipients_txt = request.form["recipients"]
                for output in recipients_txt.splitlines():
                    addresses.append(output.split(",")[0].strip())
                    if request.form.get("amount_unit_text") == "sat":
                        amounts.append(float(output.split(",")[1].strip()) / 1e8)
                    else:
                        amounts.append(float(output.split(",")[1].strip()))
            subtract = bool(request.form.get("subtract", False))
            subtract_from = int(request.form.get("subtract_from", 1)) - 1
            rbf = bool(request.form.get("rbf", False))
            selected_coins = request.form.getlist("coinselect")
            app.logger.info("selected coins: {}".format(selected_coins))
            if "dynamic" in request.form.get("fee_options"):
                fee_rate = float(request.form.get("fee_rate_dynamic"))
            else:
                if request.form.get("fee_rate"):
                    fee_rate = float(request.form.get("fee_rate"))
            try:
                psbt = wallet.createpsbt(
                    addresses,
                    amounts,
                    subtract=subtract,
                    subtract_from=subtract_from,
                    fee_rate=fee_rate,
                    selected_coins=selected_coins,
                    readonly="estimate_fee" in request.form,
                    rbf=rbf,
                )
                if psbt is None:
                    err = "Probably you don't have enough funds, or something else..."
                else:
                    # calculate new amount if we need to subtract
                    if subtract:
                        for v in psbt["tx"]["vout"]:
                            if addresses[0] in v["scriptPubKey"]["addresses"]:
                                amounts[0] = v["value"]
            except Exception as e:
                err = e
                app.logger.error(e)
            if err is None:
                if "estimate_fee" in request.form:
                    return psbt
                return render_template(
                    "wallet/send/sign/wallet_send_sign_psbt.jinja",
                    psbt=psbt,
                    labels=labels,
                    wallet_alias=wallet_alias,
                    wallet=wallet,
                    specter=app.specter,
                    rand=rand,
                )
        elif action == "importpsbt":
            try:
                b64psbt = "".join(request.form["rawpsbt"].split())
                psbt = wallet.importpsbt(b64psbt)
            except Exception as e:
                flash("Could not import PSBT: %s" % e, "error")
                return redirect(
                    url_for("wallets_endpoint.import_psbt", wallet_alias=wallet_alias)
                )
            return render_template(
                "wallet/send/sign/wallet_send_sign_psbt.jinja",
                psbt=psbt,
                labels=labels,
                wallet_alias=wallet_alias,
                wallet=wallet,
                specter=app.specter,
                rand=rand,
            )
        elif action == "openpsbt":
            psbt = ast.literal_eval(request.form["pending_psbt"])
            return render_template(
                "wallet/send/sign/wallet_send_sign_psbt.jinja",
                psbt=psbt,
                labels=labels,
                wallet_alias=wallet_alias,
                wallet=wallet,
                specter=app.specter,
                rand=rand,
            )
        elif action == "deletepsbt":
            try:
                wallet.delete_pending_psbt(
                    ast.literal_eval(request.form["pending_psbt"])["tx"]["txid"]
                )
            except Exception as e:
                flash("Could not delete Pending PSBT!", "error")
        elif action == "rbf":
            try:
                rbf_tx_id = request.form["rbf_tx_id"]
                rbf_fee_rate = float(request.form["rbf_fee_rate"])
                psbt = wallet.send_rbf_tx(rbf_tx_id, rbf_fee_rate)
                return render_template(
                    "wallet/send/sign/wallet_send_sign_psbt.jinja",
                    psbt=psbt,
                    labels=[],
                    wallet_alias=wallet_alias,
                    wallet=wallet,
                    specter=app.specter,
                    rand=rand,
                )
            except Exception as e:
                flash("Failed to perform RBF. Error: %s" % e, "error")
        elif action == "signhotwallet":
            passphrase = request.form["passphrase"]
            psbt = ast.literal_eval(request.form["psbt"])
            b64psbt = wallet.pending_psbts[psbt["tx"]["txid"]]["base64"]
            device = request.form["device"]
            if "devices_signed" not in psbt or device not in psbt["devices_signed"]:
                try:
                    # get device and sign with it
                    signed_psbt = app.specter.device_manager.get_by_alias(
                        device
                    ).sign_psbt(b64psbt, wallet, passphrase)
                    if signed_psbt["complete"]:
                        if "devices_signed" not in psbt:
                            psbt["devices_signed"] = []
                        psbt["devices_signed"].append(device)
                        psbt["sigs_count"] = len(psbt["devices_signed"])
                        raw = wallet.rpc.finalizepsbt(b64psbt)
                        if "hex" in raw:
                            psbt["raw"] = raw["hex"]
                    signed_psbt = signed_psbt["psbt"]
                except Exception as e:
                    signed_psbt = None
                    flash("Failed to sign PSBT: %s" % e, "error")
            else:
                signed_psbt = None
                flash("Device already signed the PSBT", "error")
            return render_template(
                "wallet/send/sign/wallet_send_sign_psbt.jinja",
                signed_psbt=signed_psbt,
                psbt=psbt,
                labels=labels,
                wallet_alias=wallet_alias,
                wallet=wallet,
                specter=app.specter,
                rand=rand,
            )
    return render_template(
        "wallet/send/new/wallet_send.jinja",
        psbt=psbt,
        ui_option=ui_option,
        recipients_txt=recipients_txt,
        labels=labels,
        wallet_alias=wallet_alias,
        wallet=wallet,
        specter=app.specter,
        rand=rand,
        error=err,
    )


@wallets_endpoint.route("/wallet/<wallet_alias>/send/pending/", methods=["GET", "POST"])
@login_required
def send_pending(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
    except SpecterError as se:
        app.logger.error("SpecterError while wallet_sendpending: %s" % se)
        return render_template("base.jinja", error=se, specter=app.specter, rand=rand)
    if request.method == "POST":
        action = request.form["action"]
        if action == "deletepsbt":
            try:
                wallet.delete_pending_psbt(
                    ast.literal_eval(request.form["pending_psbt"])["tx"]["txid"]
                )
            except Exception as e:
                app.logger.error("Could not delete Pending PSBT: %s" % e)
                flash("Could not delete Pending PSBT!", "error")
    pending_psbts = wallet.pending_psbts
    ######## Migration to multiple recipients format ###############
    for psbt in pending_psbts:
        if not isinstance(pending_psbts[psbt]["address"], list):
            pending_psbts[psbt]["address"] = [pending_psbts[psbt]["address"]]
            pending_psbts[psbt]["amount"] = [pending_psbts[psbt]["amount"]]
    ###############################################################
    return render_template(
        "wallet/send/pending/wallet_sendpending.jinja",
        pending_psbts=pending_psbts,
        wallet_alias=wallet_alias,
        wallet=wallet,
        specter=app.specter,
    )


@wallets_endpoint.route("/wallet/<wallet_alias>/send/import")
@login_required
def import_psbt(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
    except SpecterError as se:
        app.logger.error("SpecterError while wallet_send: %s" % se)
        return render_template("base.jinja", error=se, specter=app.specter, rand=rand)
    err = None
    return render_template(
        "wallet/send/import/wallet_importpsbt.jinja",
        wallet_alias=wallet_alias,
        wallet=wallet,
        specter=app.specter,
        rand=rand,
        error=err,
    )


###### Wallet settings ######


@wallets_endpoint.route("/wallet/<wallet_alias>/settings/", methods=["GET", "POST"])
@login_required
def settings(wallet_alias):
    error = None
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
    except SpecterError as se:
        app.logger.error("SpecterError while wallet_receive: %s" % se)
        return render_template("base.jinja", error=se, specter=app.specter, rand=rand)
    if request.method == "POST":
        action = request.form["action"]
        if action == "rescanblockchain":
            startblock = int(request.form["startblock"])
            try:
                delete_file(wallet._transactions.path)
                wallet.fetch_transactions()
                res = wallet.rpc.rescanblockchain(startblock, timeout=1)
            except requests.exceptions.ReadTimeout:
                # this is normal behaviour in our usecase
                pass
            except Exception as e:
                app.logger.error("%s while rescanblockchain" % e)
                error = "%r" % e
            wallet.getdata()
        elif action == "abortrescan":
            res = wallet.rpc.abortrescan()
            if not res:
                error = "Failed to abort rescan. Maybe already complete?"
            wallet.getdata()
        elif action == "rescanutxo":
            explorer = None
            if "use_explorer" in request.form:
                explorer = request.form["explorer_url"]
            wallet.rescanutxo(explorer, app.specter.requests_session(explorer))
            app.specter.info["utxorescan"] = 1
            app.specter.utxorescanwallet = wallet.alias
        elif action == "abortrescanutxo":
            app.specter.abortrescanutxo()
            app.specter.info["utxorescan"] = None
            app.specter.utxorescanwallet = None
        elif action == "keypoolrefill":
            delta = int(request.form["keypooladd"])
            wallet.keypoolrefill(wallet.keypool, wallet.keypool + delta)
            wallet.keypoolrefill(
                wallet.change_keypool, wallet.change_keypool + delta, change=True
            )
            wallet.getdata()
        elif action == "deletewallet":
            app.specter.wallet_manager.delete_wallet(
                wallet, app.specter.bitcoin_datadir, app.specter.chain
            )
            response = redirect(url_for("index"))
            return response
        elif action == "rename":
            wallet_name = request.form["newtitle"]
            if not wallet_name:
                flash("Wallet name cannot be empty", "error")
            elif wallet_name == wallet.name:
                pass
            elif wallet_name in app.specter.wallet_manager.wallets_names:
                flash("Wallet already exists", "error")
            else:
                app.specter.wallet_manager.rename_wallet(wallet, wallet_name)

        return render_template(
            "wallet/settings/wallet_settings.jinja",
            purposes=purposes,
            wallet_alias=wallet_alias,
            wallet=wallet,
            specter=app.specter,
            rand=rand,
            error=error,
        )
    else:
        return render_template(
            "wallet/settings/wallet_settings.jinja",
            purposes=purposes,
            wallet_alias=wallet_alias,
            wallet=wallet,
            specter=app.specter,
            rand=rand,
            error=error,
        )


################## Wallet util endpoints #######################
# TODO: move these to an API endpoint


@wallets_endpoint.route("/wallet/<wallet_alias>/combine/", methods=["POST"])
@login_required
def combine(wallet_alias):
    # only post requests
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
    except SpecterError as se:
        app.logger.error("SpecterError while combine: %s" % se)
        return "SpecterError while combine: %s" % se, 500
    # FIXME: ugly...
    txid = request.form.get("txid")
    psbts = [request.form.get("psbt0").strip(), request.form.get("psbt1").strip()]
    raw = {}
    combined = None

    for i, psbt in enumerate(psbts):
        if not psbt:
            return "Cannot parse empty data as PSBT", 500
        if "UR:BYTES/" in psbt.upper():
            psbt = bcur2base64(psbt).decode()

        # if electrum then it's base43
        try:
            decoded = b43_decode(psbt)
            if decoded.startswith(b"psbt\xff"):
                psbt = b2a_base64(decoded).decode()
            else:
                psbt = decoded.hex()
        except:
            pass

        psbts[i] = psbt
        # psbt should start with cHNi
        # if not - maybe finalized hex tx
        if not psbt.startswith("cHNi"):
            raw["hex"] = psbt
            combined = psbts[1 - i]
            # check it's hex
            try:
                bytes.fromhex(psbt)
            except:
                return "Invalid transaction format", 500

    try:
        if "hex" in raw:
            raw["complete"] = True
            raw["psbt"] = combined
        else:
            combined = app.specter.combine(psbts)
            raw = app.specter.finalize(combined)
            if "psbt" not in raw:
                raw["psbt"] = combined
        psbt = wallet.update_pending_psbt(combined, txid, raw)
        raw["devices"] = psbt["devices_signed"]
    except RpcError as e:
        return e.error_msg, e.status_code
    except Exception as e:
        return "Unknown error: %r" % e, 500
    return json.dumps(raw)


@wallets_endpoint.route("/wallet/<wallet_alias>/broadcast/", methods=["GET", "POST"])
@login_required
def broadcast(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
    except SpecterError as se:
        app.logger.error("SpecterError while broadcast: %s" % se)
        return render_template("base.jinja", error=se, specter=app.specter, rand=rand)
    if request.method == "POST":
        tx = request.form.get("tx")
        res = wallet.rpc.testmempoolaccept([tx])[0]
        if res["allowed"]:
            app.specter.broadcast(tx)
            wallet.delete_pending_psbt(get_txid(tx))
            return jsonify(success=True)
        else:
            return jsonify(
                success=False,
                error="Failed to broadcast transaction: transaction is invalid\n%s"
                % res["reject-reason"],
            )
    return jsonify(success=False, error="broadcast tx request must use POST")


@wallets_endpoint.route("/wallet/<wallet_alias>/decoderawtx/", methods=["GET", "POST"])
@login_required
def decoderawtx(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
        txid = request.form.get("txid", "")
        if txid:
            tx = wallet.rpc.gettransaction(txid)
            return {
                "success": True,
                "tx": tx,
                "rawtx": decoderawtransaction(tx["hex"], app.specter.chain),
                "walletName": wallet.name,
            }
    except Exception as e:
        app.logger.warning("Failed to fetch transaction data. Exception: {}".format(e))
    return {"success": False}


@wallets_endpoint.route(
    "/wallet/<wallet_alias>/rescan_progress", methods=["GET", "POST"]
)
@login_required
def rescan_progress(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
        wallet.get_info()
        return {
            "active": wallet.rescan_progress is not None,
            "progress": wallet.rescan_progress,
        }
    except SpecterError as se:
        app.logger.error("SpecterError while get wallet rescan_progress: %s" % se)
        return {}


################## Wallet export data endpoints #######################
# Export wallet transaction history
@wallets_endpoint.route("/wallet/<wallet_alias>/addresses.csv")
@login_required
def addresses_csv(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
        # stream the response as the data is generated
        response = Response(
            addresses_list_to_csv(wallet),
            mimetype="text/csv",
        )
        # add a filename
        response.headers.set(
            "Content-Disposition", "attachment", filename="addresses.csv"
        )
        return response
    except Exception as e:
        app.logger.error("Failed to export wallet history. Error: %s" % e)
        flash("Failed to export wallet history. Error: %s" % e, "error")
        return redirect(url_for("index"))


# Export wallet transaction history
@wallets_endpoint.route("/wallet/<wallet_alias>/transactions.csv")
@login_required
def tx_history_csv(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
        validate_merkle_proofs = app.specter.config.get("validate_merkle_proofs", False)
        txlist = wallet.txlist(validate_merkle_proofs=validate_merkle_proofs)
        search = request.args.get("search", None)
        sortby = request.args.get("sortby", "time")
        sortdir = request.args.get("sortdir", "desc")
        txlist = json.loads(
            process_txlist(
                txlist, idx=0, limit=0, search=search, sortby=sortby, sortdir=sortdir
            )["txlist"]
        )
        includePricesHistory = request.args.get("exportPrices", "false") == "true"

        # stream the response as the data is generated
        response = Response(
            txlist_to_csv(
                wallet, txlist, app.specter, current_user, includePricesHistory
            ),
            mimetype="text/csv",
        )
        # add a filename
        response.headers.set(
            "Content-Disposition", "attachment", filename="transactions.csv"
        )
        return response
    except Exception as e:
        logging.exception(e)
        return "Failed to export wallet history. Error: %s" % e, 500


# Export wallet UTXO list
@wallets_endpoint.route("/wallet/<wallet_alias>/utxo.csv")
@login_required
def utxo_csv(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
        includePricesHistory = request.args.get("exportPrices", "false") == "true"
        search = request.args.get("search", None)
        sortby = request.args.get("sortby", "time")
        sortdir = request.args.get("sortdir", "desc")
        txlist = json.loads(
            process_txlist(
                wallet.utxo,
                idx=0,
                limit=0,
                search=search,
                sortby=sortby,
                sortdir=sortdir,
            )["txlist"]
        )
        # stream the response as the data is generated
        response = Response(
            txlist_to_csv(
                wallet,
                txlist,
                app.specter,
                current_user,
                includePricesHistory,
            ),
            mimetype="text/csv",
        )
        # add a filename
        response.headers.set("Content-Disposition", "attachment", filename="utxo.csv")
        return response
    except Exception as e:
        logging.exception(e)
        return "Failed to export wallet utxo. Error: %s" % e, 500


# Export all wallets transaction history combined
@wallets_endpoint.route("/wallets_overview/full_transactions.csv")
@login_required
def wallet_overview_txs_csv():
    try:
        validate_merkle_proofs = app.specter.config.get("validate_merkle_proofs", False)
        txlist = app.specter.wallet_manager.full_txlist(
            validate_merkle_proofs=validate_merkle_proofs
        )
        search = request.args.get("search", None)
        sortby = request.args.get("sortby", "time")
        sortdir = request.args.get("sortdir", "desc")
        txlist = json.loads(
            process_txlist(
                txlist, idx=0, limit=0, search=search, sortby=sortby, sortdir=sortdir
            )["txlist"]
        )
        includePricesHistory = request.args.get("exportPrices", "false") == "true"
        # stream the response as the data is generated
        response = Response(
            txlist_to_csv(
                None, txlist, app.specter, current_user, includePricesHistory
            ),
            mimetype="text/csv",
        )
        # add a filename
        response.headers.set(
            "Content-Disposition", "attachment", filename="full_transactions.csv"
        )
        return response
    except Exception as e:
        logging.exception(e)
        return "Failed to export wallets overview history. Error: %s" % e, 500


# Export all wallets transaction history combined
@wallets_endpoint.route("/wallets_overview/full_utxo.csv")
@login_required
def wallet_overview_utxo_csv():
    try:
        txlist = app.specter.wallet_manager.full_utxo()
        search = request.args.get("search", None)
        sortby = request.args.get("sortby", "time")
        sortdir = request.args.get("sortdir", "desc")
        txlist = json.loads(
            process_txlist(
                txlist, idx=0, limit=0, search=search, sortby=sortby, sortdir=sortdir
            )["txlist"]
        )
        includePricesHistory = request.args.get("exportPrices", "false") == "true"
        # stream the response as the data is generated
        response = Response(
            txlist_to_csv(
                None, txlist, app.specter, current_user, includePricesHistory
            ),
            mimetype="text/csv",
        )
        # add a filename
        response.headers.set(
            "Content-Disposition", "attachment", filename="full_utxo.csv"
        )
        return response
    except Exception as e:
        logging.exception(e)
        return "Failed to export wallets overview utxo. Error: %s" % e, 500


@wallets_endpoint.route("/wallet/<wallet_alias>/get_label", methods=["POST"])
@login_required
def get_label(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
        address = request.form.get("address", "")
        label = wallet.getlabel(address)
        return {
            "address": address,
            "label": label,
        }
    except Exception as e:
        logging.exception(e)
        return "Error while get_label: %s" % e, 500


@wallets_endpoint.route("/wallet/<wallet_alias>/set_label", methods=["POST"])
@login_required
def set_label(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
        address = request.form["address"]
        label = request.form["label"]
        wallet.setlabel(address, label)
        return {"success": True}
    except Exception as e:
        logging.exception(e)
        return "Error while set_label: %s" % e, 500


@wallets_endpoint.route("/wallet/<wallet_alias>/txlist", methods=["POST"])
@login_required
def txlist(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
        idx = int(request.form.get("idx", 0))
        limit = int(request.form.get("limit", 100))
        search = request.form.get("search", None)
        sortby = request.form.get("sortby", None)
        sortdir = request.form.get("sortdir", "asc")
        fetch_transactions = request.form.get("fetch_transactions", False)
        txlist = wallet.txlist(
            fetch_transactions=fetch_transactions,
            validate_merkle_proofs=app.specter.config.get(
                "validate_merkle_proofs", False
            ),
            current_blockheight=app.specter.info["blocks"],
        )
        return process_txlist(
            txlist, idx=idx, limit=limit, search=search, sortby=sortby, sortdir=sortdir
        )
    except Exception as e:
        logging.exception(e)
        return "Error while getting txlist: %s" % e, 500


@wallets_endpoint.route("/wallet/<wallet_alias>/utxo_list", methods=["POST"])
@login_required
def utxo_list(wallet_alias):
    try:
        wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
        idx = int(request.form.get("idx", 0))
        limit = int(request.form.get("limit", 100))
        search = request.form.get("search", None)
        sortby = request.form.get("sortby", None)
        sortdir = request.form.get("sortdir", "asc")
        txlist = wallet.utxo
        for tx in txlist:
            if not tx.get("label", None):
                tx["label"] = wallet.getlabel(tx["address"])
        return process_txlist(
            txlist, idx=idx, limit=limit, search=search, sortby=sortby, sortdir=sortdir
        )
    except Exception as e:
        logging.exception(e)
        return "Error while getting utxo list: %s" % e, 500


@wallets_endpoint.route("/wallets_overview/txlist", methods=["POST"])
@login_required
def wallets_overview_txlist():
    try:
        idx = int(request.form.get("idx", 0))
        limit = int(request.form.get("limit", 100))
        search = request.form.get("search", None)
        sortby = request.form.get("sortby", None)
        sortdir = request.form.get("sortdir", "asc")
        fetch_transactions = request.form.get("fetch_transactions", False)
        txlist = app.specter.wallet_manager.full_txlist(
            fetch_transactions=fetch_transactions,
            validate_merkle_proofs=app.specter.config.get(
                "validate_merkle_proofs", False
            ),
            current_blockheight=app.specter.info["blocks"],
        )
        return process_txlist(
            txlist, idx=idx, limit=limit, search=search, sortby=sortby, sortdir=sortdir
        )
    except Exception as e:
        logging.exception(e)
        return "Error while getting full txlist: %s" % e, 500


@wallets_endpoint.route("/wallets_overview/utxo_list", methods=["POST"])
@login_required
def wallets_overview_utxo_list():
    try:
        idx = int(request.form.get("idx", 0))
        limit = int(request.form.get("limit", 100))
        search = request.form.get("search", None)
        sortby = request.form.get("sortby", None)
        sortdir = request.form.get("sortdir", "asc")
        fetch_transactions = request.form.get("fetch_transactions", False)
        txlist = app.specter.wallet_manager.full_utxo()
        return process_txlist(
            txlist, idx=idx, limit=limit, search=search, sortby=sortby, sortdir=sortdir
        )
    except Exception as e:
        logging.exception(e)
        return "Error while getting full utxo list: %s" % e, 500


def process_txlist(txlist, idx=0, limit=100, search=None, sortby=None, sortdir="asc"):
    if search:
        txlist = [
            tx
            for tx in txlist
            if search in tx["txid"]
            or (
                any(search in address for address in tx["address"])
                if isinstance(tx["address"], list)
                else search in tx["address"]
            )
            or (
                any(search in label for label in tx["label"])
                if isinstance(tx["label"], list)
                else search in tx["label"]
            )
            or (
                any(search in str(amount) for amount in tx["amount"])
                if isinstance(tx["amount"], list)
                else search in str(tx["amount"])
            )
            or search in str(tx["confirmations"])
            or search in str(tx["time"])
            or search
            in str(format(datetime.fromtimestamp(tx["time"]), "%d.%m.%Y %H:%M"))
        ]
    if sortby:

        def sort(tx):
            val = tx.get(sortby, None)
            final = val
            if val:
                if isinstance(val, list):
                    if isinstance(val[0], Number):
                        final = sum(val)
                    elif isinstance(val[0], str):
                        final = sorted(
                            val, key=lambda s: s.lower(), reverse=sortdir != "asc"
                        )[0].lower()
                elif isinstance(val, str):
                    final = val.lower()
            return final

        txlist = sorted(txlist, key=sort, reverse=sortdir != "asc")
    if limit:
        page_count = (len(txlist) // limit) + (0 if len(txlist) % limit == 0 else 1)
        txlist = txlist[limit * idx : limit * (idx + 1)]
    else:
        page_count = 1
    return {"txlist": json.dumps(txlist), "pageCount": page_count}


################## Helpers #######################

# Transactions list to user-friendly CSV format
def txlist_to_csv(wallet, _txlist, specter, current_user, includePricesHistory=False):
    txlist = []
    for tx in _txlist:
        if isinstance(tx["address"], list):
            _tx = tx.copy()
            for i in range(0, len(tx["address"])):
                _tx["address"] = tx["address"][i]
                _tx["amount"] = tx["amount"][i]
                txlist.append(_tx.copy())
        else:
            txlist.append(tx.copy())
    data = StringIO()
    w = csv.writer(data)
    # write header
    symbol = "USD"
    if specter.price_provider.endswith("_eur"):
        symbol = "EUR"
    elif specter.price_provider.endswith("_gbp"):
        symbol = "GBP"
    row = (
        "Date",
        "Label",
        "Category",
        "Amount ({})".format(specter.unit.upper()),
        "Value ({})".format(symbol),
        "Rate (BTC/{})".format(symbol)
        if specter.unit != "sat"
        else "Rate ({}/SAT)".format(symbol),
        "TxID",
        "Address",
        "Block Height",
        "Timestamp",
        "Raw Transaction",
    )
    if not wallet:
        row = ("Wallet",) + row
    w.writerow(row)
    yield data.getvalue()
    data.seek(0)
    data.truncate(0)

    # write each log item
    _wallet = wallet
    for tx in txlist:
        if not wallet:
            wallet_alias = tx.get("wallet_alias", None)
            try:
                _wallet = specter.wallet_manager.get_by_alias(wallet_alias)
            except Exception as e:
                continue
        label = _wallet.getlabel(tx["address"])
        if label == tx["address"]:
            label = ""
        tx_raw = _wallet.gettransaction(tx["txid"])
        if tx_raw:
            tx_hex = tx_raw["hex"]
        else:
            tx_hex = ""
        if not tx.get("blockheight", None):
            if tx_raw.get("blockheight", None):
                tx["blockheight"] = tx_raw["blockheight"]
            else:
                tx["blockheight"] = "Unconfirmed"
        if specter.unit == "sat":
            value = float(tx["amount"])
            tx["amount"] = round(value * 1e8)
        if includePricesHistory:
            success, rate, symbol = get_price_at(
                specter, current_user, timestamp=tx["time"]
            )
        else:
            success = False
        if success:
            rate = float(rate)
            if specter.unit == "sat":
                rate = rate / 1e8
            amount_price = float(tx["amount"]) * rate
            if specter.unit == "sat":
                rate = round(1 / rate)
        else:
            amount_price = None
            rate = "-"

        row = (
            time.strftime("%Y-%m-%d", time.localtime(tx["time"])),
            label,
            tx["category"],
            round(tx["amount"], (0 if specter.unit == "sat" else 8)),
            round(amount_price * 100) / 100 if amount_price is not None else "-",
            rate,
            tx["txid"],
            tx["address"],
            tx["blockheight"],
            tx["time"],
            tx_hex,
        )
        if not wallet:
            row = (tx.get("wallet_alias", ""),) + row
        w.writerow(row)
        yield data.getvalue()
        data.seek(0)
        data.truncate(0)


# Addresses list to user-friendly CSV format
def addresses_list_to_csv(wallet):
    data = StringIO()
    w = csv.writer(data)
    # write header
    row = (
        "Address",
        "Label",
        "Index",
        "Used",
        "Current balance",
    )
    w.writerow(row)
    yield data.getvalue()
    data.seek(0)
    data.truncate(0)

    # write each log item
    for address in wallet._addresses:
        address_info = wallet.get_address_info(address)
        if not address_info.is_labeled and not address_info.used:
            continue
        row = (
            address,
            address_info.label,
            "(external)"
            if address_info.is_external
            else (
                str(address_info.index) + (" (change)" if address_info.change else "")
            ),
            address_info.used,
        )
        if address_info.is_external:
            balance_on_address = "unknown (external address)"
        else:
            balance_on_address = 0
            if address_info.used:
                for tx in wallet.utxo:
                    if tx.get("address", "") == address:
                        balance_on_address += tx.get("amount", 0)
        row += (balance_on_address,)

        w.writerow(row)
        yield data.getvalue()
        data.seek(0)
        data.truncate(0)
