# Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved.
# 
# This software is released under the MIT License with a copyleft clause.
# See the LICENSE file for full terms.
"""Hermes Node Agent — Windows GUI Manager

System tray application for managing the Hermes Node Agent service.
Provides start/stop/restart, config editing, and log viewing.

Author: Lisa (Hermes AI)
Date: 2026-04-30
"""

import os
import sys
import json
import time
import threading
import subprocess
from pathlib import Path
from datetime import datetime
import webbrowser

import pystray
from pystray import MenuItem as item, Menu
from PIL import Image, ImageDraw
import wx  # For config editor dialog

# ── Configuration ────────────────────────────────────────────────────────────
CONFIG_DIR = Path(os.environ.get('PROGRAMDATA', 'C:\\ProgramData')) / 'hermes-node'
CONFIG_FILE = CONFIG_DIR / 'config.json'
AGENT_DIR = Path('C:\\Program Files\\Hermes Node')
AGENT_SCRIPT = AGENT_DIR / 'hermes-node-agent.py'
LOG_FILE = CONFIG_DIR / 'hermes-node-agent.log'
SERVICE_NAME = 'HermesNodeAgent'
NSSM_PATH = AGENT_DIR / 'nssm.exe'

# ── Status checking ──────────────────────────────────────────────────────────
def get_service_status() -> str:
    """Return: 'running', 'stopped', or 'unknown'."""
    try:
        result = subprocess.run(
            ['sc', 'query', SERVICE_NAME],
            capture_output=True, text=True, timeout=5
        )
        if 'RUNNING' in result.stdout:
            return 'running'
        elif 'STOPPED' in result.stdout:
            return 'stopped'
        else:
            return 'unknown'
    except Exception:
        return 'unknown'

def start_service() -> bool:
    try:
        subprocess.run(['sc', 'start', SERVICE_NAME], check=True, timeout=10)
        return True
    except subprocess.CalledProcessError:
        return False

def stop_service() -> bool:
    try:
        subprocess.run(['sc', 'stop', SERVICE_NAME], check=True, timeout=10)
        return True
    except subprocess.CalledProcessError:
        return False

def restart_service() -> bool:
    stop_service()
    time.sleep(2)
    return start_service()

def get_config() -> dict:
    if CONFIG_FILE.exists():
        try:
            with open(CONFIG_FILE) as f:
                return json.load(f)
        except Exception:
            return {}
    return {}

def save_config(config: dict) -> bool:
    try:
        CONFIG_DIR.mkdir(parents=True, exist_ok=True)
        with open(CONFIG_FILE, 'w') as f:
            json.dump(config, f, indent=2)
        return True
    except Exception as e:
        print(f"Save config error: {e}")
        return False

def tail_log(lines: int = 50) -> str:
    """Return last N lines of log file."""
    if LOG_FILE.exists():
        try:
            with open(LOG_FILE) as f:
                all_lines = f.readlines()
            return ''.join(all_lines[-lines:])
        except Exception:
            pass
    return "Log file not found."

# ── GUI: Config Editor ────────────────────────────────────────────────────────
class ConfigEditor(wx.Frame):
    """Window for editing node configuration."""

    def __init__(self, parent=None):
        super().__init__(parent, title="Hermes Node Configuration", size=(500, 400))
        self.panel = wx.Panel(self)
        self.sizer = wx.BoxSizer(wx.VERTICAL)

        # Form fields
        grid = wx.FlexGridSizer(6, 2, 10, 10)

        # Gateway URL
        grid.Add(wx.StaticText(self.panel, label="Gateway URL:"), 0, wx.ALIGN_RIGHT)
        self.gateway_url = wx.TextCtrl(self.panel, value="")
        grid.Add(self.gateway_url, 1, wx.EXPAND)

        # Node name
        grid.Add(wx.StaticText(self.panel, label="Node Name:"), 0, wx.ALIGN_RIGHT)
        self.node_name = wx.TextCtrl(self.panel, value="")
        grid.Add(self.node_name, 1, wx.EXPAND)

        # Token
        grid.Add(wx.StaticText(self.panel, label="Token:"), 0, wx.ALIGN_RIGHT)
        self.token = wx.TextCtrl(self.panel, value="")
        grid.Add(self.token, 1, wx.EXPAND)

        # sexec path
        grid.Add(wx.StaticText(self.panel, label="sexec Path:"), 0, wx.ALIGN_RIGHT)
        self.sexec_path = wx.TextCtrl(self.panel, value="")
        grid.Add(self.sexec_path, 1, wx.EXPAND)

        # Reconnect interval
        grid.Add(wx.StaticText(self.panel, label="Reconnect (s):"), 0, wx.ALIGN_RIGHT)
        self.reconnect = wx.TextCtrl(self.panel, value="5")
        grid.Add(self.reconnect, 1, wx.EXPAND)

        # Heartbeat interval
        grid.Add(wx.StaticText(self.panel, label="Heartbeat (s):"), 0, wx.ALIGN_RIGHT)
        self.heartbeat = wx.TextCtrl(self.panel, value="30")
        grid.Add(self.heartbeat, 1, wx.EXPAND)


        # Capabilities
        cap_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.cb_exec = wx.CheckBox(self.panel, label="exec")
        self.cb_exec.SetValue(True)
        self.cb_browser = wx.CheckBox(self.panel, label="browser_control")
        self.cb_computer = wx.CheckBox(self.panel, label="computer_control")
        label_txt = wx.StaticText(self.panel, label="Capabilities:")
        cap_sizer.Add(label_txt, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 10)
        cap_sizer.Add(self.cb_exec, 0, wx.RIGHT, 10)
        cap_sizer.Add(self.cb_browser, 0, wx.RIGHT, 10)
        cap_sizer.Add(self.cb_computer, 0)
        self.sizer.Add(cap_sizer, 0, wx.ALL | wx.EXPAND, 15)

        grid.AddGrowableCol(1, 1)
        self.sizer.Add(grid, 0, wx.ALL | wx.EXPAND, 15)

        # Buttons
        btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
        save_btn = wx.Button(self.panel, label="Save")
        save_btn.Bind(wx.EVT_BUTTON, self.on_save)
        cancel_btn = wx.Button(self.panel, label="Cancel")
        cancel_btn.Bind(wx.EVT_BUTTON, lambda e: self.Close())
        btn_sizer.Add(save_btn, 0, wx.ALL, 5)
        btn_sizer.Add(cancel_btn, 0, wx.ALL, 5)
        self.sizer.Add(btn_sizer, 0, wx.ALIGN_RIGHT)

        self.panel.SetSizer(self.sizer)

        # Load existing config
        self.load_config()

    def load_config(self):
        cfg = get_config()
        self.gateway_url.SetValue(cfg.get('gateway_url', 'wss://localhost:8765'))
        self.node_name.SetValue(cfg.get('node_name', os.environ.get('COMPUTERNAME', '')))
        self.token.SetValue(cfg.get('token', ''))
        self.sexec_path.SetValue(cfg.get('sexec_path', str(Path.home() / '.openclaw' / 'skills' / 'sexec' / 'sexec.ps1')))
        self.reconnect.SetValue(str(cfg.get('reconnect_interval', 5)))
        self.heartbeat.SetValue(str(cfg.get('heartbeat_interval', 30)))
        # Capabilities checkboxes
        caps = cfg.get('capabilities', ['exec'])
        self.cb_exec.SetValue('exec' in caps)
        self.cb_browser.SetValue('browser_control' in caps)
        self.cb_computer.SetValue('computer_control' in caps)
        # Load capabilities
        caps = cfg.get('capabilities', ['exec'])
        self.cb_exec.SetValue('exec' in caps)
        self.cb_browser.SetValue('browser_control' in caps)
        self.cb_computer.SetValue('computer_control' in caps)


    def _get_capabilities(self) -> list:
        """Collect capabilities from checkboxes."""
        caps = []
        if self.cb_exec.GetValue():
            caps.append('exec')
        if self.cb_browser.GetValue():
            caps.append('browser_control')
        if self.cb_computer.GetValue():
            caps.append('computer_control')
        return caps

    def on_save(self, event):
        cfg = {
            'gateway_url': self.gateway_url.GetValue(),
            'node_name': self.node_name.GetValue(),
            'token': self.token.GetValue(),
            'sexec_path': self.sexec_path.GetValue(),
            'reconnect_interval': int(self.reconnect.GetValue()),
            'heartbeat_interval': int(self.heartbeat.GetValue()),
            'capabilities': self._get_capabilities(),
        }
        if not cfg['gateway_url'] or not cfg['token']:
            wx.MessageBox("Gateway URL and Token are required", "Error", wx.OK | wx.ICON_ERROR)
            return
        if save_config(cfg):
            wx.MessageBox("Configuration saved.\nRestart the agent to apply changes.", "Success", wx.OK | wx.ICON_INFORMATION)
            self.Close()
        else:
            wx.MessageBox("Failed to save config", "Error", wx.OK | wx.ICON_ERROR)

# ── GUI: Log Viewer ───────────────────────────────────────────────────────────
class LogViewer(wx.Frame):
    """Window for viewing agent logs."""

    def __init__(self, parent=None):
        super().__init__(parent, title="Hermes Node Agent — Logs", size=(700, 500))
        panel = wx.Panel(self)
        sizer = wx.BoxSizer(wx.VERTICAL)

        self.log_text = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL)
        sizer.Add(self.log_text, 1, wx.ALL | wx.EXPAND, 10)

        btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
        refresh_btn = wx.Button(panel, label="Refresh")
        refresh_btn.Bind(wx.EVT_BUTTON, lambda e: self.refresh())
        tail_btn = wx.Button(panel, label="Tail (last 50)")
        tail_btn.Bind(wx.EVT_BUTTON, lambda e: self.tail())
        clear_btn = wx.Button(panel, label="Clear")
        clear_btn.Bind(wx.EVT_BUTTON, lambda e: self.log_text.SetValue(""))
        btn_sizer.Add(refresh_btn, 0, wx.ALL, 5)
        btn_sizer.Add(tail_btn, 0, wx.ALL, 5)
        btn_sizer.Add(clear_btn, 0, wx.ALL, 5)
        sizer.Add(btn_sizer, 0, wx.ALIGN_RIGHT)

        panel.SetSizer(sizer)
        self.refresh()

    def refresh(self):
        if LOG_FILE.exists():
            try:
                with open(LOG_FILE) as f:
                    content = f.read()
                self.log_text.SetValue(content)
            except Exception as e:
                self.log_text.SetValue(f"Error reading log: {e}")
        else:
            self.log_text.SetValue("Log file not found.")

    def tail(self):
        content = tail_log(50)
        self.log_text.SetValue(content)

# ── GUI: About/Status ─────────────────────────────────────────────────────────
class StatusDialog(wx.Frame):
    """Window showing detailed node status."""

    def __init__(self, parent=None):
        super().__init__(parent, title="Node Status", size=(400, 300))
        panel = wx.Panel(self)
        sizer = wx.BoxSizer(wx.VERTICAL)

        self.status_text = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY)
        sizer.Add(self.status_text, 1, wx.ALL | wx.EXPAND, 10)
        panel.SetSizer(sizer)
        self.update_status()

        # Auto-refresh timer
        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, lambda e: self.update_status(), self.timer)
        self.timer.Start(5000)  # 5 second update

    def update_status(self):
        status = get_service_status()
        cfg = get_config()
        lines = [
            "=== Hermes Node Agent ===\n",
            f"Service Status : {status.upper()}",
            f"Node Name      : {cfg.get('node_name', 'Not set')}",
            f"Gateway        : {cfg.get('gateway_url', 'Not set')}",
            f"Token          : {cfg.get('token', 'Not set')[:16]}...",
            f"sexec Path     : {cfg.get('sexec_path', 'Not set')}",
            f"Config File    : {str(CONFIG_FILE)}",
            f"Log File       : {str(LOG_FILE)}",
            f"Agent Dir      : {str(AGENT_DIR)}",
            "",
            "Usage: Right-click tray icon for menu",
        ]
        self.status_text.SetValue('\n'.join(lines))

# ── Tray Icon Creation ─────────────────────────────────────────────────────────
def create_image() -> Image.Image:
    """Generate a 16x16 icon for the system tray."""
    # Simple "H" icon on dark blue
    img = Image.new('RGB', (16, 16), color='#003366')
    draw = ImageDraw.Draw(img)
    # Draw "H"
    draw.rectangle([3, 3, 5, 13], fill='white')
    draw.rectangle([3, 7, 12, 9], fill='white')
    draw.rectangle([10, 3, 12, 13], fill='white')
    return img

# ── Tray Menu Actions ──────────────────────────────────────────────────────────
def on_config_click(icon, item):
    """Open configuration editor window."""
    app = wx.App(False)
    frame = ConfigEditor()
    frame.Show()
    app.MainLoop()

def on_logs_click(icon, item):
    """Open log viewer window."""
    app = wx.App(False)
    frame = LogViewer()
    frame.Show()
    app.MainLoop()

def on_status_click(icon, item):
    """Open status window."""
    app = wx.App(False)
    frame = StatusDialog()
    frame.Show()
    app.MainLoop()

def on_start_click(icon, item):
    """Start the agent service."""
    if start_service():
        show_notification("Hermes Node Agent", "Service started")
    else:
        show_notification("Hermes Node Agent", "Failed to start service", "error")

def on_stop_click(icon, item):
    """Stop the agent service."""
    if stop_service():
        show_notification("Hermes Node Agent", "Service stopped")
    else:
        show_notification("Hermes Node Agent", "Failed to stop service", "error")

def on_restart_click(icon, item):
    """Restart the agent service."""
    if restart_service():
        show_notification("Hermes Node Agent", "Service restarted")
    else:
        show_notification("Hermes Node Agent", "Failed to restart service", "error")

def on_open_gateway_click(icon, item):
    """Open gateway web UI in browser (if available)."""
    cfg = get_config()
    gw_url = cfg.get('gateway_url', '')
    if gw_url:
        # Convert ws/wss URL to http
        http_url = gw_url.replace('ws://', 'http://').replace('wss://', 'https://')
        http_url = http_url.rstrip('/') + '/'
        webbrowser.open(http_url)
    else:
        show_notification("Hermes Node Agent", "Gateway URL not configured", "warning")

def on_exit_click(icon, item):
    """Exit the tray application."""
    icon.stop()

# ── Notification helper ─────────────────────────────────────────────────────────
def show_notification(title: str, message: str, level: str = 'info'):
    """Show Windows toast notification."""
    try:
        from win10toast import ToastNotifier
        toaster = ToastNotifier()
        duration = 5 if level == 'error' else 3
        toaster.show_toast(title, message, duration=duration, threaded=True)
    except ImportError:
        # Fallback: just print
        print(f"[{level.upper()}] {title}: {message}")

# ── Tray Icon ───────────────────────────────────────────────────────────────────
def build_tray_icon() -> pystray.Icon:
    """Construct and return the system tray icon."""
    icon = pystray.Icon(
        'HermesNodeAgent',
        create_image(),
        'Hermes Node Agent',
        menu=Menu(
            item('Configuration', on_config_click),
            item('Logs', on_logs_click),
            item('Status', on_status_click),
            Menu.SEPARATOR,
            item('Start Agent', on_start_click),
            item('Stop Agent', on_stop_click),
            item('Restart Agent', on_restart_click),
            Menu.SEPARATOR,
            item('Open Gateway UI', on_open_gateway_click),
            Menu.SEPARATOR,
            item('Exit', on_exit_click)
        )
    )
    return icon

# ── Main ────────────────────────────────────────────────────────────────────────
def main():
    """Entry point for the tray manager."""
    # Verify required files exist
    if not CONFIG_FILE.exists():
        print(f"Config not found at {CONFIG_FILE}")
        print("Run the installer first, or edit/create the config file.")
        sys.exit(1)

    # Check if agent script exists
    if not AGENT_SCRIPT.exists():
        print(f"Agent script not found at {AGENT_SCRIPT}")
        sys.exit(1)

    # Check service is installed
    result = subprocess.run(['sc', 'query', SERVICE_NAME], capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Service '{SERVICE_NAME}' not found.")
        print("Run the installer to register the service first.")
        sys.exit(1)

    print("Starting Hermes Node Agent Manager...")
    print(f"Config: {CONFIG_FILE}")
    print(f"Log:    {LOG_FILE}")

    icon = build_tray_icon()
    icon.run()

if __name__ == '__main__':
    main()
