#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
UARM Agent v2.11 - PRODUCTION READY
Compatible con Backend v3.1
Correcciones:
- Username real (sin $)
- Productividad limitada a 100%
- Productividad calculada correctamente
- Sin dependencia de yaml
- FIX v2.7: Idle time absurdo al inicio (RDP/uptime largo) resetea a 0
- FIX v2.8: Idle time usa deltas incrementales en lugar de resetear siempre a 0
- FIX v2.9: Timestamps en UTC + reset automático de sesión al cambiar de día
- FIX v2.10: Ignora valores absurdos continuos de idle_time (RDP persistente)
- Fix v2.11: Corregido idle y lectura de usuario
"""

from __future__ import annotations

import os
import sys
import json
import time
import uuid
import socket
import ctypes
import logging
import platform
import getpass
from pathlib import Path
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional

# Dependencias: pip install requests psutil pywin32
import psutil
import requests

# --------------------------------------------------------------------------------------
# Configuración
# --------------------------------------------------------------------------------------

AGENT_DIR = Path(__file__).resolve().parent
LOG_FILE = AGENT_DIR / "uarm_agent.log"
AGENT_ID_FILE = AGENT_DIR / "agent_id.txt"

DEFAULT_CONFIG = {
    "server_url": os.getenv("UARM_SERVER_URL", "http://10.52.150.131:5000"),
    "heartbeat_interval": int(os.getenv("UARM_HEARTBEAT_INTERVAL", "60")),
    "verify_tls": os.getenv("UARM_VERIFY_TLS", "false").lower() not in ("0", "false", "no"),
    "requests_timeout": int(os.getenv("UARM_HTTP_TIMEOUT", "10")),
    "idle_threshold": int(os.getenv("UARM_IDLE_THRESHOLD", "60")),  # Segundos para considerar idle
    "lock_threshold": int(os.getenv("UARM_LOCK_THRESHOLD", "300")),  # Segundos para detectar bloqueo
}

# --------------------------------------------------------------------------------------
# Logging
# --------------------------------------------------------------------------------------

def cleanup_old_logs():
    """Limpia logs antiguos (mantiene últimos 30 días)"""
    try:
        if LOG_FILE.exists():
            # Rotar log si es muy grande (> 10MB)
            if LOG_FILE.stat().st_size > 10 * 1024 * 1024:
                backup_file = LOG_FILE.with_suffix(f".log.{datetime.now().strftime('%Y%m%d')}")
                LOG_FILE.rename(backup_file)
                
                # Eliminar backups antiguos (> 30 días)
                for old_log in AGENT_DIR.glob("uarm_agent.log.*"):
                    try:
                        mtime = datetime.fromtimestamp(old_log.stat().st_mtime)
                        if (datetime.now() - mtime).days > 30:
                            old_log.unlink()
                    except Exception:
                        pass
    except Exception as e:
        print(f"Error cleaning logs: {e}")

def setup_logging() -> logging.Logger:
    """Configura el sistema de logging con rotación automática"""
    # Limpiar logs antiguos primero
    cleanup_old_logs()
    
    logger = logging.getLogger("uarm")
    logger.setLevel(logging.INFO)
    
    # Evitar duplicados
    if logger.handlers:
        logger.handlers.clear()
    
    fmt = logging.Formatter(
        "[%(asctime)s] %(levelname)s: %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S"
    )
    
    # Archivo
    fh = logging.FileHandler(LOG_FILE, encoding="utf-8")
    fh.setFormatter(fmt)
    logger.addHandler(fh)
    
    # Consola (solo para debug, comentar en producción)
    sh = logging.StreamHandler(sys.stdout)
    sh.setFormatter(fmt)
    logger.addHandler(sh)
    
    return logger

log = setup_logging()

# --------------------------------------------------------------------------------------
# Agent ID
# --------------------------------------------------------------------------------------

def load_or_create_agent_id() -> str:
    """Carga o crea un agent_id persistente"""
    try:
        if AGENT_ID_FILE.exists():
            return AGENT_ID_FILE.read_text(encoding="utf-8").strip()
    except Exception:
        pass
    
    # Crear nuevo ID
    new_id = str(uuid.uuid4())
    try:
        AGENT_ID_FILE.write_text(new_id, encoding="utf-8")
    except Exception:
        pass
    return new_id

def get_logged_in_user() -> str:
    """Obtiene el usuario REAL de la sesión activa (no hostname$) - CORREGIDO v2.7"""
    try:
        # Intentar obtener el usuario de la sesión interactiva (consola)
        try:
            import win32ts
            import win32security
            
            # Obtener el usuario de la sesión activa (consola)
            session_id = win32ts.WTSGetActiveConsoleSessionId()
            username = win32ts.WTSQuerySessionInformation(None, session_id, win32ts.WTSUserName)
            
            if username and not username.endswith("$"):
                return username
        except Exception as e:
            log.debug(f"win32ts failed: {e}")
        
        # Fallback: getpass
        username = getpass.getuser()
        if username and not username.endswith("$"):
            return username
        
        # Último recurso: quitar el $
        if username.endswith("$"):
            return username[:-1]
        
        return username or "Unknown"
    except Exception as e:
        log.error(f"Error getting username: {e}")
        return "Unknown"

AGENT_ID = load_or_create_agent_id()

# --------------------------------------------------------------------------------------
# Variables Globales para Sesión
# --------------------------------------------------------------------------------------

class SessionState:
    """Gestiona el estado de la sesión de Windows"""
    
    def __init__(self):
        self.session_start = datetime.utcnow()  # UTC para consistencia con backend
        self.is_locked = False
        self.accumulated_active_time = 0  # Segundos
        self.accumulated_idle_time = 0  # Segundos
        self.last_check_time = datetime.utcnow()
        self.last_idle_time = 0
        
        log.info(f"Session initialized at {self.session_start.strftime('%Y-%m-%d %H:%M:%S')} UTC")
    
    def check_lock_state(self, current_idle: int) -> Optional[str]:
        """
        Detecta si la sesión se bloqueó o desbloqueó
        
        Returns:
            "locked" si se detectó bloqueo
            "unlocked" si se detectó desbloqueo
            "new_day" si cambió el día
            None si no hay cambio
        """
        lock_threshold = DEFAULT_CONFIG["lock_threshold"]
        now = datetime.utcnow()
        
        # Detectar cambio de día: reiniciar sesión automáticamente
        if now.date() > self.session_start.date():
            log.info(f"New day detected ({now.date()} > {self.session_start.date()}) - Resetting session")
            self.session_start = now
            self.accumulated_active_time = 0
            self.accumulated_idle_time = 0
            self.last_check_time = now
            self.is_locked = False
            return "new_day"
        
        # Detectar bloqueo: idle_time > umbral y no estaba bloqueado
        if current_idle > lock_threshold and not self.is_locked:
            self.is_locked = True
            log.info(f"Session LOCKED detected (idle: {current_idle}s)")
            return "locked"
        
        # Detectar desbloqueo: idle_time < 60s y estaba bloqueado
        if current_idle < 60 and self.is_locked:
            self.is_locked = False
            # Reiniciar sesión
            self.session_start = now
            self.accumulated_active_time = 0
            self.accumulated_idle_time = 0
            self.last_check_time = now
            log.info(f"Session UNLOCKED detected - New session started at {self.session_start.strftime('%Y-%m-%d %H:%M:%S')} UTC")
            return "unlocked"
        
        return None
    
    def update_accumulated_times(self, current_idle: int):
        """Actualiza los tiempos acumulados de activo e idle - CORREGIDO v2.4"""
        now = datetime.utcnow()
        elapsed = (now - self.last_check_time).total_seconds()
        
        # Acumular siempre, independientemente del estado de bloqueo
        idle_threshold = DEFAULT_CONFIG["idle_threshold"]
        
        if current_idle < idle_threshold:
            # Usuario ACTIVO
            self.accumulated_active_time += elapsed
        else:
            # Usuario IDLE
            self.accumulated_idle_time += elapsed
        
        self.last_check_time = now
        self.last_idle_time = current_idle
    
    def get_session_info(self, current_idle: int) -> Dict[str, Any]:
        """Obtiene información de la sesión actual - CORREGIDO v2.1"""
        now = datetime.utcnow()
        session_duration = (now - self.session_start).total_seconds()
        
        # Calcular productividad (limitar a 100%) - CORREGIDO v2.1
        if session_duration > 0:
            productivity = min(int((self.accumulated_active_time / session_duration) * 100), 100)
        else:
            productivity = 0
        
        # Convertir a horas
        active_hours = self.accumulated_active_time / 3600
        idle_hours = self.accumulated_idle_time / 3600
        
        return {
            "login_time": self.session_start.strftime("%H:%M:%S"),
            "logout_time": None,
            "idle_time": current_idle,
            "total_active": int(self.accumulated_active_time),
            "total_idle": int(self.accumulated_idle_time),
            "total_connected": int(session_duration),
            "productivity": productivity,
            "active_hours": round(active_hours, 2),
            "idle_hours": round(idle_hours, 2),
            "is_locked": self.is_locked
        }

# Instancia global
session = SessionState()

# --------------------------------------------------------------------------------------
# APIs de Windows
# --------------------------------------------------------------------------------------

# Variable global para tracking de idle_time
_last_raw_idle = None
_last_valid_idle = 0
_idle_initialized = False

def get_idle_time() -> int:
    """Obtiene el tiempo de inactividad en segundos - FIX v2.11 (anti-freeze)"""
    global _last_raw_idle, _last_valid_idle, _idle_initialized

    try:
        class LASTINPUTINFO(ctypes.Structure):
            _fields_ = [
                ("cbSize", ctypes.c_uint),
                ("dwTime", ctypes.c_uint),
            ]

        lii = LASTINPUTINFO()
        lii.cbSize = ctypes.sizeof(LASTINPUTINFO)

        if ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lii)):
            millis = ctypes.windll.kernel32.GetTickCount() - lii.dwTime
            raw_idle = millis // 1000

            # Primera lectura
            if not _idle_initialized:
                if raw_idle > 3600:  # > 1 hora = valor absurdo al inicio
                    log.warning(f"Initial idle time too high ({raw_idle}s), starting fresh")
                    _last_raw_idle = raw_idle
                    _last_valid_idle = 0
                else:
                    _last_raw_idle = raw_idle
                    _last_valid_idle = raw_idle

                _idle_initialized = True
                return _last_valid_idle

            # Lecturas posteriores
            if _last_raw_idle is not None:
                if raw_idle < _last_raw_idle:
                    # Hay input de usuario -> reseteamos idle lógico
                    _last_valid_idle = 0
                    log.debug(f"User input detected (idle reset: {raw_idle}s < {_last_raw_idle}s)")
                else:
                    # Siempre acumulamos, pero limitando saltos absurdos
                    delta = raw_idle - _last_raw_idle

                    # Si el salto es enorme (bug RDP / GetLastInputInfo), lo capamos
                    if delta > 300:  # máx 5 minutos por iteración
                        log.debug(f"Idle delta too big ({delta}s), capping to 300s")
                        delta = 300

                    if delta > 0:
                        _last_valid_idle += delta

            _last_raw_idle = raw_idle
            return _last_valid_idle

        # Si falla la API, devolvemos el último valor válido
        return _last_valid_idle

    except Exception as e:
        log.error(f"Error getting idle time: {e}")
        return _last_valid_idle


def get_primary_ip() -> str:
    """Obtiene la IP principal de la máquina"""
    try:
        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_metrics() -> Dict[str, Any]:
    """Obtiene métricas del sistema"""
    try:
        cpu_percent = psutil.cpu_percent(interval=1)
        memory = psutil.virtual_memory()
        disk = psutil.disk_usage('/')
        
        return {
            "cpu_percent": round(cpu_percent, 1),
            "memory_percent": round(memory.percent, 1),
            "memory_used_gb": round(memory.used / (1024**3), 2),
            "memory_total_gb": round(memory.total / (1024**3), 2),
            "disk_percent": round(disk.percent, 1),
            "disk_used_gb": round(disk.used / (1024**3), 2),
            "disk_total_gb": round(disk.total / (1024**3), 2),
        }
    except Exception as e:
        log.error(f"Error getting system metrics: {e}")
        return {
            "cpu_percent": 0,
            "memory_percent": 0,
            "memory_used_gb": 0,
            "memory_total_gb": 0,
            "disk_percent": 0,
            "disk_used_gb": 0,
            "disk_total_gb": 0,
        }

def get_app_usage() -> List[Dict[str, Any]]:
    """Obtiene lista de aplicaciones en uso"""
    apps = []
    try:
        for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
            try:
                info = proc.info
                if info['name'] and info['cpu_percent'] is not None:
                    apps.append({
                        "name": info['name'],
                        "cpu": round(info['cpu_percent'], 1),
                        "memory": round(info['memory_percent'], 1)
                    })
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                pass
    except Exception as e:
        log.error(f"Error getting app usage: {e}")
    
    # Ordenar por CPU y tomar top 20
    apps.sort(key=lambda x: x['cpu'], reverse=True)
    return apps[:20]

def get_file_transfers() -> List[Dict[str, Any]]:
    """Obtiene lista de transferencias de archivos recientes"""
    transfers = []
    
    # Placeholder - implementar según necesidad
    # Aquí se podría monitorear carpetas de descargas, red, etc.
    
    return transfers

def check_alerts(session_info: Dict[str, Any], metrics: Dict[str, Any]) -> List[Dict[str, Any]]:
    """Genera alertas basadas en el estado actual"""
    alerts = []
    
    productivity = session_info.get("productivity", 0)
    cpu_percent = metrics.get("cpu_percent", 0)
    memory_percent = metrics.get("memory_percent", 0)
    idle_time = session_info.get("idle_time", 0)
    
    # Alerta de productividad baja (solo si la sesión lleva > 30 minutos)
    if session_info.get("total_connected", 0) > 1800:  # 30 minutos
        if productivity < 50:
            alerts.append({
                "type": "low_productivity",
                "severity": "medium",
                "message": f"Productivity is {productivity}%"
            })
    
    # Alerta de inactividad prolongada (> 30 minutos sin bloqueo)
    if idle_time > 1800 and not session_info.get("is_locked", False):
        alerts.append({
            "type": "prolonged_inactivity",
            "severity": "low",
            "message": f"User inactive for {idle_time//60} minutes"
            })
    
    # Alerta de CPU alto
    if cpu_percent > 90:
        alerts.append({
            "type": "high_cpu",
            "severity": "medium",
            "message": f"CPU usage is {cpu_percent}%"
        })
    
    # Alerta de memoria alta
    if memory_percent > 90:
        alerts.append({
            "type": "high_memory",
            "severity": "medium",
            "message": f"Memory usage is {memory_percent}%"
        })
    
    return alerts
    
def compute_status(current_idle: int) -> str:
    """Calcula el status lógico del agente según idle y bloqueo"""
    idle_threshold = DEFAULT_CONFIG["idle_threshold"]
    lock_threshold = DEFAULT_CONFIG["lock_threshold"]

    # Si la sesión está bloqueada, priorizamos ese estado
    if session.is_locked:
        return "locked"

    # Idle prolongado por encima del lock_threshold
    if current_idle > lock_threshold:
        return "idle"

    # Idle moderado por encima del idle_threshold
    if current_idle > idle_threshold:
        return "away"

    # Por defecto, consideramos activo
    return "active"


# --------------------------------------------------------------------------------------
# Heartbeat
# --------------------------------------------------------------------------------------

def send_heartbeat() -> bool:
    """Envía heartbeat al servidor"""
    try:
        # Obtener estado actual
        current_idle = get_idle_time()
        
        # Actualizar sesión
        lock_event = session.check_lock_state(current_idle)
        session.update_accumulated_times(current_idle)
        
        # Recopilar datos
        session_info = session.get_session_info(current_idle)
        metrics = get_system_metrics()
        status = compute_status(current_idle)
        
        # Construir payload
        payload = {
            "agent_id": AGENT_ID,
            "hostname": platform.node(),
            "user": get_logged_in_user(),
            "ip": get_primary_ip(),
            "os": f"{platform.system()} {platform.release()}",
            "status": status,
            "metrics": metrics,
            "session": session_info,
            "app_usage": get_app_usage(),
            "file_transfers": get_file_transfers(),
            "alerts": check_alerts(session_info, metrics),
            "timestamp": datetime.utcnow().isoformat()
        }
        
        # Enviar al servidor
        url = f"{DEFAULT_CONFIG['server_url']}/api/agent/heartbeat"
        timeout = DEFAULT_CONFIG['requests_timeout']
        verify = DEFAULT_CONFIG['verify_tls']
        
        response = requests.post(
            url,
            json=payload,
            timeout=timeout,
            verify=verify
        )
        
        if response.status_code == 200:
            session_info = payload["session"]
            metrics = payload["metrics"]
            
            log.info(
                f"Heartbeat OK | "
                f"CPU: {metrics['cpu_percent']}% | "
                f"Prod: {session_info['productivity']}% | "
                f"Status: {status} | "
                f"Conn: {session_info['total_connected']}s | "
                f"Active: {session_info['total_active']}s | "
                f"Idle: {session_info['total_idle']}s | "
                f"IdleNow: {session_info['idle_time']}s | "
                f"Apps: {len(payload['app_usage'])} | "
                f"Transfers: {len(payload['file_transfers'])}"
            )
            return True
        else:
            log.warning(f"Heartbeat failed: HTTP {response.status_code}")
            return False
            
    except requests.exceptions.Timeout:
        log.error("Heartbeat timeout")
        return False
    except requests.exceptions.ConnectionError:
        log.error("Heartbeat connection error")
        return False
    except Exception as e:
        log.error(f"Heartbeat error: {e}")
        return False

# --------------------------------------------------------------------------------------
# Main Loop
# --------------------------------------------------------------------------------------

def main():
    """Loop principal del agente"""
    log.info("=" * 60)
    log.info("UARM Agent v2.10 Started (PRODUCTION READY)")
    log.info(f"Agent ID: {AGENT_ID}")
    log.info(f"Server: {DEFAULT_CONFIG['server_url']}")
    log.info(f"Hostname: {platform.node()}")
    log.info(f"User: {get_logged_in_user()}")
    log.info(f"Interval: {DEFAULT_CONFIG['heartbeat_interval']}s")
    log.info(f"Idle Threshold: {DEFAULT_CONFIG['idle_threshold']}s")
    log.info(f"Lock Threshold: {DEFAULT_CONFIG['lock_threshold']}s")
    log.info("=" * 60)
    
    interval = DEFAULT_CONFIG['heartbeat_interval']
    
    while True:
        try:
            send_heartbeat()
            time.sleep(interval)
        except KeyboardInterrupt:
            log.info("Agent stopped by user")
            break
        except Exception as e:
            log.error(f"Main loop error: {e}")
            time.sleep(interval)

if __name__ == "__main__":
    main()

