"""
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 .common import *
from .common import _, _logger  # underscore names are not re-exported by "import *"
from .widgets import (BalCheckBox, BalLineEdit, BalTextEdit, BalTxFeesWidget,
                      LockTimeWidget, PercAmountEdit, ThresholdTimeWidget,
                      WillSettingsWidget, WillWidget)
from .calendar import BalCalendar
# 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"):
        import signal
        from PyQt6.QtCore import QMetaObject, Qt
        from PyQt6.QtWidgets import QApplication
        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()
        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():
                        if index < 1:
                            for we in self.bal_window.willexecutors:
                                if self.bal_window.willexecutors[we]["status"] == 200:
                                    self.bal_window.willexecutors[we]["selected"] = True
                        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.finished.connect(self.finished)
        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 finished(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:"))
        self.vbox = QVBoxLayout(self)
        self.vbox.addWidget(self.message_label, 0)
        self.qwidget = QWidget(self)
        self.vbox.addWidget(self.qwidget, 1)
        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
        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
            )
            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,
                self.bal_window.window.wallet.get_utxos(),
                self.bal_window.date_to_check,
                self.bal_window.window.wallet.dust_threshold(),
            )
            _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"
                )
            )

        self.msg_set_checking()
        have_to_build = False
        try:
            self.bal_window.check_will()
            self.msg_set_checking(self.msg_ok())
        except WillExpiredException:
            _logger.debug("expired")
            self.msg_set_checking("Expired")
            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
            )
        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
            )
        except NoHeirsException as e:
            _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
            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):
                message = _("Heir not found")
            if message:
                _logger.debug(f"message: {message}")
                self.msg_set_checking(message)
            else:
                self.msg_set_checking("New")

        if have_to_build:
            self.msg_set_building()
            try:
                txs = self.bal_window.build_will()
                if not txs:
                    self.msg_set_building(
                        _("Balance is too low, or CheckAlive is in the past.Skipped"),
                        color = self.COLOR_ERROR,
                    )
                    return False, None

                self.bal_window.check_will()
                for wid in Will.only_valid(self.bal_window.willitems):
                    self.bal_window.wallet.set_label(wid, "BAL Transaction")
                self.msg_set_building(self.msg_ok())
            except WillExecutorNotPresent:
                self.msg_set_status(
                    _("Will-Executor excluded"), None, _("Skipped"), self.COLOR_ERROR
                )

            except Exception as e:
                _logger.exception("build_will failed")
                self.msg_set_building(self.msg_error(e))
                return False, None

        # excluded_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]):
                    self.msg_set_status(
                        f"{hid},{heir[HEIR_DUST_AMOUNT]} is DUST",
                        None,
                        f"Excluded from will {wid}",
                        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 on_accept(self):
        self.bal_window.update_all()
        pass

    def on_accept_phase2(self):
        self.bal_window.update_all()
        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))
            txid = self.network.run_from_another_thread(
                self.network.broadcast_transaction(tx, timeout=120), timeout=120
            )
            self.msg_set_invalidating(self.msg_ok())
            if not txid:
                _logger.debug(f"should not be none txid: {txid}")

        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):
        if self._stopping:
            return
        self.msg_set_pushing(_("Broadcasting"))
        retry = False
        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))
            }

            # 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 = []
            retry_flag = {"value": False}
            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:
                    for wid in willexecutor["txsids"]:
                        self.bal_window.willitems[wid].set_status("PUSH_FAIL", True)
                    retry_flag["value"] = 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 (so the user can retry later) and show it.
                for wid in willexecutor.get("txsids", []):
                    self.bal_window.willitems[wid].set_status("PUSH_FAIL", True)
                retry_flag["value"] = 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))
            )
            retry = retry_flag["value"]

            # 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,
                    )

            if retry:
                raise Exception("retry")

        except Exception as e:
            self.msg_set_pushing(self.msg_error(e))
            self.wait(10)
            if not self._stopping:
                pass
                # self.loop_push()

    def invalidate_task(self, password, bal_window, tx):
        if self._stopping:
            return
        _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)
                    self.wait(5)
                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):
        if self._stopping:
            return
        self.have_to_sign, tx = list(result)
        _logger.debug("have to sign {}".format(self.have_to_sign))
        password = None
        if self.have_to_sign is None:
            _logger.debug("have to invalidate")
            self.msg_set_invalidating()

            password = self.bal_window.get_wallet_password(
                _("Invalidate your old will"), parent=self
            )
            if password is False:
                self.msg_set_invalidating(_("Aborted"))
                self.wait(3)
                self.close()
                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_plugin.AUTO_SIGN.get()
            if auto_sign:
                self.msg_set_signing(_("Auto-signing..."))
            else:
                password = self.bal_window.get_wallet_password(
                    _("Sign your will"), parent=self
                )
                if password is False:
                    self.msg_set_signing(_("Password cancelled"))
                    self._show_next_steps_hint()
                    self._add_close_button()
                    return
        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()
        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 to dismiss the dialog manually.

        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
        self._close_button = QPushButton(_("Close"))
        self._close_button.clicked.connect(self._on_close_clicked)
        button_row = QHBoxLayout()
        button_row.addStretch(1)
        button_row.addWidget(self._close_button)
        self.vbox.addLayout(button_row)
        self._close_button.setFocus()

    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
        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}"))
        _logger.error(f"error phase1: {b}")
        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 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()
        # self.setMinimumHeight(len(self.labels)*40)
        self.resize(self.sizeHint())

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

    pass



class WillDetailDialog(BalDialog):
    def __init__(self, bal_window):

        self.will = bal_window.willitems
        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(bal_window.invalidate_will)
        hlayout.addWidget(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)
        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)
        self.update()

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

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

    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):
        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()


