# mikrotik_service/main.py
"""
MikroTik Controller Microservice
Handles all RouterOS API interactions for bandwidth management and user control
"""
from fastapi import FastAPI, Header, HTTPException, Depends, Request
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field
from typing import Optional, List, Dict
import itertools
import librouteros
import redis
import logging
from datetime import datetime, timedelta
import os

# When true, no real RouterOS connection is ever opened -- every router talks
# to an in-memory FakeRouterAPI instead. Lets the whole payment -> provision
# -> auto-login loop be tested with no MikroTik hardware at all. See
# docs/LOCAL_TESTING_WITHOUT_ROUTER.md. Never enable this in production.
MIKROTIK_FAKE_MODE = os.getenv('MIKROTIK_FAKE_MODE', 'false').lower() == 'true'

# Initialize FastAPI
app = FastAPI(title="MikroTik Controller Service", version="1.0.0")

# Redis for caching router stats
redis_client = redis.Redis(
    host=os.getenv('REDIS_HOST', 'localhost'),
    port=int(os.getenv('REDIS_PORT', 6379)),
    db=0,
    decode_responses=True
)

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Pydantic Models
class RouterConfig(BaseModel):
    host: str = Field(..., description="Router IP address")
    username: str = Field(..., description="Router admin username")
    password: str = Field(..., description="Router admin password")
    port: int = Field(default=8728, description="RouterOS API port")

class PPPoEUser(BaseModel):
    username: str
    password: str
    service: str = "any"
    profile: str = "default"
    disabled: bool = False

class HotspotUser(BaseModel):
    """A captive-portal (walled-garden) login -- distinct from a PPPoE
    secret. Used by the /connect flow: no dialer on the customer's device,
    just a browser redirect and a form POST to the router's own login CGI."""
    username: str
    password: str
    profile: str = "default"
    disabled: bool = False
    limit_uptime: Optional[str] = Field(default=None, description="e.g. '1d' -- router-enforced session cap")

class HotspotBandwidthProfile(BaseModel):
    name: str
    download_speed: str
    upload_speed: str
    shared_users: int = 1

class BandwidthProfile(BaseModel):
    name: str
    download_speed: str  # e.g., "10M" for 10 Mbps
    upload_speed: str
    shared_users: int = 1
    burst_limit: Optional[str] = None
    burst_threshold: Optional[str] = None
    burst_time: Optional[str] = "8s/8s"

class UserBandwidthUpdate(BaseModel):
    username: str
    profile_name: str

class ConnectionStats(BaseModel):
    username: str
    uptime: str
    address: str
    bytes_in: int
    bytes_out: int

# Fake Router (local testing without hardware)
class FakeRouterAPI:
    """Stands in for the callable librouteros.connect(...) normally returns,
    so every existing endpoint below (which just calls router.api(path, **kwargs))
    works completely unchanged in fake mode. Keeps per-router state in memory
    only -- nothing persists across a service restart, and nothing ever
    touches a real network socket."""

    _id_counter = itertools.count(1)

    def __init__(self):
        self._store: Dict[str, List[dict]] = {
            '/system/resource': [{
                'version': '7.15 (stable) [FAKE]',
                'board-name': 'Fake CHR (simulated -- no real router)',
                'uptime': '1d2h3m4s',
                'cpu-load': '3',
                'free-memory': '128000000',
                'total-memory': '256000000',
            }],
            '/interface': [{'name': 'ether1'}, {'name': 'wlan1'}],
        }

    def __call__(self, path: str, **kwargs):
        *resource_parts, action = path.strip('/').split('/')
        resource = '/' + '/'.join(resource_parts)
        records = self._store.setdefault(resource, [])

        if action == 'print':
            filters = {k[1:]: v for k, v in kwargs.items() if k.startswith('?')}
            if not filters:
                return list(records)
            return [r for r in records if all(r.get(k) == v for k, v in filters.items())]

        if action == 'add':
            record = {'.id': f'*{next(self._id_counter)}', **kwargs}
            records.append(record)
            return [record['.id']]

        if action == 'set':
            record_id = kwargs.pop('.id')
            for r in records:
                if r['.id'] == record_id:
                    r.update(kwargs)
            return []

        if action == 'remove':
            record_id = kwargs.get('.id')
            self._store[resource] = [r for r in records if r['.id'] != record_id]
            return []

        raise ValueError(f"FakeRouterAPI: unsupported action '{action}' for {path}")

    def close(self):
        pass

# Router Connection Manager
class MikroTikRouter:
    def __init__(self, config: RouterConfig):
        self.config = config
        self.api = None

    def connect(self):
        """Establish connection to MikroTik router (or a simulated one, in fake mode)"""
        if MIKROTIK_FAKE_MODE:
            self.api = FakeRouterAPI()
            logger.info(f"[FAKE MODE] Simulating router at {self.config.host} -- no real network call made")
            return True
        try:
            self.api = librouteros.connect(
                host=self.config.host,
                username=self.config.username,
                password=self.config.password,
                port=self.config.port
            )
            logger.info(f"Connected to MikroTik router at {self.config.host}")
            return True
        except Exception as e:
            logger.error(f"Failed to connect to router: {str(e)}")
            raise HTTPException(status_code=503, detail=f"Router connection failed: {str(e)}")

    def disconnect(self):
        """Close router connection"""
        if self.api:
            self.api.close()
            logger.info("Disconnected from MikroTik router")

# Multi-tenant: each caller (a specific Hotspot in the Django app) owns its
# own MikroTik router, so the router to talk to is supplied per-request via
# X-Router-* headers rather than a single global env-configured router.
# Connections are cached per (host, port, username) so repeated requests for
# the same router reuse the same librouteros session instead of reconnecting
# every call. Falls back to MIKROTIK_HOST/USERNAME/PASSWORD/PORT env vars
# when headers are absent, which keeps local single-router dev working.
router_connections: Dict[tuple, "MikroTikRouter"] = {}

def router_cache_key(config: RouterConfig) -> str:
    """Stable identifier for a router, used to namespace shared caches (Redis)."""
    return f"{config.host}:{config.port}"

def _resolve_router_config(
    x_router_host: Optional[str],
    x_router_username: Optional[str],
    x_router_password: Optional[str],
    x_router_port: Optional[str],
) -> RouterConfig:
    host = x_router_host or os.getenv('MIKROTIK_HOST')
    username = x_router_username or os.getenv('MIKROTIK_USERNAME')
    password = x_router_password or os.getenv('MIKROTIK_PASSWORD')
    port = int(x_router_port or os.getenv('MIKROTIK_PORT', 8728))

    if not all([host, username, password]):
        raise HTTPException(
            status_code=400,
            detail="Router connection details required: send X-Router-Host / "
                   "X-Router-Username / X-Router-Password headers (or configure "
                   "MIKROTIK_HOST/USERNAME/PASSWORD env vars for local dev).",
        )
    return RouterConfig(host=host, username=username, password=password, port=port)

def get_router(
    x_router_host: Optional[str] = Header(default=None),
    x_router_username: Optional[str] = Header(default=None),
    x_router_password: Optional[str] = Header(default=None),
    x_router_port: Optional[str] = Header(default=None),
):
    """Dependency to get (or open) a connection to the router for this request."""
    config = _resolve_router_config(x_router_host, x_router_username, x_router_password, x_router_port)
    cache_key = (config.host, config.port, config.username)

    cached = router_connections.get(cache_key)
    if cached:
        return cached

    router = MikroTikRouter(config)
    router.connect()
    router_connections[cache_key] = router
    return router

# Health Check
@app.get("/health")
async def health_check(
    x_router_host: Optional[str] = Header(default=None),
    x_router_username: Optional[str] = Header(default=None),
    x_router_password: Optional[str] = Header(default=None),
    x_router_port: Optional[str] = Header(default=None),
):
    """Check if the service can reach and authenticate against this router.
    Builds its own connection (rather than depending on get_router) so a
    connection failure is reported as {"status": "unhealthy", ...} instead
    of a 503 — this endpoint's whole purpose is to answer that question."""
    try:
        config = _resolve_router_config(x_router_host, x_router_username, x_router_password, x_router_port)
        cache_key = (config.host, config.port, config.username)
        router = router_connections.get(cache_key)
        if not router:
            router = MikroTikRouter(config)
            router.connect()
            router_connections[cache_key] = router

        system_resource = router.api('/system/resource/print')
        return {
            "status": "healthy",
            "router_connected": True,
            "router_info": {
                "version": system_resource[0].get('version', 'unknown'),
                "board_name": system_resource[0].get('board-name', 'unknown')
            }
        }
    except Exception as e:
        return {
            "status": "unhealthy",
            "router_connected": False,
            "error": str(e)
        }

# Fake router login endpoints -- only meaningful in MIKROTIK_FAKE_MODE, used
# to simulate what the router itself would do: serve a redirect-stub login
# page, and accept the credential POST that completes a captive-portal login.
# See docs/LOCAL_TESTING_WITHOUT_ROUTER.md for the full walkthrough.
@app.get("/fake-router/login-page", response_class=HTMLResponse)
async def fake_router_login_page(hotspot_id: str = "1"):
    """Simulates a MikroTik Hotspot's login.html redirect stub -- what a real
    router would serve a freshly-connected device, immediately forwarding the
    browser to /connect with RouterOS-style template variables filled in with
    made-up (but structurally realistic) values."""
    frontend_origin = os.getenv('FAKE_ROUTER_FRONTEND_URL', 'http://localhost:5173')
    return HTMLResponse(f"""
    <!DOCTYPE html><html><head><meta charset="utf-8"><title>Connecting…</title></head>
    <body>
      <script>
        var params = new URLSearchParams({{
          hotspot_id: "{hotspot_id}",
          mac: "AA:BB:CC:DD:EE:FF",
          ip: "10.20.0.50",
          link_login: "{os.getenv('FAKE_ROUTER_BASE_URL', 'http://localhost:8001')}/fake-router/login",
          link_orig: "http://example.com/"
        }});
        window.location.href = "{frontend_origin}/connect?" + params.toString();
      </script>
      <p>[FAKE ROUTER] Redirecting to the platform's connect page…</p>
    </body></html>
    """)

@app.post("/fake-router/login", response_class=HTMLResponse)
async def fake_router_login(request: Request):
    """Simulates the router's own link-login-only handler -- the URL
    ConnectPage's hidden form auto-submits credentials to once payment
    completes. A real router would grant network access here; this just
    confirms what it received, proving the handshake completed end to end."""
    form = await request.form()
    username = form.get('username', '(none)')
    return HTMLResponse(f"""
    <!DOCTYPE html><html><head><meta charset="utf-8"><title>Connected (simulated)</title></head>
    <body style="font-family: sans-serif; padding: 2rem;">
      <h1>[FAKE ROUTER] Login accepted</h1>
      <p>A real MikroTik would now grant this device network access. Received username:
      <code>{username}</code></p>
    </body></html>
    """)

# PPPoE User Management
@app.post("/api/pppoe/users", status_code=201)
async def create_pppoe_user(user: PPPoEUser, router: MikroTikRouter = Depends(get_router)):
    """Create a new PPPoE user on the router"""
    try:
        # Check if user already exists
        existing_users = router.api('/ppp/secret/print', **{'?name': user.username})
        if existing_users:
            raise HTTPException(status_code=409, detail="User already exists")
        
        # Create the user
        router.api('/ppp/secret/add', **{
            'name': user.username,
            'password': user.password,
            'service': user.service,
            'profile': user.profile,
            'disabled': 'yes' if user.disabled else 'no'
        })
        
        logger.info(f"Created PPPoE user: {user.username}")
        return {"message": "User created successfully", "username": user.username}
    
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error creating PPPoE user: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.delete("/api/pppoe/users/{username}")
async def delete_pppoe_user(username: str, router: MikroTikRouter = Depends(get_router)):
    """Delete a PPPoE user from the router"""
    try:
        users = router.api('/ppp/secret/print', **{'?name': username})
        if not users:
            raise HTTPException(status_code=404, detail="User not found")
        
        user_id = users[0]['.id']
        router.api('/ppp/secret/remove', **{'.id': user_id})
        
        logger.info(f"Deleted PPPoE user: {username}")
        return {"message": "User deleted successfully", "username": username}
    
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error deleting PPPoE user: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/api/pppoe/users/{username}")
async def get_pppoe_user(username: str, router: MikroTikRouter = Depends(get_router)):
    """Get PPPoE user details"""
    try:
        users = router.api('/ppp/secret/print', **{'?name': username})
        if not users:
            raise HTTPException(status_code=404, detail="User not found")
        
        user_data = users[0]
        return {
            "username": user_data.get('name'),
            "service": user_data.get('service'),
            "profile": user_data.get('profile'),
            "disabled": user_data.get('disabled') == 'true'
        }
    
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error fetching PPPoE user: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.put("/api/pppoe/users/{username}/enable")
async def enable_pppoe_user(username: str, router: MikroTikRouter = Depends(get_router)):
    """Enable a disabled PPPoE user"""
    try:
        users = router.api('/ppp/secret/print', **{'?name': username})
        if not users:
            raise HTTPException(status_code=404, detail="User not found")
        
        user_id = users[0]['.id']
        router.api('/ppp/secret/set', **{'.id': user_id, 'disabled': 'no'})
        
        logger.info(f"Enabled PPPoE user: {username}")
        return {"message": "User enabled successfully", "username": username}
    
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error enabling PPPoE user: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.put("/api/pppoe/users/{username}/disable")
async def disable_pppoe_user(username: str, router: MikroTikRouter = Depends(get_router)):
    """Disable a PPPoE user (suspend access)"""
    try:
        users = router.api('/ppp/secret/print', **{'?name': username})
        if not users:
            raise HTTPException(status_code=404, detail="User not found")

        user_id = users[0]['.id']
        router.api('/ppp/secret/set', **{'.id': user_id, 'disabled': 'yes'})

        logger.info(f"Disabled PPPoE user: {username}")
        return {"message": "User disabled successfully", "username": username}

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error disabling PPPoE user: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

# Hotspot (captive portal) User Management -- mirrors the PPPoE section
# above exactly, but against /ip/hotspot/user instead of /ppp/secret. This
# is what the /connect captive-portal flow provisions: a login the walled
# garden accepts directly, no PPPoE dialer needed on the customer's device.
@app.post("/api/hotspot/users", status_code=201)
async def create_hotspot_user(user: HotspotUser, router: MikroTikRouter = Depends(get_router)):
    """Create a new Hotspot (captive portal) user on the router"""
    try:
        existing_users = router.api('/ip/hotspot/user/print', **{'?name': user.username})
        if existing_users:
            raise HTTPException(status_code=409, detail="User already exists")

        params = {
            'name': user.username,
            'password': user.password,
            'profile': user.profile,
            'disabled': 'yes' if user.disabled else 'no',
        }
        if user.limit_uptime:
            params['limit-uptime'] = user.limit_uptime
        router.api('/ip/hotspot/user/add', **params)

        logger.info(f"Created Hotspot user: {user.username}")
        return {"message": "User created successfully", "username": user.username}

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error creating Hotspot user: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.delete("/api/hotspot/users/{username}")
async def delete_hotspot_user(username: str, router: MikroTikRouter = Depends(get_router)):
    """Delete a Hotspot user from the router"""
    try:
        users = router.api('/ip/hotspot/user/print', **{'?name': username})
        if not users:
            raise HTTPException(status_code=404, detail="User not found")

        router.api('/ip/hotspot/user/remove', **{'.id': users[0]['.id']})

        logger.info(f"Deleted Hotspot user: {username}")
        return {"message": "User deleted successfully", "username": username}

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error deleting Hotspot user: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.put("/api/hotspot/users/{username}/enable")
async def enable_hotspot_user(username: str, router: MikroTikRouter = Depends(get_router)):
    """Enable a disabled Hotspot user"""
    try:
        users = router.api('/ip/hotspot/user/print', **{'?name': username})
        if not users:
            raise HTTPException(status_code=404, detail="User not found")

        router.api('/ip/hotspot/user/set', **{'.id': users[0]['.id'], 'disabled': 'no'})

        logger.info(f"Enabled Hotspot user: {username}")
        return {"message": "User enabled successfully", "username": username}

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error enabling Hotspot user: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.put("/api/hotspot/users/{username}/disable")
async def disable_hotspot_user(username: str, router: MikroTikRouter = Depends(get_router)):
    """Disable a Hotspot user (suspend access)"""
    try:
        users = router.api('/ip/hotspot/user/print', **{'?name': username})
        if not users:
            raise HTTPException(status_code=404, detail="User not found")

        router.api('/ip/hotspot/user/set', **{'.id': users[0]['.id'], 'disabled': 'yes'})

        logger.info(f"Disabled Hotspot user: {username}")
        return {"message": "User disabled successfully", "username": username}

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error disabling Hotspot user: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/api/hotspot/profiles", status_code=201)
async def create_hotspot_profile(profile: HotspotBandwidthProfile, router: MikroTikRouter = Depends(get_router)):
    """Create a Hotspot user profile (bandwidth control, the Hotspot equivalent of a PPP profile)"""
    try:
        existing = router.api('/ip/hotspot/user/profile/print', **{'?name': profile.name})
        if existing:
            raise HTTPException(status_code=409, detail="Profile already exists")

        router.api('/ip/hotspot/user/profile/add', **{
            'name': profile.name,
            'rate-limit': f"{profile.upload_speed}/{profile.download_speed}",
            'shared-users': str(profile.shared_users),
        })

        logger.info(f"Created Hotspot profile: {profile.name}")
        return {"message": "Profile created successfully", "profile": profile.name}

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error creating Hotspot profile: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/api/hotspot/active", response_model=List[ConnectionStats])
async def get_active_hotspot_connections(router: MikroTikRouter = Depends(get_router)):
    """Get all active Hotspot (captive portal) connections"""
    try:
        active_users = router.api('/ip/hotspot/active/print')

        return [
            ConnectionStats(
                username=user.get('user', 'unknown'),
                uptime=user.get('uptime', '0s'),
                address=user.get('address', 'unknown'),
                bytes_in=int(user.get('bytes-in', 0)),
                bytes_out=int(user.get('bytes-out', 0)),
            )
            for user in active_users
        ]

    except Exception as e:
        logger.error(f"Error fetching active Hotspot connections: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/api/hotspot/active/{username}/disconnect")
async def disconnect_hotspot_user(username: str, router: MikroTikRouter = Depends(get_router)):
    """Force disconnect a Hotspot user (e.g. subscription expired/data exhausted)"""
    try:
        active_users = router.api('/ip/hotspot/active/print', **{'?user': username})
        if not active_users:
            raise HTTPException(status_code=404, detail="User not connected")

        router.api('/ip/hotspot/active/remove', **{'.id': active_users[0]['.id']})

        logger.info(f"Disconnected Hotspot user: {username}")
        return {"message": "User disconnected successfully", "username": username}

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error disconnecting Hotspot user: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

# Bandwidth Profile Management
@app.post("/api/bandwidth/profiles", status_code=201)
async def create_bandwidth_profile(profile: BandwidthProfile, router: MikroTikRouter = Depends(get_router)):
    """Create a bandwidth profile (PPP profile with queue)"""
    try:
        # Check if profile exists
        existing = router.api('/ppp/profile/print', **{'?name': profile.name})
        if existing:
            raise HTTPException(status_code=409, detail="Profile already exists")
        
        # Create PPP profile
        router.api('/ppp/profile/add', **{
            'name': profile.name,
            'local-address': '10.0.0.1',
            'remote-address': 'dhcp-pool',
            'rate-limit': f"{profile.upload_speed}/{profile.download_speed}"
        })
        
        logger.info(f"Created bandwidth profile: {profile.name}")
        return {"message": "Profile created successfully", "profile": profile.name}
    
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error creating bandwidth profile: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.put("/api/bandwidth/users/{username}")
async def update_user_bandwidth(
    username: str,
    update: UserBandwidthUpdate,
    router: MikroTikRouter = Depends(get_router)
):
    """Update user's bandwidth by changing their profile"""
    try:
        # Find user
        users = router.api('/ppp/secret/print', **{'?name': username})
        if not users:
            raise HTTPException(status_code=404, detail="User not found")
        
        # Verify profile exists
        profiles = router.api('/ppp/profile/print', **{'?name': update.profile_name})
        if not profiles:
            raise HTTPException(status_code=404, detail="Profile not found")
        
        # Update user profile
        user_id = users[0]['.id']
        router.api('/ppp/secret/set', **{'.id': user_id, 'profile': update.profile_name})
        
        # Disconnect active session to apply new bandwidth
        active_sessions = router.api('/ppp/active/print', **{'?name': username})
        if active_sessions:
            session_id = active_sessions[0]['.id']
            router.api('/ppp/active/remove', **{'.id': session_id})
        
        logger.info(f"Updated bandwidth for user: {username} to profile: {update.profile_name}")
        return {
            "message": "Bandwidth updated successfully",
            "username": username,
            "new_profile": update.profile_name
        }
    
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error updating user bandwidth: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

# Connection Monitoring
@app.get("/api/connections/active", response_model=List[ConnectionStats])
async def get_active_connections(router: MikroTikRouter = Depends(get_router)):
    """Get all active PPPoE connections"""
    try:
        active_users = router.api('/ppp/active/print')
        
        connections = []
        for user in active_users:
            connections.append(ConnectionStats(
                username=user.get('name', 'unknown'),
                uptime=user.get('uptime', '0s'),
                address=user.get('address', 'unknown'),
                bytes_in=int(user.get('bytes-in', 0)),
                bytes_out=int(user.get('bytes-out', 0))
            ))
        
        return connections
    
    except Exception as e:
        logger.error(f"Error fetching active connections: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/api/connections/user/{username}")
async def get_user_connection(username: str, router: MikroTikRouter = Depends(get_router)):
    """Get connection details for specific user"""
    try:
        # Check cache first — namespaced by router, since the same username
        # could exist on multiple resellers' routers.
        cache_key = f"user_connection:{router_cache_key(router.config)}:{username}"
        cached = redis_client.get(cache_key)
        if cached:
            import json
            return json.loads(cached)
        
        active_users = router.api('/ppp/active/print', **{'?name': username})
        if not active_users:
            return {"connected": False, "username": username}
        
        user = active_users[0]
        connection_data = {
            "connected": True,
            "username": username,
            "uptime": user.get('uptime', '0s'),
            "address": user.get('address', 'unknown'),
            "caller_id": user.get('caller-id', 'unknown'),
            "bytes_in": int(user.get('bytes-in', 0)),
            "bytes_out": int(user.get('bytes-out', 0)),
            "session_id": user.get('.id')
        }
        
        # Cache for 30 seconds
        import json
        redis_client.setex(cache_key, 30, json.dumps(connection_data))
        
        return connection_data
    
    except Exception as e:
        logger.error(f"Error fetching user connection: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/api/connections/disconnect/{username}")
async def disconnect_user(username: str, router: MikroTikRouter = Depends(get_router)):
    """Force disconnect a user"""
    try:
        active_users = router.api('/ppp/active/print', **{'?name': username})
        if not active_users:
            raise HTTPException(status_code=404, detail="User not connected")
        
        session_id = active_users[0]['.id']
        router.api('/ppp/active/remove', **{'.id': session_id})
        
        logger.info(f"Disconnected user: {username}")
        return {"message": "User disconnected successfully", "username": username}
    
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error disconnecting user: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

# Router Statistics
@app.get("/api/router/stats")
async def get_router_stats(router: MikroTikRouter = Depends(get_router)):
    """Get router system statistics"""
    try:
        # Check cache — namespaced by router, one cache entry per router.
        cache_key = f"router_stats:{router_cache_key(router.config)}"
        cached = redis_client.get(cache_key)
        if cached:
            import json
            return json.loads(cached)
        
        resource = router.api('/system/resource/print')[0]
        interface_stats = router.api('/interface/print')
        
        stats = {
            "uptime": resource.get('uptime', 'unknown'),
            "cpu_load": resource.get('cpu-load', 0),
            "free_memory": resource.get('free-memory', 0),
            "total_memory": resource.get('total-memory', 0),
            "version": resource.get('version', 'unknown'),
            "board_name": resource.get('board-name', 'unknown'),
            "interfaces": len(interface_stats),
            "timestamp": datetime.now().isoformat()
        }
        
        # Cache for 60 seconds
        import json
        redis_client.setex(cache_key, 60, json.dumps(stats))
        
        return stats
    
    except Exception as e:
        logger.error(f"Error fetching router stats: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8001)