"""
MCP Apps Phase 1 — UI resource registration.

Registers ``ui://`` resources that serve self-contained HTML dashboards.
Phase 1 is read-only (no postMessage bridge); the HTML renders a static
snapshot of tool state injected via ``window.__MCP_TOOLS__``.
"""

import json

from fastmcp import FastMCP
from fastmcp.server.apps import AppConfig

_TOOLS_PLACEHOLDER = "/*__MCP_TOOLS_DATA__*/"

_PROTECTED_TOOLS = {
    "manage_tools",
    "search",
    "health_check",
    "start_google_auth",
    "check_drive_auth",
}


def _collect_tools_json(mcp: FastMCP) -> str:
    """Collect tool info from the server registry and return as JSON.

    Reuses the same enabled-state logic as ``manage_tools(action="list")``:
    global disabled state is read from FastMCP transforms, and the protected
    tools set mirrors ``server_tools.py``.

    Also includes session-specific state and service grouping for the UI.
    """
    # Import here to avoid circular imports at module level
    from auth.context import get_session_context_sync, get_session_disabled_tools_sync
    from middleware.qdrant_core.query_parser import extract_service_from_tool
    from tools.server_tools import _get_tool_enabled_state

    tools = []
    session_disabled = set()
    has_session = False

    try:
        session_id = get_session_context_sync()
        if session_id:
            has_session = True
            session_disabled = get_session_disabled_tools_sync(session_id)
    except Exception:
        pass

    try:
        components = mcp.local_provider._components
        for key, comp in sorted(components.items()):
            if not key.startswith("tool:"):
                continue
            name = comp.name
            if name.startswith("_"):
                continue
            globally_enabled = _get_tool_enabled_state(comp, mcp)
            is_session_disabled = name in session_disabled
            service = extract_service_from_tool(name)

            tools.append(
                {
                    "name": name,
                    "enabled": globally_enabled,
                    "sessionDisabled": is_session_disabled,
                    "isProtected": name in _PROTECTED_TOOLS,
                    "description": comp.description,
                    "service": service,
                }
            )
    except Exception:
        pass

    result = {
        "tools": tools,
        "sessionState": {
            "sessionDisabledCount": len(session_disabled),
            "sessionAvailable": has_session,
        }
        if has_session
        else None,
    }
    return json.dumps(result)


def _build_manage_tools_html(tools_json: str = "[]") -> str:
    """Return a complete HTML SPA for the manage-tools dashboard."""
    html = """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tool Management Dashboard</title>
<style>
  :root {
    --bg: #1a1b26; --surface: #24283b; --surface2: #292e42; --border: #414868;
    --text: #c0caf5; --text-dim: #565f89; --accent: #7aa2f7;
    --green: #9ece6a; --gray: #565f89; --orange: #ff9e64; --red: #f7768e;
    --purple: #bb9af7; --cyan: #7dcfff; --teal: #73daca;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
    background: var(--bg); color: var(--text); padding: 1.5rem;
  }

  /* Header */
  header {
    display: flex; align-items: center; gap: 1rem;
    margin-bottom: 1.25rem; flex-wrap: wrap;
  }
  header h1 { font-size: 1.25rem; font-weight: 600; }
  .header-badge {
    font-size: 0.65rem; padding: 0.2rem 0.55rem; border-radius: 999px;
    background: var(--border); color: var(--text-dim); font-weight: 500;
  }

  /* Stats bar */
  .stats-bar {
    display: flex; gap: 0.5rem; margin-bottom: 1.25rem;
    flex-wrap: wrap;
  }
  .stat-chip {
    font-size: 0.75rem; padding: 0.3rem 0.7rem; border-radius: 6px;
    background: var(--surface); border: 1px solid var(--border);
    color: var(--text-dim); display: flex; align-items: center; gap: 0.35rem;
  }
  .stat-chip strong { font-weight: 600; }
  .stat-chip .dot-sm {
    width: 6px; height: 6px; border-radius: 50%; display: inline-block;
  }

  /* Service filter chips */
  .filter-bar {
    display: flex; gap: 0.4rem; margin-bottom: 1.25rem;
    flex-wrap: wrap; align-items: center;
  }
  .filter-label {
    font-size: 0.7rem; color: var(--text-dim); margin-right: 0.25rem;
    text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600;
  }
  .chip {
    font-size: 0.7rem; padding: 0.25rem 0.65rem; border-radius: 999px;
    border: 1px solid var(--border); background: var(--surface);
    color: var(--text-dim); cursor: pointer; transition: all 0.15s;
    display: flex; align-items: center; gap: 0.35rem;
    user-select: none;
  }
  .chip:hover { border-color: var(--accent); color: var(--text); }
  .chip.active {
    background: rgba(122, 162, 247, 0.15); border-color: var(--accent);
    color: var(--accent); font-weight: 500;
  }
  .chip .svc-icon { font-size: 0.8rem; }
  .chip .chip-count {
    font-size: 0.6rem; background: var(--border); color: var(--text-dim);
    padding: 0.05rem 0.35rem; border-radius: 999px; font-weight: 600;
  }
  .chip.active .chip-count {
    background: rgba(122, 162, 247, 0.25); color: var(--accent);
  }

  /* Service groups */
  .service-group { margin-bottom: 1.5rem; }
  .group-header {
    display: flex; align-items: center; gap: 0.5rem;
    margin-bottom: 0.6rem; padding-bottom: 0.4rem;
    border-bottom: 1px solid var(--border);
  }
  .group-icon { font-size: 1rem; }
  .group-name {
    font-size: 0.85rem; font-weight: 600; text-transform: capitalize;
  }
  .group-count {
    font-size: 0.65rem; color: var(--text-dim);
    background: var(--surface); padding: 0.15rem 0.5rem;
    border-radius: 999px; border: 1px solid var(--border);
  }
  .group-status {
    margin-left: auto; font-size: 0.65rem; color: var(--text-dim);
    display: flex; gap: 0.6rem;
  }

  /* Tool grid */
  .tool-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 0.6rem;
  }
  .tool-card {
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 8px; padding: 0.75rem 0.85rem;
    display: flex; align-items: flex-start; gap: 0.55rem;
    transition: all 0.15s;
  }
  .tool-card:hover {
    border-color: var(--accent); background: var(--surface2);
    transform: translateY(-1px);
  }
  .tool-card.session-disabled {
    border-left: 2px solid var(--orange);
  }
  .tool-card.globally-disabled { opacity: 0.5; }
  .tool-card.active-tool {
    border-left: 2px solid var(--green);
  }

  /* Status dot */
  .dot {
    width: 8px; height: 8px; border-radius: 50%;
    margin-top: 0.3rem; flex-shrink: 0;
  }
  .dot.enabled  { background: var(--green); box-shadow: 0 0 4px rgba(158,206,106,0.4); }
  .dot.globally-disabled { background: var(--gray); }
  .dot.session-disabled { background: var(--orange); box-shadow: 0 0 4px rgba(255,158,100,0.3); }

  /* Tool info */
  .tool-info { flex: 1; min-width: 0; }
  .tool-name {
    font-size: 0.8rem; font-weight: 600;
    word-break: break-word; line-height: 1.3;
  }
  .tool-desc {
    font-size: 0.7rem; color: var(--text-dim);
    margin-top: 0.2rem; line-height: 1.35;
    display: -webkit-box; -webkit-line-clamp: 2;
    -webkit-box-orient: vertical; overflow: hidden;
  }
  .tool-badges { display: flex; gap: 0.3rem; margin-top: 0.3rem; flex-wrap: wrap; }
  .tbadge {
    font-size: 0.55rem; padding: 0.1rem 0.4rem; border-radius: 4px;
    font-weight: 500; display: inline-block; letter-spacing: 0.02em;
  }
  .tbadge.protected {
    color: var(--accent); background: rgba(122, 162, 247, 0.1);
  }
  .tbadge.session-only {
    color: var(--orange); background: rgba(255, 158, 100, 0.1);
  }
  .tbadge.svc-tag {
    color: var(--cyan); background: rgba(125, 207, 255, 0.08);
  }

  .empty {
    text-align: center; padding: 3rem; color: var(--text-dim);
    grid-column: 1 / -1;
  }
</style>
</head>
<body>
<header>
  <h1>Tool Management</h1>
  <span class="header-badge">Phase 1 &middot; Read-Only</span>
</header>
<div class="stats-bar" id="stats"></div>
<div class="filter-bar" id="filters"></div>
<div id="groups-container"></div>
<script>
  window.__MCP_TOOLS__ = /*__MCP_TOOLS_DATA__*/;

  const SERVICE_META = {
    gmail:    { icon: '\\u2709', label: 'Gmail' },
    drive:    { icon: '\\uD83D\\uDCC1', label: 'Drive' },
    docs:     { icon: '\\uD83D\\uDCC4', label: 'Docs' },
    sheets:   { icon: '\\uD83D\\uDCCA', label: 'Sheets' },
    slides:   { icon: '\\uD83C\\uDFA8', label: 'Slides' },
    calendar: { icon: '\\uD83D\\uDCC5', label: 'Calendar' },
    forms:    { icon: '\\uD83D\\uDCDD', label: 'Forms' },
    chat:     { icon: '\\uD83D\\uDCAC', label: 'Chat' },
    photos:   { icon: '\\uD83D\\uDCF7', label: 'Photos' },
    people:   { icon: '\\uD83D\\uDC64', label: 'People' },
    tasks:    { icon: '\\u2705', label: 'Tasks' },
    unknown:  { icon: '\\u2699', label: 'System' }
  };

  let activeFilter = 'all';

  function render(data) {
    const tools = Array.isArray(data) ? data : (data.tools || []);

    if (!tools.length) {
      document.getElementById('groups-container').innerHTML =
        '<div class="empty">No tool data available.</div>';
      return;
    }

    // Counts
    const enabled = tools.filter(t => t.enabled && !t.sessionDisabled).length;
    const globallyDisabled = tools.filter(t => !t.enabled).length;
    const sessionDisabled = tools.filter(t => t.sessionDisabled && t.enabled).length;

    // Stats chips
    let stats = '<div class="stat-chip"><strong>' + tools.length + '</strong> total</div>';
    stats += '<div class="stat-chip"><span class="dot-sm" style="background:var(--green)"></span><strong>' + enabled + '</strong> enabled</div>';
    if (sessionDisabled > 0)
      stats += '<div class="stat-chip"><span class="dot-sm" style="background:var(--orange)"></span><strong>' + sessionDisabled + '</strong> session-disabled</div>';
    if (globallyDisabled > 0)
      stats += '<div class="stat-chip"><span class="dot-sm" style="background:var(--gray)"></span><strong>' + globallyDisabled + '</strong> globally-disabled</div>';
    document.getElementById('stats').innerHTML = stats;

    // Group by service
    const groups = {};
    tools.forEach(t => {
      const svc = t.service || 'unknown';
      if (!groups[svc]) groups[svc] = [];
      groups[svc].push(t);
    });

    // Sort: services with enabled tools first, then alphabetical
    const sortedKeys = Object.keys(groups).sort((a, b) => {
      const aActive = groups[a].some(t => t.enabled && !t.sessionDisabled);
      const bActive = groups[b].some(t => t.enabled && !t.sessionDisabled);
      if (aActive !== bActive) return bActive - aActive;
      return a.localeCompare(b);
    });

    // Filter chips
    let chips = '<span class="filter-label">Filter</span>';
    chips += '<div class="chip' + (activeFilter === 'all' ? ' active' : '') +
      '" data-svc="all"><span class="svc-icon">\\u2B50</span>All<span class="chip-count">' +
      tools.length + '</span></div>';
    sortedKeys.forEach(svc => {
      const meta = SERVICE_META[svc] || SERVICE_META.unknown;
      const count = groups[svc].length;
      const activeCount = groups[svc].filter(t => t.enabled && !t.sessionDisabled).length;
      chips += '<div class="chip' + (activeFilter === svc ? ' active' : '') +
        '" data-svc="' + svc + '"><span class="svc-icon">' + meta.icon + '</span>' +
        meta.label + '<span class="chip-count">' + activeCount + '/' + count + '</span></div>';
    });
    const filtersEl = document.getElementById('filters');
    filtersEl.innerHTML = chips;

    // Chip click handlers
    filtersEl.querySelectorAll('.chip').forEach(el => {
      el.addEventListener('click', () => {
        activeFilter = el.dataset.svc;
        render(data);
      });
    });

    // Render groups
    const container = document.getElementById('groups-container');
    const visibleKeys = activeFilter === 'all' ? sortedKeys : sortedKeys.filter(k => k === activeFilter);

    container.innerHTML = visibleKeys.map(svc => {
      const meta = SERVICE_META[svc] || SERVICE_META.unknown;
      const svcTools = groups[svc];
      const svcEnabled = svcTools.filter(t => t.enabled && !t.sessionDisabled).length;
      const svcSession = svcTools.filter(t => t.sessionDisabled && t.enabled).length;

      let statusHTML = '<span style="color:var(--green)">' + svcEnabled + ' on</span>';
      if (svcSession > 0) statusHTML += '<span style="color:var(--orange)">' + svcSession + ' session-off</span>';

      const cards = svcTools.map(t => {
        let dotCls, cardCls;
        if (t.sessionDisabled && t.enabled) {
          dotCls = 'session-disabled'; cardCls = 'session-disabled';
        } else if (!t.enabled) {
          dotCls = 'globally-disabled'; cardCls = 'globally-disabled';
        } else {
          dotCls = 'enabled'; cardCls = 'active-tool';
        }

        let badges = '';
        if (t.isProtected) badges += '<span class="tbadge protected">protected</span>';
        if (t.sessionDisabled) badges += '<span class="tbadge session-only">session-disabled</span>';

        const desc = t.description
          ? '<div class="tool-desc">' + t.description.replace(/</g, '&lt;') + '</div>'
          : '';

        return '<div class="tool-card ' + cardCls + '">' +
          '<div class="dot ' + dotCls + '"></div>' +
          '<div class="tool-info"><div class="tool-name">' + t.name.replace(/</g, '&lt;') + '</div>' +
          desc +
          (badges ? '<div class="tool-badges">' + badges + '</div>' : '') +
          '</div></div>';
      }).join('');

      return '<div class="service-group">' +
        '<div class="group-header">' +
        '<span class="group-icon">' + meta.icon + '</span>' +
        '<span class="group-name">' + meta.label + '</span>' +
        '<span class="group-count">' + svcTools.length + ' tools</span>' +
        '<div class="group-status">' + statusHTML + '</div>' +
        '</div>' +
        '<div class="tool-grid">' + cards + '</div>' +
        '</div>';
    }).join('');
  }

  render(window.__MCP_TOOLS__);
</script>
</body>
</html>"""
    return html.replace(_TOOLS_PLACEHOLDER, tools_json)


def setup_ui_apps(mcp: FastMCP) -> None:
    """Register ``ui://`` resources for MCP Apps Phase 1."""

    @mcp.resource(
        "ui://manage-tools-dashboard",
        name="manage_tools_dashboard",
        title="Tool Management Dashboard",
        description="Read-only dashboard showing tool enable/disable state",
        tags={"ui", "dashboard", "tools"},
        app=AppConfig(prefers_border=True),
    )
    def manage_tools_dashboard() -> str:
        return _build_manage_tools_html(_collect_tools_json(mcp))

    @mcp.resource(
        "ui://data-dashboard/{tool_name}",
        name="data_dashboard",
        title="Data Dashboard",
        description="Generic data dashboard for list tool results",
        tags={"ui", "dashboard", "data"},
        app=AppConfig(prefers_border=True),
    )
    def data_dashboard(tool_name: str) -> str:
        """Serve a data dashboard populated from the middleware cache.

        :class:`~middleware.dashboard_cache_middleware.DashboardCacheMiddleware`
        intercepts list-tool calls and stores the last result.  This resource
        reads from that cache so the HTML is pre-populated with live data.
        """
        from middleware.dashboard_cache_middleware import get_cached_result

        cached = get_cached_result(tool_name) or {}
        config = get_data_dashboard_config(tool_name)
        payload = json.dumps({"data": cached, "config": config})
        return _build_data_dashboard_html(payload)


# ---------------------------------------------------------------------------
# Generic Data Dashboard — shared template for all list-tool UIs
# ---------------------------------------------------------------------------

_DATA_PLACEHOLDER = "/*__DASHBOARD_DATA__*/"

# Per-tool configuration: tells the generic JS which fields to display and how.
# Keys: items_field, title, icon, columns (list of {key, label, type}).
# Column types: "primary", "text", "timestamp", "link", "numeric", "badge",
#               "color", "nested", "boolean".
# If a tool isn't listed here the JS will auto-detect columns from the data.
_DASHBOARD_CONFIGS: dict = {
    "list_gmail_labels": {
        "itemsField": "labels",
        "title": "Gmail Labels",
        "icon": "\u2709",
        "columns": [
            {"key": "name", "label": "Label", "type": "primary"},
            {"key": "type", "label": "Type", "type": "badge"},
            {"key": "messagesTotal", "label": "Messages", "type": "numeric"},
            {"key": "messagesUnread", "label": "Unread", "type": "numeric"},
            {"key": "threadsTotal", "label": "Threads", "type": "numeric"},
            {"key": "color", "label": "Color", "type": "color"},
        ],
    },
    "list_gmail_filters": {
        "itemsField": "filters",
        "title": "Gmail Filters",
        "icon": "\u2709",
        "columns": [
            {"key": "id", "label": "Filter ID", "type": "primary"},
            {"key": "criteria", "label": "Criteria", "type": "nested"},
            {"key": "action", "label": "Actions", "type": "nested"},
        ],
    },
    "list_spaces": {
        "itemsField": "spaces",
        "title": "Chat Spaces",
        "icon": "\U0001f4ac",
        "columns": [
            {"key": "displayName", "label": "Name", "type": "primary"},
            {"key": "spaceType", "label": "Type", "type": "badge"},
            {"key": "id", "label": "ID", "type": "text"},
            {"key": "threaded", "label": "Threaded", "type": "boolean"},
        ],
    },
    "list_messages": {
        "itemsField": "messages",
        "title": "Chat Messages",
        "icon": "\U0001f4ac",
        "columns": [
            {"key": "senderName", "label": "Sender", "type": "primary"},
            {"key": "text", "label": "Message", "type": "text"},
            {"key": "createTime", "label": "Time", "type": "timestamp"},
            {"key": "threadId", "label": "Thread", "type": "text"},
        ],
    },
    "list_calendars": {
        "itemsField": "calendars",
        "title": "Calendars",
        "icon": "\U0001f4c5",
        "columns": [
            {"key": "summary", "label": "Calendar", "type": "primary"},
            {"key": "description", "label": "Description", "type": "text"},
            {"key": "primary", "label": "Primary", "type": "boolean"},
            {"key": "timeZone", "label": "Timezone", "type": "text"},
            {"key": "backgroundColor", "label": "Color", "type": "color"},
        ],
    },
    "list_events": {
        "itemsField": "events",
        "title": "Calendar Events",
        "icon": "\U0001f4c5",
        "columns": [
            {"key": "summary", "label": "Event", "type": "primary"},
            {"key": "start", "label": "Start", "type": "timestamp"},
            {"key": "end", "label": "End", "type": "timestamp"},
            {"key": "location", "label": "Location", "type": "text"},
            {"key": "status", "label": "Status", "type": "badge"},
            {"key": "htmlLink", "label": "Link", "type": "link"},
        ],
    },
    "list_drive_items": {
        "itemsField": "items",
        "title": "Drive Files",
        "icon": "\U0001f4c1",
        "columns": [
            {"key": "name", "label": "Name", "type": "primary"},
            {"key": "mimeType", "label": "Type", "type": "badge"},
            {"key": "modifiedTime", "label": "Modified", "type": "timestamp"},
            {"key": "size", "label": "Size", "type": "numeric"},
            {"key": "webViewLink", "label": "Link", "type": "link"},
        ],
    },
    "list_docs_in_folder": {
        "itemsField": "docs",
        "title": "Documents",
        "icon": "\U0001f4c4",
        "columns": [
            {"key": "name", "label": "Document", "type": "primary"},
            {"key": "modifiedTime", "label": "Modified", "type": "timestamp"},
            {"key": "webViewLink", "label": "Link", "type": "link"},
        ],
    },
    "list_spreadsheets": {
        "itemsField": "items",
        "title": "Spreadsheets",
        "icon": "\U0001f4ca",
        "columns": [
            {"key": "name", "label": "Spreadsheet", "type": "primary"},
            {"key": "modifiedTime", "label": "Modified", "type": "timestamp"},
            {"key": "webViewLink", "label": "Link", "type": "link"},
        ],
    },
    "list_photos_albums": {
        "itemsField": "albums",
        "title": "Photo Albums",
        "icon": "\U0001f4f7",
        "columns": [
            {"key": "title", "label": "Album", "type": "primary"},
            {"key": "mediaItemsCount", "label": "Photos", "type": "numeric"},
            {"key": "productUrl", "label": "Link", "type": "link"},
        ],
    },
    "list_form_responses": {
        "itemsField": "responses",
        "title": "Form Responses",
        "icon": "\U0001f4dd",
        "columns": [
            {"key": "responseId", "label": "Response ID", "type": "primary"},
            {"key": "respondentEmail", "label": "Email", "type": "text"},
            {"key": "submittedTime", "label": "Submitted", "type": "timestamp"},
            {"key": "answers", "label": "Answers", "type": "nested"},
        ],
    },
    "list_people_contact_labels": {
        "itemsField": "labels",
        "title": "Contact Labels",
        "icon": "\U0001f464",
        "columns": [
            {"key": "name", "label": "Label", "type": "primary"},
            {"key": "memberCount", "label": "Members", "type": "numeric"},
            {"key": "groupType", "label": "Type", "type": "badge"},
        ],
    },
}


def _build_data_dashboard_html(data_json: str = "{}") -> str:
    """Return a generic data dashboard HTML SPA.

    The template is fully data-driven: the JS reads a config object that
    describes columns and an items array extracted from the tool response.
    It reuses the same Tokyonight design language as the manage-tools dashboard.
    """
    html = """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Dashboard</title>
<style>
  :root {
    --bg: #1a1b26; --surface: #24283b; --surface2: #292e42; --border: #414868;
    --text: #c0caf5; --text-dim: #565f89; --accent: #7aa2f7;
    --green: #9ece6a; --gray: #565f89; --orange: #ff9e64; --red: #f7768e;
    --purple: #bb9af7; --cyan: #7dcfff; --teal: #73daca;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
    background: var(--bg); color: var(--text); padding: 1.5rem;
  }
  header {
    display: flex; align-items: center; gap: 0.75rem;
    margin-bottom: 1.25rem; flex-wrap: wrap;
  }
  header .h-icon { font-size: 1.3rem; }
  header h1 { font-size: 1.2rem; font-weight: 600; }
  .header-badge {
    font-size: 0.65rem; padding: 0.2rem 0.55rem; border-radius: 999px;
    background: var(--border); color: var(--text-dim); font-weight: 500;
  }

  /* Stats */
  .stats-bar {
    display: flex; gap: 0.5rem; margin-bottom: 1.25rem; flex-wrap: wrap;
  }
  .stat-chip {
    font-size: 0.75rem; padding: 0.3rem 0.7rem; border-radius: 6px;
    background: var(--surface); border: 1px solid var(--border);
    color: var(--text-dim); display: flex; align-items: center; gap: 0.35rem;
  }
  .stat-chip strong { font-weight: 600; color: var(--text); }

  /* Search */
  .search-bar {
    margin-bottom: 1rem;
  }
  .search-bar input {
    width: 100%; max-width: 360px; padding: 0.45rem 0.75rem;
    border-radius: 6px; border: 1px solid var(--border);
    background: var(--surface); color: var(--text);
    font-family: inherit; font-size: 0.8rem; outline: none;
    transition: border-color 0.15s;
  }
  .search-bar input:focus { border-color: var(--accent); }
  .search-bar input::placeholder { color: var(--text-dim); }

  /* Table */
  .data-table {
    width: 100%; border-collapse: collapse;
    font-size: 0.8rem;
  }
  .data-table th {
    text-align: left; padding: 0.55rem 0.75rem;
    font-size: 0.7rem; text-transform: uppercase;
    letter-spacing: 0.04em; color: var(--text-dim);
    border-bottom: 1px solid var(--border);
    font-weight: 600; cursor: pointer;
    user-select: none; white-space: nowrap;
  }
  .data-table th:hover { color: var(--accent); }
  .data-table th .sort-arrow { font-size: 0.6rem; margin-left: 0.25rem; opacity: 0.4; }
  .data-table th.sorted .sort-arrow { opacity: 1; color: var(--accent); }
  .data-table td {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid rgba(65,72,104,0.3);
    max-width: 300px; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap;
    vertical-align: top;
  }
  .data-table tr:hover td { background: var(--surface2); }

  /* Cell types */
  .cell-primary { font-weight: 600; color: var(--text); }
  .cell-badge {
    display: inline-block; font-size: 0.65rem; padding: 0.1rem 0.45rem;
    border-radius: 4px; background: rgba(122, 162, 247, 0.1);
    color: var(--accent); font-weight: 500;
  }
  .cell-link {
    color: var(--cyan); text-decoration: none; font-size: 0.7rem;
  }
  .cell-link:hover { text-decoration: underline; }
  .cell-timestamp { color: var(--teal); font-size: 0.75rem; }
  .cell-numeric { font-variant-numeric: tabular-nums; color: var(--purple); }
  .cell-bool-true { color: var(--green); }
  .cell-bool-false { color: var(--gray); }
  .cell-nested {
    font-size: 0.65rem; color: var(--text-dim);
    max-width: 250px; cursor: pointer;
  }
  .cell-nested:hover { color: var(--text); }
  .cell-color {
    display: inline-block; width: 14px; height: 14px;
    border-radius: 3px; border: 1px solid var(--border);
    vertical-align: middle;
  }

  /* Card view (small screens) */
  .card-grid {
    display: none;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 0.6rem;
  }
  .data-card {
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 8px; padding: 0.75rem 0.85rem;
    transition: all 0.15s;
  }
  .data-card:hover {
    border-color: var(--accent); background: var(--surface2);
    transform: translateY(-1px);
  }
  .data-card .card-primary {
    font-size: 0.85rem; font-weight: 600; margin-bottom: 0.35rem;
  }
  .data-card .card-field {
    font-size: 0.7rem; color: var(--text-dim);
    margin-bottom: 0.15rem; display: flex; gap: 0.35rem;
  }
  .data-card .card-field .field-label {
    color: var(--text-dim); min-width: 65px; flex-shrink: 0;
  }
  .data-card .card-field .field-value {
    color: var(--text); overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap;
  }

  /* View toggle */
  .view-toggle {
    display: flex; gap: 0.25rem; margin-left: auto;
  }
  .view-btn {
    font-size: 0.7rem; padding: 0.2rem 0.5rem; border-radius: 4px;
    border: 1px solid var(--border); background: var(--surface);
    color: var(--text-dim); cursor: pointer; transition: all 0.15s;
  }
  .view-btn:hover { border-color: var(--accent); }
  .view-btn.active {
    background: rgba(122, 162, 247, 0.15); border-color: var(--accent);
    color: var(--accent);
  }

  .empty {
    text-align: center; padding: 3rem;
    color: var(--text-dim); font-size: 0.85rem;
  }

  @media (max-width: 640px) {
    .table-wrap { display: none; }
    .card-grid { display: grid; }
  }
</style>
</head>
<body>
<header>
  <span class="h-icon" id="h-icon"></span>
  <h1 id="h-title">Data Dashboard</h1>
  <span class="header-badge" id="h-count"></span>
  <div class="view-toggle">
    <button class="view-btn active" data-view="table">Table</button>
    <button class="view-btn" data-view="cards">Cards</button>
  </div>
</header>
<div class="stats-bar" id="stats"></div>
<div class="search-bar"><input type="text" id="search" placeholder="Filter items..."></div>
<div class="table-wrap"><table class="data-table"><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
<div class="card-grid" id="cards"></div>
<script>
  var __DATA__ = /*__DASHBOARD_DATA__*/;
  var DATA = __DATA__.data || {};
  var CFG = __DATA__.config || {};

  // --- Helpers ---
  function esc(s) { return s == null ? '' : String(s).replace(/</g, '&lt;').replace(/>/g, '&gt;'); }

  function fmtTimestamp(v) {
    if (!v) return '';
    try {
      var d = new Date(v);
      if (isNaN(d)) return esc(v);
      return d.toLocaleDateString(undefined, {month:'short',day:'numeric',year:'numeric'}) +
             ' ' + d.toLocaleTimeString(undefined, {hour:'2-digit',minute:'2-digit'});
    } catch(e) { return esc(v); }
  }

  function fmtNested(v) {
    if (v == null) return '';
    if (Array.isArray(v)) return esc(v.length + ' items');
    if (typeof v === 'object') {
      var parts = [];
      for (var k in v) {
        if (v[k] != null && v[k] !== '' && v[k] !== false)
          parts.push(k + ': ' + (typeof v[k] === 'object' ? JSON.stringify(v[k]) : v[k]));
      }
      return esc(parts.join(', ').substring(0, 120));
    }
    return esc(v);
  }

  function renderCell(val, col) {
    var t = col.type || 'text';
    if (val == null || val === '') return '<span style="color:var(--gray)">—</span>';
    switch(t) {
      case 'primary': return '<span class="cell-primary">' + esc(val) + '</span>';
      case 'badge': return '<span class="cell-badge">' + esc(val) + '</span>';
      case 'link':
        var url = String(val);
        return '<a class="cell-link" href="' + esc(url) + '" target="_blank" rel="noopener">Open</a>';
      case 'timestamp': return '<span class="cell-timestamp">' + fmtTimestamp(val) + '</span>';
      case 'numeric':
        var n = val;
        if (typeof n === 'string') n = parseInt(n, 10);
        return '<span class="cell-numeric">' + (isNaN(n) ? esc(val) : n.toLocaleString()) + '</span>';
      case 'boolean':
        return val ? '<span class="cell-bool-true">\\u2713</span>' : '<span class="cell-bool-false">\\u2717</span>';
      case 'color':
        var c = val;
        if (typeof c === 'object') c = c.backgroundColor || c.textColor || '#888';
        return '<span class="cell-color" style="background:' + esc(c) + '"></span>';
      case 'nested': return '<span class="cell-nested" title="' + esc(JSON.stringify(val)) + '">' + fmtNested(val) + '</span>';
      default: return '<span>' + esc(String(val).substring(0, 120)) + '</span>';
    }
  }

  // --- Extract items from response ---
  function findItems(data, cfg) {
    if (cfg.itemsField && data[cfg.itemsField]) return data[cfg.itemsField];
    // Auto-detect: find first array field
    for (var k in data) {
      if (Array.isArray(data[k]) && data[k].length > 0 && typeof data[k][0] === 'object')
        return data[k];
    }
    // If data itself is an array
    if (Array.isArray(data)) return data;
    return [];
  }

  // --- Auto-detect columns if not configured ---
  function detectColumns(items) {
    if (!items.length) return [];
    var sample = items[0];
    var primaryNames = ['name','displayName','summary','title','text','label'];
    var timeNames = ['modifiedTime','createTime','submittedTime','start','end','createdTime','updatedTime'];
    var linkNames = ['webViewLink','htmlLink','productUrl','url','link'];
    var cols = [];
    for (var k in sample) {
      if (k === 'id' || k === 'resourceName') continue; // skip raw IDs
      var v = sample[k];
      var type = 'text';
      if (primaryNames.indexOf(k) >= 0) type = 'primary';
      else if (timeNames.indexOf(k) >= 0) type = 'timestamp';
      else if (linkNames.indexOf(k) >= 0) type = 'link';
      else if (typeof v === 'boolean') type = 'boolean';
      else if (typeof v === 'number') type = 'numeric';
      else if (typeof v === 'object' && v !== null) type = 'nested';
      cols.push({key: k, label: k.replace(/([A-Z])/g, ' $1').replace(/^./, function(s){return s.toUpperCase();}), type: type});
      if (cols.length >= 8) break; // cap at 8 columns
    }
    return cols;
  }

  // --- Sort state ---
  var sortCol = null, sortAsc = true;

  function sortItems(items, col) {
    if (!col) return items;
    var copy = items.slice();
    copy.sort(function(a, b) {
      var va = a[col.key], vb = b[col.key];
      if (va == null) return 1; if (vb == null) return -1;
      if (col.type === 'numeric') {
        va = typeof va === 'string' ? parseFloat(va) : va;
        vb = typeof vb === 'string' ? parseFloat(vb) : vb;
        return sortAsc ? va - vb : vb - va;
      }
      va = String(va).toLowerCase(); vb = String(vb).toLowerCase();
      return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
    });
    return copy;
  }

  // --- Render ---
  var currentView = 'table';
  var allItems = findItems(DATA, CFG);
  var columns = (CFG.columns && CFG.columns.length) ? CFG.columns : detectColumns(allItems);

  function render(filter) {
    var items = allItems;
    if (filter) {
      var q = filter.toLowerCase();
      items = items.filter(function(item) {
        return columns.some(function(c) {
          var v = item[c.key];
          return v != null && String(v).toLowerCase().indexOf(q) >= 0;
        });
      });
    }
    items = sortItems(items, sortCol);

    // Header
    document.getElementById('h-icon').textContent = CFG.icon || '\\u2699';
    document.getElementById('h-title').textContent = CFG.title || 'Data Dashboard';
    document.getElementById('h-count').textContent = items.length + ' / ' + allItems.length + ' items';

    // Stats
    var envelope = [];
    var skipKeys = [CFG.itemsField || '', 'error', 'jinjaTemplateApplied', 'jinjaTemplateError'];
    for (var k in DATA) {
      if (skipKeys.indexOf(k) >= 0) continue;
      var v = DATA[k];
      if (v == null || typeof v === 'object') continue;
      envelope.push('<div class="stat-chip"><strong>' + esc(String(v)) + '</strong> ' + esc(k) + '</div>');
    }
    document.getElementById('stats').innerHTML = envelope.join('');

    // Table
    var thead = '<tr>' + columns.map(function(c) {
      var cls = sortCol && sortCol.key === c.key ? ' sorted' : '';
      var arrow = sortCol && sortCol.key === c.key ? (sortAsc ? '\\u25B2' : '\\u25BC') : '\\u25B4';
      return '<th class="' + cls + '" data-key="' + c.key + '">' + esc(c.label) +
             '<span class="sort-arrow">' + arrow + '</span></th>';
    }).join('') + '</tr>';
    document.getElementById('thead').innerHTML = thead;

    var tbody = items.map(function(item) {
      return '<tr>' + columns.map(function(c) {
        return '<td>' + renderCell(item[c.key], c) + '</td>';
      }).join('') + '</tr>';
    }).join('');
    document.getElementById('tbody').innerHTML = tbody || '<tr><td colspan="' + columns.length + '" class="empty">No items match filter</td></tr>';

    // Cards
    var cards = items.map(function(item) {
      var primaryCol = columns.find(function(c) { return c.type === 'primary'; });
      var primaryVal = primaryCol ? (item[primaryCol.key] || '') : '';
      var fields = columns.filter(function(c) { return c.type !== 'primary'; }).map(function(c) {
        return '<div class="card-field"><span class="field-label">' + esc(c.label) + '</span>' +
               '<span class="field-value">' + renderCell(item[c.key], c) + '</span></div>';
      }).join('');
      return '<div class="data-card"><div class="card-primary">' + esc(primaryVal) + '</div>' + fields + '</div>';
    }).join('');
    document.getElementById('cards').innerHTML = cards || '<div class="empty">No items match filter</div>';

    // Sort click handlers
    document.querySelectorAll('#thead th').forEach(function(th) {
      th.onclick = function() {
        var key = th.dataset.key;
        var col = columns.find(function(c) { return c.key === key; });
        if (sortCol && sortCol.key === key) { sortAsc = !sortAsc; }
        else { sortCol = col; sortAsc = true; }
        render(document.getElementById('search').value);
      };
    });
  }

  // View toggle
  document.querySelectorAll('.view-btn').forEach(function(btn) {
    btn.addEventListener('click', function() {
      currentView = btn.dataset.view;
      document.querySelectorAll('.view-btn').forEach(function(b) { b.classList.remove('active'); });
      btn.classList.add('active');
      document.querySelector('.table-wrap').style.display = currentView === 'table' ? '' : 'none';
      document.querySelector('.card-grid').style.display = currentView === 'cards' ? 'grid' : 'none';
    });
  });

  // Search
  document.getElementById('search').addEventListener('input', function(e) {
    render(e.target.value);
  });

  render('');
</script>
</body>
</html>"""
    return html.replace(_DATA_PLACEHOLDER, data_json)


def get_data_dashboard_config(tool_name: str) -> dict:
    """Return the dashboard config for a given tool, or empty dict for auto-detect."""
    return _DASHBOARD_CONFIGS.get(tool_name, {})


def build_data_dashboard_for_tool(tool_name: str, tool_result: dict) -> str:
    """Build a data dashboard HTML string for any list tool's result.

    Args:
        tool_name: The tool that produced the data (e.g. ``"list_gmail_labels"``).
        tool_result: The dict/JSON-serializable result from the tool.

    Returns:
        Complete HTML string ready to serve as a ``ui://`` resource.
    """
    config = get_data_dashboard_config(tool_name)
    payload = json.dumps({"data": tool_result, "config": config})
    return _build_data_dashboard_html(payload)


def wire_dashboard_to_list_tools(mcp: FastMCP) -> int:
    """Patch ``meta["ui"]`` on every known list tool and register cache watchers.

    This must be called **after** all service tools have been registered.
    It does two things:

    1. Iterates :data:`_DASHBOARD_CONFIGS` and patches each tool's
       ``meta["ui"]["resourceUri"]`` to ``"ui://data-dashboard/{tool_name}"``.
    2. Registers those tool names with
       :func:`~middleware.dashboard_cache_middleware.register_watched_tools`
       so the cache middleware knows which tool calls to intercept.

    Returns:
        Number of tools patched.
    """
    from fastmcp.server.apps import app_config_to_meta_dict

    from middleware.dashboard_cache_middleware import register_watched_tools

    patched = 0
    lp = mcp.local_provider
    for key, component in lp._components.items():
        if not key.startswith("tool:"):
            continue
        tool_name = component.name
        if tool_name not in _DASHBOARD_CONFIGS:
            continue
        component.meta = component.meta or {}
        component.meta["ui"] = app_config_to_meta_dict(
            AppConfig(resource_uri=f"ui://data-dashboard/{tool_name}")
        )
        patched += 1

    # Tell the cache middleware which tools to watch
    register_watched_tools(set(_DASHBOARD_CONFIGS.keys()))

    return patched
