#!/usr/bin/env python3
"""
UsefulMiner Server - Dashboard Monitor for Distributed Computing Platforms
Port: 9004
Features:
- Monitor if Grass/NodePay apps are running
- Track GRASS token balance via Solana RPC
- Store manual earnings logs in SQLite
- Real-time WebSocket updates to iOS app
"""

from flask import Flask, jsonify, request
from flask_cors import CORS
from flask_socketio import SocketIO, emit
import sqlite3
import subprocess
import threading
import time
import json
import requests
import socket
import platform
from datetime import datetime, timedelta
from pathlib import Path
from collections import deque

# Bonjour/Zeroconf for service discovery
try:
    from zeroconf import ServiceInfo, Zeroconf
    ZEROCONF_AVAILABLE = True
except ImportError:
    ZEROCONF_AVAILABLE = False
    print("⚠️ zeroconf not installed. Run: pip3 install zeroconf")

# System info
try:
    import psutil
    PSUTIL_AVAILABLE = True
except ImportError:
    PSUTIL_AVAILABLE = False

# ============================================================================
# Configuration
# ============================================================================

PORT = 9004
DB_PATH = Path(__file__).parent / "usefulminer.db"
SOLANA_RPC = "https://api.mainnet-beta.solana.com"
GRASS_TOKEN_MINT = "Grass7B4RdKfBCjTKgSqnXkqjwiGvQyFbuSCUJr3XXjs"  # Official GRASS token

# Bonjour/mDNS Service Discovery Configuration
SERVICE_TYPE = "_usefulminer._tcp.local."
SERVICE_NAME = "MacBook-Air-M4-UsefulMiner._usefulminer._tcp.local."

# Default stats when psutil is not available (prevents iOS decode errors)
DEFAULT_STATS = {
    "cpu": {
        "percent": 0.0,
        "percent_per_core": [],
        "count_logical": 0,
        "count_physical": 0,
        "frequency_mhz": None
    },
    "memory": {
        "total_gb": 0.0,
        "available_gb": 0.0,
        "used_gb": 0.0,
        "percent": 0.0,
        "free_gb": 0.0,
        "swap": {
            "total_gb": 0.0,
            "used_gb": 0.0,
            "percent": 0.0
        }
    },
    "disk": {
        "total_gb": 0.0,
        "used_gb": 0.0,
        "free_gb": 0.0,
        "percent": 0.0
    },
    "network": {
        "bytes_sent_mb": 0.0,
        "bytes_recv_mb": 0.0,
        "packets_sent": 0,
        "packets_recv": 0
    },
    "uptime": {
        "boot_time": datetime.now().isoformat(),
        "uptime_seconds": 0,
        "uptime_hours": 0.0,
        "uptime_days": 0.0,
        "uptime_formatted": "Unknown"
    }
}

# Server Info (MacBook Air M4 specs)
SERVER_INFO = {
    "name": "MacBook Air M4",
    "hostname": platform.node() or "MacBook-Air-M4",
    "chip": "M4",
    "cores": "10-core CPU, 10-core GPU",
    "memory": "16GB",
    "storage": "256GB SSD",
    "version": "1.0.0"
}

# Process names to monitor
PROCESS_NAMES = {
    "grass": ["Grass", "grass", "getgrass"],
    "nodepay": ["NodePay", "nodepay", "Nodepay"],
    "honeygain": ["Honeygain", "honeygain"]
}

# NodePay Chrome Extension Detection
NODEPAY_EXTENSION_ID = "lgmpfmgeabnnlemejacfljbmonaomfmm"
import os

def check_nodepay_extension():
    """Check if NodePay Chrome extension is installed and Chrome is running."""
    # 1. Check if Chrome is running
    chrome_running = False
    try:
        result = subprocess.run(['pgrep', '-x', 'Google Chrome'], capture_output=True)
        chrome_running = result.returncode == 0
    except Exception:
        pass

    if not chrome_running:
        return False

    # 2. Check if NodePay extension is installed
    chrome_extensions_path = os.path.expanduser(
        "~/Library/Application Support/Google/Chrome/Default/Extensions"
    )
    nodepay_path = os.path.join(chrome_extensions_path, NODEPAY_EXTENSION_ID)

    return os.path.exists(nodepay_path)

# ============================================================================
# Flask App Setup
# ============================================================================

app = Flask(__name__)
CORS(app, origins=["*"])
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')

# Global state
state = {
    "apps": {
        "grass": {"is_running": False, "last_checked": None},
        "nodepay": {"is_running": False, "last_checked": None},
        "honeygain": {"is_running": False, "last_checked": None}
    },
    "wallet": {
        "address": None,
        "grass_balance": 0,
        "last_updated": None
    },
    "stats": {
        "total_earnings_usd": 0,
        "this_month_usd": 0,
        "earnings_count": 0
    }
}

# Stats history for charts (circular buffer - keeps last 720 samples = 1 hour at 5s intervals)
stats_history = deque(maxlen=720)

# ============================================================================
# Database Setup
# ============================================================================

def init_db():
    """Initialize SQLite database with required tables."""
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()

    # Earnings table
    c.execute('''
        CREATE TABLE IF NOT EXISTS earnings (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            platform TEXT NOT NULL,
            amount REAL NOT NULL,
            amount_usd REAL,
            notes TEXT,
            recorded_at TEXT NOT NULL,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        )
    ''')

    # Settings table
    c.execute('''
        CREATE TABLE IF NOT EXISTS settings (
            key TEXT PRIMARY KEY,
            value TEXT NOT NULL
        )
    ''')

    # Expenses table (for profit tracking)
    c.execute('''
        CREATE TABLE IF NOT EXISTS expenses (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            category TEXT NOT NULL,
            amount REAL NOT NULL,
            description TEXT,
            platform TEXT,
            recorded_at TEXT NOT NULL,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        )
    ''')

    # Cost allocations table (Budget Thinking - allocate each expense to platforms)
    c.execute('''
        CREATE TABLE IF NOT EXISTS cost_allocations (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            expense_id INTEGER NOT NULL,
            platform TEXT,
            amount REAL NOT NULL,
            percentage REAL NOT NULL,
            notes TEXT,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (expense_id) REFERENCES expenses(id) ON DELETE CASCADE
        )
    ''')

    # Create indexes
    c.execute('CREATE INDEX IF NOT EXISTS idx_earnings_platform ON earnings(platform)')
    c.execute('CREATE INDEX IF NOT EXISTS idx_earnings_recorded_at ON earnings(recorded_at)')
    c.execute('CREATE INDEX IF NOT EXISTS idx_expenses_category ON expenses(category)')
    c.execute('CREATE INDEX IF NOT EXISTS idx_expenses_recorded_at ON expenses(recorded_at)')
    c.execute('CREATE INDEX IF NOT EXISTS idx_cost_allocations_expense ON cost_allocations(expense_id)')
    c.execute('CREATE INDEX IF NOT EXISTS idx_cost_allocations_platform ON cost_allocations(platform)')

    conn.commit()
    conn.close()
    print(f"Database initialized at {DB_PATH}")

def get_db():
    """Get database connection with row factory."""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn

# ============================================================================
# Process Monitoring
# ============================================================================

def check_process_running(process_names: list) -> bool:
    """Check if any of the given process names are running."""
    try:
        result = subprocess.run(
            ["pgrep", "-i", "-l", "|".join(process_names)],
            capture_output=True, text=True
        )
        return result.returncode == 0
    except Exception:
        pass

    # Fallback: use ps aux
    try:
        result = subprocess.run(["ps", "aux"], capture_output=True, text=True)
        output = result.stdout.lower()
        for name in process_names:
            if name.lower() in output:
                return True
    except Exception:
        pass

    return False

def update_app_status():
    """Update the running status of all monitored apps."""
    now = datetime.now().isoformat()
    for platform, names in PROCESS_NAMES.items():
        # NodePay: detect via Chrome extension
        if platform == "nodepay":
            is_running = check_nodepay_extension()
        else:
            # Grass/Honeygain: detect via process
            is_running = check_process_running(names)

        state["apps"][platform]["is_running"] = is_running
        state["apps"][platform]["last_checked"] = now

# ============================================================================
# Solana Integration (Optional)
# ============================================================================

def get_grass_balance(wallet_address: str) -> float:
    """Query GRASS token balance from Solana blockchain."""
    if not wallet_address:
        return 0.0

    try:
        # Get token accounts for the wallet
        payload = {
            "jsonrpc": "2.0",
            "id": 1,
            "method": "getTokenAccountsByOwner",
            "params": [
                wallet_address,
                {"mint": GRASS_TOKEN_MINT},
                {"encoding": "jsonParsed"}
            ]
        }

        response = requests.post(SOLANA_RPC, json=payload, timeout=10)
        data = response.json()

        if "result" in data and data["result"]["value"]:
            accounts = data["result"]["value"]
            total = 0
            for account in accounts:
                info = account["account"]["data"]["parsed"]["info"]
                amount = float(info["tokenAmount"]["uiAmount"] or 0)
                total += amount
            return total
    except Exception as e:
        print(f"Error fetching GRASS balance: {e}")

    return 0.0

def update_wallet_balance():
    """Update wallet balance from Solana."""
    if state["wallet"]["address"]:
        balance = get_grass_balance(state["wallet"]["address"])
        state["wallet"]["grass_balance"] = balance
        state["wallet"]["last_updated"] = datetime.now().isoformat()

# ============================================================================
# Statistics
# ============================================================================

def update_stats():
    """Update earnings statistics."""
    conn = get_db()
    c = conn.cursor()

    # Total earnings
    c.execute("SELECT COALESCE(SUM(amount_usd), 0) FROM earnings WHERE amount_usd IS NOT NULL")
    state["stats"]["total_earnings_usd"] = c.fetchone()[0]

    # This month
    first_of_month = datetime.now().replace(day=1).strftime("%Y-%m-%d")
    c.execute(
        "SELECT COALESCE(SUM(amount_usd), 0) FROM earnings WHERE amount_usd IS NOT NULL AND recorded_at >= ?",
        (first_of_month,)
    )
    state["stats"]["this_month_usd"] = c.fetchone()[0]

    # Count
    c.execute("SELECT COUNT(*) FROM earnings")
    state["stats"]["earnings_count"] = c.fetchone()[0]

    conn.close()

# ============================================================================
# Bonjour/mDNS Service Discovery
# ============================================================================

# Global Bonjour state
zeroconf_instance = None
service_info = None

def get_local_ip():
    """Get the local IP address of this machine."""
    try:
        # Create a socket to determine the local IP
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return "127.0.0.1"

def get_system_info():
    """Get real system information if psutil is available."""
    info = SERVER_INFO.copy()

    if PSUTIL_AVAILABLE:
        try:
            # Memory info
            mem = psutil.virtual_memory()
            info["memory_total_gb"] = round(mem.total / (1024**3))
            info["memory_available_gb"] = round(mem.available / (1024**3))
            info["memory_percent"] = mem.percent

            # CPU info
            info["cpu_percent"] = psutil.cpu_percent(interval=0.1)
            info["cpu_count"] = psutil.cpu_count()

            # Disk info
            disk = psutil.disk_usage('/')
            info["disk_total_gb"] = round(disk.total / (1024**3))
            info["disk_free_gb"] = round(disk.free / (1024**3))

            # Uptime
            boot_time = datetime.fromtimestamp(psutil.boot_time())
            uptime = datetime.now() - boot_time
            info["uptime_hours"] = round(uptime.total_seconds() / 3600, 1)
        except Exception as e:
            print(f"Error getting system info: {e}")

    return info

def start_bonjour():
    """Start Bonjour/mDNS service advertisement."""
    global zeroconf_instance, service_info

    if not ZEROCONF_AVAILABLE:
        print("⚠️ Bonjour: zeroconf not available, skipping service advertisement")
        return False

    try:
        local_ip = get_local_ip()
        sys_info = get_system_info()

        # Service properties (advertised via mDNS TXT records)
        properties = {
            "version": SERVER_INFO["version"],
            "hostname": SERVER_INFO["hostname"],
            "chip": SERVER_INFO["chip"],
            "memory": SERVER_INFO["memory"],
            "cores": SERVER_INFO["cores"],
            "ip": local_ip
        }

        service_info = ServiceInfo(
            SERVICE_TYPE,
            SERVICE_NAME,
            addresses=[socket.inet_aton(local_ip)],
            port=PORT,
            properties=properties,
            server=f"{SERVER_INFO['hostname']}.local."
        )

        zeroconf_instance = Zeroconf()
        zeroconf_instance.register_service(service_info)

        print(f"✅ Bonjour: Service registered as '{SERVICE_NAME}'")
        print(f"   IP: {local_ip}:{PORT}")
        print(f"   Properties: {properties}")
        return True

    except Exception as e:
        print(f"❌ Bonjour: Failed to start - {e}")
        return False

def stop_bonjour():
    """Stop Bonjour/mDNS service advertisement."""
    global zeroconf_instance, service_info

    if zeroconf_instance and service_info:
        try:
            zeroconf_instance.unregister_service(service_info)
            zeroconf_instance.close()
            print("✅ Bonjour: Service unregistered")
        except Exception as e:
            print(f"⚠️ Bonjour: Error stopping - {e}")

    zeroconf_instance = None
    service_info = None

# ============================================================================
# Background Worker
# ============================================================================

def collect_stats_sample():
    """Collect a stats sample for history tracking."""
    if not PSUTIL_AVAILABLE:
        return

    try:
        mem = psutil.virtual_memory()
        sample = {
            "timestamp": datetime.now().isoformat(),
            "cpu_percent": psutil.cpu_percent(interval=0.1),
            "memory_percent": mem.percent,
            "memory_used_gb": round(mem.used / (1024**3), 2)
        }
        stats_history.append(sample)
    except Exception as e:
        print(f"Stats collection error: {e}")

def background_worker():
    """Background thread that updates status periodically."""
    cycle_counter = 0
    while True:
        try:
            # Collect stats sample every cycle (every 5 seconds)
            collect_stats_sample()

            # Update app status and broadcast every 6 cycles (every 30 seconds)
            cycle_counter += 1
            if cycle_counter % 6 == 0:
                update_app_status()
                update_wallet_balance()
                update_stats()
                socketio.emit('status_update', get_full_status())
                cycle_counter = 0

        except Exception as e:
            print(f"Background worker error: {e}")

        time.sleep(5)  # Run every 5 seconds for stats collection

# ============================================================================
# REST API Endpoints
# ============================================================================

def get_full_status():
    """Get complete status object."""
    return {
        "apps": state["apps"],
        "wallet": state["wallet"],
        "stats": state["stats"],
        "timestamp": datetime.now().isoformat()
    }

@app.route('/')
def index():
    """API info."""
    return jsonify({
        "name": "UsefulMiner Server",
        "version": SERVER_INFO["version"],
        "server": SERVER_INFO,
        "port": PORT,
        "ip": get_local_ip(),
        "bonjour": ZEROCONF_AVAILABLE,
        "endpoints": [
            "GET /api/discovery",
            "GET /api/health",
            "GET /api/status",
            "GET /api/earnings",
            "POST /api/earnings",
            "DELETE /api/earnings/<id>",
            "GET /api/earnings/summary",
            "GET /api/wallet/balance",
            "POST /api/wallet/address",
            "GET /api/expenses",
            "POST /api/expenses",
            "DELETE /api/expenses/<id>",
            "GET /api/analytics/profit",
            "GET /api/budget/summary",
            "POST /api/expenses/<id>/allocate",
            "GET /api/expenses/<id>/allocations",
            "POST /api/budget/allocate-auto"
        ]
    })

@app.route('/api/health')
def health():
    """Health check endpoint."""
    return jsonify({
        "status": "healthy",
        "timestamp": datetime.now().isoformat(),
        "uptime": "running"
    })

@app.route('/api/discovery')
def discovery():
    """Discovery endpoint - returns server info for iOS app auto-discovery."""
    sys_info = get_system_info()
    local_ip = get_local_ip()

    return jsonify({
        "name": SERVER_INFO["name"],
        "hostname": SERVER_INFO["hostname"],
        "chip": SERVER_INFO["chip"],
        "cores": SERVER_INFO["cores"],
        "memory": SERVER_INFO["memory"],
        "storage": SERVER_INFO["storage"],
        "version": SERVER_INFO["version"],
        "ip": local_ip,
        "port": PORT,
        "url": f"http://{local_ip}:{PORT}",
        "capabilities": ["status", "earnings", "wallet", "websocket"],
        "system": {
            "memory_total_gb": sys_info.get("memory_total_gb"),
            "memory_available_gb": sys_info.get("memory_available_gb"),
            "memory_percent": sys_info.get("memory_percent"),
            "cpu_percent": sys_info.get("cpu_percent"),
            "cpu_count": sys_info.get("cpu_count"),
            "disk_total_gb": sys_info.get("disk_total_gb"),
            "disk_free_gb": sys_info.get("disk_free_gb"),
            "uptime_hours": sys_info.get("uptime_hours")
        },
        "timestamp": datetime.now().isoformat()
    })

@app.route('/api/server/stats')
def server_stats():
    """Comprehensive server statistics endpoint."""
    import copy
    # Use deep copy of DEFAULT_STATS to avoid mutating the original
    stats = {
        "timestamp": datetime.now().isoformat(),
        "server": {
            "name": SERVER_INFO["name"],
            "hostname": SERVER_INFO["hostname"],
            "chip": SERVER_INFO["chip"],
            "version": SERVER_INFO["version"]
        },
        "cpu": copy.deepcopy(DEFAULT_STATS["cpu"]),
        "memory": copy.deepcopy(DEFAULT_STATS["memory"]),
        "disk": copy.deepcopy(DEFAULT_STATS["disk"]),
        "network": copy.deepcopy(DEFAULT_STATS["network"]),
        "processes": [],
        "uptime": copy.deepcopy(DEFAULT_STATS["uptime"])
    }

    if PSUTIL_AVAILABLE:
        try:
            # CPU Stats
            stats["cpu"] = {
                "percent": psutil.cpu_percent(interval=0.1),
                "percent_per_core": psutil.cpu_percent(interval=0.1, percpu=True),
                "count_logical": psutil.cpu_count(),
                "count_physical": psutil.cpu_count(logical=False),
                "frequency_mhz": psutil.cpu_freq().current if psutil.cpu_freq() else None
            }

            # Memory Stats
            mem = psutil.virtual_memory()
            stats["memory"] = {
                "total_gb": round(mem.total / (1024**3), 2),
                "available_gb": round(mem.available / (1024**3), 2),
                "used_gb": round(mem.used / (1024**3), 2),
                "percent": mem.percent,
                "free_gb": round(mem.free / (1024**3), 2)
            }

            # Swap Memory
            swap = psutil.swap_memory()
            stats["memory"]["swap"] = {
                "total_gb": round(swap.total / (1024**3), 2),
                "used_gb": round(swap.used / (1024**3), 2),
                "percent": swap.percent
            }

            # Disk Stats
            disk = psutil.disk_usage('/')
            stats["disk"] = {
                "total_gb": round(disk.total / (1024**3), 2),
                "used_gb": round(disk.used / (1024**3), 2),
                "free_gb": round(disk.free / (1024**3), 2),
                "percent": round(disk.percent, 1)
            }

            # Network Stats
            net = psutil.net_io_counters()
            stats["network"] = {
                "bytes_sent_mb": round(net.bytes_sent / (1024**2), 2),
                "bytes_recv_mb": round(net.bytes_recv / (1024**2), 2),
                "packets_sent": net.packets_sent,
                "packets_recv": net.packets_recv
            }

            # System Uptime
            boot_time = datetime.fromtimestamp(psutil.boot_time())
            uptime = datetime.now() - boot_time
            stats["uptime"] = {
                "boot_time": boot_time.isoformat(),
                "uptime_seconds": int(uptime.total_seconds()),
                "uptime_hours": round(uptime.total_seconds() / 3600, 2),
                "uptime_days": round(uptime.total_seconds() / 86400, 2),
                "uptime_formatted": str(uptime).split('.')[0]  # HH:MM:SS format
            }

            # Mining App Processes
            mining_apps = ["Grass", "Honeygain", "Google Chrome"]
            for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'status']):
                try:
                    pinfo = proc.info
                    if any(app.lower() in pinfo['name'].lower() for app in mining_apps):
                        stats["processes"].append({
                            "pid": pinfo['pid'],
                            "name": pinfo['name'],
                            "cpu_percent": round(pinfo['cpu_percent'] or 0, 1),
                            "memory_percent": round(pinfo['memory_percent'] or 0, 1),
                            "status": pinfo['status']
                        })
                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    pass

        except Exception as e:
            stats["error"] = str(e)
    else:
        stats["error"] = "psutil not available"

    return jsonify(stats)

@app.route('/api/server/stats/history')
def server_stats_history():
    """Get historical server statistics (stored in memory)."""
    # Return last N samples from the stats history
    limit = request.args.get('limit', 60, type=int)
    return jsonify({
        "history": list(stats_history)[-limit:],
        "count": len(stats_history),
        "interval_seconds": 5
    })

@app.route('/api/status')
def get_status():
    """Get current status of all platforms."""
    update_app_status()
    update_stats()
    return jsonify(get_full_status())

@app.route('/api/apps/<app_name>/toggle', methods=['POST'])
def toggle_app_status(app_name):
    """Manually toggle app running status (for extensions that can't be detected)."""
    if app_name not in state["apps"]:
        return jsonify({"error": f"Unknown app: {app_name}"}), 404

    data = request.get_json() or {}
    is_running = data.get("is_running", not state["apps"][app_name]["is_running"])

    state["apps"][app_name]["is_running"] = is_running
    state["apps"][app_name]["last_checked"] = datetime.now().isoformat()
    state["apps"][app_name]["manual_override"] = True

    # Broadcast to all connected clients
    socketio.emit('status_update', get_full_status())

    return jsonify({
        "app": app_name,
        "is_running": is_running,
        "message": f"{app_name} marked as {'running' if is_running else 'stopped'}"
    })

# ============================================================================
# App Control Endpoints (Start/Stop)
# ============================================================================

# App bundle identifiers and paths
APP_CONFIGS = {
    "grass": {
        "bundle_id": "io.getgrass.desktop",
        "app_name": "Grass.app",
        "process_name": "Grass"
    },
    "nodepay": {
        "bundle_id": "com.google.Chrome",
        "extension_id": NODEPAY_EXTENSION_ID,
        "is_extension": True
    },
    "honeygain": {
        "bundle_id": "com.honeygain.Honeygain",
        "app_name": "Honeygain.app",
        "process_name": "Honeygain"
    }
}

def find_app_path(app_name):
    """Find the full path to an application."""
    common_paths = [
        f"/Applications/{app_name}",
        os.path.expanduser(f"~/Applications/{app_name}"),
        f"/System/Applications/{app_name}"
    ]
    for path in common_paths:
        if os.path.exists(path):
            return path
    return None

@app.route('/api/apps', methods=['GET'])
def list_apps():
    """List all controllable apps with their status."""
    update_app_status()
    apps = []
    for app_name, config in APP_CONFIGS.items():
        app_info = {
            "name": app_name,
            "display_name": app_name.capitalize(),
            "is_running": state["apps"][app_name]["is_running"],
            "last_checked": state["apps"][app_name]["last_checked"],
            "is_extension": config.get("is_extension", False),
            "can_control": not config.get("is_extension", False)
        }
        # Check if app is installed
        if not config.get("is_extension"):
            app_path = find_app_path(config.get("app_name", ""))
            app_info["is_installed"] = app_path is not None
            app_info["path"] = app_path
        else:
            app_info["is_installed"] = True  # Extensions checked differently
        apps.append(app_info)
    return jsonify({"apps": apps})

@app.route('/api/apps/<app_name>/start', methods=['POST'])
def start_app(app_name):
    """Start a mining app."""
    if app_name not in APP_CONFIGS:
        return jsonify({"error": f"Unknown app: {app_name}"}), 404

    config = APP_CONFIGS[app_name]

    # Can't start browser extensions directly
    if config.get("is_extension"):
        return jsonify({
            "error": f"{app_name} is a browser extension. Open Chrome to activate it.",
            "suggestion": "Launch Chrome with NodePay extension enabled"
        }), 400

    # Find and launch the app
    app_path = find_app_path(config.get("app_name", ""))
    if not app_path:
        return jsonify({
            "error": f"{app_name} app not found",
            "searched_paths": ["/Applications", "~/Applications"]
        }), 404

    try:
        # Use 'open' command on macOS
        result = subprocess.run(
            ["open", app_path],
            capture_output=True, text=True
        )

        if result.returncode == 0:
            # Wait a bit and update status
            time.sleep(2)
            update_app_status()
            socketio.emit('status_update', get_full_status())

            return jsonify({
                "success": True,
                "app": app_name,
                "message": f"{app_name.capitalize()} started successfully",
                "is_running": state["apps"][app_name]["is_running"]
            })
        else:
            return jsonify({
                "error": f"Failed to start {app_name}",
                "details": result.stderr
            }), 500

    except Exception as e:
        return jsonify({"error": str(e)}), 500

@app.route('/api/apps/<app_name>/stop', methods=['POST'])
def stop_app(app_name):
    """Stop a mining app."""
    if app_name not in APP_CONFIGS:
        return jsonify({"error": f"Unknown app: {app_name}"}), 404

    config = APP_CONFIGS[app_name]

    # Can't stop browser extensions directly
    if config.get("is_extension"):
        return jsonify({
            "error": f"{app_name} is a browser extension. Close the Chrome tab to stop it.",
            "suggestion": "Disable the extension in Chrome settings"
        }), 400

    process_name = config.get("process_name")
    if not process_name:
        return jsonify({"error": f"No process name configured for {app_name}"}), 400

    try:
        # Use pkill to stop the process
        result = subprocess.run(
            ["pkill", "-x", process_name],
            capture_output=True, text=True
        )

        # Also try with -i for case insensitive
        subprocess.run(
            ["pkill", "-i", process_name],
            capture_output=True, text=True
        )

        # Wait a bit and update status
        time.sleep(1)
        update_app_status()
        socketio.emit('status_update', get_full_status())

        return jsonify({
            "success": True,
            "app": app_name,
            "message": f"{app_name.capitalize()} stopped",
            "is_running": state["apps"][app_name]["is_running"]
        })

    except Exception as e:
        return jsonify({"error": str(e)}), 500

@app.route('/api/apps/<app_name>/restart', methods=['POST'])
def restart_app(app_name):
    """Restart a mining app (stop then start)."""
    if app_name not in APP_CONFIGS:
        return jsonify({"error": f"Unknown app: {app_name}"}), 404

    config = APP_CONFIGS[app_name]

    if config.get("is_extension"):
        return jsonify({
            "error": f"{app_name} is a browser extension and cannot be restarted remotely"
        }), 400

    # Stop first
    stop_response = stop_app(app_name)
    if hasattr(stop_response, 'get_json'):
        stop_data = stop_response.get_json()
    else:
        stop_data = stop_response[0].get_json() if isinstance(stop_response, tuple) else {}

    # Wait a moment
    time.sleep(2)

    # Start
    start_response = start_app(app_name)
    if hasattr(start_response, 'get_json'):
        start_data = start_response.get_json()
    else:
        start_data = start_response[0].get_json() if isinstance(start_response, tuple) else {}

    return jsonify({
        "success": start_data.get("success", False),
        "app": app_name,
        "message": f"{app_name.capitalize()} restarted",
        "is_running": state["apps"][app_name]["is_running"]
    })

@app.route('/api/apps/start-all', methods=['POST'])
def start_all_apps():
    """Start all controllable mining apps."""
    results = {}
    for app_name, config in APP_CONFIGS.items():
        if not config.get("is_extension"):
            response = start_app(app_name)
            if hasattr(response, 'get_json'):
                results[app_name] = response.get_json()
            else:
                results[app_name] = {"skipped": True, "reason": "extension"}
        else:
            results[app_name] = {"skipped": True, "reason": "browser_extension"}

    return jsonify({
        "success": True,
        "results": results,
        "message": "Start all apps completed"
    })

@app.route('/api/apps/stop-all', methods=['POST'])
def stop_all_apps():
    """Stop all controllable mining apps."""
    results = {}
    for app_name, config in APP_CONFIGS.items():
        if not config.get("is_extension"):
            response = stop_app(app_name)
            if hasattr(response, 'get_json'):
                results[app_name] = response.get_json()
            else:
                results[app_name] = {"skipped": True, "reason": "extension"}
        else:
            results[app_name] = {"skipped": True, "reason": "browser_extension"}

    return jsonify({
        "success": True,
        "results": results,
        "message": "Stop all apps completed"
    })

@app.route('/api/earnings', methods=['GET'])
def get_earnings():
    """Get earnings history."""
    platform = request.args.get('platform')
    limit = request.args.get('limit', 100, type=int)
    offset = request.args.get('offset', 0, type=int)

    conn = get_db()
    c = conn.cursor()

    if platform:
        c.execute(
            "SELECT * FROM earnings WHERE platform = ? ORDER BY recorded_at DESC LIMIT ? OFFSET ?",
            (platform, limit, offset)
        )
    else:
        c.execute(
            "SELECT * FROM earnings ORDER BY recorded_at DESC LIMIT ? OFFSET ?",
            (limit, offset)
        )

    rows = c.fetchall()
    earnings = [dict(row) for row in rows]
    conn.close()

    return jsonify({
        "earnings": earnings,
        "count": len(earnings),
        "limit": limit,
        "offset": offset
    })

@app.route('/api/earnings', methods=['POST'])
def add_earning():
    """Add a new earnings record."""
    data = request.json

    if not data or 'platform' not in data or 'amount' not in data:
        return jsonify({"error": "Missing required fields: platform, amount"}), 400

    platform = data['platform']
    amount = float(data['amount'])
    amount_usd = float(data.get('amount_usd', 0)) if data.get('amount_usd') else None
    notes = data.get('notes', '')
    recorded_at = data.get('recorded_at', datetime.now().isoformat())

    conn = get_db()
    c = conn.cursor()
    c.execute(
        "INSERT INTO earnings (platform, amount, amount_usd, notes, recorded_at) VALUES (?, ?, ?, ?, ?)",
        (platform, amount, amount_usd, notes, recorded_at)
    )
    conn.commit()
    earning_id = c.lastrowid
    conn.close()

    # Update stats and broadcast
    update_stats()
    socketio.emit('earnings_added', {
        "id": earning_id,
        "platform": platform,
        "amount": amount,
        "amount_usd": amount_usd
    })

    return jsonify({
        "success": True,
        "id": earning_id,
        "message": f"Earning recorded for {platform}"
    })

@app.route('/api/earnings/<int:earning_id>', methods=['DELETE'])
def delete_earning(earning_id):
    """Delete an earnings record."""
    conn = get_db()
    c = conn.cursor()
    c.execute("DELETE FROM earnings WHERE id = ?", (earning_id,))
    deleted = c.rowcount > 0
    conn.commit()
    conn.close()

    if deleted:
        update_stats()
        return jsonify({"success": True, "message": f"Earning {earning_id} deleted"})
    else:
        return jsonify({"error": "Earning not found"}), 404

@app.route('/api/wallet/balance', methods=['GET'])
def get_wallet_balance():
    """Get current GRASS wallet balance."""
    if not state["wallet"]["address"]:
        return jsonify({
            "error": "No wallet address configured",
            "balance": 0
        })

    update_wallet_balance()
    return jsonify({
        "address": state["wallet"]["address"],
        "grass_balance": state["wallet"]["grass_balance"],
        "last_updated": state["wallet"]["last_updated"]
    })

@app.route('/api/wallet/address', methods=['POST'])
def set_wallet_address():
    """Set the Solana wallet address to track."""
    data = request.json

    if not data or 'address' not in data:
        return jsonify({"error": "Missing wallet address"}), 400

    address = data['address']
    state["wallet"]["address"] = address

    # Save to database
    conn = get_db()
    c = conn.cursor()
    c.execute(
        "INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
        ("wallet_address", address)
    )
    conn.commit()
    conn.close()

    # Update balance
    update_wallet_balance()

    return jsonify({
        "success": True,
        "address": address,
        "balance": state["wallet"]["grass_balance"]
    })

@app.route('/api/earnings/summary', methods=['GET'])
def get_earnings_summary():
    """Get earnings summary by platform."""
    conn = get_db()
    c = conn.cursor()

    c.execute('''
        SELECT
            platform,
            COUNT(*) as count,
            SUM(amount) as total_amount,
            SUM(COALESCE(amount_usd, 0)) as total_usd
        FROM earnings
        GROUP BY platform
    ''')

    rows = c.fetchall()
    summary = {row['platform']: {
        "count": row['count'],
        "total_amount": row['total_amount'],
        "total_usd": row['total_usd']
    } for row in rows}

    conn.close()
    return jsonify(summary)

# ============================================================================
# Expenses API (Profit Tracking)
# ============================================================================

@app.route('/api/expenses', methods=['GET'])
def get_expenses():
    """Get expenses history."""
    category = request.args.get('category')
    limit = request.args.get('limit', 100, type=int)
    offset = request.args.get('offset', 0, type=int)

    conn = get_db()
    c = conn.cursor()

    if category:
        c.execute(
            "SELECT * FROM expenses WHERE category = ? ORDER BY recorded_at DESC LIMIT ? OFFSET ?",
            (category, limit, offset)
        )
    else:
        c.execute(
            "SELECT * FROM expenses ORDER BY recorded_at DESC LIMIT ? OFFSET ?",
            (limit, offset)
        )

    rows = c.fetchall()
    expenses = [dict(row) for row in rows]
    conn.close()

    return jsonify({
        "expenses": expenses,
        "count": len(expenses),
        "limit": limit,
        "offset": offset
    })

@app.route('/api/expenses', methods=['POST'])
def add_expense():
    """Add a new expense record."""
    data = request.json

    if not data or 'category' not in data or 'amount' not in data:
        return jsonify({"error": "Missing required fields: category, amount"}), 400

    category = data['category']
    amount = float(data['amount'])
    description = data.get('description', '')
    platform = data.get('platform')
    recorded_at = data.get('recorded_at', datetime.now().isoformat())

    conn = get_db()
    c = conn.cursor()
    c.execute(
        "INSERT INTO expenses (category, amount, description, platform, recorded_at) VALUES (?, ?, ?, ?, ?)",
        (category, amount, description, platform, recorded_at)
    )
    conn.commit()
    expense_id = c.lastrowid
    conn.close()

    # Broadcast expense added
    socketio.emit('expense_added', {
        "id": expense_id,
        "category": category,
        "amount": amount
    })

    return jsonify({
        "success": True,
        "id": expense_id,
        "message": f"Expense recorded: {category} ${amount:.2f}"
    })

@app.route('/api/expenses/<int:expense_id>', methods=['DELETE'])
def delete_expense(expense_id):
    """Delete an expense record."""
    conn = get_db()
    c = conn.cursor()
    c.execute("DELETE FROM expenses WHERE id = ?", (expense_id,))
    deleted = c.rowcount > 0
    conn.commit()
    conn.close()

    if deleted:
        return jsonify({"success": True, "message": f"Expense {expense_id} deleted"})
    else:
        return jsonify({"error": "Expense not found"}), 404

@app.route('/api/expenses/summary', methods=['GET'])
def get_expenses_summary():
    """Get expenses summary by category."""
    conn = get_db()
    c = conn.cursor()

    c.execute('''
        SELECT
            category,
            COUNT(*) as count,
            SUM(amount) as total_amount
        FROM expenses
        GROUP BY category
    ''')

    rows = c.fetchall()
    summary = {row['category']: {
        "count": row['count'],
        "total_amount": row['total_amount']
    } for row in rows}

    conn.close()
    return jsonify(summary)

@app.route('/api/analytics/profit', methods=['GET'])
def get_profit_analytics():
    """Get profit analytics (earnings - expenses)."""
    period = request.args.get('period', 'month')  # month, year, all

    conn = get_db()
    c = conn.cursor()

    # Date filter
    if period == 'month':
        date_filter = datetime.now().replace(day=1).strftime("%Y-%m-%d")
    elif period == 'year':
        date_filter = datetime.now().replace(month=1, day=1).strftime("%Y-%m-%d")
    else:
        date_filter = "1970-01-01"

    # Get total earnings
    c.execute(
        "SELECT COALESCE(SUM(amount_usd), 0) FROM earnings WHERE amount_usd IS NOT NULL AND recorded_at >= ?",
        (date_filter,)
    )
    total_earnings = c.fetchone()[0]

    # Get total expenses
    c.execute(
        "SELECT COALESCE(SUM(amount), 0) FROM expenses WHERE recorded_at >= ?",
        (date_filter,)
    )
    total_expenses = c.fetchone()[0]

    # Calculate profit
    net_profit = total_earnings - total_expenses
    profit_margin = (net_profit / total_earnings * 100) if total_earnings > 0 else 0
    roi = (net_profit / total_expenses * 100) if total_expenses > 0 else 0

    # Get expenses by category
    c.execute('''
        SELECT category, SUM(amount) as total
        FROM expenses
        WHERE recorded_at >= ?
        GROUP BY category
    ''', (date_filter,))
    expenses_by_category = {row['category']: row['total'] for row in c.fetchall()}

    # Get earnings by platform
    c.execute('''
        SELECT platform, SUM(COALESCE(amount_usd, 0)) as total
        FROM earnings
        WHERE recorded_at >= ?
        GROUP BY platform
    ''', (date_filter,))
    earnings_by_platform = {row['platform']: row['total'] for row in c.fetchall()}

    conn.close()

    return jsonify({
        "period": period,
        "total_earnings_usd": total_earnings,
        "total_expenses_usd": total_expenses,
        "net_profit_usd": net_profit,
        "profit_margin_percent": round(profit_margin, 2),
        "roi_percent": round(roi, 2),
        "expenses_breakdown": expenses_by_category,
        "earnings_breakdown": earnings_by_platform,
        "timestamp": datetime.now().isoformat()
    })

# ============================================================================
# Budget API (100% Cost Allocation)
# ============================================================================

@app.route('/api/budget/summary', methods=['GET'])
def get_budget_summary():
    """Get budget summary with 100% cost allocation breakdown."""
    period = request.args.get('period')  # YYYY-MM format, e.g., '2024-11'

    # Default to current month if not specified
    if not period:
        period = datetime.now().strftime("%Y-%m")

    # Parse period to date range
    year, month = map(int, period.split('-'))
    start_date = f"{year:04d}-{month:02d}-01"
    if month == 12:
        end_date = f"{year + 1:04d}-01-01"
    else:
        end_date = f"{year:04d}-{month + 1:02d}-01"

    conn = get_db()
    c = conn.cursor()

    # Get total expenses for period
    c.execute('''
        SELECT COALESCE(SUM(amount), 0) FROM expenses
        WHERE recorded_at >= ? AND recorded_at < ?
    ''', (start_date, end_date))
    total_costs = c.fetchone()[0]

    # Get total earnings for period
    c.execute('''
        SELECT COALESCE(SUM(amount_usd), 0) FROM earnings
        WHERE amount_usd IS NOT NULL AND recorded_at >= ? AND recorded_at < ?
    ''', (start_date, end_date))
    total_earnings = c.fetchone()[0]

    net_profit = total_earnings - total_costs

    # Get expenses by category with allocations
    c.execute('''
        SELECT
            e.id,
            e.category,
            e.amount,
            e.description,
            e.platform as default_platform
        FROM expenses e
        WHERE e.recorded_at >= ? AND e.recorded_at < ?
        ORDER BY e.category
    ''', (start_date, end_date))
    expenses = c.fetchall()

    # Build category breakdown
    categories_map = {}
    for expense in expenses:
        category = expense['category']
        if category not in categories_map:
            categories_map[category] = {
                'category': category,
                'amount': 0.0,
                'allocations': []
            }
        categories_map[category]['amount'] += expense['amount']

        # Get allocations for this expense
        c.execute('''
            SELECT platform, amount, percentage, notes
            FROM cost_allocations
            WHERE expense_id = ?
        ''', (expense['id'],))
        allocations = c.fetchall()

        if allocations:
            for alloc in allocations:
                # Add to existing platform allocation or create new
                platform_key = alloc['platform'] or 'shared'
                found = False
                for existing in categories_map[category]['allocations']:
                    if existing.get('platform') == alloc['platform']:
                        existing['amount'] += alloc['amount']
                        found = True
                        break
                if not found:
                    categories_map[category]['allocations'].append({
                        'platform': alloc['platform'],
                        'amount': alloc['amount'],
                        'percentage': alloc['percentage']
                    })
        else:
            # No allocation - mark as shared
            found = False
            for existing in categories_map[category]['allocations']:
                if existing.get('platform') is None:
                    existing['amount'] += expense['amount']
                    found = True
                    break
            if not found:
                categories_map[category]['allocations'].append({
                    'platform': None,  # Shared
                    'amount': expense['amount'],
                    'percentage': 100.0
                })

    # Calculate percentages for categories
    by_category = []
    for cat_data in categories_map.values():
        percentage = (cat_data['amount'] / total_costs * 100) if total_costs > 0 else 0
        # Recalculate allocation percentages
        for alloc in cat_data['allocations']:
            alloc['percentage'] = (alloc['amount'] / cat_data['amount'] * 100) if cat_data['amount'] > 0 else 0
        by_category.append({
            'category': cat_data['category'],
            'amount': round(cat_data['amount'], 2),
            'percentage': round(percentage, 2),
            'allocations': cat_data['allocations']
        })

    # Sort by amount descending
    by_category.sort(key=lambda x: x['amount'], reverse=True)

    # Get platform breakdown (costs + earnings)
    platforms = ['grass', 'nodepay', 'honeygain']
    by_platform = []

    for platform in platforms:
        # Get allocated costs for this platform
        c.execute('''
            SELECT COALESCE(SUM(ca.amount), 0)
            FROM cost_allocations ca
            JOIN expenses e ON ca.expense_id = e.id
            WHERE ca.platform = ? AND e.recorded_at >= ? AND e.recorded_at < ?
        ''', (platform, start_date, end_date))
        platform_costs = c.fetchone()[0]

        # Get direct expenses for this platform (if not using allocations)
        c.execute('''
            SELECT COALESCE(SUM(e.amount), 0)
            FROM expenses e
            WHERE e.platform = ? AND e.recorded_at >= ? AND e.recorded_at < ?
            AND e.id NOT IN (SELECT expense_id FROM cost_allocations)
        ''', (platform, start_date, end_date))
        direct_costs = c.fetchone()[0]
        platform_costs += direct_costs

        # Add shared costs (proportionally)
        c.execute('''
            SELECT COALESCE(SUM(e.amount), 0) / 3.0
            FROM expenses e
            WHERE e.platform IS NULL AND e.recorded_at >= ? AND e.recorded_at < ?
            AND e.id NOT IN (SELECT expense_id FROM cost_allocations)
        ''', (start_date, end_date))
        shared_costs = c.fetchone()[0]
        platform_costs += shared_costs

        # Get earnings for this platform
        c.execute('''
            SELECT COALESCE(SUM(amount_usd), 0)
            FROM earnings
            WHERE platform = ? AND amount_usd IS NOT NULL
            AND recorded_at >= ? AND recorded_at < ?
        ''', (platform, start_date, end_date))
        platform_earnings = c.fetchone()[0]

        platform_profit = platform_earnings - platform_costs
        profit_margin = (platform_profit / platform_earnings * 100) if platform_earnings > 0 else 0

        # Get cost breakdown by category for this platform
        c.execute('''
            SELECT e.category, COALESCE(SUM(ca.amount), 0) as total
            FROM cost_allocations ca
            JOIN expenses e ON ca.expense_id = e.id
            WHERE ca.platform = ? AND e.recorded_at >= ? AND e.recorded_at < ?
            GROUP BY e.category
        ''', (platform, start_date, end_date))
        cost_breakdown = {row['category']: round(row['total'], 2) for row in c.fetchall()}

        by_platform.append({
            'platform': platform,
            'total_costs': round(platform_costs, 2),
            'total_earnings': round(platform_earnings, 2),
            'net_profit': round(platform_profit, 2),
            'profit_margin': round(profit_margin, 2),
            'cost_breakdown': cost_breakdown
        })

    conn.close()

    return jsonify({
        'period': period,
        'total_costs': round(total_costs, 2),
        'total_earnings': round(total_earnings, 2),
        'net_profit': round(net_profit, 2),
        'by_category': by_category,
        'by_platform': by_platform,
        'timestamp': datetime.now().isoformat()
    })


@app.route('/api/expenses/<int:expense_id>/allocate', methods=['POST'])
def allocate_expense(expense_id):
    """Allocate an expense to platforms (Budget Thinking)."""
    data = request.json

    if not data or 'allocations' not in data:
        return jsonify({"error": "Missing allocations"}), 400

    allocations = data['allocations']

    # Validate allocations sum to 100%
    total_percentage = sum(a.get('percentage', 0) for a in allocations)
    if abs(total_percentage - 100) > 0.01:  # Allow small floating point error
        return jsonify({
            "error": f"Allocations must sum to 100%, got {total_percentage}%"
        }), 400

    conn = get_db()
    c = conn.cursor()

    # Get expense amount
    c.execute("SELECT amount FROM expenses WHERE id = ?", (expense_id,))
    row = c.fetchone()
    if not row:
        conn.close()
        return jsonify({"error": "Expense not found"}), 404

    expense_amount = row['amount']

    # Delete existing allocations
    c.execute("DELETE FROM cost_allocations WHERE expense_id = ?", (expense_id,))

    # Insert new allocations
    for alloc in allocations:
        platform = alloc.get('platform')  # Can be null for shared
        percentage = alloc['percentage']
        amount = expense_amount * (percentage / 100.0)
        notes = alloc.get('notes', '')

        c.execute('''
            INSERT INTO cost_allocations (expense_id, platform, amount, percentage, notes)
            VALUES (?, ?, ?, ?, ?)
        ''', (expense_id, platform, amount, percentage, notes))

    conn.commit()
    conn.close()

    return jsonify({
        "success": True,
        "expense_id": expense_id,
        "allocations_count": len(allocations),
        "message": f"Expense {expense_id} allocated to {len(allocations)} platform(s)"
    })


@app.route('/api/expenses/<int:expense_id>/allocations', methods=['GET'])
def get_expense_allocations(expense_id):
    """Get allocations for a specific expense."""
    conn = get_db()
    c = conn.cursor()

    # Get expense
    c.execute("SELECT * FROM expenses WHERE id = ?", (expense_id,))
    expense = c.fetchone()
    if not expense:
        conn.close()
        return jsonify({"error": "Expense not found"}), 404

    # Get allocations
    c.execute('''
        SELECT id, platform, amount, percentage, notes, created_at
        FROM cost_allocations
        WHERE expense_id = ?
    ''', (expense_id,))

    allocations = [dict(row) for row in c.fetchall()]
    conn.close()

    return jsonify({
        "expense_id": expense_id,
        "expense_amount": expense['amount'],
        "expense_category": expense['category'],
        "allocations": allocations,
        "is_allocated": len(allocations) > 0
    })


@app.route('/api/budget/allocate-auto', methods=['POST'])
def auto_allocate_expenses():
    """
    Auto-allocate unallocated expenses based on earnings proportion.
    Useful for batch allocation of shared costs.
    """
    data = request.json or {}
    period = data.get('period')  # Optional: limit to specific period

    conn = get_db()
    c = conn.cursor()

    # Get earnings by platform to calculate proportions
    if period:
        year, month = map(int, period.split('-'))
        start_date = f"{year:04d}-{month:02d}-01"
        if month == 12:
            end_date = f"{year + 1:04d}-01-01"
        else:
            end_date = f"{year:04d}-{month + 1:02d}-01"

        c.execute('''
            SELECT platform, COALESCE(SUM(amount_usd), 0) as total
            FROM earnings
            WHERE amount_usd IS NOT NULL AND recorded_at >= ? AND recorded_at < ?
            GROUP BY platform
        ''', (start_date, end_date))
    else:
        c.execute('''
            SELECT platform, COALESCE(SUM(amount_usd), 0) as total
            FROM earnings
            WHERE amount_usd IS NOT NULL
            GROUP BY platform
        ''')

    earnings_by_platform = {row['platform']: row['total'] for row in c.fetchall()}
    total_earnings = sum(earnings_by_platform.values())

    if total_earnings == 0:
        conn.close()
        return jsonify({
            "error": "No earnings data to calculate proportions",
            "suggestion": "Add some earnings first"
        }), 400

    # Calculate percentages
    proportions = {}
    for platform, amount in earnings_by_platform.items():
        proportions[platform] = (amount / total_earnings) * 100

    # Get unallocated expenses
    if period:
        c.execute('''
            SELECT id, amount, category FROM expenses
            WHERE id NOT IN (SELECT expense_id FROM cost_allocations)
            AND platform IS NULL
            AND recorded_at >= ? AND recorded_at < ?
        ''', (start_date, end_date))
    else:
        c.execute('''
            SELECT id, amount, category FROM expenses
            WHERE id NOT IN (SELECT expense_id FROM cost_allocations)
            AND platform IS NULL
        ''')

    unallocated = c.fetchall()

    # Allocate each expense
    allocated_count = 0
    for expense in unallocated:
        for platform, percentage in proportions.items():
            amount = expense['amount'] * (percentage / 100.0)
            c.execute('''
                INSERT INTO cost_allocations (expense_id, platform, amount, percentage, notes)
                VALUES (?, ?, ?, ?, ?)
            ''', (expense['id'], platform, amount, percentage, f"Auto-allocated based on earnings"))
        allocated_count += 1

    conn.commit()
    conn.close()

    return jsonify({
        "success": True,
        "expenses_allocated": allocated_count,
        "proportions_used": {k: round(v, 2) for k, v in proportions.items()},
        "message": f"Auto-allocated {allocated_count} expenses based on earnings proportions"
    })


# ============================================================================
# WebSocket Events
# ============================================================================

@socketio.on('connect')
def handle_connect():
    """Handle new WebSocket connection."""
    print(f"Client connected: {request.sid}")
    emit('status_update', get_full_status())

@socketio.on('disconnect')
def handle_disconnect():
    """Handle WebSocket disconnection."""
    print(f"Client disconnected: {request.sid}")

@socketio.on('get_status')
def handle_get_status():
    """Handle status request via WebSocket."""
    update_app_status()
    emit('status_update', get_full_status())

@socketio.on('refresh')
def handle_refresh():
    """Force refresh all data."""
    update_app_status()
    update_wallet_balance()
    update_stats()
    emit('status_update', get_full_status())

# ============================================================================
# Startup
# ============================================================================

def load_settings():
    """Load saved settings from database."""
    try:
        conn = get_db()
        c = conn.cursor()
        c.execute("SELECT value FROM settings WHERE key = 'wallet_address'")
        row = c.fetchone()
        if row:
            state["wallet"]["address"] = row[0]
        conn.close()
    except Exception:
        pass

# ============================================================================
# Server-Sent Events (SSE) for native WebSocket-like functionality
# ============================================================================

from flask import Response, stream_with_context

@app.route('/api/stream')
def stream_events():
    """Server-Sent Events endpoint for real-time updates."""
    def generate():
        last_status = None
        while True:
            try:
                update_app_status()
                current_status = get_full_status()

                # Send event
                data = json.dumps(current_status)
                yield f"event: status_update\ndata: {data}\n\n"

                last_status = current_status
                time.sleep(5)  # Send updates every 5 seconds

            except GeneratorExit:
                break
            except Exception as e:
                error_data = json.dumps({"error": str(e)})
                yield f"event: error\ndata: {error_data}\n\n"
                time.sleep(5)

    return Response(
        stream_with_context(generate()),
        mimetype='text/event-stream',
        headers={
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'X-Accel-Buffering': 'no'
        }
    )

# ============================================================================
# Main Entry Point
# ============================================================================

if __name__ == '__main__':
    local_ip = get_local_ip()

    print(f"""
    ╔══════════════════════════════════════════════════════════════╗
    ║           UsefulMiner Server v{SERVER_INFO['version']}                          ║
    ╠══════════════════════════════════════════════════════════════╣
    ║  Server: {SERVER_INFO['name']:<50} ║
    ║  Chip:   {SERVER_INFO['chip']:<50} ║
    ║  Memory: {SERVER_INFO['memory']:<50} ║
    ╠══════════════════════════════════════════════════════════════╣
    ║  Port:     {PORT}                                              ║
    ║  Local:    http://localhost:{PORT}                              ║
    ║  Network:  http://{local_ip}:{PORT:<29}║
    ║  Bonjour:  {'Enabled' if ZEROCONF_AVAILABLE else 'Disabled':<49} ║
    ╠══════════════════════════════════════════════════════════════╣
    ║  Monitoring: Grass, NodePay, Honeygain                       ║
    ║  Endpoints: /api/discovery, /api/status, /api/earnings       ║
    ╚══════════════════════════════════════════════════════════════╝
    """)

    # Initialize database and settings
    init_db()
    load_settings()
    update_app_status()
    update_stats()

    # Start Bonjour service advertisement
    if ZEROCONF_AVAILABLE:
        bonjour_success = start_bonjour()
        if bonjour_success:
            print("✅ Bonjour service advertisement started")
        else:
            print("⚠️ Bonjour failed to start, iOS app will use manual discovery")
    else:
        print("⚠️ Install zeroconf for auto-discovery: pip3 install zeroconf")

    # Start background worker
    worker_thread = threading.Thread(target=background_worker, daemon=True)
    worker_thread.start()
    print("✅ Background worker started (updating every 30s)")

    # Register shutdown handler
    import atexit
    atexit.register(stop_bonjour)

    print(f"\n🚀 Server ready at http://{local_ip}:{PORT}")
    print(f"📱 iOS app can discover via Bonjour or connect to /api/discovery\n")

    # Start server
    socketio.run(app, host='0.0.0.0', port=PORT, debug=False, allow_unsafe_werkzeug=True)
