"""
bal.gui.qt.lists
================

Tree/list views (subclasses of Electrum's ``MyTreeView``) and their toolbars.

    * HeirListWidget        - editable list of heirs (address / amount / locktime).
    * PreviewList           - preview of the will transactions before signing.
    * WillExecutorListWidget- list of will-executor servers.
    * WillExecutorWidget    - container combining the list with add/import buttons.

These views call back into the :class:`BalWindow` controller (passed at
construction) for all business actions, so the heavy logic stays in ``window``
and ``dialogs``.
"""

from typing import TYPE_CHECKING

from PyQt6.QtWidgets import QLineEdit as _QLineEdit
from PyQt6.QtWidgets import QMessageBox, QStyledItemDelegate

from .common import *
from .common import _, _logger  # underscore names are not re-exported by "import *"
from .dialogs import BalBuildWillDialog, BalDialog
from .widgets import BalCheckBox, WillSettingsWidget

if TYPE_CHECKING:
    from .window import BalWindow


def _can_sign(will_item):
    """True if a will transaction may still be signed (not fully signed)."""
    return bool(will_item) and not will_item.get_status("COMPLETE")


def _can_broadcast(will_item):
    """True if a will transaction is ready to broadcast (fully signed)."""
    return bool(will_item) and will_item.get_status("COMPLETE")


def _can_delete(will_item):
    """True if a will transaction may be deleted (invalid or unsigned).

    Valid AND fully-signed transactions are never deletable: removing them
    would silently drop a committed inheritance.
    """
    return bool(will_item) and (
        not will_item.get_status("VALID") or not will_item.get_status("COMPLETE")
    )


def _apply_select_all(willexecutors, select, valid=None):
    """Apply a bulk selection across a ``{url: we_dict}`` mapping.

    ``select`` is the target selection value. When ``valid`` is given (a
    ``{url: bool}`` validity map), the selection is restricted by validity:
    selecting sets valid ones to True and invalid ones to False, while
    deselecting only clears the invalid ones (valid ones keep their state).
    Without ``valid`` every entry is simply set to ``select``. The mapping is
    mutated in place and returned.
    """
    for url, we in willexecutors.items():
        if valid is not None:
            if select:
                we["selected"] = valid.get(url, False)
            elif not valid.get(url, False):
                we["selected"] = False
        else:
            we["selected"] = select
    return willexecutors


class HeirListWidget(MyTreeView, MessageBoxMixin):
    class Columns(MyTreeView.BaseColumnsEnum):
        NAME = enum.auto()
        ADDRESS = enum.auto()
        AMOUNT = enum.auto()

    headers = {
        Columns.NAME: _("Name"),
        Columns.ADDRESS: _("Address"),
        Columns.AMOUNT: _("Amount"),
    }
    filter_columns = [Columns.NAME, Columns.ADDRESS]

    ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1000

    ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 4000
    key_role = ROLE_HEIR_KEY

    def createEditor(self, parent, option, index):
        return QLineEdit(parent)

    def setEditorData(self, editor, index):
        editor.setText(index.data())

    def setModelData(self, editor, model, index):
        model.setData(index, editor.text())

    def __init__(self, bal_window: "BalWindow", parent):
        super().__init__(
            parent=parent,
            main_window=bal_window.window,
            stretch_column=self.Columns.NAME,
            editable_columns=[
                self.Columns.NAME,
                self.Columns.ADDRESS,
                self.Columns.AMOUNT,
            ],
        )
        self.decimal_point = bal_window.window.get_decimal_point()
        self.bal_window = bal_window

        try:
            self.setModel(QStandardItemModel(self))
            self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
            self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
        except Exception:
            pass

        self.setSortingEnabled(True)
        self.std_model = self.model()

        self.update()

    def on_activated(self, idx):
        self.on_double_click(idx)

    def on_double_click(self, idx):
        edit_key = self.get_edit_key_from_coordinate(idx.row(), idx.column())
        self.bal_window.heirs.get(edit_key)
        self.bal_window.new_heir_dialog(edit_key)

    def on_edited(self, idx, edit_key, *, text):
        prior_name = self.bal_window.heirs.get(edit_key)
        if not prior_name:
            return
        col = idx.column()
        try:
            if col == 2:
                text = Util.encode_amount(text, self.decimal_point)
            elif col == 0:
                self.bal_window.delete_heirs([edit_key])
                edit_key = text
            prior_name[col - 1] = text
            prior_name.insert(0, edit_key)
            prior_name = tuple(prior_name)
        except Exception:
            prior_name = (
                (edit_key,) + prior_name[: col - 1] + (text,) + prior_name[col:]
            )

        try:
            self.bal_window.set_heir(prior_name)
        except Exception:
            self.update()

    def delete_heirs(self, selected_keys):
        self.bal_window.delete_heirs(selected_keys)
        self.update()

    def create_menu(self, position):
        menu = QMenu()
        idx = self.indexAt(position)
        column = idx.column() or self.Columns.NAME
        selected_keys = []
        for s_idx in self.selected_in_column(self.Columns.NAME):
            sel_key = self.model().itemFromIndex(s_idx).data(0)
            selected_keys.append(sel_key)
        if selected_keys and idx.isValid():
            column_title = self.model().horizontalHeaderItem(column).text()
            # ok
            column_data = "\n".join(
                self.model().itemFromIndex(s_idx).text()
                for s_idx in self.selected_in_column(column)
            )
            menu.addAction(
                _("Copy {}").format(column_title),
                lambda: self.place_text_on_clipboard(column_data, title=column_title),
            )
            if column in self.editable_columns:
                item = self.model().itemFromIndex(idx)
                if item.isEditable():
                    persistent = QPersistentModelIndex(idx)
                    menu.addAction(
                        _("Edit {}").format(column_title),
                        lambda p=persistent: self.edit(QModelIndex(p)),
                    )
            menu.addAction(_("Delete"), lambda: self.delete_heirs(selected_keys))
        menu.exec(self.viewport().mapToGlobal(position))

    def update(self):
        current_key = self.get_role_data_for_current_item(
            col=self.Columns.NAME, role=self.ROLE_HEIR_KEY
        )
        self.model().clear()
        self.update_headers(self.__class__.headers)
        set_current = None
        for key in sorted(self.bal_window.heirs.keys()):
            heir = self.bal_window.heirs[key]
            labels = [""] * len(self.Columns)
            labels[self.Columns.NAME] = key
            if is_op_return_address(heir[0]):
                data_hex = heir[0][len(OP_RETURN_PREFIX):]
                try:
                    decoded = bytes.fromhex(data_hex).decode("utf-8", errors="replace")
                    if len(decoded) > 40:
                        decoded = decoded[:40] + "\u2026"
                    labels[self.Columns.ADDRESS] = decoded
                except Exception:
                    labels[self.Columns.ADDRESS] = "OP_RETURN"
            else:
                labels[self.Columns.ADDRESS] = heir[0]
            labels[self.Columns.AMOUNT] = Util.decode_amount(
                heir[1], self.decimal_point
            )

            items = [QStandardItem(x) for x in labels]
            items[self.Columns.NAME].setEditable(True)
            items[self.Columns.ADDRESS].setEditable(True)
            items[self.Columns.AMOUNT].setEditable(True)
            items[self.Columns.NAME].setData(
                key, self.ROLE_HEIR_KEY + self.Columns.NAME
            )
            items[self.Columns.ADDRESS].setData(
                key, self.ROLE_HEIR_KEY + self.Columns.ADDRESS
            )
            items[self.Columns.AMOUNT].setData(
                key, self.ROLE_HEIR_KEY + self.Columns.AMOUNT
            )

            row_count = self.model().rowCount()
            self.model().insertRow(row_count, items)

            if key == current_key:
                idx = self.model().index(row_count, self.Columns.NAME)
                set_current = QPersistentModelIndex(idx)
        try:
            self.will_settings_widget.on_locktime_change()
        except Exception:
            pass
        self.set_current_idx(set_current)
        # FIXME refresh loses sort order; so set "default" here:
        self.filter()

    def refresh_row(self, key, row):
        # nothing to update here
        pass

    def get_edit_key_from_coordinate(self, row, col):
        a = self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY + col)
        return a

    def create_toolbar(self, config):
        toolbar, menu = self.create_toolbar_with_menu("")
        menu.addAction(_("&New Heir"), self.bal_window.new_heir_dialog)
        menu.addAction(_("Import"), self.bal_window.import_heirs)
        menu.addAction(_("Export"), lambda: self.bal_window.export_heirs())

        new_heir_button = QPushButton(_("New Heir"))
        new_heir_button.clicked.connect(self.bal_window.new_heir_dialog)

        widget = QWidget(self)
        layout = QHBoxLayout(widget)
        self.will_settings_widget = WillSettingsWidget(self.bal_window, self)

        layout.addWidget(self.will_settings_widget)
        layout.addWidget(new_heir_button)

        toolbar.insertWidget(2, widget)

        return toolbar

    def build_transactions(self):
        # will = self.bal_window.prepare_will()
        self.bal_window.prepare_will()



class PreviewList(MyTreeView, MessageBoxMixin):
    class Columns(MyTreeView.BaseColumnsEnum):
        LOCKTIME = enum.auto()
        TXID = enum.auto()
        WILLEXECUTOR = enum.auto()
        STATUS = enum.auto()
        SERVER = enum.auto()

    headers = {
        Columns.LOCKTIME: _("Locktime"),
        Columns.TXID: _("Txid"),
        Columns.WILLEXECUTOR: _("Will-Executor"),
        Columns.STATUS: _("Status"),
        Columns.SERVER: _("Server"),
    }

    ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000
    key_role = ROLE_HEIR_KEY

    def createEditor(self, parent, option, index):
        return QLineEdit(parent)

    def setEditorData(self, editor, index):
        editor.setText(index.data())

    def setModelData(self, editor, model, index):
        model.setData(index, editor.text())

    def __init__(self, bal_window: "BalWindow", parent, will):
        super().__init__(
            parent=parent,
            main_window=bal_window.window,
            stretch_column=self.Columns.TXID,
        )
        # self._bal_parent = parent
        self.bal_window = bal_window
        self.decimal_point = bal_window.window.get_decimal_point

        if will is not None:
            self.will = will
        else:
            self.will = bal_window.willitems

        try:
            self.setModel(QStandardItemModel(self))
            self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
            self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
        except Exception:
            pass

        self.setSortingEnabled(True)
        self.std_model = self.model()

        self.update()

    def on_activated(self, idx):
        self.on_double_click(idx)

    def on_double_click(self, idx):
        idx = self.model().index(idx.row(), self.Columns.TXID)
        sel_key = self.model().itemFromIndex(idx).data(0)
        self.show_transaction([sel_key])

    def create_menu(self, position):
        menu = QMenu()
        idx = self.indexAt(position)
        column = idx.column() or self.Columns.TXID
        selected_keys = []
        for s_idx in self.selected_in_column(self.Columns.TXID):
            sel_key = self.model().itemFromIndex(s_idx).data(0)
            selected_keys.append(sel_key)
        if selected_keys and idx.isValid():
            column_title = self.model().horizontalHeaderItem(column).text()

            menu.addAction(
                _("details").format(column_title),
                lambda: self.show_transaction(selected_keys),
            ).setEnabled(len(selected_keys) == 1)
            menu.addAction(
                _("sign").format(column_title),
                lambda: self.sign_transactions(selected_keys),
            ).setEnabled(
                any(_can_sign(self.will.get(k)) for k in selected_keys)
            )
            menu.addAction(
                _("broadcast").format(column_title),
                lambda: self.broadcast_transactions(selected_keys),
            ).setEnabled(
                any(_can_broadcast(self.will.get(k)) for k in selected_keys)
            )
            menu.addAction(
                _("check ").format(column_title),
                lambda: self.check_transactions(selected_keys),
            )

            menu.addSeparator()
            menu.addAction(
                _("copy id").format(column_title),
                lambda: self.copy_txids(selected_keys),
            )
            menu.addAction(
                _("copy").format(column_title),
                lambda: self.copy_tx_hexes(selected_keys),
            )
            menu.addAction(
                _("merge from txn").format(column_title),
                lambda: self.merge_from_txn(),
            )

            menu.addSeparator()
            menu.addAction(
                _("delete").format(column_title), lambda: self.delete(selected_keys)
            ).setEnabled(
                any(_can_delete(self.will.get(k)) for k in selected_keys)
            )

        menu.exec(self.viewport().mapToGlobal(position))

    def is_deletable(self, key):
        """True if the will transaction ``key`` may be deleted.

        Deletion is only allowed for transactions that are NOT valid or NOT
        fully signed (invalidated/replaced/mempool/confirmed items, or
        unsigned/partially signed ones). Valid and complete transactions are
        kept (deleting them would silently drop a committed inheritance).
        """
        return _can_delete(self.will.get(key))

    def delete(self, selected_keys):
        keys = [k for k in selected_keys if self.is_deletable(k)]
        for key in keys:
            del self.will[key]
            try:
                del self.bal_window.willitems[key]
            except Exception:
                pass
            try:
                del self.bal_window.will[key]
            except Exception:
                pass
        self.update()

    def sign_transactions(self, selected_keys):
        """Sign all selected transactions that are not fully signed yet."""
        keys = [
            k for k in selected_keys
            if _can_sign(self.will.get(k))
        ]
        if keys:
            self.bal_window.ask_password_and_sign_transactions(
                callback=self.update, txids=keys
            )

    def broadcast_transactions(self, selected_keys):
        """Force-broadcast all selected fully-signed transactions.

        ``force=True`` makes the will-executors re-accept transactions that
        were already pushed before.
        """
        keys = [
            k for k in selected_keys
            if _can_broadcast(self.will.get(k))
        ]
        if keys:
            self.bal_window.broadcast_transactions(force=True, txids=keys)
            self.update()

    def copy_txids(self, selected_keys):
        """Copy the selected transaction IDs to the clipboard (one per line)."""
        self.place_text_on_clipboard(
            "\n".join(selected_keys), title=_("Transaction IDs")
        )

    def copy_tx_hexes(self, selected_keys):
        """Copy the selected transactions (serialised hex) to the clipboard."""
        hexes = "\n".join(str(self.will[k].tx) for k in selected_keys)
        self.place_text_on_clipboard(hexes, title=_("Transactions"))

    def merge_from_txn(self):
        """Merge a transaction read from the clipboard, or from a file if the
        clipboard does not contain a valid transaction.
        """
        tx = None
        try:
            tx = tx_from_any(QApplication.clipboard().text())
        except Exception:
            tx = None
        if tx is None:
            filename = getOpenFileName(
                parent=self.bal_window.window,
                title=_("Open transaction file"),
                filter="All files (*)",
                config=self.bal_window.window.config,
            )
            if not filename:
                return
            try:
                with open(filename, "r") as f:
                    data = f.read()
                tx = tx_from_any(data)
            except Exception as e:
                self.bal_window.show_error(
                    _("Invalid transaction file: {}").format(e)
                )
                return
        try:
            self.bal_window.merge_single_transaction(tx)
        except Exception as e:
            self.bal_window.show_error(str(e))
        self.update()

    def check_transactions(self, selected_keys):
        wout = {}
        for k in selected_keys:
            wout[k] = self.will[k]
        if wout:
            self.bal_window.check_transactions(wout)
        self.update()

    def show_transaction(self, selected_keys):
        for key in selected_keys:
            self.bal_window.show_transaction(self.will[key].tx)

        self.update()

    def select(self, selected_keys):
        self.selected += selected_keys
        self.update()

    def deselect(self, selected_keys):
        for key in selected_keys:
            self.selected.remove(key)
        self.update()

    def update_will(self, will):
        self.will.update(will)
        self.update()

    def replace(self, set_current, current_key, txid, bal_tx):
        if self.bal_window.bal_plugin._hide_replaced and bal_tx.get_status("REPLACED"):
            return False
        if self.bal_window.bal_plugin._hide_invalidated and bal_tx.get_status(
            "INVALIDATED"
        ):
            return False

        if not isinstance(bal_tx, WillItem):
            bal_tx = WillItem(bal_tx)

        tx = bal_tx.tx

        labels = [""] * len(self.Columns)
        labels[self.Columns.LOCKTIME] = str(BalTimestamp(tx.locktime))
        labels[self.Columns.TXID] = txid
        we = "None"
        if bal_tx.we:
            we = bal_tx.we["url"]
        labels[self.Columns.WILLEXECUTOR] = we
        status = bal_tx.status + signature_suffix(bal_tx)
        if len(status) > 53:
            status = "...{}".format(status[-50:])
        labels[self.Columns.STATUS] = status
        # Dedicated, always-readable label describing whether the inheritance
        # transaction is actually stored on the will-executor servers.
        labels[self.Columns.SERVER] = server_status_text(bal_tx)

        items = []
        for e in labels:
            if isinstance(e, list):
                try:
                    items.append(QStandardItem(*e))
                except Exception as e:
                    pass
            else:
                items.append(QStandardItem(str(e)))

            items[-1].setBackground(QColor(status_color(bal_tx)))

        # Group C / C6: emphasise the Locktime column by rendering it in bold,
        # so the delivery time stands out at a glance in the list.
        try:
            locktime_item = items[self.Columns.LOCKTIME]
            bold_font = locktime_item.font()
            bold_font.setBold(True)
            locktime_item.setFont(bold_font)
        except Exception as bold_err:
            _logger.debug(f"locktime bold error: {bold_err}")

        # Tooltip on the Server column: shows the will-executor URL (if any)
        # plus the current server state, so the user can always inspect details.
        try:
            items[self.Columns.SERVER].setToolTip(server_status_tooltip(bal_tx))
        except Exception as tip_err:
            _logger.debug(f"server tooltip error: {tip_err}")

        row_count = self.model().rowCount()
        self.model().insertRow(row_count, items)
        if txid == current_key:
            idx = self.model().index(row_count, self.Columns.TXID)
            set_current = QPersistentModelIndex(idx)
        self.set_current_idx(set_current)
        return set_current

    def update(self):
        try:
            self.menu.removeAction(self.importaction)
        except Exception:
            pass

        if self.will is None:
            return

        current_key = self.get_role_data_for_current_item(
            col=self.Columns.TXID, role=self.ROLE_HEIR_KEY
        )
        self.model().clear()
        self.update_headers(self.__class__.headers)

        set_current = None
        for txid, bal_tx in self.will.items():
            tmp = self.replace(set_current, current_key, txid, bal_tx)
            if tmp:
                set_current = tmp
        self.sortByColumn(self.Columns.LOCKTIME, Qt.SortOrder.AscendingOrder)
        self.setSortingEnabled(True)
        try:
            self.will_settings_widget.on_locktime_change()
        except Exception as _e:
            pass

    def create_toolbar(self, config):
        toolbar, menu = self.create_toolbar_with_menu("")
        menu.addAction(_("Prepare"), self.build_transactions)
        menu.addAction(_("Display"), self.bal_window.preview_modal_dialog)
        menu.addAction(_("Sign"), self.ask_password_and_sign_transactions)
        export_menu = menu.addMenu(_("Export"))
        export_menu.addAction(_("All"), self.export_will)
        export_menu.addAction(_("Valid"), self.export_will_valid)
        export_menu.addAction(_("Valid NC"), self.export_will_valid_incomplete)
        menu.addAction(_("Import"), self.import_will_into_details)
        menu.addAction(_("Merge"), self.merge_will)
        menu.addAction(_("Broadcast"), self.broadcast)
        menu.addAction(_("Check"), self.check)
        menu.addAction(_("Invalidate"), self.invalidate_will)

        # The Wizard is the main entry point to create an inheritance, so make
        # it stand out: show a bold label next to a slightly larger icon (the
        # plain icon-only button was too easy to overlook).
        wizard = QPushButton("  " + _("Build Your Will"))
        wizard.setIcon(
            read_QIcon_from_bytes(
                self.bal_window.bal_plugin.read_file("icons/wizard.png")
            )
        )
        wizard.setIconSize(QSize(28, 28))
        wizard.setMinimumHeight(40)
        wizard.setStyleSheet("QPushButton{font-weight:bold;}")
        # Tooltip so the button is self-explanatory when hovered.
        wizard.setToolTip(_("Wizard - Build your will"))
        wizard.clicked.connect(self.bal_window.init_wizard)
        # display = QPushButton(_("Display"))
        # display.clicked.connect(self.bal_window.preview_modal_dialog)

        refresh = QPushButton()
        refresh.setIcon(
            read_QIcon_from_bytes(
                self.bal_window.bal_plugin.read_file("icons/reload.png")
            )
        )
        # Tooltip so the icon is self-explanatory when hovered. "Check
        # Inheritance" makes it clear the button re-checks the inheritance/will
        # state (not a generic refresh).
        refresh.setToolTip(_("Check Inheritance"))
        refresh.clicked.connect(self.check)

        widget = QWidget(self)
        hlayout = QHBoxLayout(widget)
        hlayout.setContentsMargins(0, 0, 0, 0)
        self.will_settings_widget = WillSettingsWidget(self.bal_window, self)
        # Toolbar order (left -> right):
        #   Wizard | Delivery time | Check Alive | Calendar | Check (refresh)
        # The Wizard button goes first (leftmost); the settings widget already
        # lays out delivery/check-alive/calendar in that order internally.
        hlayout.addWidget(wizard)
        hlayout.addWidget(self.will_settings_widget)
        hlayout.addWidget(refresh)
        toolbar.insertWidget(2, widget)

        self.menu = menu
        self.toolbar = toolbar
        return toolbar

    def hide_replaced(self):
        self.bal_window.bal_plugin.hide_replaced()
        self.update()

    def hide_invalidated(self):
        self.bal_window.bal_plugin.hide_invalidated()
        self.update()

    def build_transactions(self):
        will = self.bal_window.prepare_will()
        if will:
            self.update_will(will)

    def export_json_file(self, path):
        write_json_file(path, self.will)

    def export_will(self):
        self.bal_window.export_will()
        self.update()

    def export_will_valid(self):
        """Export only the will items that are valid."""
        subset = {
            wid: wi
            for wid, wi in self.will.items()
            if wi.get_status("VALID")
        }
        if not subset:
            self.show_message(_("No valid will item to export"))
            return
        self.bal_window.export_will(will=subset)
        self.update()

    def export_will_valid_incomplete(self):
        """Export only the will items that are valid but not yet fully signed (V-NC)."""
        subset = {
            wid: wi
            for wid, wi in self.will.items()
            if wi.get_status("VALID") and not wi.get_status("COMPLETE")
        }
        if not subset:
            self.show_message(_("No valid, incomplete will item to export"))
            return
        self.bal_window.export_will(will=subset)
        self.update()

    def import_will_into_details(self):
        self.bal_window.import_will_into_details()

    def merge_will(self):
        self.bal_window.merge_will_ui()

    def ask_password_and_sign_transactions(self):
        self.bal_window.ask_password_and_sign_transactions(callback=self.update)

    def broadcast(self):
        self.bal_window.broadcast_transactions()
        self.update()

    def check(self):
        close_window = BalBuildWillDialog(self.bal_window)
        close_window.build_will_task()

        will = {}
        for wid, w in self.bal_window.willitems.items():
            # Query the will-executor server for every valid will that HAS a
            # will-executor assigned and is not yet CHECKED.  Previously only
            # transactions already marked PUSHED were checked, so a will that
            # had actually been sent in the past but whose saved status still
            # read "New" (not PUSHED) was skipped and the Check button reported
            # "nothing to do".  Will.needs_server_check now also includes such
            # non-PUSHED wills, so the server can confirm the transaction is
            # present and correct the status (see set_check_willexecutor).
            if Will.needs_server_check(w):
                will[wid] = w
        if will:
            self.bal_window.check_transactions(will)
        self.update()
        # NOTE (Group B / B2): signing + broadcasting is already performed
        # automatically by BalBuildWillDialog.build_will_task() above (called at
        # the start of check()). We must NOT trigger a second sign/broadcast
        # cycle here, otherwise the will would be broadcast twice. Whether the
        # automatic sign/broadcast runs silently or shows the manual "next step"
        # hints is controlled by the AUTO_SIGN setting inside that dialog.

    def invalidate_will(self):
        self.bal_window.invalidate_will()
        self.update()


# class PreviewDialog(BalDialog, MessageBoxMixin):
#    def __init__(self, bal_window, will):
#        self._bal_parent = bal_window.window
#        BalDialog.__init__(
#            self, bal_window=bal_window, bal_plugin=bal_window.bal_plugin
#        )
#        self.bal_plugin = bal_window.bal_plugin
#        self.gui_object = self.bal_plugin.gui_object
#        self.config = self.bal_plugin.config
#        self.bal_window = bal_window
#        self.wallet = bal_window.window.wallet
#        self.format_amount = bal_window.window.format_amount
#        self.base_unit = bal_window.window.base_unit
#        self.format_fiat_and_units = bal_window.window.format_fiat_and_units
#        self.fx = bal_window.window.fx
#        self.format_fee_rate = bal_window.window.format_fee_rate
#        self.show_address = bal_window.window.show_address
#        if not will:
#            self.will = bal_window.willitems
#        else:
#            self.will = will
#        self.setWindowTitle(_("Transactions Preview"))
#        self.setMinimumSize(1000, 200)
#        self.size_label = QLabel()
#        self.transactions_list = PreviewList(self.bal_window,self, self.will)
#
#        try:
#            self.bal_window.init_class_variables()
#        except Exception as e:
#            _logger.error(f"PreviewDialog Exception: {e}")
#        self.check_will()
#
#        vbox = QVBoxLayout(self)
#        vbox.addWidget(self.size_label)
#        vbox.addWidget(self.transactions_list)
#        buttonbox = QHBoxLayout()
#
#        b = QPushButton(_("Sign"))
#        b.clicked.connect(self.transactions_list.ask_password_and_sign_transactions)
#        buttonbox.addWidget(b)
#
#        b = QPushButton(_("Export Will"))
#        b.clicked.connect(self.transactions_list.export_will)
#        buttonbox.addWidget(b)
#
#        b = QPushButton(_("Broadcast"))
#        b.clicked.connect(self.transactions_list.broadcast)
#        buttonbox.addWidget(b)
#
#        b = QPushButton(_("Invalidate will"))
#        b.clicked.connect(self.transactions_list.invalidate_will)
#        buttonbox.addWidget(b)
#
#        vbox.addLayout(buttonbox)
#
#        self.update()
#
#    def update_will(self, will):
#        self.will.update(will)
#        self.transactions_list.update_will(will)
#        self.update()
#
#    def update(self):
#        self.transactions_list.update()
#
#    def is_hidden(self):
#        return self.isMinimized() or self.isHidden()
#
#    def show_or_hide(self):
#        if self.is_hidden():
#            self.bring_to_top()
#        else:
#            self.hide()
#
#    def bring_to_top(self):
#        self.show()
#        self.raise_()
#
#    def closeEvent(self, event):
#        event.accept()



class _FullUrlEditDelegate(QStyledItemDelegate):
    """Item delegate for the will-executor URL column.

    The URL column may display a SHORTENED form of long .onion addresses. This
    delegate ensures editing operates on the FULL url: the editor is preloaded
    from the real key role (which always holds the complete address) rather than
    from the visible (possibly shortened) cell text.
    """

    def __init__(self, url_role, parent=None):
        super().__init__(parent)
        self._url_role = url_role

    def createEditor(self, parent, option, index):
        return _QLineEdit(parent)

    def setEditorData(self, editor, index):
        # Prefer the full URL stored in the key role; fall back to the visible
        # text if for some reason the role is missing.
        full = index.data(self._url_role)
        if not full:
            full = index.data()
        editor.setText(full or "")

    def setModelData(self, editor, model, index):
        model.setData(index, editor.text())


class WillExecutorListWidget(MyTreeView):
    class Columns(MyTreeView.BaseColumnsEnum):
        SELECTED = enum.auto()
        URL = enum.auto()
        STATUS = enum.auto()
        BASE_FEE = enum.auto()
        INFO = enum.auto()
        ADDRESS = enum.auto()

    headers = {
        Columns.SELECTED: _(""),
        Columns.URL: _("Url"),
        Columns.STATUS: _("S"),
        Columns.BASE_FEE: _("Base fee"),
        Columns.INFO: _("Info"),
        Columns.ADDRESS: _("Default Address"),
    }

    filter_columns = [Columns.URL]

    ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 3000
    ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 3001
    key_role = ROLE_HEIR_KEY

    def __init__(self, parent: "WillExecutorWidget"):
        super().__init__(
            parent=parent,
            stretch_column=self.Columns.ADDRESS,
            editable_columns=[
                self.Columns.URL,
                self.Columns.BASE_FEE,
                self.Columns.ADDRESS,
                self.Columns.INFO,
            ],
        )
        self._bal_parent = parent
        try:
            self.setModel(QStandardItemModel(self))
            self.sortByColumn(self.Columns.SELECTED, Qt.SortOrder.AscendingOrder)
            self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
        except Exception:
            pass
        self.setSortingEnabled(True)
        self.std_model = self.model()
        self.config = parent.bal_plugin.config
        self.get_decimal_point = parent.bal_plugin.get_decimal_point

        # The URL column may DISPLAY a shortened form of long .onion addresses
        # (see update()).  Without this, double-clicking to edit would load the
        # shortened text (with the ellipsis) into the editor and saving it would
        # corrupt the will-executor key.  This delegate makes the editor load the
        # FULL url from the real key role instead of the visible (shortened)
        # text, so edits always operate on the complete address.
        try:
            self.setItemDelegateForColumn(
                self.Columns.URL,
                _FullUrlEditDelegate(self.ROLE_HEIR_KEY + self.Columns.URL, self),
            )
        except Exception:
            pass

        self.update()

    def create_menu(self, position):
        menu = QMenu()
        idx = self.indexAt(position)
        column = idx.column() or self.Columns.URL
        selected_keys = []
        sel_key = None
        for s_idx in self.selected_in_column(self.Columns.URL):
            item = self.model().itemFromIndex(s_idx)
            # Use the FULL url stored in the key role, NOT item.data(0): the
            # latter is the (possibly shortened) DISPLAY text, and using it as a
            # dict key breaks delete/select/deselect/ping for long .onion URLs
            # (KeyError). Fall back to the display text only if the role is
            # unexpectedly missing.
            sel_key = item.data(self.ROLE_HEIR_KEY + self.Columns.URL) or item.data(0)
            selected_keys.append(sel_key)
        if selected_keys and idx.isValid():
            column_title = self.model().horizontalHeaderItem(column).text()
            # column_data = "\n".join(
            #    self.model().itemFromIndex(s_idx).text()
            #    for s_idx in self.selected_in_column(column)
            # )
            # When exactly ONE cell is selected, offer "Copy" to copy the value
            # of that cell to the clipboard (e.g. a single url / address / fee).
            # This list has no ``main_window``, so use QApplication directly.
            if len(self.selectionModel().selectedIndexes()) == 1:
                cell_value = self.model().itemFromIndex(idx).text()
                menu.addAction(
                    _("Copy"),
                    lambda: QApplication.clipboard().setText(cell_value),
                )
            if Willexecutors.is_selected(self._bal_parent.willexecutors_list[sel_key]):
                menu.addAction(
                    _("deselect").format(column_title),
                    lambda: self.deselect(selected_keys),
                )
            else:
                menu.addAction(
                    _("select").format(column_title), lambda: self.select(selected_keys)
                )
            if column in self.editable_columns:
                item = self.model().itemFromIndex(idx)
                if item.isEditable():
                    persistent = QPersistentModelIndex(idx)
                    menu.addAction(
                        _("Edit {}").format(column_title),
                        lambda p=persistent: self.edit(QModelIndex(p)),
                    )

            menu.addAction(
                _("Ping").format(column_title),
                lambda: self.ping_willexecutors(selected_keys),
            )
            menu.addSeparator()
            menu.addAction(
                _("delete").format(column_title), lambda: self.delete(selected_keys)
            )

        menu.exec(self.viewport().mapToGlobal(position))

    def ping_willexecutors(self, selected_keys):
        wout = {}
        for k in selected_keys:
            wout[k] = self._bal_parent.willexecutors_list[k]
        self._bal_parent.update_willexecutors(wout)

        self._bal_parent.save_willexecutors()
        self.update()

    def on_activated(self, idx):
        self.on_double_click(idx)

    def on_double_click(self, idx):
        edit_key = self.get_edit_key_from_coordinate(
            idx.row(), self.Columns.URL
        )
        if edit_key and edit_key in self._bal_parent.willexecutors_list:
            self._bal_parent.add(edit_key)

    def get_edit_key_from_coordinate(self, row, col):
        role = self.ROLE_HEIR_KEY + col
        a = self.get_role_data_from_coordinate(row, col, role=role)
        return a

    def delete(self, selected_keys):
        for key in selected_keys:
            del self._bal_parent.willexecutors_list[key]

        self._bal_parent.save_willexecutors()
        self.update()

    def select(self, selected_keys):
        for wid, w in self._bal_parent.willexecutors_list.items():
            if wid in selected_keys:
                w["selected"] = True
        self._bal_parent.save_willexecutors()
        self.update()

    def deselect(self, selected_keys):
        for wid, w in self._bal_parent.willexecutors_list.items():
            if wid in selected_keys:
                w["selected"] = False
        self._bal_parent.save_willexecutors()
        self.update()

    def on_edited(self, idx, edit_key, *, text):
        # prior_name = self._bal_parent.willexecutors_list[edit_key]
        col = idx.column()
        try:
            if col == self.Columns.URL:
                self._bal_parent.willexecutors_list[text] = self._bal_parent.willexecutors_list[
                    edit_key
                ]
                del self._bal_parent.willexecutors_list[edit_key]
            if col == self.Columns.BASE_FEE:
                self._bal_parent.willexecutors_list[edit_key]["base_fee"] = (
                    Util.encode_amount(text, self.get_decimal_point())
                )
            if col == self.Columns.ADDRESS:
                self._bal_parent.willexecutors_list[edit_key]["address"] = text
            if col == self.Columns.INFO:
                self._bal_parent.willexecutors_list[edit_key]["info"] = text
            self._bal_parent.save_willexecutors()
            self.update()
        except Exception:
            pass

    def update(self):
        if self._bal_parent.willexecutors_list is None:
            return
        try:
            current_key = self.get_role_data_for_current_item(
                col=self.Columns.URL, role=self.ROLE_HEIR_KEY
            )
            self.model().clear()
            self.update_headers(self.__class__.headers)

            set_current = None

            for url, value in self._bal_parent.willexecutors_list.items():
                labels = [""] * len(self.Columns)
                # Long Tor (.onion) URLs overflow the column; show a shortened
                # form (first 37 chars + ellipsis, matching the welist site)
                # while keeping the FULL url as the real key data (setData below)
                # and in the tooltip, so nothing downstream breaks. Short URLs
                # are shown unchanged.
                display_url = url if len(url) <= 40 else url[:37] + "\u2026"
                labels[self.Columns.URL] = display_url
                if Willexecutors.is_selected(value):

                    labels[self.Columns.SELECTED] = [
                        read_QIcon_from_bytes(
                            self._bal_parent.bal_plugin.read_file("icons/confirmed.png")
                        ),
                        "",
                    ]
                else:
                    labels[self.Columns.SELECTED] = ""
                labels[self.Columns.BASE_FEE] = Util.decode_amount(
                    value.get("base_fee", 0), self.get_decimal_point()
                )
                if str(value.get("status", 0)) == "200":
                    labels[self.Columns.STATUS] = [
                        read_QIcon_from_bytes(
                            self._bal_parent.bal_plugin.read_file(
                                "icons/status_connected.png"
                            )
                        ),
                        "",
                    ]
                else:
                    labels[self.Columns.STATUS] = [
                        read_QIcon_from_bytes(
                            self._bal_parent.bal_plugin.read_file("icons/unconfirmed.png")
                        ),
                        "",
                    ]
                labels[self.Columns.ADDRESS] = str(value.get("address", ""))
                labels[self.Columns.INFO] = str(value.get("info", ""))

                items = []
                for e in labels:
                    if isinstance(e, list):
                        try:
                            items.append(QStandardItem(*e))
                        except Exception as e:
                            pass
                    else:
                        items.append(QStandardItem(e))

                max_fee = self._bal_parent.bal_plugin.MAX_WILLEXECUTOR_FEE.get()
                dust = self._bal_parent.bal_window.window.wallet.dust_threshold()
                if not Willexecutors.is_valid(value, max_fee=max_fee, dust=dust):
                    grey = QColor("#808080")
                    for item in items:
                        font = item.font()
                        font.setItalic(True)
                        item.setFont(font)
                        item.setForeground(grey)

                items[self.Columns.SELECTED].setEditable(False)
                items[self.Columns.URL].setEditable(True)
                items[self.Columns.ADDRESS].setEditable(True)
                items[self.Columns.INFO].setEditable(True)
                items[self.Columns.BASE_FEE].setEditable(True)
                items[self.Columns.STATUS].setEditable(False)

                items[self.Columns.URL].setData(
                    url, self.ROLE_HEIR_KEY + self.Columns.URL
                )
                # Full URL on hover (the visible text may be shortened above).
                items[self.Columns.URL].setToolTip(url)
                items[self.Columns.BASE_FEE].setData(
                    url, self.ROLE_HEIR_KEY + self.Columns.BASE_FEE
                )
                items[self.Columns.INFO].setData(
                    url, self.ROLE_HEIR_KEY + self.Columns.INFO
                )
                items[self.Columns.ADDRESS].setData(
                    url, self.ROLE_HEIR_KEY + self.Columns.ADDRESS
                )
                row_count = self.model().rowCount()
                self.model().insertRow(row_count, items)
                if url == current_key:
                    idx = self.model().index(row_count, self.Columns.URL)
                    set_current = QPersistentModelIndex(idx)
                self.set_current_idx(set_current)
            self.filter()
        except Exception as e:
            _logger.error(f"error updating willexcutor {e}")
            raise e



class WillExecutorWidget(QWidget, MessageBoxMixin):
    def __init__(self, parent, bal_window, willexecutors=None):
        self.bal_window = bal_window
        self.bal_plugin = bal_window.bal_plugin
        self._bal_parent = parent
        MessageBoxMixin.__init__(self)
        QWidget.__init__(self, parent)
        if willexecutors:
            self.willexecutors_list = willexecutors
        else:
            self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin)

        self.size_label = QLabel()
        self.will_executor_list_widget = WillExecutorListWidget(self)

        vbox = QVBoxLayout(self)
        vbox.addWidget(self.size_label)

        widget = QWidget()
        hbox = QHBoxLayout(widget)
        hbox.addWidget(QLabel(_("Add transactions without willexecutor")))
        heir_no_willexecutor = BalCheckBox(self.bal_plugin.NO_WILLEXECUTOR)
        hbox.addWidget(heir_no_willexecutor)
        spacer_widget = QWidget()
        spacer_widget.setSizePolicy(
            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
        )
        hbox.addWidget(spacer_widget)
        vbox.addWidget(widget)

        vbox.addWidget(self.will_executor_list_widget)
        buttonbox = QHBoxLayout()

        b = QPushButton(_("Add"))
        b.clicked.connect(self.add)
        buttonbox.addWidget(b)

        b = QPushButton(_("Download List"))
        b.clicked.connect(self.download_list)
        buttonbox.addWidget(b)

        b = QPushButton(_("Import"))
        b.clicked.connect(self.import_file)
        buttonbox.addWidget(b)

        def _menu_button(label):
            btn = QToolButton()
            btn.setText(_(label))
            btn.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
            buttonbox.addWidget(btn)
            return btn

        export_btn = _menu_button("Export")
        export_menu = QMenu(export_btn)
        export_menu.addAction(_("Export all"), lambda: self.export_file())
        export_menu.addAction(
            _("Export selected"), lambda: self.export_file(subset="selected")
        )
        export_menu.addAction(
            _("Export only valid"), lambda: self.export_file(subset="valid")
        )
        export_btn.setMenu(export_menu)

        ping_btn = _menu_button("Ping All")
        ping_menu = QMenu(ping_btn)
        ping_menu.addAction(_("Ping all"), lambda: self.update_willexecutors())
        ping_menu.addAction(
            _("Ping selected"), lambda: self.ping_selected_willexecutors()
        )
        ping_btn.setMenu(ping_menu)

        select_btn = _menu_button("Select All")
        select_menu = QMenu(select_btn)
        select_menu.addAction(_("Select all"), lambda: self.set_select_all(True))
        select_menu.addAction(
            _("Select only valid"), lambda: self.set_select_all(True, only_valid=True)
        )
        select_menu.addAction(_("Deselect all"), lambda: self.set_select_all(False))
        select_menu.addAction(
            _("Deselect only invalid"),
            lambda: self.set_select_all(False, only_valid=True),
        )
        select_btn.setMenu(select_menu)

        vbox.addLayout(buttonbox)
        # self.will_executor_list_widget.update()

    def add(self, edit_key=None):
        executor = None
        if edit_key:
            executor = self.willexecutors_list.get(edit_key)
            title = _("Edit: {}").format(edit_key)
        else:
            title = _("New Will Executor")
        d = BalDialog(
            self.bal_window.window,
            self.bal_plugin,
            self.bal_plugin.get_window_title(title),
        )

        vbox = QVBoxLayout(d)
        grid = QGridLayout()

        url_edit = QLineEdit()
        url_edit.setFixedWidth(32 * char_width_in_lineedit())
        info_edit = QLineEdit("New Will Executor")
        info_edit.setFixedWidth(32 * char_width_in_lineedit())
        base_fee_spin = QSpinBox()
        base_fee_spin.setRange(0, 1000000)
        base_fee_spin.setValue(0)
        address_edit = QLineEdit()
        address_edit.setFixedWidth(32 * char_width_in_lineedit())
        sync_btn = QPushButton("\u21BB")
        sync_btn.setFixedWidth(32)
        loading_label = QLabel()
        d._sync_active = False

        if executor:
            url_edit.setText(edit_key)
            info_edit.setText(str(executor.get("info", "New Will Executor")))
            base_fee_spin.setValue(int(executor.get("base_fee", 0)))
            address_edit.setText(str(executor.get("address", "")))

        def on_sync():
            url = url_edit.text().strip()
            if not url:
                self.show_error(_("URL is required"))
                return
            tmp = {}
            sync_btn.setEnabled(False)
            loading_label.setText("\u27F3")
            d._sync_active = True

            def task():
                return Willexecutors.get_info_task(url, tmp)

            def on_success(result):
                if not getattr(d, "_sync_active", False):
                    return
                if result.get("status") == 200:
                    info_edit.setText(str(result.get("info", "")))
                    base_fee_spin.setValue(int(result.get("base_fee", 0)))
                    address_edit.setText(str(result.get("address", "")))
                    if edit_key and edit_key in self.willexecutors_list:
                        self.willexecutors_list[edit_key].update({
                            "status": 200,
                            "info": result.get("info", ""),
                            "base_fee": result.get("base_fee", 0),
                            "address": result.get("address", ""),
                            "last_update": result.get(
                                "last_update", datetime.now().timestamp()
                            ),
                        })
                        self.will_executor_list_widget.update()
                else:
                    QMessageBox.warning(
                        d,
                        _("Error"),
                        _("Could not reach server at {}").format(url),
                    )
                    url_edit.setFocus()
                    url_edit.selectAll()
                sync_btn.setEnabled(True)
                loading_label.clear()
                d._sync_active = False

            def on_error(exc_info):
                if not getattr(d, "_sync_active", False):
                    return
                QMessageBox.warning(
                    d,
                    _("Error"),
                    _("Error contacting server: {}").format(
                        str(exc_info[1])
                    ),
                )
                url_edit.setFocus()
                url_edit.selectAll()
                sync_btn.setEnabled(True)
                loading_label.clear()
                d._sync_active = False

            def on_done():
                pass

            sync_thread = TaskThread(d)
            sync_thread.add(
                task, on_success=on_success, on_done=on_done, on_error=on_error
            )

        sync_btn.clicked.connect(on_sync)

        if not edit_key:
            add_another_btn = QPushButton(_("Add another"))
            self._add_another = False

            def add_another():
                self._add_another = True
                d.accept()

            add_another_btn.clicked.connect(add_another)
        else:
            self._add_another = False
            add_another_btn = None

        row = 0
        grid.addWidget(QLabel(_("URL")), row, 0)
        grid.addWidget(url_edit, row, 1)
        grid.addWidget(sync_btn, row, 2)
        grid.addWidget(loading_label, row, 3)
        grid.addWidget(
            HelpButton(_("Will executor server URL (e.g. http://192.168.1.100:8080)")),
            row,
            4,
        )

        row += 1
        grid.addWidget(QLabel(_("Info")), row, 0)
        grid.addWidget(info_edit, row, 1)
        grid.addWidget(
            HelpButton(_("A short description or name for this executor")),
            row,
            2,
        )

        row += 1
        grid.addWidget(QLabel(_("Base Fee (sats)")), row, 0)
        grid.addWidget(base_fee_spin, row, 1)
        grid.addWidget(
            HelpButton(_("Base fee in satoshis")),
            row,
            2,
        )

        row += 1
        grid.addWidget(QLabel(_("Address")), row, 0)
        grid.addWidget(address_edit, row, 1)
        grid.addWidget(
            HelpButton(_("Bitcoin address for fee payments (optional)")),
            row,
            2,
        )

        vbox.addLayout(grid)
        buttons = [CancelButton(d), OkButton(d)]
        if not edit_key:
            buttons.append(add_another_btn)
        vbox.addLayout(Buttons(*buttons))

        while d.exec():
            url = url_edit.text().strip()
            if not url:
                self.show_error(_("URL is required"))
                continue
            if edit_key:
                old_url = edit_key
                ex = self.willexecutors_list[old_url]
                ex.update({
                    "info": info_edit.text().strip() or "New Will Executor",
                    "base_fee": base_fee_spin.value(),
                    "address": address_edit.text().strip(),
                })
                if url != old_url:
                    self.willexecutors_list[url] = ex
                    del self.willexecutors_list[old_url]
            else:
                self.willexecutors_list[url] = {
                    "info": info_edit.text().strip() or "New Will Executor",
                    "base_fee": base_fee_spin.value(),
                    "address": address_edit.text().strip(),
                    "selected": False,
                    "status": "-1",
                }
            self.will_executor_list_widget.update()
            Willexecutors.save(self.bal_window.bal_plugin, self.willexecutors_list)
            if not self._add_another:
                break
            self._add_another = False
            url_edit.clear()
            info_edit.setText("New Will Executor")
            base_fee_spin.setValue(0)
            address_edit.clear()

    def download_list(self, wes=None):
        # Both this button and the wizard go through the same code path on
        # BalWindow, which shows a "Downloading..." dialog (non-blocking GUI),
        # tries the configured + fallback servers, logs the technical details
        # and shows a simple message on failure.
        def on_success(result):
            self.willexecutors_list.update(result)
            self.will_executor_list_widget.update()
            Willexecutors.save(self.bal_window.bal_plugin, self.willexecutors_list)
            self.update()

        self.bal_window.download_list(self.bal_window.willexecutors, on_success)

    def export_file(self, subset=None):
        data = self.export_data(subset)
        if subset and not data:
            self.show_message(_("No will-executor matches the selected filter"))
            return
        export_meta_gui(
            self.bal_window.window,
            "willexecutors.json",
            partial(self.export_json_file, subset=subset),
        )

    def export_data(self, subset=None):
        data = self.willexecutors_list
        if subset == "selected":
            data = {
                url: we
                for url, we in data.items()
                if Willexecutors.is_selected(we)
            }
        elif subset == "valid":
            valid = self._validity()
            data = {
                url: we for url, we in data.items() if valid.get(url, False)
            }
        return data

    def export_json_file(self, path, subset=None):
        write_json_file(path, self.export_data(subset))

    def _validity(self):
        max_fee = self.bal_plugin.MAX_WILLEXECUTOR_FEE.get()
        dust = self.bal_window.window.wallet.dust_threshold()
        return {
            url: Willexecutors.is_valid(we, max_fee=max_fee, dust=dust)
            for url, we in self.willexecutors_list.items()
        }

    def _selected_willexecutors(self):
        return {
            url: we
            for url, we in self.willexecutors_list.items()
            if Willexecutors.is_selected(we)
        }

    def set_select_all(self, select, only_valid=False):
        """Apply a bulk selection across all will-executors.

        ``select=True`` selects all (or only the valid ones when
        ``only_valid=True``, deselecting the invalid ones); ``select=False``
        deselects all (or only the invalid ones when ``only_valid=True``,
        leaving the valid ones selected).
        """
        valid = self._validity() if only_valid else None
        _apply_select_all(self.willexecutors_list, select, valid)
        self.save_willexecutors()

    def ping_selected_willexecutors(self):
        wes = self._selected_willexecutors()
        if not wes:
            self.show_message(_("No will-executor is selected"))
            return
        self.update_willexecutors(wes)

    def import_file(self):
        import_meta_gui(
            self.bal_window.window,
            _("willexecutors"),
            self.import_json_file,
            self.willexecutors_list.update,
        )

    def update_willexecutors(self, wes=None):
        if not wes:
            wes = self.willexecutors_list
        self.bal_window.ping_willexecutors(wes, self.save_willexecutors)

    def import_json_file(self, path):
        data = read_json_file(path)
        data = self._validate(data)
        self.willexecutors_list.update(data)
        self.will_executor_list_widget.update()

    # TODO validate willexecutor json import file
    def _validate(self, data):
        return data

    def save_willexecutors(self, wes=None):
        if not wes:
            wes = self.willexecutors_list
        self.willexecutors_list.update(wes)
        self.will_executor_list_widget.update()
        Willexecutors.save(self.bal_window.bal_plugin, self.willexecutors_list)


