from datetime import datetime
import json
import math
from pathlib import Path
import subprocess
import urllib.error
import urllib.request

from PyQt5.QtCore import (
    Qt,
    QRectF,
    QThread,
    QTimer,
    pyqtSignal,
)
from PyQt5.QtGui import (
    QBrush,
    QColor,
    QFont,
    QLinearGradient,
    QPainter,
    QPainterPath,
    QPen,
    QPixmap,
    QRadialGradient,
)
from PyQt5.QtWidgets import (
    QApplication,
    QFrame,
    QGridLayout,
    QHBoxLayout,
    QLabel,
    QPushButton,
    QSizePolicy,
    QVBoxLayout,
    QWidget,
)

from core.voice_privacy import voice_privacy_foundation


GREEN = "#00f58a"
PURPLE = "#9b4dff"
PURPLE_LIGHT = "#c89cff"
CYAN = "#18e6d2"
WHITE = "#f4efff"
MUTED = "#a89bc4"
PANEL = "rgba(7, 7, 22, 225)"
PANEL_SOFT = "rgba(10, 7, 28, 190)"
BORDER = "rgba(155, 77, 255, 165)"


def _greeting():
    hour = datetime.now().hour

    if hour < 12:
        return "Good morning, Ray."

    if hour < 18:
        return "Good afternoon, Ray."

    return "Good evening, Ray."


class VoicePrivacyBanner(QFrame):
    """Live local voice privacy and runtime-state banner."""

    def __init__(self):
        super().__init__()

        self.setObjectName("voicePrivacyBanner")
        self.setFixedHeight(52)
        self.setMinimumWidth(310)
        self.setSizePolicy(
            QSizePolicy.Preferred,
            QSizePolicy.Fixed,
        )

        root = QHBoxLayout(self)
        root.setContentsMargins(18, 6, 18, 6)
        root.setSpacing(12)

        self.icon = QLabel("◉")
        self.icon.setAlignment(Qt.AlignCenter)

        text_column = QVBoxLayout()
        text_column.setSpacing(0)

        self.top = QLabel()
        self.bottom = QLabel()

        text_column.addWidget(self.top)
        text_column.addWidget(self.bottom)

        root.addWidget(self.icon)
        root.addLayout(text_column)

        self.timer = QTimer(self)
        self.timer.timeout.connect(self.refresh)
        self.timer.start(400)

        self.refresh()

    def refresh(self):
        snapshot = voice_privacy_foundation.snapshot()
        state = str(snapshot.state).upper()

        if state == "ERROR":
            accent = "#ff5c70"
            top_text = "VOICE ERROR"
            bottom_text = snapshot.error or "VOICE POLICY ERROR"

        elif state == "LISTENING":
            accent = GREEN
            top_text = "VOICE LISTENING"
            bottom_text = "MICROPHONE OPEN  •  LOCAL ONLY"

        elif state == "PROCESSING":
            accent = CYAN
            top_text = "VOICE PROCESSING"
            bottom_text = "AUDIO DISCARDED  •  LOCAL ONLY"

        elif state == "READY":
            accent = PURPLE_LIGHT
            top_text = "VOICE READY"
            bottom_text = "MANUAL CAPTURE  •  LOCAL ONLY"

        else:
            accent = MUTED
            top_text = "VOICE MODE OFF"
            bottom_text = "MICROPHONE CLOSED"

        self.setStyleSheet(f"""
            QFrame#voicePrivacyBanner {{
                background: rgba(4, 7, 18, 220);
                border: 1px solid {accent};
                border-radius: 13px;
            }}
            QLabel {{
                background: transparent;
                border: none;
            }}
        """)

        self.icon.setStyleSheet(
            f"color:{accent}; font-size:24px; font-weight:900;"
        )

        self.top.setText(top_text)
        self.top.setStyleSheet(
            f"color:{accent}; font-size:14px; font-weight:900;"
        )

        self.bottom.setText(bottom_text)
        self.bottom.setStyleSheet(
            "color:#aa9dc6; font-size:9px; font-weight:800;"
        )


class LaptopRuneButton(QPushButton):
    """Compact Presence Console navigation control."""

    def __init__(
        self,
        icon,
        title,
        sub,
        target_index,
        stack,
        compact=False,
    ):
        super().__init__()

        self.target_index = target_index
        self.stack = stack

        if compact:
            self.setText(f"{icon}  {title}")
            self.setMinimumHeight(48)
        else:
            self.setText(f"{icon}\n{title}\n{sub}")
            self.setMinimumHeight(74)

        self.setCursor(Qt.PointingHandCursor)
        self.setSizePolicy(
            QSizePolicy.Expanding,
            QSizePolicy.Fixed,
        )

        self.setStyleSheet("""
            QPushButton {
                background: rgba(8, 5, 25, 225);
                color: #c69cff;
                border: 1px solid rgba(155, 77, 255, 185);
                border-radius: 13px;
                font-family: "DejaVu Sans Mono", monospace;
                font-size: 13px;
                font-weight: 900;
                padding: 8px 12px;
            }
            QPushButton:hover {
                background: rgba(34, 9, 66, 235);
                color: #ffffff;
                border: 1px solid #b76cff;
            }
            QPushButton:pressed {
                background: rgba(0, 245, 138, 65);
                color: #00f58a;
                border: 1px solid #00f58a;
            }
        """)

        self.clicked.connect(self.open_target)

    def open_target(self):
        if self.stack is not None:
            self.stack.setCurrentIndex(self.target_index)


class PresenceCard(QFrame):
    def __init__(self, title, accent=PURPLE):
        super().__init__()

        self.setObjectName("presenceCard")
        self.setStyleSheet(f"""
            QFrame#presenceCard {{
                background: {PANEL};
                border: 1px solid {BORDER};
                border-radius: 18px;
            }}
            QLabel {{
                background: transparent;
                border: none;
            }}
        """)

        self.column = QVBoxLayout(self)
        self.column.setContentsMargins(22, 18, 22, 18)
        self.column.setSpacing(10)

        heading = QLabel(title)
        heading.setStyleSheet(
            f"color:{accent}; font-size:12px; font-weight:900;"
        )

        self.column.addWidget(heading)


class ShireCenterCore(QWidget):
    """Face-first SHIRE presence with truthful expression states."""

    EXPRESSION_FILES = {
        "neutral": "shire_neutral.png",
        "blink_half": "shire_blink_half.png",
        "blink_closed": "shire_blink_closed.png",
        "smile": "shire_smile.png",
        "thinking": "shire_thinking.png",
        "listening": "shire_listening.png",
        "focused": "shire_focused.png",
        "acknowledge": "shire_acknowledge.png",
        "concerned": "shire_concerned.png",
        "surprised": "shire_surprised.png",
        "sentinel_standby": "shire_sentinel_standby.png",
        "sentinel_active": "shire_sentinel_active.png",
        "sentinel_lockdown": "shire_sentinel_lockdown.png",
    }

    NORMAL_BLINK_STATES = {
        "neutral",
        "smile",
        "focused",
        "acknowledge",
        "concerned",
    }

    def __init__(self):
        super().__init__()

        self.root = Path(__file__).resolve().parent.parent
        self.expression_dir = (
            self.root
            / "assets"
            / "shire"
            / "expressions"
        )

        self.fallback_face_path = (
            self.root
            / "assets"
            / "shire"
            / "shire_face_main.png"
        )

        self.fallback_face = (
            QPixmap(str(self.fallback_face_path))
            if self.fallback_face_path.exists()
            else QPixmap()
        )

        self.expression_faces = {}

        for state, filename in self.EXPRESSION_FILES.items():
            path = self.expression_dir / filename
            portrait = (
                QPixmap(str(path))
                if path.exists()
                else QPixmap()
            )

            if portrait.isNull():
                portrait = self.fallback_face

            self.expression_faces[state] = portrait

        self.face_path = (
            self.expression_dir
            / self.EXPRESSION_FILES["neutral"]
        )

        self.face = self.expression_faces["neutral"]
        self.expression_state = "neutral"

        self.tick = 0
        self.animation_interval_ms = 125
        self._face_cache = {}

        self._blink_intervals = (
            38,
            52,
            44,
            60,
            34,
            49,
        )

        self._blink_interval_index = 0
        self._blink_ticks_remaining = (
            self._blink_intervals[0]
        )
        self._blink_phase = -1
        self._blink_duration_ms = 90

        self.blink_timer = QTimer(self)
        self.blink_timer.setSingleShot(True)
        self.blink_timer.setTimerType(Qt.PreciseTimer)
        self.blink_timer.timeout.connect(
            self._finish_blink
        )

        self.setMinimumSize(390, 470)
        self.setSizePolicy(
            QSizePolicy.Expanding,
            QSizePolicy.Expanding,
        )

        self.timer = QTimer(self)
        self.timer.setTimerType(Qt.CoarseTimer)
        self.timer.timeout.connect(self.pulse)
        self.timer.start(self.animation_interval_ms)

    def pulse(self):
        self.tick = (self.tick + 1) % 28800

        if self.expression_state in self.NORMAL_BLINK_STATES:
            if self._blink_phase < 0:
                self._blink_ticks_remaining -= 1

                if self._blink_ticks_remaining <= 0:
                    self._blink_phase = 0
                    self.blink_timer.start(
                        self._blink_duration_ms
                    )
        else:
            if self.blink_timer.isActive():
                self.blink_timer.stop()

            self._blink_phase = -1

        self.update()

    def _finish_blink(self):
        self._blink_phase = -1
        self._schedule_next_blink()
        self.update()

    def _schedule_next_blink(self):
        self._blink_interval_index = (
            self._blink_interval_index + 1
        ) % len(self._blink_intervals)

        self._blink_ticks_remaining = (
            self._blink_intervals[
                self._blink_interval_index
            ]
        )

    def set_expression_state(self, state):
        """Select one approved expression without inventing state."""

        state = str(state).strip().lower()

        if state not in self.expression_faces:
            return False

        self.expression_state = state
        self.face = self.expression_faces[state]

        if self.blink_timer.isActive():
            self.blink_timer.stop()

        self._blink_phase = -1
        self._schedule_next_blink()
        self.update()

        return True

    def current_expression_state(self):
        """Return the portrait state currently being rendered."""

        if (
            self.expression_state in self.NORMAL_BLINK_STATES
            and self._blink_phase == 0
        ):
            return "blink_closed"

        return self.expression_state

    def _current_face(self):
        state = self.current_expression_state()

        return self.expression_faces.get(
            state,
            self.fallback_face,
        )

    def showEvent(self, event):
        super().showEvent(event)

        if not self.timer.isActive():
            self.timer.start(
                self.animation_interval_ms
            )

        self.update()

    def hideEvent(self, event):
        if self.timer.isActive():
            self.timer.stop()

        if self.blink_timer.isActive():
            self.blink_timer.stop()

        self._blink_phase = -1
        self._schedule_next_blink()

        super().hideEvent(event)

    def resizeEvent(self, event):
        self._face_cache.clear()
        super().resizeEvent(event)

    def _scaled_face_for(
        self,
        state,
        portrait_size,
    ):
        face = self.expression_faces.get(
            state,
            self.fallback_face,
        )

        if face.isNull():
            return QPixmap()

        target_size = max(
            1,
            int(
                math.ceil(
                    float(portrait_size) * 1.025
                )
            ),
        )

        cache_key = (
            state,
            int(face.cacheKey()),
            target_size,
        )

        cached = self._face_cache.get(
            cache_key
        )

        if cached is not None and not cached.isNull():
            return cached

        scaled = face.scaled(
            target_size,
            target_size,
            Qt.KeepAspectRatioByExpanding,
            Qt.SmoothTransformation,
        )

        if scaled.isNull():
            return face

        if len(self._face_cache) >= 18:
            self._face_cache.clear()

        self._face_cache[cache_key] = scaled

        return scaled

    def paintEvent(self, event):
        del event

        painter = QPainter(self)
        painter.setRenderHint(
            QPainter.Antialiasing,
            True,
        )
        painter.setRenderHint(
            QPainter.SmoothPixmapTransform,
            True,
        )

        w = self.width()
        h = self.height()
        cx = w / 2
        cy = h * 0.47

        seconds = (
            self.tick
            * self.animation_interval_ms
            / 1000.0
        )

        aura = QRadialGradient(
            cx,
            cy,
            min(w, h) * 0.48,
        )
        aura.setColorAt(
            0.0,
            QColor(80, 25, 135, 115),
        )
        aura.setColorAt(
            0.55,
            QColor(37, 7, 82, 60),
        )
        aura.setColorAt(
            1.0,
            QColor(0, 0, 0, 0),
        )

        painter.setPen(Qt.NoPen)
        painter.setBrush(QBrush(aura))
        painter.drawEllipse(
            QRectF(
                cx - min(w, h) * 0.48,
                cy - min(w, h) * 0.48,
                min(w, h) * 0.96,
                min(w, h) * 0.96,
            )
        )

        pulse_offset = (
            math.sin(seconds * 0.75) + 1.0
        ) / 2.0

        for index in range(5):
            radius = (
                min(w, h)
                * (0.34 + index * 0.065)
                + pulse_offset * 4.0
            )

            alpha = max(
                35,
                145 - index * 22,
            )

            painter.setPen(
                QPen(
                    QColor(
                        155,
                        77,
                        255,
                        alpha,
                    ),
                    1.4 if index else 2.2,
                )
            )

            painter.setBrush(Qt.NoBrush)
            painter.drawEllipse(
                QRectF(
                    cx - radius,
                    cy - radius,
                    radius * 2,
                    radius * 2,
                )
            )

        base_portrait_size = min(
            w * 0.72,
            h * 0.72,
        )

        breath = math.sin(
            (2.0 * math.pi * seconds) / 6.6
        )

        drift_x = (
            2.5
            * math.sin(
                ((2.0 * math.pi * seconds) / 11.2)
                + 0.35
            )
        )

        drift_y = (
            2.8
            * math.sin(
                (2.0 * math.pi * seconds) / 8.3
            )
        )

        breathing_scale = 1.0 + (
            breath * 0.008
        )

        portrait_size = (
            base_portrait_size
            * breathing_scale
        )

        portrait_rect = QRectF(
            cx - portrait_size / 2 + drift_x,
            cy - portrait_size / 2 + drift_y,
            portrait_size,
            portrait_size,
        )

        clip_path = QPainterPath()
        clip_path.addEllipse(portrait_rect)

        painter.save()
        painter.setClipPath(clip_path)

        rendered_state = (
            self.current_expression_state()
        )

        scaled = self._scaled_face_for(
            rendered_state,
            base_portrait_size,
        )

        if not scaled.isNull():
            source_size = min(
                float(scaled.width()),
                float(scaled.height()),
            )

            source_x = max(
                0.0,
                (
                    scaled.width()
                    - source_size
                ) / 2.0,
            )

            source_y = max(
                0.0,
                (
                    scaled.height()
                    - source_size
                ) / 2.0,
            )

            painter.drawPixmap(
                portrait_rect,
                scaled,
                QRectF(
                    source_x,
                    source_y,
                    source_size,
                    source_size,
                ),
            )
        else:
            painter.fillRect(
                portrait_rect,
                QColor(7, 5, 20),
            )

            painter.setPen(
                QPen(
                    QColor(155, 77, 255),
                    2,
                )
            )

            painter.setFont(
                QFont(
                    "DejaVu Sans Mono",
                    25,
                    QFont.Bold,
                )
            )

            painter.drawText(
                portrait_rect,
                Qt.AlignCenter,
                "SHIRE\nPRESENCE",
            )

        painter.restore()

        painter.setPen(
            QPen(
                QColor(190, 118, 255, 230),
                3,
            )
        )
        painter.setBrush(Qt.NoBrush)
        painter.drawEllipse(portrait_rect)

        lower_gradient = QLinearGradient(
            0,
            h * 0.62,
            0,
            h,
        )
        lower_gradient.setColorAt(
            0.0,
            QColor(0, 0, 0, 0),
        )
        lower_gradient.setColorAt(
            1.0,
            QColor(1, 1, 8, 220),
        )

        painter.setPen(Qt.NoPen)
        painter.setBrush(
            QBrush(lower_gradient)
        )
        painter.drawRect(
            QRectF(
                0,
                h * 0.60,
                w,
                h * 0.40,
            )
        )

        painter.setPen(
            QColor(PURPLE_LIGHT)
        )
        painter.setFont(
            QFont(
                "DejaVu Sans Mono",
                22,
                QFont.Bold,
            )
        )
        painter.drawText(
            QRectF(
                0,
                h - 72,
                w,
                30,
            ),
            Qt.AlignCenter,
            "SHIRE",
        )

        painter.setPen(QColor(GREEN))
        painter.setFont(
            QFont(
                "DejaVu Sans Mono",
                10,
                QFont.Bold,
            )
        )
        painter.drawText(
            QRectF(
                0,
                h - 40,
                w,
                22,
            ),
            Qt.AlignCenter,
            "PRESENCE ONLINE  •  APPROVAL GATE ACTIVE",
        )



def _brain_node_base_url():
    """Resolve the local Brain Node API on its Tailnet-only listener."""
    try:
        result = subprocess.run(
            ["tailscale", "ip", "-4"],
            check=False,
            capture_output=True,
            text=True,
            timeout=4,
        )
    except Exception:
        return ""

    for line in result.stdout.splitlines():
        address = line.strip()

        if address:
            return f"http://{address}:8765"

    return ""


def _brain_node_json(url, timeout):
    request = urllib.request.Request(url, method="GET")

    with urllib.request.urlopen(
        request,
        timeout=timeout,
    ) as response:
        return json.loads(
            response.read().decode("utf-8")
        )


def fetch_brain_node_snapshot():
    """Fetch truthful worker state without assigning it authority."""
    base_url = _brain_node_base_url()

    if not base_url:
        return {
            "reachable": False,
            "error": "Tailnet address unavailable",
        }

    try:
        heartbeat = _brain_node_json(
            base_url + "/worker/heartbeat",
            4,
        )
        capabilities = _brain_node_json(
            base_url + "/worker/capabilities",
            12,
        )

        return {
            "reachable": True,
            "base_url": base_url,
            "heartbeat": heartbeat,
            "capabilities": capabilities,
            "error": None,
        }

    except (
        OSError,
        ValueError,
        urllib.error.HTTPError,
        urllib.error.URLError,
    ) as exc:
        return {
            "reachable": False,
            "base_url": base_url,
            "error": f"{type(exc).__name__}: {exc}",
        }


class BrainNodeProbe(QThread):
    """Non-blocking heartbeat probe for the laptop dashboard."""

    completed = pyqtSignal(dict)

    def run(self):
        self.completed.emit(fetch_brain_node_snapshot())


class BrainNodeVisual(QWidget):
    """Animated Brain Node identity driven by real worker heartbeat."""

    def __init__(self):
        super().__init__()

        self.phase = 0.0
        self.node_state = "CONNECTING"
        self.state_detail = "WAITING FOR WORKER HEARTBEAT"
        self.capability_detail = "CAPABILITIES UNKNOWN"
        self.model_detail = "DEEP MODEL UNKNOWN"
        self.authority_detail = "COMMAND CORE AUTHORITY CHECK PENDING"
        self.sequence = 0
        self.probe = None
        self._shutting_down = False

        self.setMinimumHeight(255)
        self.setSizePolicy(
            QSizePolicy.Expanding,
            QSizePolicy.Expanding,
        )

        self.animation_timer = QTimer(self)
        self.animation_timer.setTimerType(Qt.PreciseTimer)
        self.animation_timer.timeout.connect(
            self._animate
        )
        self.animation_timer.start(80)

        self.poll_timer = QTimer(self)
        self.poll_timer.timeout.connect(
            self.refresh_heartbeat
        )
        self.poll_timer.start(3000)

        app = QApplication.instance()

        if app is not None:
            app.aboutToQuit.connect(self.shutdown)

        QTimer.singleShot(
            50,
            self.refresh_heartbeat,
        )

    def _animate(self):
        speed = 0.018

        if self.node_state == "ONLINE":
            speed = 0.075
        elif self.node_state == "DEGRADED":
            speed = 0.038
        elif self.node_state in {"OFFLINE", "AUTHORITY FAULT"}:
            speed = 0.006

        self.phase = (
            self.phase + speed
        ) % (math.pi * 2000.0)

        self.update()

    def refresh_heartbeat(self):
        if self._shutting_down:
            return

        if self.probe is not None:
            return

        probe = BrainNodeProbe(self)
        self.probe = probe

        probe.completed.connect(
            self.apply_snapshot
        )

        probe.finished.connect(
            lambda probe=probe: self._probe_finished(
                probe
            )
        )

        probe.start()

    def _probe_finished(self, probe):
        if self.probe is probe:
            self.probe = None

        probe.deleteLater()

    def shutdown(self):
        if self._shutting_down:
            return

        self._shutting_down = True

        if self.animation_timer.isActive():
            self.animation_timer.stop()

        if self.poll_timer.isActive():
            self.poll_timer.stop()

        probe = self.probe

        if probe is None:
            return

        try:
            probe.completed.disconnect(
                self.apply_snapshot
            )
        except (TypeError, RuntimeError):
            pass

        probe.requestInterruption()

        if probe.isRunning():
            probe.wait(15000)

        if self.probe is probe:
            self.probe = None

        probe.deleteLater()

    def apply_snapshot(self, snapshot):
        if not snapshot.get("reachable"):
            self.node_state = "OFFLINE"
            self.state_detail = "WORKER HEARTBEAT UNAVAILABLE"
            self.capability_detail = "LOCAL DISPLAY ONLY"
            self.model_detail = "NO REMOTE COMPUTE CONNECTION"
            self.authority_detail = "NO AUTHORITY TRANSFER"
            self.update()
            return

        heartbeat = snapshot.get("heartbeat", {})
        capability_payload = snapshot.get(
            "capabilities",
            {},
        )

        reported_state = str(
            heartbeat.get("state", "degraded")
        ).strip().upper()

        if reported_state not in {
            "ONLINE",
            "DEGRADED",
        }:
            reported_state = "DEGRADED"

        ownership = capability_payload.get(
            "ownership",
            heartbeat.get("ownership", {}),
        )

        prohibited_authority = (
            "owns_projects",
            "owns_approvals",
            "owns_memory",
            "owns_shirevault",
            "makes_safety_decisions",
        )

        authority_safe = all(
            ownership.get(name) is False
            for name in prohibited_authority
        )

        if not authority_safe:
            self.node_state = "AUTHORITY FAULT"
            self.authority_detail = (
                "WORKER AUTHORITY BOUNDARY VIOLATION"
            )
        else:
            self.node_state = reported_state
            self.authority_detail = (
                "MINI OWNS PROJECTS • APPROVALS • SAFETY"
            )

        self.sequence = int(
            heartbeat.get("sequence", 0)
        )

        summary = capability_payload.get(
            "summary",
            heartbeat.get("capability_summary", {}),
        )

        available = int(summary.get("available", 0))
        registered = int(summary.get("registered", 0))

        self.capability_detail = (
            f"{available}/{registered} CAPABILITIES AVAILABLE"
        )

        deep_model = "DEEP MODEL UNKNOWN"

        for capability in capability_payload.get(
            "capabilities",
            [],
        ):
            if capability.get("id") == "deep_reasoning":
                dependencies = capability.get(
                    "dependencies",
                    [],
                )

                if dependencies:
                    deep_model = str(dependencies[0])

                break

        self.model_detail = deep_model.upper()

        uptime = heartbeat.get("uptime_seconds", 0)

        self.state_detail = (
            f"HEARTBEAT {self.sequence}  •  "
            f"UPTIME {int(float(uptime))}S"
        )

        self.update()

    def showEvent(self, event):
        super().showEvent(event)

        if self._shutting_down:
            return

        if not self.animation_timer.isActive():
            self.animation_timer.start(80)

        if not self.poll_timer.isActive():
            self.poll_timer.start(3000)

        self.refresh_heartbeat()

    def hideEvent(self, event):
        if self.animation_timer.isActive():
            self.animation_timer.stop()

        if self.poll_timer.isActive():
            self.poll_timer.stop()

        super().hideEvent(event)

    def _state_color(self):
        if self.node_state == "ONLINE":
            return QColor(GREEN)

        if self.node_state == "DEGRADED":
            return QColor("#ffbd4a")

        if self.node_state == "AUTHORITY FAULT":
            return QColor("#ff4265")

        if self.node_state == "CONNECTING":
            return QColor(CYAN)

        return QColor("#7f728f")

    @staticmethod
    def _brain_paths(cx, cy, scale):
        left = QPainterPath()
        left.moveTo(cx - 2 * scale, cy - 68 * scale)
        left.cubicTo(
            cx - 34 * scale,
            cy - 84 * scale,
            cx - 65 * scale,
            cy - 66 * scale,
            cx - 62 * scale,
            cy - 38 * scale,
        )
        left.cubicTo(
            cx - 82 * scale,
            cy - 25 * scale,
            cx - 76 * scale,
            cy + 6 * scale,
            cx - 58 * scale,
            cy + 15 * scale,
        )
        left.cubicTo(
            cx - 70 * scale,
            cy + 43 * scale,
            cx - 48 * scale,
            cy + 69 * scale,
            cx - 25 * scale,
            cy + 60 * scale,
        )
        left.cubicTo(
            cx - 13 * scale,
            cy + 73 * scale,
            cx - 2 * scale,
            cy + 55 * scale,
            cx - 2 * scale,
            cy + 31 * scale,
        )
        left.closeSubpath()

        right = QPainterPath()
        right.moveTo(cx + 2 * scale, cy - 68 * scale)
        right.cubicTo(
            cx + 34 * scale,
            cy - 84 * scale,
            cx + 65 * scale,
            cy - 66 * scale,
            cx + 62 * scale,
            cy - 38 * scale,
        )
        right.cubicTo(
            cx + 82 * scale,
            cy - 25 * scale,
            cx + 76 * scale,
            cy + 6 * scale,
            cx + 58 * scale,
            cy + 15 * scale,
        )
        right.cubicTo(
            cx + 70 * scale,
            cy + 43 * scale,
            cx + 48 * scale,
            cy + 69 * scale,
            cx + 25 * scale,
            cy + 60 * scale,
        )
        right.cubicTo(
            cx + 13 * scale,
            cy + 73 * scale,
            cx + 2 * scale,
            cy + 55 * scale,
            cx + 2 * scale,
            cy + 31 * scale,
        )
        right.closeSubpath()

        return left, right

    def _draw_electricity(
        self,
        painter,
        cx,
        cy,
        radius_x,
        radius_y,
        color,
    ):
        if self.node_state not in {
            "ONLINE",
            "DEGRADED",
        }:
            return

        arc_count = (
            8 if self.node_state == "ONLINE" else 4
        )

        for arc_index in range(arc_count):
            start = (
                self.phase
                + arc_index
                * (math.pi * 2.0 / arc_count)
            )

            path = QPainterPath()

            for point_index in range(9):
                angle = start + point_index * 0.105
                jitter = (
                    math.sin(
                        self.phase * 5.0
                        + arc_index * 2.3
                        + point_index * 3.1
                    )
                    * 6.0
                )

                x = (
                    cx
                    + math.cos(angle)
                    * (radius_x + jitter)
                )
                y = (
                    cy
                    + math.sin(angle)
                    * (radius_y + jitter * 0.45)
                )

                if point_index == 0:
                    path.moveTo(x, y)
                else:
                    path.lineTo(x, y)

            pulse = (
                math.sin(
                    self.phase * 7.0
                    + arc_index
                )
                + 1.0
            ) / 2.0

            electric = QColor(color)
            electric.setAlpha(
                int(95 + pulse * 155)
            )

            painter.setPen(
                QPen(
                    electric,
                    1.2 + pulse * 1.8,
                    Qt.SolidLine,
                    Qt.RoundCap,
                    Qt.RoundJoin,
                )
            )
            painter.setBrush(Qt.NoBrush)
            painter.drawPath(path)

    def paintEvent(self, event):
        del event

        painter = QPainter(self)
        painter.setRenderHint(
            QPainter.Antialiasing,
            True,
        )

        w = float(self.width())
        h = float(self.height())
        cx = w / 2.0
        cy = h * 0.40
        scale = max(
            0.65,
            min(w / 250.0, h / 320.0),
        )

        state_color = self._state_color()

        pulse = (
            math.sin(self.phase * 2.4) + 1.0
        ) / 2.0

        aura = QRadialGradient(
            cx,
            cy,
            118.0 * scale,
        )

        center_color = QColor(state_color)
        center_color.setAlpha(
            int(45 + pulse * 55)
        )

        aura.setColorAt(0.0, center_color)
        aura.setColorAt(
            0.55,
            QColor(60, 15, 105, 40),
        )
        aura.setColorAt(
            1.0,
            QColor(0, 0, 0, 0),
        )

        painter.setPen(Qt.NoPen)
        painter.setBrush(QBrush(aura))
        painter.drawEllipse(
            QRectF(
                cx - 118 * scale,
                cy - 105 * scale,
                236 * scale,
                210 * scale,
            )
        )

        self._draw_electricity(
            painter,
            cx,
            cy,
            93 * scale,
            75 * scale,
            state_color,
        )

        left, right = self._brain_paths(
            cx,
            cy,
            scale,
        )

        brain_fill = QRadialGradient(
            cx,
            cy - 12 * scale,
            92 * scale,
        )
        brain_fill.setColorAt(
            0.0,
            QColor(75, 30, 125, 245),
        )
        brain_fill.setColorAt(
            0.52,
            QColor(36, 12, 73, 245),
        )
        brain_fill.setColorAt(
            1.0,
            QColor(10, 5, 28, 250),
        )

        outline = QColor(state_color)
        outline.setAlpha(
            int(185 + pulse * 65)
        )

        painter.setPen(
            QPen(outline, 2.3)
        )
        painter.setBrush(QBrush(brain_fill))
        painter.drawPath(left)
        painter.drawPath(right)

        groove_pen = QColor(PURPLE_LIGHT)
        groove_pen.setAlpha(150)

        painter.setPen(
            QPen(
                groove_pen,
                1.35,
                Qt.SolidLine,
                Qt.RoundCap,
            )
        )
        painter.setBrush(Qt.NoBrush)

        grooves = (
            (-47, -44, -21, -29, -42, -9),
            (-58, -15, -28, -4, -45, 18),
            (-47, 23, -22, 19, -32, 47),
            (-24, -58, -11, -37, -24, -14),
            (47, -44, 21, -29, 42, -9),
            (58, -15, 28, -4, 45, 18),
            (47, 23, 22, 19, 32, 47),
            (24, -58, 11, -37, 24, -14),
        )

        for x1, y1, cx1, cy1, x2, y2 in grooves:
            path = QPainterPath()
            path.moveTo(
                cx + x1 * scale,
                cy + y1 * scale,
            )
            path.quadTo(
                cx + cx1 * scale,
                cy + cy1 * scale,
                cx + x2 * scale,
                cy + y2 * scale,
            )
            painter.drawPath(path)

        neural_points = (
            (-42, -32),
            (-20, -46),
            (-35, 2),
            (-16, 25),
            (-31, 43),
            (42, -32),
            (20, -46),
            (35, 2),
            (16, 25),
            (31, 43),
            (0, -18),
            (0, 19),
        )

        neural_links = (
            (0, 1),
            (0, 2),
            (1, 10),
            (2, 3),
            (3, 4),
            (3, 11),
            (5, 6),
            (5, 7),
            (6, 10),
            (7, 8),
            (8, 9),
            (8, 11),
            (10, 11),
        )

        for link_index, (first, second) in enumerate(
            neural_links
        ):
            x1, y1 = neural_points[first]
            x2, y2 = neural_points[second]

            glow = (
                math.sin(
                    self.phase * 4.5
                    + link_index * 0.75
                )
                + 1.0
            ) / 2.0

            link_color = QColor(state_color)
            link_color.setAlpha(
                int(55 + glow * 145)
            )

            painter.setPen(
                QPen(link_color, 1.1)
            )
            painter.drawLine(
                int(cx + x1 * scale),
                int(cy + y1 * scale),
                int(cx + x2 * scale),
                int(cy + y2 * scale),
            )

        for point_index, (x, y) in enumerate(
            neural_points
        ):
            node_pulse = (
                math.sin(
                    self.phase * 5.5
                    + point_index * 0.9
                )
                + 1.0
            ) / 2.0

            node_color = QColor(state_color)
            node_color.setAlpha(
                int(145 + node_pulse * 110)
            )

            radius = (
                2.0 + node_pulse * 1.8
            ) * scale

            painter.setPen(Qt.NoPen)
            painter.setBrush(node_color)
            painter.drawEllipse(
                QRectF(
                    cx + x * scale - radius,
                    cy + y * scale - radius,
                    radius * 2,
                    radius * 2,
                )
            )

        label_top = h - 82

        painter.setPen(state_color)
        painter.setFont(
            QFont(
                "DejaVu Sans Mono",
                13,
                QFont.Bold,
            )
        )
        painter.drawText(
            QRectF(0, label_top, w, 20),
            Qt.AlignCenter,
            f"BRAIN NODE 01  •  {self.node_state}",
        )

        painter.setPen(QColor(PURPLE_LIGHT))
        painter.setFont(
            QFont(
                "DejaVu Sans Mono",
                8,
                QFont.Bold,
            )
        )
        painter.drawText(
            QRectF(0, label_top + 21, w, 15),
            Qt.AlignCenter,
            self.state_detail,
        )

        painter.setPen(QColor(CYAN))
        painter.drawText(
            QRectF(0, label_top + 37, w, 15),
            Qt.AlignCenter,
            self.capability_detail,
        )

        painter.setPen(QColor(MUTED))
        painter.drawText(
            QRectF(0, label_top + 53, w, 15),
            Qt.AlignCenter,
            self.model_detail,
        )

        authority_color = (
            QColor(GREEN)
            if self.node_state != "AUTHORITY FAULT"
            else QColor("#ff4265")
        )

        painter.setPen(authority_color)
        painter.drawText(
            QRectF(0, label_top + 68, w, 14),
            Qt.AlignCenter,
            self.authority_detail,
        )


class VoiceOrb(QWidget):
    """Visual mirror of the approved manual voice state."""

    def __init__(self):
        super().__init__()

        self.setMinimumHeight(104)
        self.setMaximumHeight(116)
        self.setSizePolicy(
            QSizePolicy.Expanding,
            QSizePolicy.Fixed,
        )

        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update)
        self.timer.start(300)

    def paintEvent(self, event):
        del event

        snapshot = voice_privacy_foundation.snapshot()
        state = str(snapshot.state).upper()

        listening = state == "LISTENING"
        processing = state == "PROCESSING"
        ready = state == "READY"
        error = state == "ERROR"

        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing, True)

        w = self.width()
        h = self.height()
        cx = w / 2
        cy = 42

        if error:
            orb_color = QColor("#ff5c70")
        elif listening:
            orb_color = QColor(GREEN)
        elif processing:
            orb_color = QColor(CYAN)
        elif ready:
            orb_color = QColor(PURPLE_LIGHT)
        else:
            orb_color = QColor(PURPLE)

        for index, width_ratio in enumerate(
            (0.42, 0.32, 0.23)
        ):
            rect_width = w * width_ratio

            painter.setPen(
                QPen(
                    QColor(
                        orb_color.red(),
                        orb_color.green(),
                        orb_color.blue(),
                        105 - index * 20,
                    ),
                    1.4,
                )
            )

            painter.setBrush(Qt.NoBrush)
            painter.drawEllipse(
                QRectF(
                    cx - rect_width / 2,
                    cy - 25 - index * 3,
                    rect_width,
                    50 + index * 6,
                )
            )

        painter.setPen(QPen(orb_color, 2))
        painter.setBrush(QColor(18, 7, 42, 240))
        painter.drawEllipse(
            QRectF(cx - 31, cy - 31, 62, 62)
        )

        painter.setPen(QPen(orb_color, 4))
        painter.setBrush(Qt.NoBrush)
        painter.drawRoundedRect(
            QRectF(cx - 8, cy - 16, 16, 29),
            8,
            8,
        )
        painter.drawLine(
            int(cx),
            int(cy + 14),
            int(cx),
            int(cy + 24),
        )
        painter.drawLine(
            int(cx - 9),
            int(cy + 24),
            int(cx + 9),
            int(cy + 24),
        )

        if listening:
            state_text = "LISTENING"
            detail_text = "Speech is filling the top prompt"

        elif processing:
            state_text = "PROCESSING"
            detail_text = "Finalising local transcript"

        elif ready:
            state_text = "VOICE READY"
            detail_text = "Use MIC in the top prompt bar"

        elif error:
            state_text = "VOICE ERROR"
            detail_text = snapshot.error or "Check voice configuration"

        else:
            state_text = "MICROPHONE OFF"
            detail_text = "Voice capture is unavailable"

        painter.setPen(orb_color)
        painter.setFont(
            QFont("DejaVu Sans Mono", 11, QFont.Bold)
        )
        painter.drawText(
            QRectF(0, 76, w, 18),
            Qt.AlignCenter,
            state_text,
        )

        painter.setPen(QColor(MUTED))
        painter.setFont(
            QFont("DejaVu Sans Mono", 8, QFont.Bold)
        )
        painter.drawText(
            QRectF(0, 94, w, 16),
            Qt.AlignCenter,
            detail_text,
        )


class LaptopDashboard(QWidget):
    """SHIRE AIOS face-first Presence Console."""

    def __init__(self, stack):
        super().__init__()

        self.stack = stack
        self.station_drawer = None
        self.build_ui()

    def _open_station(self, target_index):
        self.stack.setCurrentIndex(target_index)

        if self.station_drawer is not None:
            self.station_drawer.setVisible(False)

    def _make_nav_button(
        self,
        icon,
        label,
        target_index,
        compact=True,
    ):
        button = LaptopRuneButton(
            icon,
            label,
            "",
            target_index,
            self.stack,
            compact=compact,
        )

        button.clicked.disconnect()
        button.clicked.connect(
            lambda checked=False, index=target_index:
            self._open_station(index)
        )

        return button

    def _toggle_stations(self):
        visible = not self.station_drawer.isVisible()
        self.station_drawer.setVisible(visible)

    def _build_greeting_card(self):
        card = PresenceCard("SHIRE PRESENCE", PURPLE_LIGHT)

        greeting = QLabel(_greeting())
        greeting.setStyleSheet(
            f"color:{PURPLE_LIGHT}; "
            "font-size:23px; font-weight:900;"
        )

        message = QLabel(
            "The Workshop is ready.\n"
            "No proposal is active.\n\n"
            "How can I help you today?"
        )

        message.setWordWrap(True)
        message.setStyleSheet(
            "color:#d7c8f4; "
            "font-size:14px; "
            "font-weight:700; "
            "line-height:1.35;"
        )

        card.column.addWidget(greeting)
        card.column.addWidget(message)
        card.column.addStretch(1)

        return card

    def _build_status_card(self):
        card = PresenceCard("SYSTEM STATUS", CYAN)

        rows = (
            ("CORE", "ONLINE", GREEN),
            ("SESSION", "LOCAL X11", CYAN),
            ("MICROPHONE", "CLOSED", PURPLE_LIGHT),
            ("AUTHORITY", "RAY APPROVAL", GREEN),
        )

        for title, value, accent in rows:
            row = QHBoxLayout()

            left = QLabel(title)
            left.setStyleSheet(
                "color:#8e82aa; "
                "font-size:9px; "
                "font-weight:900;"
            )

            right = QLabel(value)
            right.setAlignment(Qt.AlignRight)
            right.setStyleSheet(
                f"color:{accent}; "
                "font-size:11px; "
                "font-weight:900;"
            )

            row.addWidget(left)
            row.addStretch(1)
            row.addWidget(right)

            card.column.addLayout(row)

        return card

    def _build_brain_node_card(self):
        card = PresenceCard(
            "DISTRIBUTED BRAIN NODE",
            CYAN,
        )

        self.brain_node_visual = BrainNodeVisual()
        card.column.addWidget(
            self.brain_node_visual,
            1,
        )

        return card

    def _build_thought_card(self):
        card = PresenceCard("CURRENT THOUGHT", PURPLE_LIGHT)

        concept = QLabel("◌")
        concept.setAlignment(Qt.AlignCenter)
        concept.setMinimumHeight(88)
        concept.setStyleSheet(
            "color:#b66cff; "
            "font-size:64px; "
            "font-weight:500;"
        )

        title = QLabel("NO ACTIVE DESIGN PREVIEW")
        title.setAlignment(Qt.AlignCenter)
        title.setStyleSheet(
            f"color:{PURPLE_LIGHT}; "
            "font-size:12px; "
            "font-weight:900;"
        )

        note = QLabel(
            "Approved STL concepts and Workshop ideas "
            "will appear here."
        )

        note.setWordWrap(True)
        note.setAlignment(Qt.AlignCenter)
        note.setStyleSheet(
            "color:#9e91ba; "
            "font-size:10px; "
            "font-weight:700;"
        )

        card.column.addWidget(concept)
        card.column.addWidget(title)
        card.column.addWidget(note)
        card.column.addStretch(1)

        return card

    def _build_next_card(self):
        card = PresenceCard("NEXT UP", CYAN)

        items = (
            "Open Workshop",
            "Review Sentinel",
            "Continue Education",
        )

        for item in items:
            label = QLabel(f"›  {item}")
            label.setStyleSheet(
                "color:#c8afff; "
                "font-size:12px; "
                "font-weight:800; "
                "padding:5px 0;"
            )
            card.column.addWidget(label)

        card.column.addStretch(1)

        return card

    def _build_station_drawer(self):
        drawer = QFrame()
        drawer.setObjectName("stationDrawer")
        drawer.setVisible(False)

        drawer.setStyleSheet("""
            QFrame#stationDrawer {
                background: rgba(5, 4, 18, 240);
                border: 1px solid rgba(155, 77, 255, 180);
                border-radius: 16px;
            }
        """)

        grid = QGridLayout(drawer)
        grid.setContentsMargins(12, 12, 12, 12)
        grid.setHorizontalSpacing(10)
        grid.setVerticalSpacing(10)

        stations = (
            ("▤", "MEMORY", 3),
            ("◇", "ATLAS", 4),
            ("✦", "EDUCATION", 11),
            ("♟", "AGENTS", 7),
            ("☷", "TASKS", 8),
            ("⚙", "SYSTEM", 6),
            ("⌁", "SETTINGS", 9),
            ("⌨", "COMMAND", 1),
        )

        for index, (icon, label, target) in enumerate(stations):
            button = self._make_nav_button(
                icon,
                label,
                target,
                compact=True,
            )

            grid.addWidget(
                button,
                index // 4,
                index % 4,
            )

        return drawer

    def build_ui(self):
        self.setObjectName("presenceConsole")

        self.setStyleSheet("""
            QWidget#presenceConsole {
                background: #020208;
                color: #f4efff;
                font-family: "DejaVu Sans Mono", monospace;
            }
            QLabel {
                background: transparent;
            }
        """)

        root = QVBoxLayout(self)
        root.setContentsMargins(26, 20, 26, 16)
        root.setSpacing(12)

        header = QHBoxLayout()
        header.setSpacing(18)

        brand_column = QVBoxLayout()
        brand_column.setSpacing(0)

        brand = QLabel("SHIRE BRAIN NODE 01")
        brand.setStyleSheet(
            f"color:{PURPLE}; "
            "font-size:27px; "
            "font-weight:900; "
            "letter-spacing:2px;"
        )

        core_state = QLabel("DISTRIBUTED WORKER  •  SHIRE MINI COMMAND CORE")
        core_state.setStyleSheet(
            f"color:{GREEN}; "
            "font-size:10px; "
            "font-weight:900;"
        )

        brand_column.addWidget(brand)
        brand_column.addWidget(core_state)

        header.addLayout(brand_column)
        header.addStretch(1)
        header.addWidget(VoicePrivacyBanner())

        root.addLayout(header)

        stage = QHBoxLayout()
        stage.setSpacing(14)

        left_column = QVBoxLayout()
        left_column.setSpacing(12)
        left_column.addWidget(self._build_greeting_card(), 3)
        left_column.addWidget(self._build_status_card(), 2)

        center_column = QVBoxLayout()
        center_column.setSpacing(2)

        center_title = QLabel("SHIRE PRESENCE")
        center_title.setAlignment(Qt.AlignCenter)
        center_title.setStyleSheet(
            f"color:{PURPLE_LIGHT}; "
            "font-size:12px; "
            "font-weight:900; "
            "letter-spacing:2px;"
        )

        center_column.addWidget(center_title)
        center_column.addWidget(ShireCenterCore(), 1)
        center_column.addWidget(VoiceOrb())

        right_column = QVBoxLayout()
        right_column.setSpacing(12)
        right_column.addWidget(self._build_brain_node_card(), 3)
        right_column.addWidget(self._build_next_card(), 2)

        stage.addLayout(left_column, 5)
        stage.addLayout(center_column, 8)
        stage.addLayout(right_column, 5)

        root.addLayout(stage, 1)

        dock = QHBoxLayout()
        dock.setSpacing(10)

        dock.addWidget(
            self._make_nav_button("●", "SHIRE", 1)
        )

        dock.addWidget(
            self._make_nav_button("⚒", "FORGE", 2)
        )

        dock.addWidget(
            self._make_nav_button("♜", "SENTINEL", 10)
        )

        stations_button = QPushButton("☰  STATIONS")
        stations_button.setCursor(Qt.PointingHandCursor)
        stations_button.setMinimumHeight(48)

        stations_button.setStyleSheet("""
            QPushButton {
                background: rgba(8, 5, 25, 225);
                color: #00f58a;
                border: 1px solid rgba(0, 245, 138, 185);
                border-radius: 13px;
                font-family: "DejaVu Sans Mono", monospace;
                font-size: 13px;
                font-weight: 900;
                padding: 8px 12px;
            }
            QPushButton:hover {
                background: rgba(0, 55, 39, 230);
                color: #ffffff;
                border: 1px solid #00f58a;
            }
            QPushButton:pressed {
                background: rgba(155, 77, 255, 75);
            }
        """)

        stations_button.clicked.connect(
            self._toggle_stations
        )

        dock.addWidget(stations_button)

        root.addLayout(dock)

        self.station_drawer = self._build_station_drawer()
        root.addWidget(self.station_drawer)

        hint = QLabel(
            "VOICE PREVIEW READY  •  "
            "Use MIC in the top prompt bar  •  "
            "Audio is processed locally and discarded"
        )

        hint.setAlignment(Qt.AlignCenter)
        hint.setStyleSheet(
            "color:#8f82a8; "
            "font-size:9px; "
            "font-weight:800; "
            "padding:1px;"
        )

        root.addWidget(hint)

    def paintEvent(self, event):
        del event

        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing, True)

        gradient = QLinearGradient(
            0,
            0,
            self.width(),
            self.height(),
        )

        gradient.setColorAt(0.0, QColor(2, 2, 9))
        gradient.setColorAt(0.48, QColor(7, 3, 18))
        gradient.setColorAt(1.0, QColor(2, 2, 8))

        painter.fillRect(
            self.rect(),
            QBrush(gradient),
        )

        painter.setPen(
            QPen(QColor(113, 50, 190, 22), 1)
        )

        spacing = 54

        for x in range(0, self.width(), spacing):
            painter.drawLine(x, 0, x, self.height())

        for y in range(0, self.height(), spacing):
            painter.drawLine(0, y, self.width(), y)

        center_glow = QRadialGradient(
            self.width() / 2,
            self.height() / 2,
            self.width() * 0.42,
        )

        center_glow.setColorAt(
            0.0,
            QColor(106, 25, 180, 50),
        )

        center_glow.setColorAt(
            1.0,
            QColor(0, 0, 0, 0),
        )

        painter.setPen(Qt.NoPen)
        painter.setBrush(QBrush(center_glow))
        painter.drawEllipse(
            QRectF(
                self.width() * 0.14,
                -self.height() * 0.10,
                self.width() * 0.72,
                self.height() * 1.20,
            )
        )
