"""Activation clicks and state, full-UI refresh, install/cancel, layer signals.

Part of AISegmentationDockWidget (see ai_segmentation_dockwidget.py);
split out so agents and humans work on one concern per file. Methods
are plain mixin members: widgets/signals live on the dock instance.
"""
from __future__ import annotations

from qgis.PyQt.QtCore import Qt

from ...core.activation_manager import (
    has_tos_accepted,
    has_tos_locked,
    lock_tos,
    set_tos_accepted,
)
from ...core.i18n import tr
from .styles import (
    _BTN_GREEN_AUTH,
    _msg_label_qss,
)
from .widgets import (
    Mode,
)


class DockActivationMixin:
    """Activation clicks and state, full-UI refresh, install/cancel, layer signals."""

    def set_activated_state(self, activated: bool):
        """Flip the signed-in state and refresh every dependent section."""
        self._plugin_activated = activated
        if activated:
            # A stale pairing spinner must never survive a successful activation.
            self._stop_pairing_wait()
            self._pending_pairing_code = ""
        else:
            self.show_pairing_idle()
            self.activation_message_label.setVisible(False)
            # B2: never carry a signed-out session's balance into the footer.
            # Clear the cached credit state and hide the gauge/pill/label so no
            # stale "N / M" lingers, and a later sign-in re-fetches before it
            # shows anything (avoids flashing the previous account's numbers).
            self._auto_credits = None
            self._auto_credits_total = None
            self._auto_free_left = None
            self._auto_is_subscriber = False
            # The two-envelope snapshot is account state too: a later sign-in
            # must not gate or draw on the previous account's envelopes.
            self._quota_envelopes = None
            # The card the envelope raised goes with it. Clearing the snapshot
            # alone left the flag armed and the offer card on screen, so Detect
            # stayed dead against a surface wall the account no longer has.
            try:
                self.set_auto_km2_block(None)
            except (RuntimeError, AttributeError):
                self._auto_km2_exceeded = False
            for _w in (getattr(self, "_credit_ring", None),
                       getattr(self, "_footer_credits_label", None),
                       getattr(self, "_subscribe_pill", None)):
                if _w is not None:
                    try:
                        _w.setVisible(False)
                    except (RuntimeError, AttributeError):
                        pass
            if getattr(self, "_footer_credits_label", None) is not None:
                self._footer_credits_label.setText("")
        self._update_full_ui()

    def set_activation_message(self, text: str, is_error: bool = False,
                               kind: str | None = None):
        """Public alias used by the plugin's pairing handlers."""
        self._show_activation_message(text, is_error, kind)

    def _show_activation_message(self, text: str, is_error: bool = False,
                                 kind: str | None = None):
        """Display a message in the activation section, framed per the message
        taxonomy.

        ``kind`` names the frame outright ("info", "warning", "neutral",
        "error", "success"). Without it the line falls back to the old
        two-way read, which paints every non-error lime, including a wait and
        a blocker that are neither a success nor a failure.
        """
        self.activation_message_label.setText(text)
        if not kind:
            kind = "error" if is_error else "success"
        self.activation_message_label.setStyleSheet(_msg_label_qss(kind))
        self.activation_message_label.setVisible(True)

    def _consent_flags(self) -> tuple[bool, bool]:
        """(terms sealed, terms ticked) for the refresh pass in progress.

        Both answers come from QgsSettings, and one full refresh asks for them
        from three different mixins. The pass reads them once and every reader
        takes them from here.
        """
        cached = getattr(self, "_consent_flags_cache", None)
        if cached is not None:
            return cached
        return (has_tos_locked(), has_tos_accepted())

    def _update_full_ui(self):
        """Refresh the whole dock for the current signed-in state + mode.

        Signed-out is one neutral first step BEFORE any mode concept: the mode
        switch is hidden and both former per-mode sign-in variants funnel to the
        single login page. Only once activated does the mode switch appear and
        the per-mode paths run.
        """
        self._consent_flags_cache = (has_tos_locked(), has_tos_accepted())
        try:
            self._update_full_ui_body()
        finally:
            self._consent_flags_cache = None

    def _update_full_ui_body(self):
        """The refresh itself. Split from _update_full_ui so the consent read
        is held for the whole pass and released however it ends."""
        activated = self._plugin_activated
        self._refresh_mode_switch_visibility()
        if not activated:
            self._show_signed_out_page()
            self._update_ui_state()
            self._refresh_manual_engine_ui()
            self._refresh_manual_credit_gate()
            self._update_auto_tutorial_banner_visibility()
            return
        if self._mode == Mode.INTERACTIVE:
            self._update_full_ui_interactive()
        else:
            self._update_full_ui_automatic()
        self._update_ui_state()
        self._refresh_manual_engine_ui()
        self._refresh_manual_credit_gate()
        self._update_auto_tutorial_banner_visibility()

    def _mode_flow_started(self) -> bool:
        """True once the current mode has left its Start screen for a live flow:
        Manual after 'Start Semi auto AI Segmentation' (a session is
        active), or
        Automatic after 'Start Automatic AI Segmentation' (past step 0). This is
        the single test for hiding the mode switch mid-flow."""
        if self._mode == Mode.INTERACTIVE:
            return bool(getattr(self, "_segmentation_active", False))
        return bool(getattr(self, "_auto_started", False))

    def _refresh_mode_switch_visibility(self) -> None:
        """Show the Manual/Automatic switch ONLY on a mode's Start screen.

        The switch is how you pick a mode, so it belongs to the choosing moment.
        Once a flow is launched (Manual session active, or Automatic past step 0)
        it is hidden: the mode is committed, and hiding it removes the temptation
        to switch mid-flow while giving the running flow the vertical space back.
        Exiting/finishing a flow returns to the Start screen and brings it back.
        Never shown signed-out (there is no mode concept before sign-in)."""
        try:
            visible = self._plugin_activated and not self._mode_flow_started()
            self.mode_switch.setVisible(visible)
        except (RuntimeError, AttributeError):
            pass

    def _show_signed_out_page(self):
        """One neutral login page: hide every mode surface, show the sign-in
        card with mode-agnostic copy (no price, no local/cloud wording)."""
        self.welcome_widget.setVisible(False)
        self.setup_group.setVisible(False)
        self.seg_widget.setVisible(False)
        self.batch_info_widget.setVisible(False)
        self.auto_page.setVisible(False)
        # The bottom View-as block is an Automatic review surface that lives
        # outside auto_page, so hiding the page does not take it along.
        self.auto_review_view_row.setVisible(False)

        self.activation_group.setVisible(True)
        if hasattr(self, "_connect_hint_label"):
            self._connect_hint_label.setText(
                tr("Free account - sign up takes 15 seconds in your browser."))
        if hasattr(self, "_connect_btn"):
            self._connect_btn.setStyleSheet(_BTN_GREEN_AUTH)

        # Footer: gear hidden (no account yet), cross-promo hidden (a CTA for
        # another product has no place before sign-in), credit gauge/Subscribe
        # stay hidden (an activated surface).
        self._settings_btn.setVisible(False)
        self._ai_edit_btn.setVisible(False)
        # The tutorial button stays reachable even before sign-in: a lost
        # visitor benefits most from the guide.
        self._tutorial_btn.setVisible(True)
        self._refresh_auto_credits_display()

    def _update_full_ui_interactive(self):
        """Interactive mode (activated): install gate + segmentation section."""
        # With the clicks answered off the machine there is nothing on disk to
        # be complete: no venv, no model file, no download. So the page opens
        # on the same terms an installed user gets.
        setup_complete = (self._dependencies_ok and self._checkpoint_ok) \
            or self._manual_cloud_route_picked()

        # Cloud with an empty account is not "not usable yet", it is blocked,
        # and the gate below carries the whole story. A pale green Start above
        # the refusal is the wall of dead controls the design system bans.
        blocked = (self._manual_cloud_route_picked()
                   and self._manual_credits_exhausted()
                   and not self._segmentation_active)

        # Segmentation section: shown once the mode can do something. With a
        # second engine on offer that is always, because picking Cloud AI is
        # what makes an empty machine ready and the Start button has to be
        # there to press once it is picked. Hiding this block behind the
        # install would leave the picker at the foot of the page with nothing
        # to act on. Start itself says which of the two it is about to do (see
        # _refresh_manual_start_action).
        show_segmentation = setup_complete or self._manual_engine_offered()
        self.seg_widget.setVisible(show_segmentation)
        # The Start button gives its place to the credit gate.
        _start = getattr(self, "start_button", None)
        if _start is not None:
            try:
                _start.setVisible(not blocked)
            except (RuntimeError, AttributeError):
                pass
        # The Terms row goes with them, but it has a stronger reason to stay
        # hidden and that one wins: once consent is sealed the row is gone for
        # good, and nothing here may bring it back. Showing it unconditionally
        # is exactly what did, on every refresh, to a user who agreed months
        # ago.
        _tos = getattr(self, "tos_container", None)
        if _tos is not None:
            try:
                _tos.setVisible(not blocked and not self._consent_flags()[0])
            except (RuntimeError, AttributeError):
                pass

        # Setup section (install/download group + welcome title): shown while a
        # setup is genuinely needed or running, tracked in _setup_section_wanted
        # rather than the sticky setup_group.isVisible(). The old proxy was
        # wiped by _update_full_ui_automatic (which force-hides setup_group), so
        # switching to Automatic mid-install and back left this page empty while
        # the background install kept running. Gating on the flag also still
        # avoids flashing "Click Install..." during the silent startup re-check.
        setup_needed = not setup_complete or self._manual_install_running()
        if self._manual_engine_offered():
            # The offline install has its own window now (see
            # dock/manual_local_install.py), so the page stops opening on a
            # download nobody asked for. Two states are still the panel's: an
            # install already on the wire that no window is showing, because a
            # user who closed it must not be left with minutes of downloading
            # and nothing on screen; and the moment after one fails, where the
            # card carries the real message and the Retry button.
            setup_needed = (self._manual_install_running()
                            and not self._manual_install_window_owns_it())
            setup_needed = setup_needed or bool(
                getattr(self, "_manual_install_failed", False))
        show_setup = getattr(self, "_setup_section_wanted", False) and setup_needed
        self.setup_group.setVisible(show_setup)
        self.welcome_widget.setVisible(show_setup)

        self.activation_group.setVisible(False)
        self._settings_btn.setVisible(True)
        # The credit gauge + Subscribe pill own the bottom-left slot in both
        # modes: the balance belongs to the account, so it reads the same
        # wherever the user is. The AI Edit cross-promo yields it here too.
        self._ai_edit_btn.setVisible(False)
        if not show_segmentation:
            self.batch_info_widget.setVisible(False)

        # Always hide automatic page in Interactive mode, and with it the
        # bottom View-as block (an Automatic review surface parked outside the
        # page, so it needs its own call).
        self.auto_page.setVisible(False)
        self.auto_review_view_row.setVisible(False)
        self._refresh_auto_credits_display()

    def _update_full_ui_automatic(self):
        """Automatic mode (activated): hide install sections, show auto page."""
        # Never show install/setup sections in Automatic mode
        self.welcome_widget.setVisible(False)
        self.setup_group.setVisible(False)
        self.seg_widget.setVisible(False)
        self.batch_info_widget.setVisible(False)
        self.activation_group.setVisible(False)

        self._settings_btn.setVisible(True)
        # Same bottom-left slot as Semi auto: the credit gauge + Subscribe
        # pill, never the AI Edit cross-promo.
        self._ai_edit_btn.setVisible(False)

        self.auto_page.setVisible(True)
        self._update_auto_page_state()

    def _on_install_clicked(self):
        # Log on click receipt so users stuck on the install screen with no
        # apparent reaction (observed on macOS 26) can prove the click event
        # was actually delivered to Qt - separating UI-event regressions from
        # signal-propagation or background-worker bugs.
        from ...core.logging_utils import log as _log
        _log("Install button clicked")
        self.install_button.setEnabled(False)
        # This card is the Semi-Auto setup, so the install it starts carries
        # the on-device AI. Automatic's lighter one never comes through here.
        self._manual_install_wants_model = True
        self.install_requested.emit()

    def show_install_waiting_notice(self, message: str) -> None:
        """Say why a clicked Install has not started yet.

        Same line as set_dependency_status, minus the button: the install is
        queued behind a busy local AI, so Install stays down (a second click
        would start a second wait) and only the status line moves.
        """
        try:
            self.setup_status_label.setText(message)
            self.setup_status_label.setToolTip("")
            self.setup_status_label.setStyleSheet(
                "font-weight: bold; color: palette(text);")
            self.setup_status_label.setVisible(True)
            self.setup_group.setVisible(True)
        except (RuntimeError, AttributeError):
            pass

    def _toggle_cancel_button(self):
        """Kept for the toggle still wired in dock/build.py.

        The disclosure step is gone: the Cancel button shows with the progress
        bar and the toggle never does (see set_install_progress), so stopping
        an install is the button plus its confirm rather than three actions.
        """
        visible = not self.cancel_button.isVisible()
        self.cancel_button.setVisible(visible)
        arrow = Qt.ArrowType.DownArrow if visible else Qt.ArrowType.RightArrow
        self.cancel_toggle.setArrowType(arrow)

    def _on_cancel_clicked(self):
        from qgis.PyQt.QtWidgets import QMessageBox
        reply = QMessageBox.question(
            self,
            tr("Cancel installation"),
            tr("Are you sure you want to cancel the installation?"),
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
            QMessageBox.StandardButton.No)
        if reply == QMessageBox.StandardButton.Yes:
            # The same ending the install window's Stop gets. The two used to
            # part ways: one moved the engine back to Cloud AI, this one left
            # the switch on a half with nothing behind it.
            self._end_manual_install_as_stopped()

    def _on_layer_changed(self, layer):
        # Just update UI state - layer change handling is done by the plugin
        self._update_ui_state()

    def _on_layers_added(self, _layers):
        """Handle new layers added to project.

        No auto-select here any more. The combo listens to the same signal and
        picks the raster that best fits the map view, which beats the old test
        order (it preferred an online basemap over local imagery) and, unlike a
        setLayer() call, does not read as a deliberate choice that stops the
        combo following the view.
        """
        self._update_ui_state()

    def _on_layers_removed(self, _layer_ids):
        """Handle layers removed from project."""
        self._update_ui_state()

    def _on_layer_visibility_changed(self, node):
        """Handle layer visibility toggle in the layer tree (debounced)."""
        self._visibility_debounce_timer.start(100)

    def _on_tos_toggled(self, checked: bool):
        """Persist the Terms + Privacy acceptance and refresh gating. Consent is
        one GLOBAL state shared by the Manual and Automatic checkboxes, so mirror
        the sibling box (without recursing) to keep the two modes in sync."""
        set_tos_accepted(checked)
        for _attr in ("tos_checkbox", "auto_tos_checkbox"):
            cb = getattr(self, _attr, None)
            if cb is not None and cb.isChecked() != checked:
                cb.blockSignals(True)
                cb.setChecked(checked)
                cb.blockSignals(False)
        self._update_ui_state()

    def seal_tos_consent(self):
        """Seal Terms + Privacy consent forever and drop both checkboxes.

        Called by the first committed action in either mode: Manual Start, or
        the first Automatic Detect (where the Automatic checkbox now lives,
        right above the button - consent friction at the latest possible
        moment). Subsequent sessions (or plugin updates) never re-display it.
        """
        if not has_tos_locked():
            lock_tos()
        for _attr in ("tos_container", "auto_tos_container"):
            container = getattr(self, _attr, None)
            if container is not None:
                container.setVisible(False)

    def _on_start_clicked(self):
        layer = self.layer_combo.currentLayer()
        if not layer:
            return
        # The engine box decides what this press means: a cleared box on a
        # machine with no offline AI turns the press into the download.
        if not self._manual_engine_gate_start():
            return
        self.seal_tos_consent()
        self.start_segmentation_requested.emit(layer)

    def _on_start_shortcut(self):
        """G starts the visible mode's flow: Manual Start in Manual mode,
        the Automatic Start button on its Start step in Automatic mode (K3)."""
        if self._mode == Mode.INTERACTIVE:
            if self.start_button.isEnabled() and self.start_button.isVisible():
                self._on_start_clicked()
            return
        try:
            if self.auto_start_btn.isVisible() and self.auto_start_btn.isEnabled():
                self._on_auto_start_clicked()
        except (RuntimeError, AttributeError):
            pass

    def _on_undo_clicked(self):
        self.undo_requested.emit()

    def _on_save_polygon_clicked(self):
        self.save_polygon_requested.emit()

    def _on_export_clicked(self):
        self.export_layer_requested.emit()

    def _on_clear_selection_clicked(self):
        self.clear_selection_requested.emit()

    def _on_stop_clicked(self):
        self.stop_segmentation_requested.emit()
