#!/usr/bin/env python3
"""
Dragon Crypt — Menu Bar App
Zero-trust MCP interceptor proxy. Lives in the macOS menu bar.
Mirrors the Dragon Vault menu bar pattern.

© 2026 DigiFX LLC · Dragon Crypt
"""

import rumps
import subprocess
import os
import sys
import json
import urllib.request
import objc

from AppKit import (
    NSWindow, NSWindowStyleMaskTitled, NSWindowStyleMaskClosable,
    NSWindowStyleMaskFullSizeContentView, NSBackingStoreBuffered,
    NSImage, NSImageView, NSTextField, NSFont, NSColor,
    NSTextAlignmentCenter, NSTextAlignmentLeft, NSApp,
    NSSwitch, NSView, NSObject, NSShadow, NSBezierPath,
    NSGraphicsContext,
)
from Foundation import NSMakeRect, NSBundle


# ─── Configuration ──────────────────────────────────────────────────────────

VERSION = "0.1.0"
APP_NAME = "DragonCrypt"
IPC_HOST = "127.0.0.1"
IPC_PORT = 7601
POLL_INTERVAL_SECS = 5
NODE_BIN_CANDIDATES = [
    "/opt/homebrew/bin/node",
    "/usr/local/bin/node",
]
# Resolved on launch — daemon can live in multiple locations.
# Priority: ~/.dragon/config.json → Application Support → dev source tree (fallback).
def _resolve_crypt_root():
    """Find the Dragon Crypt project root (where dist/daemon/index.js lives)."""
    # 1. Check platform discovery config (~/.dragon/config.json)
    dragon_config = os.path.expanduser("~/.dragon/config.json")
    if os.path.isfile(dragon_config):
        try:
            with open(dragon_config) as f:
                cfg = json.load(f)
            crypt_root = cfg.get("platforms", {}).get("CRYPT_ROOT", "")
            if crypt_root:
                crypt_root = os.path.expanduser(crypt_root)
                if os.path.isdir(crypt_root):
                    return crypt_root
        except Exception:
            pass
    # 2. Check app bundle resources (daemon bundled inside Crypt.app)
    bundle = NSBundle.mainBundle()
    if bundle:
        resources_dir = bundle.resourcePath()
        if resources_dir and os.path.isfile(os.path.join(resources_dir, "daemon", "index.js")):
            return resources_dir
    # 3. Standard Application Support location
    app_support = os.path.expanduser("~/Library/Application Support/DragonCrypt")
    if os.path.isdir(app_support):
        return app_support
    # 4. Dev source tree fallback — derive from this script's location (tools/ dir)
    script_dir = os.path.dirname(os.path.abspath(__file__))
    dev_root = os.path.dirname(script_dir)
    if os.path.isfile(os.path.join(dev_root, "package.json")):
        return dev_root
    # 5. Last resort — create Application Support dir
    os.makedirs(app_support, exist_ok=True)
    return app_support

CRYPT_PROJECT_ROOT = _resolve_crypt_root()


def get_resource_path(filename):
    """Find a resource file. Bundled .app → Resources/. Dev → script dir."""
    bundle = NSBundle.mainBundle()
    if bundle:
        resources_dir = bundle.resourcePath()
        if resources_dir:
            p = os.path.join(resources_dir, filename)
            if os.path.exists(p):
                return p
    dev_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
    if os.path.exists(dev_path):
        return dev_path
    return None


def find_node_bin():
    """Find the node binary — prefers bundled runtime inside .app, then system."""
    # 1. Bundled Node inside .app/Contents/Resources/bundled/node/bin/node
    bundled = get_resource_path(os.path.join("bundled", "node", "bin", "node"))
    if bundled and os.path.isfile(bundled) and os.access(bundled, os.X_OK):
        return bundled
    # 2. Flat layout fallback (legacy)
    bundled_flat = get_resource_path(os.path.join("node", "bin", "node"))
    if bundled_flat and os.path.isfile(bundled_flat) and os.access(bundled_flat, os.X_OK):
        return bundled_flat
    # 3. System Node
    for c in NODE_BIN_CANDIDATES:
        if os.path.isfile(c) and os.access(c, os.X_OK):
            return c
    from shutil import which
    return which("node")


NODE_BIN = find_node_bin()
ICON_PATH = get_resource_path("menubar_icon.png")
ABOUT_LOGO = get_resource_path("about_logo.png")

if not NODE_BIN:
    import AppKit
    alert = AppKit.NSAlert.alloc().init()
    alert.setMessageText_("Dragon Crypt — Node.js Not Found")
    alert.setInformativeText_(
        "Crypt requires Node.js to run the security daemon.\n\n"
        "The bundled runtime was not found. Please reinstall Crypt.app "
        "or install Node.js from https://nodejs.org"
    )
    alert.setAlertStyle_(AppKit.NSAlertStyleCritical)
    alert.runModal()


# ─── IPC helpers ────────────────────────────────────────────────────────────

def ipc_get(path, timeout=2):
    """GET request to the Dragon Crypt daemon IPC endpoint."""
    try:
        url = f"http://{IPC_HOST}:{IPC_PORT}{path}"
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return json.loads(resp.read().decode())
    except Exception:
        return None


def ipc_post(path, body=None, timeout=2):
    """POST request to the Dragon Crypt daemon IPC endpoint."""
    try:
        url = f"http://{IPC_HOST}:{IPC_PORT}{path}"
        data = json.dumps(body or {}).encode()
        req = urllib.request.Request(url, data=data, method="POST")
        req.add_header("Content-Type", "application/json")
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return json.loads(resp.read().decode())
    except Exception:
        return None


def daemon_is_alive():
    """Quick health check on the daemon."""
    return ipc_get("/health") is not None


# ─── Console window ────────────────────────────────────────────────────────

class ConsoleWindow:
    """Live status console — mirrors DragonVault's console aesthetic."""

    def __init__(self):
        self.window = None
        self.views = {}
        self._create_window()

    def _create_window(self):
        w, h = 520, 400
        self.window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
            NSMakeRect(0, 0, w, h),
            NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskFullSizeContentView,
            NSBackingStoreBuffered, False
        )
        self.window.setTitle_("")
        self.window.center()
        self.window.setTitlebarAppearsTransparent_(True)
        self.window.setBackgroundColor_(NSColor.blackColor())
        self.window.setReleasedWhenClosed_(False)
        content = self.window.contentView()

        # Logo
        if ABOUT_LOGO and os.path.exists(ABOUT_LOGO):
            img = NSImage.alloc().initWithContentsOfFile_(ABOUT_LOGO)
            if img:
                iv = NSImageView.alloc().initWithFrame_(NSMakeRect((w - 100) / 2, h - 130, 100, 100))
                iv.setImageScaling_(3)
                iv.setImage_(img)
                content.addSubview_(iv)

        content.addSubview_(self._label("Dragon Crypt", 0, h - 170, w, 28, size=22, bold=True))
        content.addSubview_(self._label(f"v{VERSION}", 0, h - 195, w, 16, size=11, color=NSColor.darkGrayColor()))

        y = h - 240
        content.addSubview_(self._label("MODE", 40, y, 100, 14, size=10, color=NSColor.grayColor(), align=NSTextAlignmentLeft))
        self.views['mode'] = self._label("—", 140, y, w - 180, 14, size=12, bold=True, align=NSTextAlignmentLeft)
        content.addSubview_(self.views['mode'])

        y -= 32
        content.addSubview_(self._label("SESSIONS", 40, y, 100, 14, size=10, color=NSColor.grayColor(), align=NSTextAlignmentLeft))
        self.views['sessions'] = self._label("—", 140, y, w - 180, 14, size=11, color=NSColor.lightGrayColor(), align=NSTextAlignmentLeft)
        content.addSubview_(self.views['sessions'])

        y -= 32
        content.addSubview_(self._label("TOTAL LOGGED", 40, y, 100, 14, size=10, color=NSColor.grayColor(), align=NSTextAlignmentLeft))
        self.views['bytes'] = self._label("—", 140, y, w - 180, 14, size=11, color=NSColor.lightGrayColor(), align=NSTextAlignmentLeft)
        content.addSubview_(self.views['bytes'])

        y -= 32
        content.addSubview_(self._label("LAST TOGGLED", 40, y, 100, 14, size=10, color=NSColor.grayColor(), align=NSTextAlignmentLeft))
        self.views['toggled'] = self._label("—", 140, y, w - 180, 14, size=11, color=NSColor.lightGrayColor(), align=NSTextAlignmentLeft)
        content.addSubview_(self.views['toggled'])

        # Footer
        content.addSubview_(self._label("© 2026 DigiFX LLC · Dragon Crypt — local-only, zero-trust MCP proxy",
                                        0, 16, w, 14, size=10, color=NSColor.darkGrayColor()))

    def _label(self, text, x, y, width, height, size=13, bold=False, mono=False,
               color=None, align=NSTextAlignmentCenter):
        label = NSTextField.alloc().initWithFrame_(NSMakeRect(x, y, width, height))
        label.setStringValue_(text)
        if bold:
            label.setFont_(NSFont.boldSystemFontOfSize_(size))
        elif mono:
            label.setFont_(NSFont.monospacedSystemFontOfSize_weight_(size, 0.0))
        else:
            label.setFont_(NSFont.systemFontOfSize_(size))
        label.setTextColor_(color or NSColor.whiteColor())
        label.setAlignment_(align)
        label.setBezeled_(False)
        label.setDrawsBackground_(False)
        label.setEditable_(False)
        label.setSelectable_(True)
        return label

    def update(self, status):
        if not self.window:
            return
        if status is None:
            self.views['mode'].setStringValue_("OFFLINE")
            self.views['mode'].setTextColor_(NSColor.colorWithCalibratedRed_green_blue_alpha_(0.95, 0.55, 0.20, 1.0))
            self.views['sessions'].setStringValue_("—")
            self.views['bytes'].setStringValue_("—")
            self.views['toggled'].setStringValue_("Daemon not running")
            return

        if status.get('bypass', True):
            self.views['mode'].setStringValue_("PASSTHROUGH ⚡")
            self.views['mode'].setTextColor_(NSColor.colorWithCalibratedRed_green_blue_alpha_(0.95, 0.75, 0.20, 1.0))
        else:
            self.views['mode'].setStringValue_("SECURE 🔒")
            self.views['mode'].setTextColor_(NSColor.colorWithCalibratedRed_green_blue_alpha_(0.30, 0.90, 0.40, 1.0))

        self.views['sessions'].setStringValue_(str(status.get('activeSessions', 0)))

        total = status.get('totalBytesLogged', 0)
        if total > 1_000_000:
            self.views['bytes'].setStringValue_(f"{total / 1_000_000:.1f} MB")
        elif total > 1_000:
            self.views['bytes'].setStringValue_(f"{total / 1_000:.1f} KB")
        else:
            self.views['bytes'].setStringValue_(f"{total} bytes")

        self.views['toggled'].setStringValue_(f"{status.get('lastToggled', '—')}  (by {status.get('toggledBy', '?')})")

    def show(self):
        if self.window:
            self.window.makeKeyAndOrderFront_(None)
            NSApp.activateIgnoringOtherApps_(True)


# ─── Glowing status indicator (custom drawn NSView) ─────────────────────────

class _GlowDot(NSView):
    """A circle with a colored glow halo, drawn via Core Graphics."""

    def initWithFrame_(self, frame):
        self = objc.super(_GlowDot, self).initWithFrame_(frame)
        if self is None:
            return None
        self._color = NSColor.grayColor()
        return self

    def setGlowColor_(self, color):
        self._color = color
        self.setNeedsDisplay_(True)

    def drawRect_(self, rect):
        ctx = NSGraphicsContext.currentContext()
        if not ctx:
            return
        ctx.saveGraphicsState()

        bounds = self.bounds()
        cx = bounds.size.width / 2
        cy = bounds.size.height / 2
        radius = min(cx, cy) - 6  # leave room for the glow

        # Outer glow (shadow)
        shadow = NSShadow.alloc().init()
        shadow.setShadowColor_(self._color.colorWithAlphaComponent_(0.75))
        shadow.setShadowBlurRadius_(10.0)
        shadow.setShadowOffset_((0, 0))
        shadow.set()

        # Draw filled circle
        circle = NSMakeRect(cx - radius, cy - radius, radius * 2, radius * 2)
        path = NSBezierPath.bezierPathWithOvalInRect_(circle)
        self._color.setFill()
        path.fill()

        # Inner highlight (brighter center)
        ctx.setShouldAntialias_(True)
        inner_r = radius * 0.45
        inner = NSMakeRect(cx - inner_r, cy - inner_r + 1, inner_r * 2, inner_r * 2)
        inner_path = NSBezierPath.bezierPathWithOvalInRect_(inner)
        self._color.colorWithAlphaComponent_(0.4).setFill()
        inner_path.fill()

        ctx.restoreGraphicsState()


# ─── Toggle switch target (NSObject subclass for NSSwitch action) ────────────

class _ToggleTarget(NSObject):
    """Receives the NSSwitch toggle action and delegates to the app."""
    app = objc.ivar()

    def switchFlipped_(self, sender):
        if self.app:
            self.app.toggle_mode(sender)


# ─── Menu bar application ───────────────────────────────────────────────────

class DragonCryptApp(rumps.App):

    def __init__(self):
        icon = ICON_PATH if ICON_PATH and os.path.exists(ICON_PATH) else None
        super().__init__(name=APP_NAME, title=None, icon=icon, template=False, quit_button=None)

        self._status = None
        self._daemon_starting = False
        self.console = ConsoleWindow()
        self._init_menu()

        # Auto-start daemon if not running
        self._ensure_daemon()

        # Initial poll, then schedule
        self._poll(None)
        self.timer = rumps.Timer(self._poll, POLL_INTERVAL_SECS)
        self.timer.start()

    def _init_menu(self):
        self.hero_item = rumps.MenuItem("")
        self._build_menu()
        self._setup_hero_view()

    def _setup_hero_view(self):
        """Build the custom hero status card with toggle."""
        w, h = 280, 72
        view = NSView.alloc().initWithFrame_(NSMakeRect(0, 0, w, h))

        # ── Mode label (large, bold) ─────────────────────────────────────
        self._hero_mode = NSTextField.alloc().initWithFrame_(NSMakeRect(16, h - 26, 150, 20))
        self._hero_mode.setStringValue_("INITIALIZING")
        self._hero_mode.setBezeled_(False)
        self._hero_mode.setDrawsBackground_(False)
        self._hero_mode.setEditable_(False)
        self._hero_mode.setSelectable_(False)
        self._hero_mode.setFont_(NSFont.boldSystemFontOfSize_(14))
        self._hero_mode.setTextColor_(NSColor.labelColor())
        view.addSubview_(self._hero_mode)

        # ── Subtitle (traffic status) ────────────────────────────────────
        self._hero_subtitle = NSTextField.alloc().initWithFrame_(NSMakeRect(16, h - 44, 160, 14))
        self._hero_subtitle.setStringValue_("Starting up…")
        self._hero_subtitle.setBezeled_(False)
        self._hero_subtitle.setDrawsBackground_(False)
        self._hero_subtitle.setEditable_(False)
        self._hero_subtitle.setSelectable_(False)
        self._hero_subtitle.setFont_(NSFont.systemFontOfSize_(11))
        self._hero_subtitle.setTextColor_(NSColor.secondaryLabelColor())
        view.addSubview_(self._hero_subtitle)

        # ── Detail line (daemon + sessions) ──────────────────────────────
        self._hero_detail = NSTextField.alloc().initWithFrame_(NSMakeRect(16, h - 60, 160, 13))
        self._hero_detail.setStringValue_("")
        self._hero_detail.setBezeled_(False)
        self._hero_detail.setDrawsBackground_(False)
        self._hero_detail.setEditable_(False)
        self._hero_detail.setSelectable_(False)
        self._hero_detail.setFont_(NSFont.monospacedSystemFontOfSize_weight_(9.5, 0.0))
        self._hero_detail.setTextColor_(
            NSColor.colorWithCalibratedRed_green_blue_alpha_(0.55, 0.55, 0.55, 1.0)
        )
        view.addSubview_(self._hero_detail)

        # ── Toggle switch (right side) ───────────────────────────────────
        self._toggle_switch = NSSwitch.alloc().initWithFrame_(NSMakeRect(w - 60, (h - 22) / 2, 44, 22))
        self._toggle_target = _ToggleTarget.alloc().init()
        self._toggle_target.app = self
        self._toggle_switch.setTarget_(self._toggle_target)
        self._toggle_switch.setAction_(objc.selector(self._toggle_target.switchFlipped_, signature=b'v@:@'))
        view.addSubview_(self._toggle_switch)

        # Attach custom view to the underlying NSMenuItem
        self.hero_item._menuitem.setView_(view)

    # --- Launch at Login ---

    LAUNCHD_LABEL = "com.digifx.crypt.menubar"
    LAUNCHD_PLIST_PATH = os.path.expanduser(
        f"~/Library/LaunchAgents/{LAUNCHD_LABEL}.plist"
    )
    LAUNCHD_PLIST_CONTENT = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>{LAUNCHD_LABEL}</string>

    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/open</string>
        <string>/Applications/DragonSource/Crypt.app</string>
    </array>

    <key>RunAtLoad</key>
    <true/>

    <key>KeepAlive</key>
    <false/>
</dict>
</plist>
"""

    def _is_launch_at_login(self):
        return os.path.isfile(self.LAUNCHD_PLIST_PATH)

    def _toggle_launch_at_login(self, sender):
        if self._is_launch_at_login():
            # Unload and remove
            subprocess.run(
                ["launchctl", "unload", self.LAUNCHD_PLIST_PATH],
                capture_output=True
            )
            try:
                os.remove(self.LAUNCHD_PLIST_PATH)
            except OSError:
                pass
        else:
            # Write plist and load
            os.makedirs(os.path.dirname(self.LAUNCHD_PLIST_PATH), exist_ok=True)
            with open(self.LAUNCHD_PLIST_PATH, "w") as f:
                f.write(self.LAUNCHD_PLIST_CONTENT)
            subprocess.run(
                ["launchctl", "load", self.LAUNCHD_PLIST_PATH],
                capture_output=True
            )
        # Rebuild menu to reflect new state
        self._build_menu()

    def _build_menu(self):
        launch_label = "✓ Launch at Login" if self._is_launch_at_login() else "  Launch at Login"
        self.menu.clear()
        self.menu = [
            self.hero_item,
            rumps.separator,
            rumps.MenuItem("Open Console", callback=self.open_console),
            rumps.MenuItem("View Audit Logs", callback=self.view_logs),
            rumps.separator,
            rumps.MenuItem(launch_label, callback=self._toggle_launch_at_login),
            rumps.separator,
            rumps.MenuItem("About", callback=self.show_about),
            rumps.MenuItem("Quit", callback=rumps.quit_application),
        ]

    # --- daemon auto-start ---

    _daemon_fail_count = 0
    _DAEMON_MAX_RETRIES = 3

    def _ensure_daemon(self):
        """Start the daemon automatically if it isn't running."""
        if daemon_is_alive():
            # Daemon is healthy — reset failure counter
            self._daemon_fail_count = 0
            return
        if self._daemon_starting:
            return
        if not NODE_BIN:
            return
        # Stop retrying after repeated failures
        if self._daemon_fail_count >= self._DAEMON_MAX_RETRIES:
            return
        # Look for bundled daemon (inside .app or project root), then source dist
        daemon_candidates = [
            get_resource_path(os.path.join("bundled", "daemon.js")),  # .app with dir structure
            get_resource_path("daemon.js"),                           # .app flat (legacy)
            os.path.join(CRYPT_PROJECT_ROOT, "bundled", "daemon.js"),  # standalone install
            os.path.join(CRYPT_PROJECT_ROOT, "dist", "daemon", "index.js"),  # dev source tree
        ]
        daemon_script = None
        for c in daemon_candidates:
            if c and os.path.isfile(c):
                daemon_script = c
                break
        if not daemon_script:
            return
        try:
            self._daemon_starting = True
            self._daemon_fail_count += 1
            env = os.environ.copy()
            env["PATH"] = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:" + env.get("PATH", "")

            # Set NODE_PATH so the bundled daemon.js can find better-sqlite3 next to it
            daemon_dir = os.path.dirname(daemon_script)
            node_modules = os.path.join(daemon_dir, "node_modules")
            if os.path.isdir(node_modules):
                env["NODE_PATH"] = node_modules + ":" + env.get("NODE_PATH", "")

            # Set CRYPT_STATE_DIR so the daemon writes state to a writable location
            # (the .app bundle is read-only, so we must point elsewhere on fresh installs)
            state_dir = os.path.expanduser("~/Library/Application Support/DragonCrypt/.crypt-state")
            os.makedirs(state_dir, exist_ok=True)
            env["CRYPT_STATE_DIR"] = state_dir

            # Log daemon output to a file for diagnostics
            log_dir = os.path.expanduser("~/Library/Application Support/DragonCrypt")
            os.makedirs(log_dir, exist_ok=True)
            log_file = open(os.path.join(log_dir, "daemon.log"), "a")

            subprocess.Popen(
                [NODE_BIN, daemon_script],
                cwd=os.path.expanduser("~"),
                env=env,
                stdout=log_file,
                stderr=log_file,
            )
        except Exception:
            pass

    # --- polling ---

    def _poll(self, _):
        self._status = ipc_get("/status")
        self._update_hero()
        self.console.update(self._status)

        # If daemon went down, try to bring it back
        if self._status is None:
            self._daemon_starting = False
            self._ensure_daemon()
        else:
            self._daemon_starting = False

    def _update_hero(self):
        """Update the hero card: glow color, labels, toggle state."""
        if self._status is None:
            # Offline
            self._hero_mode.setStringValue_("OFFLINE")
            self._hero_mode.setTextColor_(
                NSColor.colorWithCalibratedRed_green_blue_alpha_(0.90, 0.30, 0.30, 1.0)
            )
            self._hero_subtitle.setStringValue_("Daemon not reachable")
            self._hero_subtitle.setTextColor_(NSColor.secondaryLabelColor())
            self._hero_detail.setStringValue_("auto-restarting…" if self._daemon_starting else "")
            self._toggle_switch.setEnabled_(False)
            self._toggle_switch.setState_(0)
            return

        bypass = self._status.get('bypass', True)
        sessions = self._status.get('activeSessions', 0)
        self._toggle_switch.setEnabled_(True)

        if bypass:
            # Passthrough — amber
            amber = NSColor.colorWithCalibratedRed_green_blue_alpha_(0.95, 0.75, 0.20, 1.0)
            self._hero_mode.setStringValue_("PASSTHROUGH")
            self._hero_mode.setTextColor_(amber)
            self._hero_subtitle.setStringValue_("MCP traffic is unfiltered")
            self._hero_subtitle.setTextColor_(amber.colorWithAlphaComponent_(0.7))
            self._toggle_switch.setState_(0)
        else:
            # Secure — green
            green = NSColor.colorWithCalibratedRed_green_blue_alpha_(0.20, 0.88, 0.35, 1.0)
            self._hero_mode.setStringValue_("SECURE")
            self._hero_mode.setTextColor_(green)
            self._hero_subtitle.setStringValue_("MCP traffic is masked")
            self._hero_subtitle.setTextColor_(green.colorWithAlphaComponent_(0.7))
            self._toggle_switch.setState_(1)

        self._hero_detail.setStringValue_(f"daemon online · {sessions} active sessions")

    # --- actions ---

    def open_console(self, _):
        self.console.show()

    def toggle_mode(self, _):
        result = ipc_post("/toggle", {"by": "menubar"})
        if result is None:
            rumps.notification("Crypt", "Error", "Could not reach daemon — restarting…")
            self._daemon_starting = False
            self._ensure_daemon()
        else:
            mode = result.get("mode", "Unknown")
            rumps.notification("Crypt", "Mode Changed", mode)
        self._poll(None)

    def view_logs(self, _):
        data = ipc_get("/logs")
        if data is None:
            rumps.alert(title="Daemon Offline",
                        message="Start the daemon to view audit logs.",
                        ok="OK")
            return
        if data.get("count", 0) == 0:
            rumps.alert(title="Audit Log", message="No entries yet.", ok="Close")
            return
        lines = []
        for log in data.get("logs", [])[:15]:
            ts = log.get("timestamp", "?")[:19]
            dest = log.get("destination", "?")
            size = log.get("payload_bytes", 0)
            lines.append(f"{ts}  →  {dest}  ·  {size} bytes")
        rumps.alert(title="Dragon Crypt — Audit Log",
                    message="\n".join(lines),
                    ok="Close")

    def show_about(self, _):
        rumps.alert(
            title="Dragon Crypt",
            message=(
                f"Version {VERSION}\n\n"
                "Zero-trust MCP interceptor proxy.\n"
                "Masks proprietary IP before it reaches cloud LLMs.\n"
                "Local-only · zero network calls · full audit trail.\n\n"
                "© 2026 DigiFX LLC · Dragon Crypt"
            ),
            ok="Close",
        )


# ─── Main ───────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    DragonCryptApp().run()
