import json
import urllib.error
import urllib.request
from pathlib import Path

from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal
from PyQt5.QtGui import QDesktopServices, QFont
from PyQt5.QtWidgets import (
    QButtonGroup,
    QComboBox,
    QFrame,
    QGridLayout,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QPushButton,
    QScrollArea,
    QSizePolicy,
    QSplitter,
    QVBoxLayout,
    QWidget,
)

from framework.warden_theme import *
from framework.warden_widgets import WardenBackground


ARMOR_ROOT = Path("/home/shire3d/ARMOR")
REGISTRY_FILE = ARMOR_ROOT / "config/agents/registry.json"
BONUS_ROOT = Path("/SHiREVault/BONUS_APPS")
AGENT_HUB_URL = "http://127.0.0.1:8770"

TAILSCALE_IP = "100.116.93.90"

SOURCE_AGENT_MAP = {
    "FlexiForge_SHIRE_v0.2.0.zip": "flexiforge",
    "limb-forge-canvas.zip": "covercanvas",
    "shire-limb-forge.zip": "limbforge",
    "shire-scan-forge-shire-agent-ready.zip": "scanforge",
    "SHIRE-Marketing-Forge-Easy-v1.3.0.zip": "marketing-boss",
    "SHIRE3D-Manager-Standalone-v0.1.0.zip": "shire3d-manager",
    "SHiRE-Dental-Studio-Independent-v2.0.0.zip": "dental-studio",
    "shire-link-prime-live-shell.zip": "link-prime",
}

SYNTHETIC_AGENTS = [
    {
        "id": "relay",
        "name": "RELAY",
        "nickname": "Remote Thingy",
        "category": "Infrastructure and Devices",
        "description": (
            "Private remote-device manager for SHiRE Mobile OTA releases, "
            "approved phones, wearables and future SHiRE nodes."
        ),
        "declaredStatus": "STAGED_OTA_AND_DEVICE_BRIDGE_REQUIRED",
        "enabled": False,
        "route": "/agents/relay",
        "localUrl": None,
        "appPath": "agents/apps/relay",
    },
    {
        "id": "shire3d-manager",
        "name": "SHiRE3D Manager",
        "category": "Commerce and Operations",
        "description": (
            "Orders, marketplace listings, pricing, shipping and product "
            "operations source recovered from BONUS_APPS."
        ),
        "declaredStatus": "SOURCE_AVAILABLE_IN_BONUS_APPS",
        "enabled": False,
        "route": "/agents/shire3d-manager",
        "localUrl": None,
        "appPath": "BONUS_APPS/SHIRE3D-Manager-Standalone-v0.1.0.zip",
    },
    {
        "id": "dental-studio",
        "name": "SHiRE Dental Studio",
        "category": "Specialist Design",
        "description": (
            "Independent dental-design workspace source. Clinical and "
            "manufacturing functions remain safety-gated."
        ),
        "declaredStatus": "SOURCE_AVAILABLE_SAFETY_REVIEW_REQUIRED",
        "enabled": False,
        "route": "/agents/dental-studio",
        "localUrl": None,
        "appPath": "BONUS_APPS/SHiRE-Dental-Studio-Independent-v2.0.0.zip",
    },
    {
        "id": "link-prime",
        "name": "SHiRE Link Prime",
        "category": "Companion and Communication",
        "description": (
            "Live SHiRE companion shell with chat, device connection, "
            "printer selection and command-centre concepts."
        ),
        "declaredStatus": "SOURCE_AVAILABLE_BASE44_REMOVAL_REQUIRED",
        "enabled": False,
        "route": "/agents/link-prime",
        "localUrl": None,
        "appPath": "BONUS_APPS/shire-link-prime-live-shell.zip",
    },
]


def http_json(url, timeout=2.5):
    try:
        request = urllib.request.Request(
            url,
            headers={"Accept": "application/json"},
        )
        with urllib.request.urlopen(request, timeout=timeout) as response:
            return response.status, json.load(response)
    except Exception as exc:
        return 0, {"error": str(exc)}


def load_registry():
    try:
        payload = json.loads(REGISTRY_FILE.read_text(encoding="utf-8"))
        return payload.get("agents", [])
    except Exception:
        return []


def source_files():
    if not BONUS_ROOT.is_dir():
        return {}

    result = {}
    for filename, agent_id in SOURCE_AGENT_MAP.items():
        path = BONUS_ROOT / filename
        if path.exists():
            result.setdefault(agent_id, []).append(path)
    return result


def friendly_status(raw, enabled=False, online=False):
    value = str(raw or "").upper()

    if online:
        return "LIVE"

    if "ERROR" in value or "FAILED" in value:
        return "ERROR"

    if "BLOCK" in value:
        return "BLOCKED"

    if enabled:
        return "READY"

    if "READY" in value and "REQUIRED" not in value:
        return "READY"

    if "SOURCE_AVAILABLE" in value:
        return "SOURCE"

    if "STAGED" in value or "REQUIRED" in value:
        return "STAGED"

    return "OFFLINE"


def status_colour(status):
    return {
        "LIVE": "#20e887",
        "READY": "#53b7ff",
        "STAGED": "#b174ff",
        "SOURCE": "#f2b84b",
        "BLOCKED": "#ff8c42",
        "ERROR": "#ff4d5f",
        "OFFLINE": "#758391",
    }.get(status, "#758391")


class AgentCard(QFrame):
    selected = pyqtSignal(str)

    def __init__(self, agent_id):
        super().__init__()
        self.agent_id = agent_id
        self.data = {}
        self.setCursor(Qt.PointingHandCursor)
        self.setMinimumHeight(190)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

        root = QVBoxLayout(self)
        root.setContentsMargins(17, 15, 17, 15)
        root.setSpacing(8)

        top = QHBoxLayout()

        self.icon = QLabel("◆")
        self.icon.setFixedSize(42, 42)
        self.icon.setAlignment(Qt.AlignCenter)

        name_box = QVBoxLayout()
        name_box.setSpacing(1)

        self.name = QLabel()
        self.name.setWordWrap(True)

        self.category = QLabel()
        self.category.setWordWrap(True)

        name_box.addWidget(self.name)
        name_box.addWidget(self.category)

        self.status = QLabel()
        self.status.setAlignment(Qt.AlignCenter)
        self.status.setMinimumWidth(78)

        top.addWidget(self.icon)
        top.addLayout(name_box, 1)
        top.addWidget(self.status)

        self.description = QLabel()
        self.description.setWordWrap(True)
        self.description.setMaximumHeight(48)

        self.task = QLabel()
        self.task.setWordWrap(True)

        footer = QHBoxLayout()

        self.source_badge = QLabel()
        self.brain_badge = QLabel()
        self.open_button = QPushButton("OPEN")
        self.open_button.clicked.connect(self.open_workspace)

        footer.addWidget(self.source_badge)
        footer.addWidget(self.brain_badge)
        footer.addStretch(1)
        footer.addWidget(self.open_button)

        root.addLayout(top)
        root.addWidget(self.description)
        root.addWidget(self.task)
        root.addStretch(1)
        root.addLayout(footer)

    def mousePressEvent(self, event):
        self.selected.emit(self.agent_id)
        super().mousePressEvent(event)

    def update_agent(self, data):
        self.data = data
        status = data.get("uiStatus", "OFFLINE")
        colour = status_colour(status)

        self.setStyleSheet(
            f"""
            AgentCard {{
                background: rgba(7, 13, 22, 244);
                border: 2px solid {colour};
                border-radius: 18px;
            }}
            AgentCard:hover {{
                background: rgba(12, 22, 35, 248);
                border: 2px solid #e8fff2;
            }}
            """
        )

        self.icon.setText(data.get("icon", "◆"))
        self.icon.setStyleSheet(
            f"""
            background: {colour};
            color: #02040a;
            border-radius: 21px;
            font-size: 21px;
            font-weight: 900;
            """
        )

        self.name.setText(data.get("name", self.agent_id))
        self.name.setStyleSheet(
            "color:#e8fff2;font-size:16px;font-weight:900;"
            "background:transparent;border:none;"
        )

        self.category.setText(data.get("category", "Specialist Agent"))
        self.category.setStyleSheet(
            "color:#91a0aa;font-size:10px;font-weight:700;"
            "background:transparent;border:none;"
        )

        self.status.setText(status)
        self.status.setStyleSheet(
            f"""
            color:{colour};
            background:rgba(0,0,0,100);
            border:1px solid {colour};
            border-radius:10px;
            padding:6px 9px;
            font-size:9px;
            font-weight:900;
            """
        )

        self.description.setText(data.get("description", "No description supplied."))
        self.description.setStyleSheet(
            "color:#cbd6dc;font-size:10px;background:transparent;border:none;"
        )

        detail = data.get("statusText") or data.get("declaredStatus") or "No live status."
        self.task.setText(detail.replace("_", " ").title())
        self.task.setStyleSheet(
            f"""
            color:{colour};
            background:rgba(0,0,0,80);
            border:none;
            border-radius:8px;
            padding:6px;
            font-family:monospace;
            font-size:9px;
            """
        )

        sources = data.get("sourceFiles", [])
        self.source_badge.setText(f"SOURCE {len(sources)}" if sources else "NO SOURCE")
        self.source_badge.setStyleSheet(
            "color:#f2b84b;font-size:8px;font-weight:800;"
            "background:transparent;border:none;"
        )

        brain = data.get("brain", {})
        fast = bool(brain.get("fastAvailable"))
        deep = bool(brain.get("deepAvailable"))
        self.brain_badge.setText(
            "BRAINS ✓✓" if fast and deep else "BRAINS PARTIAL"
        )
        self.brain_badge.setStyleSheet(
            "color:#53b7ff;font-size:8px;font-weight:800;"
            "background:transparent;border:none;"
        )

        url = data.get("localUrl")
        self.open_button.setEnabled(bool(url))
        self.open_button.setText("OPEN" if url else "STAGED")
        self.open_button.setStyleSheet(
            f"""
            QPushButton {{
                color:#02040a;
                background:{colour if url else '#53606a'};
                border:none;
                border-radius:9px;
                padding:7px 12px;
                font-size:9px;
                font-weight:900;
            }}
            QPushButton:disabled {{
                color:#b9c1c6;
                background:#303942;
            }}
            """
        )

    def open_workspace(self):
        url = self.data.get("localUrl")
        if url:
            QDesktopServices.openUrl(QUrl(url))


class AgentDetails(QFrame):
    refresh_requested = pyqtSignal()

    def __init__(self):
        super().__init__()
        self.data = {}

        self.setMinimumWidth(380)
        self.setStyleSheet(
            """
            AgentDetails {
                background:rgba(4,8,14,248);
                border:1px solid #263847;
                border-radius:20px;
            }
            """
        )

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

        self.heading = QLabel("SELECT AN AGENT")
        self.heading.setWordWrap(True)
        self.heading.setStyleSheet(
            "color:#00ff66;font-size:22px;font-weight:900;"
            "background:transparent;border:none;"
        )

        self.subheading = QLabel("Agent details and controls will appear here.")
        self.subheading.setWordWrap(True)
        self.subheading.setStyleSheet(
            "color:#91a0aa;font-size:11px;background:transparent;border:none;"
        )

        self.status = QLabel("NO AGENT SELECTED")
        self.status.setAlignment(Qt.AlignCenter)

        self.summary = QLabel()
        self.summary.setWordWrap(True)
        self.summary.setTextInteractionFlags(Qt.TextSelectableByMouse)
        self.summary.setStyleSheet(
            """
            color:#dce8ed;
            background:rgba(0,0,0,80);
            border:1px solid #1d2b35;
            border-radius:12px;
            padding:12px;
            font-family:monospace;
            font-size:10px;
            """
        )

        self.open_button = QPushButton("OPEN WORKSPACE")
        self.open_button.clicked.connect(self.open_workspace)

        self.health_button = QPushButton("REFRESH HEALTH")
        self.health_button.clicked.connect(self.refresh_requested.emit)

        self.source_button = QPushButton("OPEN SOURCE")
        self.source_button.clicked.connect(self.open_source)

        self.evidence_button = QPushButton("OPEN EVIDENCE")
        self.evidence_button.clicked.connect(self.open_evidence)

        for button in (
            self.open_button,
            self.health_button,
            self.source_button,
            self.evidence_button,
        ):
            button.setMinimumHeight(40)
            button.setStyleSheet(
                """
                QPushButton {
                    color:#e8fff2;
                    background:#17232d;
                    border:1px solid #385063;
                    border-radius:10px;
                    padding:8px;
                    font-size:10px;
                    font-weight:900;
                }
                QPushButton:hover {
                    background:#24394a;
                    border-color:#00ff66;
                }
                QPushButton:disabled {
                    color:#65727c;
                    background:#111820;
                    border-color:#27323a;
                }
                """
            )

        root.addWidget(self.heading)
        root.addWidget(self.subheading)
        root.addWidget(self.status)
        root.addWidget(self.summary, 1)
        root.addWidget(self.open_button)
        root.addWidget(self.health_button)
        root.addWidget(self.source_button)
        root.addWidget(self.evidence_button)

    def show_agent(self, data):
        self.data = data
        status = data.get("uiStatus", "OFFLINE")
        colour = status_colour(status)

        self.heading.setText(data.get("name", "Unnamed Agent"))

        nickname = data.get("nickname")
        subtitle = data.get("category", "Specialist Agent")
        if nickname:
            subtitle = f"{subtitle} · {nickname}"

        self.subheading.setText(subtitle)

        self.status.setText(status)
        self.status.setStyleSheet(
            f"""
            color:{colour};
            background:rgba(0,0,0,100);
            border:1px solid {colour};
            border-radius:12px;
            padding:9px;
            font-size:11px;
            font-weight:900;
            """
        )

        brain = data.get("brain", {})
        memory = data.get("memory", {})
        application = data.get("application", {})
        source_names = [
            Path(path).name for path in data.get("sourceFiles", [])
        ]

        lines = [
            data.get("description", "No description."),
            "",
            f"ID: {data.get('id', 'unknown')}",
            f"Declared: {data.get('declaredStatus', 'unknown')}",
            f"Enabled: {data.get('enabled', False)}",
            f"Route: {data.get('route') or 'not assigned'}",
            f"Workspace: {data.get('localUrl') or 'not yet available'}",
            "",
            f"Fast brain: {brain.get('fastModel', 'not declared')}",
            f"Fast available: {brain.get('fastAvailable', False)}",
            f"Deep brain: {brain.get('deepModel', 'not declared')}",
            f"Deep available: {brain.get('deepAvailable', False)}",
            "",
            f"Memory root: {memory.get('root', 'not reported')}",
            f"Memory isolated: {memory.get('isolated', 'unknown')}",
            f"Application online: {application.get('online', False)}",
            "",
            "Source packages:",
        ]

        lines.extend(
            [f"• {name}" for name in source_names]
            if source_names
            else ["• No matching BONUS_APPS source detected"]
        )

        lines.extend(
            [
                "",
                "Safety:",
                "• External actions remain approval-gated",
                "• Staged Agents are not presented as operational",
                "• No service controls are enabled by this screen yet",
            ]
        )

        self.summary.setText("\n".join(lines))

        self.open_button.setEnabled(bool(data.get("localUrl")))
        self.source_button.setEnabled(bool(data.get("sourceFiles")))

        evidence = (
            ARMOR_ROOT / "data" / "agents" / str(data.get("id")) / "evidence"
        )
        self.evidence_button.setEnabled(evidence.exists())

    def open_workspace(self):
        url = self.data.get("localUrl")
        if url:
            QDesktopServices.openUrl(QUrl(url))

    def open_source(self):
        sources = self.data.get("sourceFiles", [])
        if sources:
            QDesktopServices.openUrl(QUrl.fromLocalFile(str(Path(sources[0]).parent)))

    def open_evidence(self):
        path = ARMOR_ROOT / "data" / "agents" / str(self.data.get("id")) / "evidence"
        if path.exists():
            QDesktopServices.openUrl(QUrl.fromLocalFile(str(path)))


class AgentsPage(WardenBackground):
    def __init__(self, stack):
        super().__init__()
        self.stack = stack
        self.cards = {}
        self.agents = {}
        self.selected_agent_id = None

        main = QVBoxLayout(self)
        main.setContentsMargins(18, 14, 18, 14)
        main.setSpacing(12)

        header = QHBoxLayout()

        title_box = QVBoxLayout()
        title_box.setSpacing(1)

        title = QLabel("SHiRE AGENT OPERATIONS CENTRE")
        title.setStyleSheet(
            "color:#00ff66;font-size:23px;font-weight:900;"
            "background:transparent;border:none;"
        )

        subtitle = QLabel(
            "Live specialist brains, workspaces, source packages and safety states"
        )
        subtitle.setStyleSheet(
            "color:#91a0aa;font-size:10px;font-weight:700;"
            "background:transparent;border:none;"
        )

        title_box.addWidget(title)
        title_box.addWidget(subtitle)

        self.hub_status = QLabel("CONNECTING")
        self.hub_status.setAlignment(Qt.AlignCenter)
        self.hub_status.setMinimumWidth(145)

        back = QPushButton("RETURN HOME")
        back.setMinimumHeight(42)
        back.clicked.connect(lambda: self.stack.setCurrentIndex(0))
        back.setStyleSheet(
            """
            QPushButton {
                color:#02040a;
                background:#00ff66;
                border:none;
                border-radius:12px;
                padding:9px 16px;
                font-size:10px;
                font-weight:900;
            }
            QPushButton:hover { background:#78ffaa; }
            """
        )

        header.addLayout(title_box, 1)
        header.addWidget(self.hub_status)
        header.addWidget(back)

        main.addLayout(header)

        metrics = QHBoxLayout()
        metrics.setSpacing(9)

        self.total_metric = self.metric("0", "REGISTERED")
        self.live_metric = self.metric("0", "LIVE")
        self.ready_metric = self.metric("0", "READY")
        self.staged_metric = self.metric("0", "STAGED")
        self.source_metric = self.metric("0", "SOURCE PACKAGES")

        for widget in (
            self.total_metric,
            self.live_metric,
            self.ready_metric,
            self.staged_metric,
            self.source_metric,
        ):
            metrics.addWidget(widget)

        main.addLayout(metrics)

        toolbar = QHBoxLayout()

        self.search = QLineEdit()
        self.search.setPlaceholderText("Search Agents, categories or capabilities…")
        self.search.textChanged.connect(self.apply_filters)
        self.search.setMinimumHeight(40)
        self.search.setStyleSheet(
            """
            QLineEdit {
                color:#e8fff2;
                background:#08111a;
                border:1px solid #304657;
                border-radius:11px;
                padding:8px 13px;
                font-size:11px;
            }
            QLineEdit:focus { border-color:#00ff66; }
            """
        )

        self.filter = QComboBox()
        self.filter.addItems(
            ["ALL STATES", "LIVE", "READY", "STAGED", "SOURCE", "BLOCKED", "ERROR"]
        )
        self.filter.currentTextChanged.connect(self.apply_filters)
        self.filter.setMinimumHeight(40)
        self.filter.setStyleSheet(
            """
            QComboBox {
                color:#e8fff2;
                background:#08111a;
                border:1px solid #304657;
                border-radius:11px;
                padding:7px 12px;
                min-width:145px;
            }
            """
        )

        refresh = QPushButton("REFRESH LIVE STATUS")
        refresh.clicked.connect(self.refresh)
        refresh.setMinimumHeight(40)
        refresh.setStyleSheet(
            """
            QPushButton {
                color:#e8fff2;
                background:#162736;
                border:1px solid #3d5b70;
                border-radius:11px;
                padding:8px 14px;
                font-size:10px;
                font-weight:900;
            }
            QPushButton:hover { border-color:#00ff66; }
            """
        )

        toolbar.addWidget(self.search, 1)
        toolbar.addWidget(self.filter)
        toolbar.addWidget(refresh)

        main.addLayout(toolbar)

        splitter = QSplitter(Qt.Horizontal)
        splitter.setChildrenCollapsible(False)

        self.scroll = QScrollArea()
        self.scroll.setWidgetResizable(True)
        self.scroll.setFrameShape(QFrame.NoFrame)
        self.scroll.setStyleSheet("background:transparent;border:none;")

        self.card_host = QWidget()
        self.card_host.setStyleSheet("background:transparent;")

        self.card_grid = QGridLayout(self.card_host)
        self.card_grid.setContentsMargins(2, 2, 8, 2)
        self.card_grid.setHorizontalSpacing(13)
        self.card_grid.setVerticalSpacing(13)
        self.card_grid.setAlignment(Qt.AlignTop)

        self.scroll.setWidget(self.card_host)

        self.details = AgentDetails()
        self.details.refresh_requested.connect(self.refresh)

        splitter.addWidget(self.scroll)
        splitter.addWidget(self.details)
        splitter.setStretchFactor(0, 3)
        splitter.setStretchFactor(1, 1)
        splitter.setSizes([1150, 410])

        main.addWidget(splitter, 1)

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

        self.refresh()

    @staticmethod
    def metric(value, label):
        frame = QFrame()
        frame.setStyleSheet(
            """
            QFrame {
                background:rgba(5,11,18,238);
                border:1px solid #263c4c;
                border-radius:13px;
            }
            """
        )

        layout = QVBoxLayout(frame)
        layout.setContentsMargins(13, 8, 13, 8)
        layout.setSpacing(0)

        number = QLabel(value)
        number.setObjectName("value")
        number.setAlignment(Qt.AlignCenter)
        number.setStyleSheet(
            "color:#e8fff2;font-size:18px;font-weight:900;"
            "background:transparent;border:none;"
        )

        caption = QLabel(label)
        caption.setAlignment(Qt.AlignCenter)
        caption.setStyleSheet(
            "color:#91a0aa;font-size:8px;font-weight:900;"
            "background:transparent;border:none;"
        )

        layout.addWidget(number)
        layout.addWidget(caption)

        return frame

    @staticmethod
    def set_metric(frame, value):
        label = frame.findChild(QLabel, "value")
        if label:
            label.setText(str(value))

    def build_agent_data(self):
        registry_agents = load_registry()
        source_map = source_files()

        status_code, live_payload = http_json(
            f"{AGENT_HUB_URL}/api/v1/agents",
            timeout=2.5,
        )

        live_map = {}
        if status_code == 200:
            for item in live_payload.get("agents", []):
                live_map[str(item.get("id"))] = item

        combined = {}

        for declared in registry_agents:
            agent_id = str(declared.get("id"))
            merged = dict(declared)
            merged.update(live_map.get(agent_id, {}))

            application = merged.get("application") or {}
            online = bool(application.get("online"))

            merged["description"] = declared.get(
                "description",
                merged.get("description", "Specialist SHiRE Agent."),
            )
            merged["category"] = declared.get(
                "category",
                merged.get("category", "Specialist Agent"),
            )
            merged["appPath"] = declared.get("appPath")
            merged["sourceFiles"] = [
                str(path) for path in source_map.get(agent_id, [])
            ]
            merged["uiStatus"] = friendly_status(
                merged.get("declaredStatus") or declared.get("status"),
                enabled=bool(merged.get("enabled")),
                online=online,
            )
            merged["statusText"] = (
                application.get("health", {}).get("app")
                if isinstance(application.get("health"), dict)
                else None
            ) or merged.get("declaredStatus") or declared.get("status")

            combined[agent_id] = merged

        for synthetic in SYNTHETIC_AGENTS:
            agent_id = synthetic["id"]
            if agent_id in combined:
                continue

            merged = dict(synthetic)
            merged["sourceFiles"] = [
                str(path) for path in source_map.get(agent_id, [])
            ]
            merged["uiStatus"] = friendly_status(
                merged.get("declaredStatus"),
                enabled=False,
                online=False,
            )
            merged["brain"] = {
                "fastModel": "shire-mini-fast",
                "fastAvailable": True,
                "deepModel": "shire-mini-deep",
                "deepAvailable": True,
            }
            merged["memory"] = {
                "root": str(ARMOR_ROOT / "data" / "agents" / agent_id),
                "isolated": True,
            }
            combined[agent_id] = merged

        return combined, status_code == 200

    def refresh(self):
        self.agents, hub_online = self.build_agent_data()

        if hub_online:
            self.hub_status.setText("AGENT HUB LIVE")
            self.hub_status.setStyleSheet(
                """
                color:#20e887;
                background:rgba(0,0,0,100);
                border:1px solid #20e887;
                border-radius:12px;
                padding:10px;
                font-size:10px;
                font-weight:900;
                """
            )
        else:
            self.hub_status.setText("AGENT HUB OFFLINE")
            self.hub_status.setStyleSheet(
                """
                color:#ff4d5f;
                background:rgba(0,0,0,100);
                border:1px solid #ff4d5f;
                border-radius:12px;
                padding:10px;
                font-size:10px;
                font-weight:900;
                """
            )

        for agent_id, data in self.agents.items():
            card = self.cards.get(agent_id)

            if card is None:
                card = AgentCard(agent_id)
                card.selected.connect(self.select_agent)
                self.cards[agent_id] = card

            card.update_agent(data)

        for agent_id in list(self.cards):
            if agent_id not in self.agents:
                self.cards[agent_id].deleteLater()
                del self.cards[agent_id]

        statuses = [
            data.get("uiStatus", "OFFLINE")
            for data in self.agents.values()
        ]

        self.set_metric(self.total_metric, len(self.agents))
        self.set_metric(self.live_metric, statuses.count("LIVE"))
        self.set_metric(self.ready_metric, statuses.count("READY"))
        self.set_metric(self.staged_metric, statuses.count("STAGED"))
        self.set_metric(
            self.source_metric,
            sum(len(data.get("sourceFiles", [])) for data in self.agents.values()),
        )

        self.apply_filters()

        if self.selected_agent_id in self.agents:
            self.details.show_agent(self.agents[self.selected_agent_id])
        elif self.agents:
            first = sorted(self.agents)[0]
            self.select_agent(first)

    def select_agent(self, agent_id):
        self.selected_agent_id = agent_id
        data = self.agents.get(agent_id)
        if data:
            self.details.show_agent(data)

    def apply_filters(self):
        query = self.search.text().strip().lower()
        selected_state = self.filter.currentText()

        visible = []

        for agent_id, card in self.cards.items():
            data = self.agents.get(agent_id, {})
            haystack = " ".join(
                [
                    str(data.get("name", "")),
                    str(data.get("category", "")),
                    str(data.get("description", "")),
                    str(data.get("declaredStatus", "")),
                    agent_id,
                ]
            ).lower()

            state_match = (
                selected_state == "ALL STATES"
                or data.get("uiStatus") == selected_state
            )
            text_match = not query or query in haystack

            if state_match and text_match:
                visible.append(card)
            else:
                card.hide()

        while self.card_grid.count():
            item = self.card_grid.takeAt(0)
            if item.widget():
                item.widget().setParent(self.card_host)

        columns = 2

        for index, card in enumerate(visible):
            row = index // columns
            column = index % columns
            self.card_grid.addWidget(card, row, column)
            card.show()

        for column in range(columns):
            self.card_grid.setColumnStretch(column, 1)
