"""
bal.gui.qt.dialogs
==================

All modal/non-modal dialogs of the plugin.

    * BalDialog                  - common base dialog (icon, close handling).
    * BalWizard* (Dialog/Widget) - the step-by-step "create your will" wizard.
    * BalWaitingDialog /
      BalBlockingWaitingDialog   - progress dialogs for background tasks.
    * BalBuildWillDialog         - the central build/sign/push/broadcast flow.
    * WillDetailDialog           - shows the full will tree for one wallet.
    * WillExecutorDialog         - manage the list of will-executor servers.

To keep the dialogs verbatim while avoiding import cycles with the list views,
the few list classes they reference are imported lazily inside the methods that
use them (see ``lists`` imports below).
"""

from typing import TYPE_CHECKING

from .calendar import BalCalendar, BalCalendarButton
from .common import *
from .common import _, _logger  # underscore names are not re-exported by "import *"
from .widgets import (
    WillSettingsWidget,
    WillWidget,
    basic_reminder_offsets,
    compute_reminder_offsets,
)

if TYPE_CHECKING:
    from .window import BalWindow

# NOTE: list views (HeirListWidget, PreviewList, WillExecutorWidget) are
# imported lazily where needed to avoid a dialogs<->lists import cycle.


class BalDialog(QDialog,MessageBoxMixin):
    _stopping = False
    def __init__(self, parent, bal_plugin, title=None, icon="icons/bal16x16.png"):
        from PyQt6.QtCore import QMetaObject, Qt
        def handler(signum, frame):
            QMetaObject.invokeMethod(self, "close", Qt.ConnectionType.QueuedConnection)

        #signal.signal(signal.SIGINT, handler)
        # NOTE: do NOT store this as ``self.parent`` - that would shadow
        # QWidget.parent() and can make the dialog disappear behind Electrum.
        self._bal_parent = parent
        self.thread = None
        # Anchor the dialog to the *top-level* Electrum window so it always
        # stays in front of it (instead of falling behind).
        super().__init__(top_level_of(parent))
        if title:
            self.setWindowTitle(title)
        # WindowModalDialog.__init__(self,parent)
        self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon)))

    def closeEvent(self, event):
        self._stopping = True
        # NOTE: we deliberately do NOT stop ``self.thread`` here.
        #
        # Electrum's ``TaskThread`` delivers results via ``on_done`` which calls
        # ``cb_done`` (often ``self.accept`` -> closes this dialog) *before*
        # ``cb_result`` (``on_success`` -> e.g. updating the will-executor
        # list).  If we stop/join the thread inside ``closeEvent`` the close
        # triggered by ``accept`` tears the thread down *before* ``on_success``
        # runs, so the downloaded data is silently dropped.  The original plugin
        # left this commented out for exactly this reason; subclasses that own a
        # genuinely long-lived thread stop it explicitly in their own close
        # handler.
        super().closeEvent(event)

    def hideEvent(self, event):
        self._stopping = True
        super().hideEvent(event)


class BalWizardDialog(BalDialog):
    def __init__(self, bal_window: "BalWindow"):
        assert bal_window
        BalDialog.__init__(
            self, bal_window.window, bal_window.bal_plugin, _("Bal Wizard Setup")
        )
        self.setMinimumSize(800, 400)
        self.bal_window = bal_window
        self._bal_parent = bal_window.window
        self.layout = QVBoxLayout(self)
        self.widget = BalWizardHeirsWidget(
            bal_window, self, self.on_next_heir, None, self.on_cancel_heir
        )
        self.layout.addWidget(self.widget)

    def next_widget(self, widget):
        self.layout.removeWidget(self.widget)
        self.widget.close()
        self.widget = widget
        self.layout.addWidget(self.widget)
        # self.update()
        # self.repaint()

    def on_next_heir(self):
        self.next_widget(
            BalWizardLocktimeAndFeeWidget(
                self.bal_window,
                self,
                self.on_next_locktimeandfee,
                self.on_previous_heir,
                self.on_cancel_heir,
            )
        )

    def on_previous_heir(self):
        self.next_widget(
            BalWizardHeirsWidget(
                self.bal_window, self, self.on_next_heir, None, self.on_cancel_heir
            )
        )

    def on_cancel_heir(self):
        pass

    def on_next_wedonwload(self):
        self.next_widget(
            BalWizardWEWidget(
                self.bal_window,
                self,
                self.on_next_we,
                self.on_next_locktimeandfee,
                self.on_cancel_heir,
            )
        )

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

        # Run the SAME final server check as the "Check" button (allegato15,
        # case B): previously the wizard only ran build_will_task() and skipped
        # the will-executor verification, so the user always had to press
        # "Check" manually after finishing the wizard. We now replicate exactly
        # the lists.py check() logic: after building, query every will that
        # needs a server check (Will.needs_server_check) and run
        # check_transactions(), which shows the "Checking transactions" dialog.
        will = {}
        for wid, w in self.bal_window.willitems.items():
            if Will.needs_server_check(w):
                will[wid] = w
        if will:
            self.bal_window.check_transactions(will)

        self.close()
        # self.next_widget(BalWizardLocktimeAndFeeWidget(self.bal_window,self,self.on_next_locktimeandfee,self.on_next_wedonwload,self.on_next_wedonwload.on_cancel_heir))

    def on_next_locktimeandfee(self):
        self.next_widget(
            BalWizardWEDownloadWidget(
                self.bal_window,
                self,
                self.on_next_wedonwload,
                self.on_next_heir,
                self.on_cancel_heir,
            )
        )

    def on_accept(self):
        self.bal_window.update_all()
        pass

    def on_reject(self):
        pass

    def on_close(self):
        self.bal_window.update_all()
        pass

    def closeEvent(self, event):
        self._stopping = True
        # self.bal_window.heir_list_widget.will_settings_widget.update_will_settings()
        pass



class BalWizardWidget(QWidget):
    title = None
    message = None

    def __init__(
        self, bal_window: "BalWindow", parent, on_next, on_previous, on_cancel
    ):
        QWidget.__init__(self, parent)
        self.vbox = QVBoxLayout(self)
        self.bal_window = bal_window
        self._bal_parent = parent
        self.on_next = on_next
        self.on_cancel = on_cancel
        self.titleLabel = QLabel(self.title)
        self.vbox.addWidget(self.titleLabel)
        self.messageLabel = QLabel(_(self.message))
        self.vbox.addWidget(self.messageLabel)

        self.content = self.get_content()
        self.content_container = QWidget()
        self.containrelayout = QVBoxLayout(self.content_container)
        self.containrelayout.addWidget(self.content)

        self.vbox.addWidget(self.content_container)

        spacer_widget = QWidget()
        spacer_widget.setSizePolicy(
            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
        )
        self.vbox.addWidget(spacer_widget)

        self.buttons = []
        if on_previous:
            self.on_previous = on_previous
            self.previous_button = QPushButton(_("Previous"))
            self.previous_button.clicked.connect(self._on_previous)
            self.buttons.append(self.previous_button)

        self.next_button = QPushButton(_("Next"))
        self.next_button.clicked.connect(self._on_next)
        self.buttons.append(self.next_button)

        self.abort_button = QPushButton(_("Cancel"))
        self.abort_button.clicked.connect(self._on_cancel)
        self.buttons.append(self.abort_button)

        self.vbox.addLayout(Buttons(*self.buttons))

    def _on_cancel(self):
        self.on_cancel()
        self._bal_parent.close()

    def _on_next(self):
        if self.validate():
            self.on_next()

    def _on_previous(self):
        self.on_previous()

    def get_content(self):
        pass

    def validate(self):
        return True



class BalWizardHeirsWidget(BalWizardWidget):
    title = "Bitcoin After Life Heirs"
    message = (
        "Please add your heirs\n remember that 100% of wallet balance will be spent"
    )

    def get_content(self):
        # Lazy import to avoid a dialogs<->lists import cycle (lists imports
        # BalBuildWillDialog from this module at load time).
        from .lists import HeirListWidget
        self.heir_list_widget = HeirListWidget(self.bal_window, self)
        button_add = QPushButton(_("Add"))
        button_add.clicked.connect(self.add_heir)
        button_import = QPushButton(_("Import"))
        button_import.clicked.connect(self.import_from_file)
        button_export = QPushButton(_("Export"))
        button_export.clicked.connect(self.export_to_file)
        widget = QWidget()
        vbox = QVBoxLayout(widget)
        vbox.addWidget(self.heir_list_widget)
        vbox.addLayout(Buttons(button_add, button_import, button_export))
        return widget

    def import_from_file(self):
        self.bal_window.import_heirs()
        self.heir_list_widget.update()

    def export_to_file(self):
        self.bal_window.export_heirs()

    def add_heir(self):
        self.bal_window.new_heir_dialog()
        self.heir_list_widget.update()

    def validate(self):
        return True



class BalWizardWEDownloadWidget(BalWizardWidget):
    title = _("Bitcoin After Life Will-Executors")
    message = _("Choose willexecutors download method")

    def get_content(self):
        # question = QLabel()
        self.combo = QComboBox()
        self.combo.addItems(
            [
                "Automatically download and select willexecutors",
                "Only download willexecutors list",
                "Import willexecutor list from file",
                "Manual",
            ]
        )
        # heir_name.setFixedWidth(32 * char_width_in_lineedit())
        return self.combo

    def validate(self):
        return True

    def _on_next(self):

        index = self.combo.currentIndex()
        _logger.debug(f"selected index:{index}")
        if index < 3:
            self.bal_window.willexecutors = Willexecutors.get_willexecutors(
                self.bal_window.bal_plugin
            )

            if index == 2:

                def do_nothing():
                    self.bal_window.willexecutors.update(self.willexecutors)
                    Willexecutors.save(
                        self.bal_window.bal_plugin, self.bal_window.willexecutors
                    )
                    pass

                import_meta_gui(
                    self.bal_window.window,
                    _("willexecutors"),
                    self.import_json_file,
                    do_nothing,
                )

            if index < 2:

                def on_success(willexecutors):
                    def ping_on_success(result):
                        ping_on_done()

                    def ping_on_failure(exec_info):
                        ping_on_done()

                    def ping_on_done():
                        # Task #02 - "Automatically download and select"
                        # (index 0): the green SELECTED tick must follow the
                        # green ping dot, i.e. select ONLY servers that actually
                        # answered the ping (status == 200) and DESELECT every
                        # server that did not (timeout / error / never pinged).
                        #
                        # Why the explicit deselect matters: previously a server
                        # that had been selected on an earlier download but is
                        # now unreachable stayed selected, so the plugin kept
                        # broadcasting to a dead server and got stuck. Forcing
                        # selected=False for non-200 servers discards them at the
                        # source. Re-running this (each "Automatically download"
                        # action) re-evaluates every server: one that failed
                        # before but now answers is selected again.
                        #
                        # We compare the status as a string ("200") to stay
                        # consistent with the will-executor list view
                        # (lists.py uses str(status) == "200"), and use .get()
                        # so a missing "status" key never raises.
                        if index < 1:
                            for we in self.bal_window.willexecutors:
                                wedict = self.bal_window.willexecutors[we]
                                responded = str(wedict.get("status", "")) == "200"
                                wedict["selected"] = responded
                        Willexecutors.save(
                            self.bal_window.bal_plugin, self.bal_window.willexecutors
                        )

                    self.bal_window.ping_willexecutors(
                        self.bal_window.willexecutors, ping_on_success, ping_on_failure
                    )

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

        elif index == 3:
            # TODO DO NOTHING
            pass

        self.bal_window.will_list_widget.update()
        if self.validate():
            return self.on_next()

    def import_json_file(self, path):
        data = read_json_file(path)
        data = self._validate(data)
        self.willexecutors = data

    def _validate(self, data):
        return data



class BalWizardWEWidget(BalWizardWidget):
    title = "Bitcoin After Life Will-Executors"
    message = _("Configure and select your willexecutors")

    def get_content(self):
        # Lazy import to avoid a dialogs<->lists import cycle.
        from .lists import WillExecutorWidget
        widget = QWidget()
        vbox = QVBoxLayout(widget)
        vbox.addWidget(
            WillExecutorWidget(
                self,
                self.bal_window,
                Willexecutors.get_willexecutors(self.bal_window.bal_plugin),
            )
        )
        return widget



class BalWizardLocktimeAndFeeWidget(BalWizardWidget):
    title = "Bitcoin After Life Will Settings"
    message = _("")

    def get_content(self):
        widget = QWidget()
        layout = QVBoxLayout(widget)

        # The wizard ("Build your will") is the ONLY place the delivery time,
        # check alive and fee can be edited, so it is the only read_only=False.
        layout.addWidget(WillSettingsWidget(self.bal_window, self, "v",
                                            read_only=False))
        spacer_widget = QWidget()
        spacer_widget.setSizePolicy(
            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
        )
        layout.addWidget(spacer_widget)
        return widget



class BalWaitingDialog(BalDialog):
    updatemessage = pyqtSignal([str], arguments=["message"])

    def __init__(
        self,
        bal_window: "BalWindow",
        message: str,
        task,
        on_success=None,
        on_error=None,
        on_cancel=None,
        exe=True,
    ):
        assert bal_window
        BalDialog.__init__(
            self, bal_window.window, bal_window.bal_plugin, _("Please wait")
        )
        self.message_label = QLabel(message)
        vbox = QVBoxLayout(self)
        vbox.addWidget(self.message_label)
        self.updatemessage.connect(self.update_message)
        if on_cancel:
            self.cancel_button = CancelButton(self)
            self.cancel_button.clicked.connect(on_cancel)
            vbox.addLayout(Buttons(self.cancel_button))
        self.accepted.connect(self.on_accepted)
        self.task = task
        self.on_success = on_success
        self.on_error = on_error
        self.on_cancel = on_cancel
        if exe:
            self.exe()

    def exe(self):
        self.thread = TaskThread(self)
        self.thread.finished.connect(self.deleteLater)  # see #3956
        self.thread.add(self.task, self.on_success, self.accept, self.on_error)
        # IMPORTANT: keep the *application-modal* exec() of the original code.
        # This dialog is driven by a TaskThread whose result (on_success, e.g.
        # populating the will-executor list) is delivered via a queued signal
        # while exec() spins the modal event loop.  Switching to window-modal
        # changed how the modal loop interacts with that delivery and could
        # cause the downloaded list to never be applied.  We only add the
        # raise/activate so the dialog stays visible, without altering modality.
        bring_to_front(self)
        self.exec()

    def hello(self):
        pass


    def on_accepted(self):
        pass

    def update_message(self, msg):
        self.message_label.setText(msg)

    def update(self, msg):
        self.updatemessage.emit(msg)

    def getText(self):
        return self.message_label.text()




class BalBlockingWaitingDialog(BalDialog):
    def __init__(self, bal_window: "BalWindow", message: str, task: Callable[[], Any]):
        BalDialog.__init__(self, bal_window, bal_window.bal_plugin, _("Please wait"))
        self.message_label = QLabel(message)
        vbox = QVBoxLayout(self)
        vbox.addWidget(self.message_label)
        self.finished.connect(self.deleteLater)  # see #3956
        # show popup (window-modal + on top so it is actually visible)
        show_on_top(self)
        # Refresh the GUI so the popup is painted (and message_label drawn)
        # BEFORE we block the GUI thread running the task; otherwise the popup
        # appears empty/frozen.
        from PyQt6.QtWidgets import QApplication
        QApplication.processEvents()
        QApplication.processEvents()
        try:
            # block and run given task
            task()
        finally:
            # close popup
            self.accept()


class BalBuildWillDialog(BalDialog):
    updatemessage = pyqtSignal()
    COLOR_WARNING = "#cfa808"
    COLOR_ERROR = "#ff0000"
    COLOR_OK = "#05ad05"

    def __init__(self, bal_window, parent=None):
        if not parent:
            parent = bal_window.window
        BalDialog.__init__(self, parent, bal_window.bal_plugin, _("Building Will"))
        # (parent already stored as self._bal_parent by BalDialog.__init__)
        self.updatemessage.connect(self.msg_update)
        self.bal_window = bal_window
        self.bal_plugin = bal_window.bal_plugin
        self.message_label = QLabel(_("Building Will:"))
        # Allow the long report text to wrap instead of forcing the dialog ever
        # wider, and let it grow downward inside the scroll area below.
        self.message_label.setWordWrap(True)
        self.message_label.setAlignment(
            Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
        )
        self.vbox = QVBoxLayout(self)

        # SCROLLABLE message area (allegato14): with many will-executors the
        # report can reach dozens of lines. Previously the dialog kept resizing
        # itself taller for every new line (see msg_update's resize), so with
        # e.g. 50 will-executors the window grew past the screen and the bottom
        # buttons (Close) became unreachable. We now put the message label in a
        # QScrollArea with a capped maximum height: once the text exceeds that
        # height a vertical scrollbar appears and the buttons stay visible.
        self.scroll_area = QScrollArea(self)
        self.scroll_area.setWidgetResizable(True)
        self.scroll_area.setWidget(self.message_label)
        # Open the report area ~450px tall (owner request: 500px left too much
        # empty space below the short report; 450px lines up with the desired
        # window height). The dialog may still grow up to 700px to fit a few more
        # lines; beyond that the vertical scrollbar takes over and the bottom
        # buttons stay reachable.
        self.scroll_area.setMinimumHeight(450)
        self.scroll_area.setMaximumHeight(700)
        self.vbox.addWidget(self.scroll_area, 1)

        # Kept for backward compatibility (referenced by old/commented code);
        # no longer used to lay out the messages.
        self.qwidget = QWidget(self)
        self.labelsbox = QVBoxLayout(self.qwidget)
        self.setMinimumWidth(600)
        self.setMinimumHeight(100)
        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        self.labels = []
        self.check_row = None
        self.inval_row = None
        self.build_row = None
        self.sign_row = None
        self.push_row = None
        # Manual next-steps hint (Sign / Broadcast) shown to the user after the
        # dialog finishes; None when nothing is left to do.
        self._next_steps_hint = None
        # Set to True by _sync_locktime_to_built_txs when the delivery date was
        # automatically anticipated during a rebuild. Used to explain to the
        # user WHY signing is being requested (otherwise the sign prompt appears
        # without any reason, as the owner reported).
        self._date_was_anticipated = False
        # Set to True right after we broadcast an automatic invalidation
        # transaction (the "postpone" path). On the very next phase-1 re-check
        # Electrum may not have seen the invalidation tx yet, so it would still
        # report a postpone and the wizard would re-prompt to invalidate over
        # and over (the reported loop). When this flag is set and a postpone is
        # STILL detected, we STOP with a clear message instead of re-prompting.
        self._invalidation_broadcast = False
        self.network = Network.get_instance()
        self._stopping = False
        self.thread = TaskThread(self)
        self.thread.finished.connect(self.task_finished)  # see #3956

    def task_finished(self):
        pass

    def build_will_task(self):
        _logger.debug("build will task to be started")
        self.thread.add(
            self.task_phase1,
            on_success=self.on_success_phase1,
            on_done=self.on_accept,
            on_error=self.on_error_phase1,
        )
        # exec() already shows the dialog modally; route through the helper so
        # it is window-modal and brought to the front (no separate show()).
        show_modal(self)

    def task_phase1(self):
        if self._stopping:
            return
        txs = None
        _logger.debug("close plugin phase 1 started")
        varrow = self.msg_set_status("Checking variables")
        try:
            self.bal_window.init_class_variables()
        except CheckAliveError as cae:
            fee_per_byte = self.bal_window.will_settings.get("baltx_fees", 1)
            tx = Will.invalidate_will(
                self.bal_window.willitems, self.bal_window.wallet, fee_per_byte,
                history_label=self.bal_window.bal_plugin.HISTORY_LABEL.get(),
                will_locktime=Will.get_min_locktime(
                    self.bal_window.willitems,
                    default_value=self.bal_window.date_to_check,
                ),
            )
            if tx:
                _logger.debug(
                    "during phase1 CAE: {}, Continue to invalidate".format(cae)
                )
                self.msg_set_status("Checking variables",varrow, "Check Alive Threshold Passed: you have to Invalidate your old Will",self.COLOR_ERROR)
            else:
                raise cae
            return None, tx
        except NoHeirsException:
            self.msg_set_status("Checking variables", varrow,"No Heirs",self.COLOR_ERROR)
            #self.msg_set_checking("No Heirs")
            return False, None
        except Exception as e:
            raise e
        try:
            _logger.debug("checking variables")
            Will.check_amounts(
                self.bal_window.heirs,
                self.bal_window.willexecutors,
                Util.get_available_utxos(
                    self.bal_window.window.wallet,
                    self.bal_window.bal_plugin.HISTORY_LABEL.get(),
                    Will.get_min_locktime(
                        self.bal_window.willitems,
                        default_value=self.bal_window.date_to_check,
                    ),
                ),
                self.bal_window.date_to_check,
                self.bal_window.window.wallet.dust_threshold(),
                max_fee=self.bal_window.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
            )
            _logger.debug("variables ok")
            self.msg_set_status("Checking variables", varrow, "Ok", self.COLOR_OK)
        except AmountException:
            self.msg_set_checking(
                self.msg_warning(
                    "In the inheritance process, "
                    + "the entire wallet will always be fully emptied. \n"
                    + "Your settings require an adjustment of the amounts"
                )
            )
        except WillExecutorFeeTooHighException as e:
            self.msg_set_checking(
                self.msg_warning(f"Will-executor fee too high: {e}")
            )

        self.msg_set_checking()
        have_to_build = False
        try:
            self.bal_window.check_will()
            self.msg_set_checking(self.msg_ok())
        except WillExpiredException:
            # UNIFY INVALIDATE PROCEDURE (+ task #03):
            #
            # The will is already expired (e.g. the CHECK button is pressed on an
            # expired will). Previously this returned (None, invalidate_tx),
            # which routed to the automatic invalidate path (password prompt +
            # auto-broadcast) that did NOT set the "BAL Invalidate transaction"
            # history label.
            #
            # We now return the SAME "invalidate_classic" signal used elsewhere,
            # so on_success_phase1 shows the warning popup and auto-opens
            # Electrum's classic transaction window (which sets the label). This
            # makes the CHECK button and the WIZARD behave identically and fixes
            # the missing-label bug (#03).
            _logger.debug("expired")
            self.msg_set_checking("Expired")
            return "invalidate_classic", None
        except WillPostponedException as e:
            # An already signed/sent will is being postponed.  Like an expired
            # will, the previously committed coins must be invalidated on-chain
            # FIRST (otherwise a will-executor could broadcast the old,
            # earlier-locktime tx and execute the inheritance too early).  We
            # return (None, tx) so phase 2 asks the user to sign and broadcast
            # the invalidation; afterwards the user presses Prepare again to
            # rebuild the new (postponed) inheritance.
            _logger.debug(f"postponed {e}")
            self.msg_set_checking(_("Postponed: invalidating old will"))
            fee_per_byte = self.bal_window.will_settings.get("baltx_fees", 1)
            return None, Will.invalidate_will(
                self.bal_window.willitems, self.bal_window.wallet, fee_per_byte,
                history_label=self.bal_window.bal_plugin.HISTORY_LABEL.get(),
                will_locktime=Will.get_min_locktime(
                    self.bal_window.willitems,
                    default_value=self.bal_window.date_to_check,
                ),
            )
        except NoHeirsException:
            _logger.debug("no heirs")
            self.msg_set_checking("No Heirs")
        except NotCompleteWillException as e:
            _logger.debug(f"not complete {e} true")
            message = False
            have_to_build = True
            # Task #7a: if the will was already executed, the wallet is empty and
            # this exception is expected. Before showing the alarming "Found
            # CHANGES ... a NEW WILL must be prepared" message, add a clear,
            # reassuring note on the "Checking your will" row telling the user
            # the inheritance is already on its way (mempool) or done (on-chain).
            # The original message is still shown afterwards (owner request: the
            # red/"changes" message stays, this is only an extra, more precise
            # informative line).
            # NOTE: we add the informative note as its OWN extra row (not via
            # msg_set_checking, which reuses self.check_row and would be
            # overwritten by the "Found CHANGES" line set below). Passing row=None
            # to msg_set_status appends a new line, so the executed/mempool note
            # and the original message are BOTH visible.
            executed_status = self._executed_inheritance_status()
            if executed_status == "CONFIRMED":
                # Green: the inheritance transaction is confirmed on the
                # blockchain, so it has been executed correctly.
                self.msg_set_status(
                    _("Checking your will"),
                    None,
                    _("An inheritance of this wallet is already executed (on blockchain)"),
                    self.COLOR_OK,
                )
            elif executed_status == "MEMPOOL":
                # Orange (warning colour): the transaction is in the mempool,
                # waiting to be confirmed. Not an error, just "in progress".
                self.msg_set_status(
                    _("Checking your will"),
                    None,
                    _("Inheritance in mempool (waiting confirmation)"),
                    self.COLOR_WARNING,
                )
            if isinstance(e, HeirChangeException):
                message = _("Heirs changed:")
            elif isinstance(e, WillExecutorNotPresent):
                message = _("Will-Executor not present")
            elif isinstance(e, WillexecutorChangeException):
                message = _("Will-Executor changed")
            elif isinstance(e, TxFeesChangedException):
                message = _("Txfees are changed")
            elif isinstance(e, HeirNotFoundException):
                # Task #01b: the old text "Heir not found" was misleading.
                # In practice this branch is reached whenever the will is no
                # longer coherent and must be rebuilt - very often simply
                # because the delivery date was anticipated, NOT because an heir
                # is genuinely missing. We therefore show a clear, accurate
                # message that covers both the DATE and the HEIRS cases.
                message = _(
                    "Found CHANGES to the DATE or the HEIRS,\n"
                    "a NEW WILL must be prepared."
                )
            if message:
                _logger.debug(f"message: {message}")
                self.msg_set_checking(message)
            else:
                # Task #01b: the old fallback text "New" was unclear. When the
                # will is incomplete without a more specific reason, it still
                # means the will has to be rebuilt, so we use the same clear
                # message as the HeirNotFoundException branch above.
                self.msg_set_checking(
                    _(
                        "Found CHANGES to the DATE or the HEIRS,\n"
                        "a NEW WILL must be prepared."
                    )
                )

        if have_to_build:
            self.msg_set_building()
            try:
                txs = self.bal_window.build_will()
                if not txs:
                    self.msg_set_building(
                        _(
                            "Could not build the will ! Possible reasons:\n"
                            "1- the Balance of wallet is too low to cover the "
                            "fees for miners and will executors,\n"
                            "2- the Heirs' shares are below the minimum (Dust "
                            "UTXO, less than 546 Satoshi),\n"
                            "3- the Check Alive Date/Time is later than the "
                            "delivery time (it must be earlier),\n"
                            "Skipped"
                        ),
                        # Orange (warning) instead of red (error): an empty
                        # wallet after the inheritance was executed is a NORMAL
                        # situation, not a failure, so the colour should not
                        # alarm the user (owner request).
                        color=self.COLOR_WARNING,
                    )
                    return False, None

                self.bal_window.check_will()
                self._build_success_report()
            except WillExecutorNotPresent:
                self.msg_set_status(
                    _("Will-Executor excluded"), None, _("Skipped"), self.COLOR_ERROR
                )

            except NoWillExecutorNotPresent:
                _logger.debug("no will-executor selected, build interrupted")
                self.msg_set_status(
                    _("Will-Executor"), None,
                    _("Not present - select one or enable backup mode"),
                    self.COLOR_ERROR,
                )
                return "no_willexecutor", None

            except WillExpiredException as e:
                # An expired will is an EXPECTED situation (the locktime has
                # passed). After adding/changing an heir the will is rebuilt
                # above (build_will), and the freshly rebuilt transactions can
                # themselves already be expired.
                #
                # We must NOT trigger the wizard's automatic invalidation loop
                # here (return None, invalidate_tx). That loop re-runs
                # task_phase1 right after broadcasting the invalidation, but the
                # invalidation tx is not yet visible in the mempool, so the will
                # is still detected as expired and the user is asked to
                # invalidate again and again (infinite loop). It also never sets
                # the "BAL Invalidate transaction" history label.
                #
                # Instead we reproduce EXACTLY what the "Tools -> invalidate"
                # menu does (BalWalletWindow.invalidate_will): open Electrum's
                # classic transaction dialog so the user can sign and broadcast
                # the invalidation manually, set the proper history label, and
                # stop. This is robust regardless of mempool confirmation state.
                #
                # The actual call to invalidate_will() (which opens GUI windows)
                # must run in the GUI thread, so we only RETURN a signal here
                # ("invalidate_classic"); on_success_phase1 performs the call.
                # We still show the expired notice as a WARNING (orange).
                self.msg_set_building(self.msg_warning(e))
                return "invalidate_classic", None

            except NotCompleteWillException as e:
                # IMPORTANT (bugs E/F/K): build_will() above has just REBUILT the
                # whole will because the heirs (or the date) changed. The
                # post-build re-validation (check_will) then legitimately reports
                # that the new will differs from the previous one - e.g. it
                # raises HeirNotFoundException for a heir that is not yet covered
                # by an already-signed transaction. That is NOT an error: it is
                # exactly the signal that the freshly built transactions still
                # need to be SIGNED (and then broadcast).
                #
                # Previously this fell through to the generic "except Exception"
                # below, which (1) printed the heir name in RED and (2) returned
                # have_to_sign=False, so the rebuilt will was never signed
                # ("Nothing to do"). We now treat it as a successful rebuild:
                # show the green "Ok" + the heir list and FALL THROUGH to the
                # have_to_sign detection, so the new (status "New", not COMPLETE)
                # transactions are correctly detected and signed/broadcast.
                _logger.debug(f"will rebuilt, needs signing: {e}")
                self._build_success_report()

            except HeirAmountIsDustException:
                # ALL-DUST CASE (owner request): every heir's share is below the
                # Bitcoin dust limit, so the inheritance would pay nobody. We do
                # NOT build, sign or check anything - we show a clear message in
                # red and stop, so no "empty" will ends up in the list. This is
                # raised by Heirs.prepare_lists only when ALL real heirs are
                # dust; a mix of dust + valid heirs never reaches here.
                self.msg_set_building(
                    self.msg_error(
                        _(
                            "All heirs' shares are below the dust limit: "
                            "the inheritance cannot be created. "
                            "Increase the amounts or reduce the number of heirs."
                        )
                    )
                )
                return False, None

            except Exception as e:
                self.msg_set_building(self.msg_error(e))
                raise e
                return False, None

        # DUST report (one line PER HEIR, not per will-executor).
        #
        # WHY: the wallet balance can be so small that each heir's share falls
        # below Bitcoin's dust limit, so the inheritance is not feasible. The
        # "is DUST" condition depends ONLY on the heir's amount, NOT on which
        # will-executor transaction we are looking at. The previous code looped
        # over every valid will (one per will-executor) AND every heir, so with
        # e.g. 20 will-executors and 10 heirs it printed 20x10 = 200 identical
        # "is DUST ... Excluded from will <wid>" rows (owner report, allegato13).
        #
        # We now collect the dust heirs in a de-duplicated dict (heir id ->
        # dust amount) across all valid wills and print ONE row per heir,
        # without the will-executor reference. So N heirs => at most N rows,
        # regardless of how many will-executors exist.
        dust_heirs = {}
        for wid in Will.only_valid(self.bal_window.willitems):
            heirs = self.bal_window.willitems[wid].heirs
            for hid, heir in heirs.items():
                if "DUST" in str(heir[HEIR_REAL_AMOUNT]):
                    # Keep the first dust amount seen for this heir; it is the
                    # same share in every will-executor copy of the will.
                    dust_heirs.setdefault(hid, heir[HEIR_DUST_AMOUNT])
        for hid, dust_amount in dust_heirs.items():
            self.msg_set_status(
                f"{_('Heir')} {hid}",
                None,
                f"{dust_amount} is DUST - excluded (amount below dust limit)",
                self.COLOR_WARNING,
            )

        have_to_sign = False
        for wid in Will.only_valid(self.bal_window.willitems):
            if not self.bal_window.willitems[wid].get_status("COMPLETE"):
                have_to_sign = True
                break
        return have_to_sign, txs

    def _build_success_report(self):
        """Mark the build step as done and list every heir in green.

        Called after a successful (re)build of the will. It:
          1. labels each inheritance transaction in Electrum's history;
          2. shows the green "Ok" result on the "Building your will" row;
          3. lists EVERY heir of the freshly built will, one per line, in green
             (the "Ok" colour), so the user can confirm at a glance who will
             inherit.

        Previously a heir name could appear in RED (it was the text of a rebuild
        exception) and the heir list was only shown on the "all clean" path,
        which is why after changing/deleting an heir the user saw a single red
        heir name and no green list. This helper is now used both on the clean
        path and on the "will was rebuilt and needs signing" path, so the green
        heir list is always shown.

        Internal will-executor pseudo-heirs (reserved ``w!ll3x3c"`` prefix) are
        skipped, exactly as the core coherence check does. A set keeps the list
        unique even when an heir appears in several transactions.
        """
        for wid in Will.only_valid(self.bal_window.willitems):
            # Label shown in Electrum's History tab for inheritance txs.
            self.bal_window.wallet.set_label(wid, "BAL Inheritance transaction")
        # Keep the plugin's stored delivery date in sync with the (possibly
        # auto-anticipated) transactions, otherwise the next Check would wrongly
        # ask to invalidate. See _sync_locktime_to_built_txs for the full why.
        self._sync_locktime_to_built_txs()
        self.msg_set_building(self.msg_ok())
        # List EACH heir on its OWN line, green + bold (owner request: revert the
        # one-line "Heirs: a, b, c" form of v0.4.6). Heir names can be long, and
        # now that the report area scrolls (allegato1) there is no need to cram
        # them onto a single line. We de-duplicate (an heir can appear in several
        # will-executor transactions) and skip the internal will-executor
        # pseudo-heirs (reserved ``w!ll3x3c"`` prefix), exactly as before.
        shown_heirs = set()
        for wid in Will.only_valid(self.bal_window.willitems):
            for hname in self.bal_window.willitems[wid].heirs:
                if str(hname)[:9] == 'w!ll3x3c"':
                    continue
                if hname in shown_heirs:
                    continue
                shown_heirs.add(hname)
                self.msg_set_status(_("Heir"), None, str(hname), self.COLOR_OK)

    def _sync_locktime_to_built_txs(self):
        """Align the plugin's stored delivery date with the built transactions.

        WHY this is needed (bug reported by the owner):
        When the will is rebuilt while it still spends the same coins as a
        previous one (e.g. after deleting an heir WITHOUT changing the date),
        the core engine AUTOMATICALLY anticipates the transaction locktime by
        one day (see Will.check_anticipate / Util.anticipate_locktime). This is
        correct and required so the new transaction can be mined BEFORE the old
        one it replaces.

        However the plugin's own stored delivery date
        (WILL_SETTINGS["locktime"]) was NOT updated and stayed at the original
        date. On the next Check the plugin compared the stored date (original)
        with the transaction locktime (original minus one day) and, since
        stored > tx, mistook the automatic anticipation for a user POSTPONE,
        wrongly asking to invalidate the will.

        Fix: after a (re)build, set the stored delivery date to the MINIMUM
        locktime among the valid built transactions. We only ever move the date
        EARLIER (anticipation): if the minimum is not strictly below the current
        stored date we leave it untouched, so a genuine user-chosen postpone is
        never silently overwritten. The owner confirmed that, when several
        transactions carry different locktimes, taking the minimum is the
        desired behaviour, and that the date shown in the panel/wizard must
        reflect this anticipated date (so the calendar .ics also uses it).

        We route the update through BalWindow.update_setting_widgets, which is
        the single place that (1) stores the value in WILL_SETTINGS, (2)
        persists it to Electrum's database and (3) refreshes the date widgets in
        every panel/wizard, so the visible date and the .ics export stay
        consistent.
        """
        # Minimum locktime across the valid (just built) inheritance txs.
        # Will.get_min_locktime returns None when there is no valid tx.
        min_locktime = Will.get_min_locktime(self.bal_window.willitems, None)
        if min_locktime is None:
            return
        min_locktime = int(min_locktime)
        # Current stored delivery date, as a comparable UNIX timestamp.
        try:
            current = int(
                Util.parse_locktime_string(
                    self.bal_window.will_settings["locktime"]
                )
            )
        except Exception:
            # If the stored value cannot be parsed, fall back to syncing.
            current = None
        # Only anticipate (move the date EARLIER); never overwrite a postpone.
        if current is not None and min_locktime >= current:
            return
        _logger.debug(
            f"sync delivery date to anticipated tx locktime: "
            f"{current} -> {min_locktime}"
        )
        # Remember that we anticipated the date, so the later sign prompt can
        # explain WHY signing is needed (see on_success_phase1).
        self._date_was_anticipated = True
        # update_setting_widgets stores the value, persists it and refreshes the
        # date widgets in all panels/wizard (so the .ics calendar uses it too).
        self.bal_window.update_setting_widgets(
            min_locktime, "locktime", update_all=True
        )

    def on_accept(self):
        try:
            self.bal_window.update_all()
        except Exception as e:
            import traceback
            _logger.error(f"NoneType_catch on_accept: {e}\n{traceback.format_exc()}")
        pass

    def on_accept_phase2(self):
        try:
            self.bal_window.update_all()
        except Exception as e:
            import traceback
            _logger.error(f"NoneType_catch on_accept_phase2: {e}\n{traceback.format_exc()}")
        pass

    def on_error_push(self):
        pass

    def wait(self, secs):
        wait_row = None
        for i in range(secs, 0, -1):
            if self._stopping:
                return
            wait_row = self.msg_edit_row(_(f"Please wait {i}secs"), wait_row)
            time.sleep(1)
        self.msg_del_row(wait_row)

    def loop_broadcast_invalidating(self, tx):
        if self._stopping:
            return
        self.msg_set_invalidating("Broadcasting")
        try:
            tx.add_info_from_wallet(self.bal_window.wallet)
            self.network.run_from_another_thread(tx.add_info_from_network(self.network))

            # IMPORTANT (task #21 fix): get the txid from the transaction
            # object, NOT from broadcast_transaction()'s return value.
            # Network.broadcast_transaction is declared "-> None" and ALWAYS
            # returns None (it only raises on failure). The previous code stored
            # that None into `txid` and put set_label() in the `else: # txid`
            # branch, which was therefore NEVER reached - that is why the
            # "BAL Invalidate transaction" history label kept missing on this
            # automatic ("postpone") path. The transaction is already signed and
            # complete here, so tx.txid() is the correct, stable id - exactly
            # what the working Tools -> Invalidate path uses
            # (BalWalletWindow.invalidate_will -> result.txid()).
            txid = tx.txid()

            # Set the history label BEFORE broadcasting. set_label only writes to
            # the local wallet metadata (no network needed), so doing it first
            # guarantees the label exists the moment the transaction shows up in
            # the History tab, regardless of how fast the broadcast/notification
            # arrives.
            if txid:
                self.bal_window.wallet.set_label(txid, "BAL Invalidate transaction")
            else:
                _logger.debug(f"invalidate tx has no txid: {tx}")

            self.network.run_from_another_thread(
                self.network.broadcast_transaction(tx, timeout=120), timeout=120
            )
            self.msg_set_invalidating(self.msg_ok())

        except TxBroadcastError as e:
            _logger.error(f"fail to broadcast transaction:{e}")
            msg = e.get_message_for_gui()
            self.msg_set_invalidating(self.msg_error(msg))
        except BestEffortRequestFailed as e:
            self.msg_set_invalidating(self.msg_error(e))

    def loop_push(self):
        # Broadcast is "one-shot" (Group B / B2 follow-up): each selected
        # will-executor is contacted ONCE. Transactions that are broadcast
        # successfully become PUSHED; transactions whose server fails or times
        # out are left as PUSH_FAIL and simply skipped - they are NOT retried
        # automatically. A dead will-executor could otherwise never answer and
        # make the plugin retry forever. The user can broadcast a failed
        # transaction manually later with the "Broadcast" button. Note that
        # get_willexecutor_transactions already excludes PUSHED transactions, so
        # the successful ones are never re-sent on a subsequent run.
        if self._stopping:
            return
        self.msg_set_pushing(_("Broadcasting"))
        try:

            willexecutors = Willexecutors.get_willexecutor_transactions(
                self.bal_window.willitems
            )

            # Only push to the will-executors the user actually selected.  We
            # filter the mapping up-front so push_transactions_parallel only
            # talks to the relevant servers.
            selected = {
                url: we
                for url, we in willexecutors.items()
                if Willexecutors.is_selected(
                    self.bal_window.willexecutors.get(url),
                ) and Willexecutors.is_valid(
                    self.bal_window.willexecutors.get(url),
                    max_fee=self.bal_window.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
                    dust=self.bal_window.window.wallet.dust_threshold(),
                )
            }

            # Servers that report "already present" need their stored tx
            # verified afterwards (network I/O); collect them here and process
            # them sequentially after the parallel push, keeping the original
            # check logic untouched.
            already_present = []
            total = len(selected)
            done = {"count": 0}

            deadline = Willexecutors.PUSH_GLOBAL_DEADLINE

            def _status_line():
                # e.g. "Broadcasting your will to executors: 2/3 (5s / 30s)".
                # The "/ 30s" makes the maximum wait explicit, so the user knows
                # the wizard will proceed by then (the global deadline) instead
                # of wondering how long the counter will keep climbing.
                return "{} {}/{} ({}s / {}s)".format(
                    _("Broadcasting"), done["count"], total,
                    min(int(time.time() - push_start), deadline), deadline,
                )

            def on_each(url, willexecutor, ok, exc):
                # Runs from a worker thread.  Do only thread-safe book-keeping
                # plus a signal-based UI update (msg_edit_row emits a pyqtSignal,
                # which is marshalled to the GUI thread).
                if isinstance(exc, Willexecutors.AlreadyPresentException):
                    already_present.append(url)
                elif ok:
                    for wid in willexecutor["txsids"]:
                        self.bal_window.willitems[wid].set_status("PUSHED", True)
                else:
                    # One-shot: mark the failed transactions and move on. They
                    # are left as PUSH_FAIL (no automatic retry).
                    for wid in willexecutor["txsids"]:
                        self.bal_window.willitems[wid].set_status("PUSH_FAIL", True)
                done["count"] += 1
                # Show the per-server result (Ok/Ko) in bold + color so the
                # outcome stands out, keeping the server URL in normal weight.
                result = self.msg_ok("Ok") if ok else self.msg_error("Ko")
                self.msg_edit_row("{} : {}".format(url, result))
                self.msg_set_pushing(_status_line())

            def on_timeout(url, willexecutor):
                # The global deadline elapsed before this server answered. Mark
                # its txs as failed and move on (one-shot: no automatic retry).
                # The user can broadcast them manually later if desired.
                for wid in willexecutor.get("txsids", []):
                    self.bal_window.willitems[wid].set_status("PUSH_FAIL", True)
                self.msg_edit_row(
                    "{} : {}".format(url, self.msg_error(_("Timeout - no answer")))
                )

            if self._stopping:
                return
            # Push to all selected will-executors in parallel: a slow/dead
            # server no longer blocks the others, so the wizard's "Broadcasting"
            # step is no longer sequential.  Each server keeps a short retry
            # behaviour, and a global deadline guarantees the wizard always
            # proceeds even if a server never answers.
            push_start = time.time()
            self.msg_set_pushing(_status_line())

            # Refresh the elapsed-seconds counter while the (blocking) parallel
            # push runs, so the user sees time advancing instead of a frozen
            # "Trasmissione".  The tick is driven from THIS (Task) thread by
            # push_transactions_parallel, the same thread that drives on_each, so
            # the pyqtSignal repaint is reliable (a separate heartbeat thread's
            # signal emissions were not being marshalled and never repainted).
            def on_tick():
                if self._stopping:
                    return
                self.msg_set_pushing(_status_line())

            Willexecutors.push_transactions_parallel(
                selected, on_each=on_each, on_timeout=on_timeout, on_tick=on_tick
            )

            # Final summary line with the total elapsed time.
            self.msg_set_pushing(
                "{}/{} ({}s)".format(done["count"], total,
                                     int(time.time() - push_start))
            )

            # Verify the "already present" servers (sequential, original logic).
            self.bal_plugin = self.bal_window.bal_plugin
            for url in already_present:
                for wid in willexecutors[url]["txsids"]:
                    if self._stopping:
                        return
                    row = self.msg_edit_row(
                        "checking {} - {} : <b>{}</b>".format(
                            self.bal_window.willitems[wid].we["url"], wid, "Waiting"
                        )
                    )
                    w = self.bal_window.willitems[wid]
                    w.set_check_willexecutor(
                        Willexecutors.check_transaction(wid, w.we["url"])
                    )
                    # Show the CHECKED result in bold + color (green True /
                    # red False) so the outcome stands out, keeping the server
                    # URL and tx id in normal weight.
                    checked = self.bal_window.willitems[wid].get_status("CHECKED")
                    result = self.msg_ok(checked) if checked else self.msg_error(checked)
                    row = self.msg_edit_row(
                        "checked {} - {} : {}".format(
                            self.bal_window.willitems[wid].we["url"],
                            wid,
                            result,
                        ),
                        row,
                    )

            # One-shot broadcast: we deliberately do NOT raise/retry when some
            # will-executors failed. Their transactions stay PUSH_FAIL and are
            # left for the user to broadcast manually. This prevents an endless
            # retry loop against a will-executor that may never answer.

        except Exception as e:
            # Only genuine, unexpected errors reach here now (not the old
            # "retry" signal). Report the error; do not loop.
            self.msg_set_pushing(self.msg_error(e))
            self.wait(10)
            if not self._stopping:
                pass

    def invalidate_task(self, password, bal_window, tx):
        if self._stopping:
            return
        print("invalidate task")
        _logger.debug(f"invalidate tx: {tx}")
        # fee_per_byte = bal_window.will_settings.get("baltx_fees", 1)
        tx = self.bal_window.wallet.sign_transaction(tx, password)
        try:
            if tx:
                if tx.is_complete():
                    self.loop_broadcast_invalidating(tx)
                    # Wait 10 seconds (was 5) AFTER broadcasting the
                    # invalidation so Electrum's wallet/network has time to see
                    # the new transaction before we re-run phase 1. Without this
                    # pause the immediate re-check still detected the old
                    # (not-yet-invalidated) will and re-prompted to invalidate,
                    # producing the reported loop. This runs in the worker
                    # thread, so the GUI is not frozen by the sleep.
                    self.wait(10)
                    # Remember that we just broadcast an invalidation. If the
                    # next phase-1 re-check STILL reports a postpone (because
                    # Electrum has not registered the tx yet), we stop with a
                    # clear message instead of looping (see on_success_phase1).
                    self._invalidation_broadcast = True
                else:
                    raise Exception("tx not complete")
            else:
                raise Exception("not tx")
        except Exception as e:
            (f"exception:{e}")
            self.msg_set_invalidating(f"Error: {e}")
            raise Exception("Impossible to sign") from e

    def on_success_invalidate(self, success):
        self.thread.add(
            self.task_phase1,
            on_success=self.on_success_phase1,
            on_done=self.on_accept,
            on_error=self.on_error_phase1,
        )

    def on_success_phase1(self, result):
        try:
            self._on_success_phase1_body(result)
        except Exception as e:
            import traceback
            _logger.error(f"NoneType_catch on_success_phase1: {e}\n{traceback.format_exc()}")

    def _on_success_phase1_body(self, result):
        if self._stopping:
            return
        self.have_to_sign, tx = list(result)
        # if not tx:
        #    self.msg_edit_row(self.msg_error("Error, no tx was built"))
        #    return

        # Special signal raised by task_phase1 when the freshly rebuilt will is
        # already expired (e.g. an heir was added to an expired will). Instead of
        # running the wizard's automatic invalidation loop (which would re-check
        # before the invalidation tx reaches the mempool and loop forever), we
        # behave exactly like the "Tools -> invalidate" menu: open Electrum's
        # classic transaction dialog so the user signs and broadcasts the
        # invalidation manually, with the "BAL Invalidate transaction" label.
        # This runs in the GUI thread (on_success callback), so opening windows
        # is safe. We then stop and close the wizard.
        if self.have_to_sign == "invalidate_classic":
            self.thread.stop()
            # UNIFY INVALIDATE PROCEDURE (+ task #03):
            #
            # When an heir is added to an already-expired will, the rebuilt will
            # is itself expired and the old will must be invalidated on-chain
            # before the new one can be used.
            #
            # We make the CHECK button and the WIZARD behave IDENTICALLY:
            #   1. show a WARNING popup (no "Tools -> Invalidate" wording);
            #   2. AUTOMATICALLY open Electrum's classic transaction dialog via
            #      BalWalletWindow.invalidate_will() (the same code used by the
            #      Tools -> Invalidate menu). That path already sets the
            #      "BAL Invalidate transaction" history label - which fixes
            #      task #03 (the label was previously missing on the automatic
            #      path).
            #
            # Window-stacking note (history): opening the transaction window
            # straight from within the closing wizard used to leave it BEHIND
            # the main window on some window managers. The robust fix is to
            # close the wizard FIRST and defer the call with QTimer.singleShot
            # so it runs on the next event-loop iteration, when the wizard is
            # already gone and the transaction window becomes the front-most,
            # focused window.
            self.close()
            self.bal_window.show_message(
                _(
                    "Your will has expired and must be invalidated before it "
                    "can be rebuilt.\n"
                    "A transaction window will now open:\n"
                    "please SIGN and then BROADCAST it to invalidate your old "
                    "will.\n"
                    "After the invalidation is confirmed, press the Check "
                    "button to finish the will."
                )
            )
            # Deferred so the wizard is fully closed before the classic
            # invalidate window opens (keeps it in front, fixes the old
            # "window behind" problem).
            QTimer.singleShot(0, self.bal_window.invalidate_will)
            return

        if self.have_to_sign == "no_willexecutor":
            self._add_no_willexecutor_buttons()
            return

        _logger.debug("have to sign {}".format(self.have_to_sign))
        password = None
        if self.have_to_sign is None:
            _logger.debug("have to invalidate")

            # LOOP GUARD (task #21): if we already broadcast an invalidation on
            # the previous pass and phase 1 STILL reports a postpone, Electrum
            # has simply not seen the invalidation transaction yet. Re-prompting
            # to invalidate here is exactly what produced the reported endless
            # loop ("Invalidate your old will" reappearing right after signing).
            # Instead of re-prompting, STOP cleanly with a clear message and
            # tell the user to retry the Check once the invalidation confirms.
            if self._invalidation_broadcast:
                self.thread.stop()
                self.msg_set_invalidating(self.msg_ok())
                self.bal_window.show_message(
                    _(
                        "Your old will has been invalidated and the "
                        "transaction was broadcast.\n"
                        "Electrum may need a little time to register it.\n"
                        "Please wait until the invalidation transaction is "
                        "confirmed, then press the Check button again to "
                        "finish updating your will."
                    )
                )
                self._add_close_button()
                return

            self.msg_set_invalidating()
            # need to sign invalidate and restart phase 1

            password = self.bal_window.get_wallet_password(
                _("Invalidate your old will"), parent=self
            )
            if password is False:
                # The user cancelled the password prompt for the invalidation.
                # We must NOT call self.wait(3) here: on_success_phase1 runs in
                # the GUI thread, so wait()'s time.sleep() would freeze the UI
                # for several seconds. While frozen the dialog cannot repaint
                # the area it just resized, leaving a black, undrawn rectangle
                # at the bottom of the "Building Will" window (the reported bug).
                #
                # Instead, mark the invalidation as "Aborted" and offer a
                # non-blocking "Close" button (the same helper used by the
                # normal finish path), so the user can read the outcome and
                # dismiss the dialog when they want, without blocking the GUI.
                self.msg_set_invalidating(_("Aborted"))
                self._add_close_button()
                return
            self.thread.add(
                partial(self.invalidate_task, password, self.bal_window, tx),
                on_success=self.on_success_invalidate,
                on_done=self.on_accept,
                on_error=self.on_error,
            )

            return

        elif self.have_to_sign:
            auto_sign = (
                self.bal_window.bal_plugin.is_basic_mode()
                or self.bal_window.bal_plugin.AUTO_SIGN.get()
            )
            if not auto_sign:
                self.msg_set_signing(
                    _("Auto-sign disabled — sign manually")
                )
                self.have_to_sign = False
            else:
                # If the will was rebuilt with an automatically anticipated
                # delivery date, explain WHY we are now asking to sign:
                # otherwise the sign prompt appears with no reason (owner
                # feedback). The note is shown on the "Building your will"
                # row (orange warning) before the modal password prompt
                # opens, so the user can read it.
                if self._date_was_anticipated:
                    self.msg_set_building(
                        "<b>{}</b>".format(
                            _(
                                "The delivery date was automatically moved "
                                "one day earlier so the updated will can "
                                "correctly replace the previous one.\n"
                                "Please sign (and broadcast) to confirm "
                                "the change."
                            )
                        )
                    )
                password = self.bal_window.get_wallet_password(
                    _("Sign your will"), parent=self
                )
                if password is False:
                    self.msg_set_signing(_("Aborted"))
        else:
            self.msg_set_signing(_("Nothing to do"))
        self.thread.add(
            partial(self.task_phase2, password),
            on_success=self.on_success_phase2,
            on_done=self.on_accept_phase2,
            on_error=self.on_error_phase2,
        )
        return

    def on_success_phase2(self, arg=False):
        self.thread.stop()
        self.bal_window.save_willitems()
        # After the whole check/sign/broadcast cycle, keep the wallet's local
        # history in sync with the current will state (save the still "New"
        # incomplete txs, remove the now-complete ones).
        try:
            self.bal_window._save_will_to_history()
        except Exception as e:
            _logger.error(f"save_will_to_history after phase2 failed: {e}")
        self.msg_edit_row(_("Finished"))
        # Instead of auto-closing after a countdown, let the user decide when to
        # dismiss the dialog: they can read the full "Building Will" report at
        # their own pace and then press "Close".  This runs in the GUI thread
        # (on_success callback) so building the button here is safe.
        self._add_close_button()

    def _add_close_button(self):
        """Add a right-aligned "Close" button and a "Calendar" dropdown button.

        Replaces the old automatic countdown (self.wait(5) + self.close()).
        Guarded so it is only built once even if called again.
        """
        if getattr(self, "_close_button", None) is not None:
            return

        calendar_button = BalCalendarButton(self.bal_window, self._ics_provider)
        calendar_button.setText(_("Calendar"))

        self._close_button = QPushButton(_("Close"))
        self._close_button.clicked.connect(self._on_close_clicked)
        button_row = QHBoxLayout()
        button_row.addStretch(1)
        button_row.addWidget(calendar_button)
        button_row.addWidget(self._close_button)
        self.vbox.addLayout(button_row)
        self._close_button.setFocus()

    # ------------------------------------------------------------------ #
    # No-willexecutor error handling
    # ------------------------------------------------------------------ #

    def _add_no_willexecutor_buttons(self):
        """Add "Will-Executor" and "Close" buttons when no executor is
        selected and ``no_willexecutor`` is ``False``."""
        if getattr(self, "_no_we_buttons_added", False):
            return
        self._no_we_buttons_added = True
        btn_row = QHBoxLayout()
        btn_row.addStretch(1)

        we_btn = QPushButton(_("Will-Executor"))
        we_btn.clicked.connect(self._open_willexecutor_dialog)
        btn_row.addWidget(we_btn)

        download_btn = QPushButton(_("\U0001f52e Wizard"))
        download_btn.clicked.connect(self._open_willexecutor_download_widget)
        btn_row.addWidget(download_btn)

        close_btn = QPushButton(_("Close"))
        close_btn.clicked.connect(self.close)
        btn_row.addWidget(close_btn)

        self._no_we_layout = btn_row
        self.vbox.addLayout(btn_row)
        self.resize(self.vbox.sizeHint())

    def _open_willexecutor_dialog(self):
        """Open the will-executor management dialog, then auto-retry
        the build when it closes."""
        d = WillExecutorDialog(self.bal_window, parent=self)
        d.exec()
        self._retry_build_after_willexecutor()

    def _open_willexecutor_download_widget(self):
        """Close the build-will dialog and re-open the wizard at the
        will-executor download step so the user can add one."""
        self.close()
        wizard = BalWizardDialog(self.bal_window)
        wizard.on_next_heir()
        wizard.on_next_locktimeandfee()
        wizard.exec()

    def _retry_build_after_willexecutor(self):
        """Remove the no-willexecutor buttons, reset the message panel,
        and re-run ``task_phase1`` on the same thread."""
        self._no_we_buttons_added = False
        if self._no_we_layout:
            while self._no_we_layout.count():
                item = self._no_we_layout.takeAt(0)
                w = item.widget()
                if w:
                    w.setParent(None)
                    w.deleteLater()
            self.vbox.removeItem(self._no_we_layout)
            self._no_we_layout = None
        self.labels = []
        self.msg_update()
        self.thread.add(
            self.task_phase1,
            on_success=self.on_success_phase1,
            on_done=self.on_accept,
            on_error=self.on_error_phase1,
        )

    def _ics_provider(self):
        """Return the .ics content for the current will data."""
        from datetime import datetime, timedelta

        try:
            locktime_ts = Util.parse_locktime_string(
                self.bal_window.will_settings["locktime"]
            )
            locktime = datetime.fromtimestamp(locktime_ts)

            if self.bal_window.bal_plugin.is_basic_mode():
                days_to_deadline = (locktime - datetime.now()).days
                offsets = basic_reminder_offsets(days_to_deadline)
            else:
                threshold_ts = BalTimestamp(
                    self.bal_window.will_settings["threshold"]
                ).to_timestamp()
                threshold = datetime.fromtimestamp(threshold_ts)
                days = (locktime - threshold).days
                try:
                    count = int(self.bal_window.bal_plugin.NUM_REMINDERS.get())
                except Exception:
                    count = 3
                offsets = compute_reminder_offsets(days, count)

            now = BalCalendar.format_time(datetime.now())

            heirs_details = "\r\n".join(
                f" {heir} - {self.bal_window.heirs[heir][0]}, "
                f"{self.bal_window.heirs[heir][1]}"
                for heir in self.bal_window.heirs
            )

            if self.bal_window.bal_plugin.is_basic_mode():
                raw_description = self.bal_window.bal_plugin.EVENT_DESCRIPTION.default
                raw_summary = self.bal_window.bal_plugin.EVENT_SUMMARY.default
            else:
                raw_description = self.bal_window.bal_plugin.EVENT_DESCRIPTION.get()
                raw_summary = self.bal_window.bal_plugin.EVENT_SUMMARY.get()

            event_description = BalCalendar.ical_escape(
                f"{raw_description}"
                .replace("$wallet_name", str(self.bal_window.wallet))
                .replace("$heirs_complete", heirs_details)
            )
            summary_base = (
                f"{raw_summary}"
                .replace("$wallet_name", str(self.bal_window.wallet))
            )

            lines = [
                "BEGIN:VCALENDAR",
                "VERSION:2.0",
                "PRODID:-//Bitcoin After Life//Electrum Plugin/"
                f"{self.bal_window.bal_plugin.version}",
            ]

            total = len(offsets)
            # ToDo #2: if no reminder falls in the future (the delivery date is
            # too close or already passed), there are no events to write.
            # Return None so the caller shows a clear warning instead of
            # producing an empty, seemingly-broken .ics file.
            if total == 0:
                return None
            for idx, offset in enumerate(offsets, start=1):
                event_dt = BalCalendar.format_time(locktime - timedelta(days=offset))
                summary = BalCalendar.ical_escape(
                    f"{summary_base} (reminder {idx}/{total})"
                )
                lines.extend([
                    "BEGIN:VEVENT",
                    f"UID:bal-{str(self.bal_window.wallet)}-{offset}d",
                    f"DTSTAMP:{now}",
                    f"DTSTART:{event_dt}",
                    f"DTEND:{event_dt}",
                    f"SUMMARY:{summary}",
                    f"DESCRIPTION:{event_description}",
                    "END:VEVENT",
                ])

            lines.append("END:VCALENDAR")
            lines = [s.rstrip("\r\n") for s in lines]
            return "\r\n".join(lines) + "\r\n"
        except Exception as e:
            _logger.error(f"failed to generate .ics: {e}")
            return None

    def _on_close_clicked(self):
        # Close the dialog first, then show the persistent popup guiding the
        # user through any remaining MANUAL steps (Sign / Broadcast).  Showing
        # the (modal) hint after close() mirrors the previous behaviour where
        # the hint appeared once the auto-closing dialog was gone.
        self.close()
        if self._next_steps_hint:
            self.bal_window.show_message(self._next_steps_hint)

    def closeEvent(self, event):
        self._stopping = True
        # Stop AND join the thread, then propagate the close event (previously
        # it neither waited nor called super().closeEvent()).
        stop_thread(getattr(self, "thread", None))
        super().closeEvent(event)

    def task_phase2(self, password):
        if self._stopping:
            return
        if self.have_to_sign:
            try:
                if txs := self.bal_window.sign_transactions(password):
                    for txid, tx in txs.items():
                        self.bal_window.willitems[txid].tx = copy.deepcopy(tx)
                    self.bal_window.save_willitems()
                    self.msg_set_signing(self.msg_ok())
            except Exception as e:
                self.msg_set_signing(self.msg_error(e))

        self.msg_set_pushing()
        have_to_push = False
        for wid in Will.only_valid(self.bal_window.willitems):
            w = self.bal_window.willitems[wid]
            if w.we and w.get_status("COMPLETE") and not w.get_status("PUSHED"):
                have_to_push = True
        if not have_to_push:
            self.msg_set_pushing(_("Nothing to do"))
        else:
            try:
                self.loop_push()
                self.msg_set_pushing(self.msg_ok())

            except Exception as e:
                # td = traceback.format_exc()
                self.msg_set_pushing(self.msg_error(e))
        # Blank separator row: visually detach the final "All done" summary
        # from the per-step result rows above it, so the closing line stands
        # out as the overall outcome rather than just another step.
        self.msg_edit_row("")
        # Final summary row: the whole "Building Will" sequence above (check /
        # sign / broadcast) finished without errors.  Give it an explicit
        # left-side label ("All done") so this closing Ok is not an orphan
        # result like the other rows have.
        self.msg_edit_row("{}:\t{}".format(_("All done"), self.msg_ok()))

        # Guide the user through any remaining MANUAL steps.  After the will is
        # (re)built -- e.g. because an heir was removed/added from the Wizard --
        # the new transactions may still need to be SIGNED and/or BROADCAST by
        # the user.  This dialog only signs/pushes automatically when it already
        # has the password and the will is in the right state; in every other
        # case the user is otherwise left without any indication of what to do
        # next.  We inspect the real status of the valid wills and tell the user
        # exactly which buttons to press.
        self._show_next_steps_hint()

    def _show_next_steps_hint(self):
        """Append a clear "what to do next" line to the Building Will dialog.

        Pure UX guidance (no logic change): looks at the valid wills and, if any
        still needs signing or broadcasting, tells the user to press 'Sign'
        and/or 'Broadcast' manually.  The computed hint is also stored in
        ``self._next_steps_hint`` so a persistent popup can be shown after the
        dialog closes (this dialog auto-closes after a few seconds, which is too
        short to be sure the user noticed the in-dialog line).
        """
        self._next_steps_hint = None
        # Group B / B2: when AUTO_SIGN is ON the dialog has already signed and
        # broadcast the will automatically, so the manual "press Sign/Broadcast"
        # hints (and the follow-up popup) would be wrong/confusing. Suppress
        # them in that case. When AUTO_SIGN is OFF, keep the previous behaviour
        # and guide the user through the remaining manual steps.
        try:
            if (
                self.bal_window.bal_plugin.is_basic_mode()
                or self.bal_window.bal_plugin.AUTO_SIGN.get()
            ):
                return
        except Exception:
            # If the setting cannot be read for any reason, fall back to the
            # original behaviour (show the manual hints).
            pass
        try:
            need_sign = False
            need_push = False
            for wid in Will.only_valid(self.bal_window.willitems):
                w = self.bal_window.willitems[wid]
                if not w.get_status("COMPLETE"):
                    # Not signed yet.
                    need_sign = True
                elif w.we and not w.get_status("PUSHED"):
                    # Signed but not yet sent to its will-executor.
                    need_push = True

            if need_sign and need_push:
                hint = _(
                    "Next steps (manual): press 'Sign' to sign your will, "
                    "then 'Broadcast' to send it to the will-executors."
                )
            elif need_sign:
                hint = _(
                    "Next step (manual): press 'Sign' to sign your will."
                )
            elif need_push:
                hint = _(
                    "Next step (manual): press 'Broadcast' to send your will "
                    "to the will-executors."
                )
            else:
                # Nothing left to do (already signed and, if needed, sent).
                return

            self._next_steps_hint = hint
            self.msg_edit_row("<b>{}</b>".format(hint))
        except Exception as hint_err:
            _logger.debug(f"next-steps hint error: {hint_err}")

    def on_error(self, error):
        _logger.error(error)
        pass

    def on_error_phase1(self, error):
        self.bal_window.update_all()
        a, b, c = error
        self.msg_edit_row(self.msg_error(f"Error: {b}"))
        import traceback
        _logger.error(f"error phase1: {b}\n{''.join(traceback.format_exception(a, b, c))}")
        button=QPushButton(_("Close"))
        button.clicked.connect(self.close)
        self.vbox.addWidget(button)
        self.resize(self.vbox.sizeHint()+button.sizeHint()*2)
        self.repaint()
    def on_error_phase2(self, error):
        self.bal_window.upade_all()
        a, b, c = error
        self.msg_edit_row(self.msg_error(f"Error: {b}"))
        _logger.error(f"error phase2: {b}")

    def _executed_inheritance_status(self):
        """Return the on-chain status of an already-executed inheritance.

        Task #7a (owner request). After an inheritance is executed the wallet is
        fully emptied (the plugin always empties the wallet). Pressing CHECK on
        such an empty wallet makes ``check_will`` raise NotCompleteWillException
        (the heirs no longer match the empty wallet), which previously showed the
        alarming "Found CHANGES ... a NEW WILL must be prepared" message even
        though the inheritance was, in fact, correctly executed.

        To reassure the user we look at the will items, whose status is already
        set to CONFIRMED / MEMPOOL by ``Will.check_will`` (see core/will.py), and
        return a short code describing the real situation:

        Returns:
            "CONFIRMED" if at least one will transaction is confirmed on-chain
                (the inheritance is already executed);
            "MEMPOOL" if none is confirmed but at least one is in the mempool
                (waiting for confirmation);
            None if neither (the change really has to be rebuilt).

        CONFIRMED takes precedence over MEMPOOL, since a confirmed transaction is
        the strongest evidence that the inheritance has gone through.
        """
        has_mempool = False
        try:
            for _wid, witem in self.bal_window.willitems.items():
                if witem.get_status("CONFIRMED"):
                    return "CONFIRMED"
                if witem.get_status("MEMPOOL"):
                    has_mempool = True
        except Exception as _err:
            # Never let this purely informational check break the flow.
            _logger.debug(f"_executed_inheritance_status error: {_err}")
        return "MEMPOOL" if has_mempool else None

    def msg_set_checking(self, status="Waiting", row=None):
        row = self.check_row if row is None else row
        self.check_row = self.msg_set_status(_("Checking your will"), row, status)

    def msg_set_invalidating(self, status=None, row=None):
        row = self.inval_row if row is None else row
        self.inval_row = self.msg_set_status(
            _("Invalidating old will"), self.inval_row, status
        )

    def msg_set_building(self, status=None, row=None,color=None):
        row = self.build_row if row is None else row
        self.build_row = self.msg_set_status(
            "Building your will", self.build_row, status, color
        )

    def msg_set_signing(self, status=None, row=None):
        row = self.sign_row if row is None else row
        self.sign_row = self.msg_set_status("Signing your will", self.sign_row, status)

    def msg_set_pushing(self, status=None, row=None):
        row = self.push_row if row is None else row
        self.push_row = self.msg_set_status(
            "Broadcasting your will to executors", self.push_row, status
        )

    def msg_set_waiting(self, status=None, row=None):
        row = self.wait_row if row is None else row
        self.wait_row = self.msg_edit_row(f"Please wait {status}secs", self.wait_row)

    def msg_error(self, e):
        # Results are shown in bold so the outcome stands out from the
        # left-side state label (which stays in normal weight).
        return "<font color='{}'><b>{}</b></font>".format(self.COLOR_ERROR, e)

    def msg_ok(self, e="Ok"):
        # Results are shown in bold (see msg_error).
        return "<font color='{}'><b>{}</b></font>".format(self.COLOR_OK, e)

    def msg_warning(self, e):
        # Results are shown in bold (see msg_error).
        return "<font color='{}'><b>{}</b></font>".format(self.COLOR_WARNING, e)

    def msg_set_status(self, msg, row=None, status=None, color=None):
        # The left "state" label keeps its normal weight; only the right-side
        # result (``status``) is rendered in bold so it is easy to read at a
        # glance.  ``status`` may already contain rich-text emitted by
        # msg_ok/msg_error/msg_warning (which add their own <b>...</b>); wrapping
        # it again in <b> is harmless for those cases.
        status = "Wait" if status is None else status
        if color is None:
            line = "{}:\t<b>{}</b>".format(_(msg), status)
        else:
            line = "{}:\t<font color={}><b>{}</b></font>".format(
                _(msg), color, status
            )
        return self.msg_edit_row(line, row)

    def ask_password(self, msg=None):
        self.password = self.bal_window.get_wallet_password(msg, parent=self)

    def msg_edit_row(self, line, row=None):
        try:
            self.labels[row] = line
        except Exception:
            self.labels.append(line)
            row = len(self.labels) - 1

        self.updatemessage.emit()

        return row

    def msg_del_row(self, row):
        try:
            del self.labels[row]
        except Exception:
            pass
        self.updatemessage.emit()

    # def clear_layout(self,layout):
    #    while layout.count():
    #        item = layout.takeAt(0)
    #        w = item.widget()
    #        if w:
    #            w.setParent(None)
    #            w.deleteLater()

    # def msg_update(self):
    #    self.clear_layout(self.labelsbox)
    #    for label in self.labels:
    #        label=label.replace("\n","<br>")
    #        qlabel=QLabel(label)
    #        qlabel.setWordWrap(True)
    #        self.labelsbox.addWidget(qlabel)

    #    self.labelsbox.activate()
    #    self.qwidget.setMinimumSize(self.labelsbox.sizeHint())
    #    self.qwidget.adjustSize()
    #    from PyQt6.QtWidgets import QApplication
    #    QApplication.processEvents()
    #
    #    self.adjustSize()
    def msg_update(self):
        full_text = "<br><br>".join(self.labels).replace("\n", "<br>")
        self.message_label.setText(full_text)
        self.message_label.adjustSize()
        # Auto-scroll the report to the BOTTOM so the newest line is always
        # visible (allegato14). The QScrollArea caps the height, so instead of
        # resizing the whole dialog taller we move its vertical scrollbar to the
        # maximum. ensureWidgetVisible is deferred via the scrollbar range so it
        # reflects the just-added text.
        scrollbar = self.scroll_area.verticalScrollBar()
        scrollbar.setValue(scrollbar.maximum())
        # Let the dialog grow only up to the scroll area's capped height; beyond
        # that the scrollbar handles overflow and the buttons stay reachable.
        self.resize(self.sizeHint())
        # Re-assert the bottom position after the resize/relayout settled.
        scrollbar.setValue(scrollbar.maximum())

    def get_text(self):
        return self.message_label.text()

    pass



class WillDetailDialog(BalDialog):
    def __init__(self, bal_window, will=None, threshold=None):
        # ``will``/``threshold`` are passed when showing an IMPORTED (read-only)
        # will. In that case every action button below operates on the imported
        # will, never on the live wallet state.
        self._external_will = will is not None
        self.will = will if self._external_will else bal_window.willitems
        if threshold is not None:
            self.threshold = threshold
        elif self._external_will:
            self.threshold = max(wi.tx.locktime for wi in self.will.values())
        else:
            self.threshold = bal_window.will_settings["real_threshold"]

        self.bal_window = bal_window
        Will.add_willtree(self.will)
        super().__init__(bal_window.window, bal_window.bal_plugin)
        self.config = bal_window.window.config
        self.wallet = bal_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.decimal_point = bal_window.window.get_decimal_point()
        self.base_unit_name = decimal_point_to_base_unit_name(self.decimal_point)
        self.setWindowTitle(_("Will Details"))
        self.setMinimumSize(670, 700)
        self.vlayout = QVBoxLayout()
        w = QWidget()
        hlayout = QHBoxLayout(w)

        b = QPushButton(_("Sign"))
        b.clicked.connect(self.ask_password_and_sign_transactions)
        hlayout.addWidget(b)

        b = QPushButton(_("Broadcast"))
        b.clicked.connect(self.broadcast_transactions)
        hlayout.addWidget(b)

        b = QPushButton(_("Export"))
        b.clicked.connect(self.export_will)
        hlayout.addWidget(b)
        b = QPushButton(_("Invalidate"))
        b.clicked.connect(self.invalidate_will)
        hlayout.addWidget(b)
        self.merge_button = None
        if self._external_will:
            b = QPushButton(_("Merge"))
            b.clicked.connect(self.merge_will)
            hlayout.addWidget(b)
            self.merge_button = b
        self.vlayout.addWidget(w)

        self.paint_scroll_area()
        self.vlayout.addWidget(
            QLabel(_("Expiration date: ") + str(BalTimestamp(self.threshold)))
        )
        self.vlayout.addWidget(self.scrollbox)
        w = QWidget()
        hlayout = QHBoxLayout(w)
        hlayout.addWidget(
            QLabel(_("Valid Txs:") + str(len(Will.only_valid_list(self.will))))
        )
        hlayout.addWidget(QLabel(_("Total Txs:") + str(len(self.will))))
        self.vlayout.addWidget(w)
        self.setLayout(self.vlayout)

    def paint_scroll_area(self):
        self.scrollbox = QScrollArea()
        viewport = QWidget(self.scrollbox)
        self.willlayout = QVBoxLayout(viewport)
        self.detailsWidget = WillWidget(parent=self, will=self.will)
        self.willlayout.addWidget(self.detailsWidget)

        self.scrollbox.setWidget(viewport)
        viewport.setLayout(self.willlayout)

    def ask_password_and_sign_transactions(self):
        self.bal_window.ask_password_and_sign_transactions(
            callback=self.update, will=self.will if self._external_will else None
        )
        self.update()

    def broadcast_transactions(self):
        self.bal_window.broadcast_transactions(
            will=self.will if self._external_will else None
        )
        self.update()

    def export_will(self):
        self.bal_window.export_will(will=self.will if self._external_will else None)

    def invalidate_will(self):
        self.bal_window.invalidate_will(
            will=self.will if self._external_will else None
        )

    def merge_will(self):
        """Merge the imported will into the live will and switch to it.

        The merge is performed by :meth:`BalWindow.merge_will` (the same
        common method used by the tools-menu "Merge" action). Afterwards the
        dialog stops showing the read-only imported will and operates on the
        live wallet willitems directly, so any further Sign/Broadcast/Export/
        Invalidate action targets the saved will items.
        """
        self.bal_window.merge_will(self.will)
        self._external_will = False
        self.will = self.bal_window.willitems
        self.threshold = self.bal_window.will_settings["real_threshold"]
        if self.merge_button:
            self.merge_button.hide()
        self.update()

    def toggle_replaced(self):
        self.bal_window.bal_plugin.hide_replaced()
        toggle = _("Hide")
        if self.bal_window.bal_plugin._hide_replaced:
            toggle = _("Unhide")
        self.toggle_replace_button.setText(f"{toggle} {_('replaced')}")
        self.update()

    def toggle_invalidated(self):
        self.bal_window.bal_plugin.hide_invalidated()
        toggle = _("Hide")
        if self.bal_window.bal_plugin._hide_invalidated:
            toggle = _("Unhide")
        self.toggle_invalidate_button.setText(_(f"{toggle} {_('invalidated')}"))
        self.update()

    def update(self):
        if not self._external_will:
            self.will = self.bal_window.willitems
        pos = self.vlayout.indexOf(self.scrollbox)
        self.vlayout.removeWidget(self.scrollbox)
        self.paint_scroll_area()
        self.vlayout.insertWidget(pos, self.scrollbox)
        super().update()



class WillExecutorDialog(BalDialog, MessageBoxMixin):
    def __init__(self, bal_window, parent=None):
        if not parent:
            parent = bal_window.window
        BalDialog.__init__(self, parent, bal_window.bal_plugin)
        self.bal_plugin = bal_window.bal_plugin
        self.config = self.bal_plugin.config
        self.bal_window = bal_window
        self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin)

        self.setWindowTitle(_("Will-Executor Service List"))
        self.setMinimumSize(1000, 200)

        # Lazy import to avoid a dialogs<->lists import cycle.
        from .lists import WillExecutorWidget
        vbox = QVBoxLayout(self)
        self.will_executor_list_widget = WillExecutorWidget(
            self, self.bal_window, self.willexecutors_list
        )
        vbox.addWidget(self.will_executor_list_widget)

    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()
        # raise_() alone does not grab focus on some window managers (Windows);
        # activateWindow() ensures the dialog actually comes to the front.
        bring_to_front(self)

    def closeEvent(self, event):
        event.accept()


