# -*- coding: utf-8 -*-
"""
UARM Agent v2.5 - Compatible con Backend v3.1
Agente Windows optimizado para servicio con detección de bloqueo/desbloqueo,
cálculo correcto de productividad, manejo robusto de errores, y limpieza automática de logs
"""

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
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 7 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 (> 7 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 > 7:
                            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()

# --------------------------------------------------------------------------------------
# Utilidades
# --------------------------------------------------------------------------------------

def load_or_create_agent_id() -> str:
    """Carga o crea un ID único para este agente"""
    if AGENT_ID_FILE.exists():
        try:
            return AGENT_ID_FILE.read_text(encoding="utf-8").strip()
        except Exception:
            pass
    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$)"""
    try:
        username = getpass.getuser()
        
        # Si termina en $, es cuenta de sistema
        if username.endswith("$"):
            # Intentar obtener el usuario de otra forma
            try:
                import win32api
                return win32api.GetUserName()
            except:
                # Eliminar el $ como último recurso
                return username[:-1]
        
        return username
    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.now()
        self.is_locked = False
        self.accumulated_active_time = 0  # Segundos
        self.accumulated_idle_time = 0  # Segundos
        self.last_check_time = datetime.now()
        self.last_idle_time = 0
        
        log.info(f"Session initialized at {self.session_start.strftime('%Y-%m-%d %H:%M:%S')}")
    
    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
            None si no hay cambio
        """
        lock_threshold = DEFAULT_CONFIG["lock_threshold"]
        
        # 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 = datetime.now()
            self.accumulated_active_time = 0
            self.accumulated_idle_time = 0
            self.last_check_time = datetime.now()
            log.info(f"Session UNLOCKED detected - New session started at {self.session_start.strftime('%Y-%m-%d %H:%M:%S')}")
            return "unlocked"
        
        return None
    
    def update_accumulated_times(self, current_idle: int):
        """Actualiza los tiempos acumulados de activo e idle"""
        now = datetime.now()
        elapsed = (now - self.last_check_time).total_seconds()
        
        # Solo acumular si no está bloqueado
        if not self.is_locked:
            idle_threshold = DEFAULT_CONFIG["idle_threshold"]
            
            if current_idle < idle_threshold:
                # Usuario ACTIVO
                self.accumulated_active_time += elapsed
            else:
                # Usuario IDLE (pero no bloqueado)
                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"""
        now = datetime.now()
        session_duration = (now - self.session_start).total_seconds()

        # Calcular productividad
        if session_duration > 0:
            productivity = int((self.accumulated_active_time / session_duration) * 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,
            # v3.4 format: use accumulated times with correct field names
            "active_time": int(self.accumulated_active_time),
            "idle_time": int(self.accumulated_idle_time),
            "total_connected": int(session_duration),
            "productivity": productivity,
            # Legacy fields for backward compatibility
            "current_idle": current_idle,
            "total_active": int(self.accumulated_active_time),
            "total_idle": int(self.accumulated_idle_time),
            "active_hours": round(active_hours, 2),
            "idle_hours": round(idle_hours, 2),
            "is_locked": self.is_locked
        }

# Instancia global
session = SessionState()

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

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

def get_idle_time_seconds() -> int:
    """Tiempo de inactividad del usuario en segundos"""
    try:
        lii = LASTINPUTINFO()
        lii.cbSize = ctypes.sizeof(LASTINPUTINFO)
        if ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lii)):
            tick_count = ctypes.windll.kernel32.GetTickCount()
            millis = tick_count - lii.dwTime
            return int(millis // 1000)
    except Exception as e:
        log.error(f"Error getting idle time: {e}")
    return 0

def get_primary_ip() -> str:
    """Obtiene la IP primaria del equipo"""
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        return ip
    except Exception:
        try:
            return socket.gethostbyname(socket.gethostname())
        except Exception:
            return "0.0.0.0"
    finally:
        s.close()

# --------------------------------------------------------------------------------------
# Recolección de datos
# --------------------------------------------------------------------------------------

def get_metrics() -> Dict[str, Any]:
    """Obtiene métricas del sistema"""
    try:
        cpu = psutil.cpu_percent(interval=0.5)
        mem = psutil.virtual_memory()
        disk = psutil.disk_usage("/")
        
        return {
            "cpu_percent": round(cpu, 1),
            "memory_percent": round(mem.percent, 1),
            "disk_percent": round(disk.percent, 1)
        }
    except Exception as e:
        log.error(f"Error getting metrics: {e}")
        return {
            "cpu_percent": 0,
            "memory_percent": 0,
            "disk_percent": 0
        }

def get_app_usage() -> List[Dict[str, Any]]:
    """Obtiene lista de aplicaciones en uso"""
    apps = []
    try:
        for proc in psutil.process_iter(['name', 'cpu_times']):
            try:
                name = proc.info.get('name')
                if name and name.endswith('.exe'):
                    cpu_time = proc.info.get('cpu_times')
                    if cpu_time:
                        total_time = int(cpu_time.user + cpu_time.system)
                        if total_time > 0:
                            apps.append({
                                "exe": name,
                                "duration": total_time,
                                "timestamp": datetime.utcnow().isoformat() + "Z"
                            })
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                continue
    except Exception as e:
        log.error(f"Error getting app usage: {e}")
    
    # Ordenar por duración y tomar top 10
    apps.sort(key=lambda x: x['duration'], reverse=True)
    return apps[:10]

def get_file_transfers() -> List[Dict[str, Any]]:
    """Detecta transferencias de archivos recientes (descargas)"""
    transfers = []
    try:
        # Buscar en Downloads del usuario actual
        downloads_path = Path.home() / "Downloads"
        if downloads_path.exists():
            cutoff_time = datetime.now() - timedelta(hours=24)
            for file in downloads_path.iterdir():
                if file.is_file():
                    try:
                        mtime = datetime.fromtimestamp(file.stat().st_mtime)
                        if mtime > cutoff_time:
                            transfers.append({
                                "file_path": str(file),
                                "filename": file.name,
                                "bytes": file.stat().st_size,
                                "direction": "download",
                                "timestamp": mtime.isoformat()
                            })
                    except Exception:
                        continue
    except Exception as e:
        log.error(f"Error getting file transfers: {e}")
    
    # Ordenar por timestamp y tomar últimos 20
    transfers.sort(key=lambda x: x['timestamp'], reverse=True)
    return transfers[:20]

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 alta
    if cpu_percent > 90:
        alerts.append({
            "type": "high_cpu",
            "severity": "high",
            "message": f"CPU usage is {cpu_percent}%"
        })
    
    # Alerta de memoria alta
    if memory_percent > 90:
        alerts.append({
            "type": "high_memory",
            "severity": "high",
            "message": f"Memory usage is {memory_percent}%"
        })
    
    return alerts

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

def build_heartbeat_payload() -> Dict[str, Any]:
    """Construye el payload del heartbeat"""
    # Obtener idle time actual
    idle_seconds = get_idle_time_seconds()
    
    # Detectar cambio de estado de sesión (bloqueo/desbloqueo)
    lock_event = session.check_lock_state(idle_seconds)
    
    # Actualizar tiempos acumulados
    session.update_accumulated_times(idle_seconds)
    
    # Obtener información de sesión
    session_info = session.get_session_info(idle_seconds)
    
    # Obtener métricas
    metrics = get_metrics()
    
    # 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()}",
        "metrics": metrics,
        "session": session_info,
        "app_usage": get_app_usage(),
        "file_transfers": get_file_transfers(),
        "alerts": check_alerts(session_info, metrics)
    }
    
    # Añadir evento de bloqueo/desbloqueo si ocurrió
    if lock_event:
        payload["session_event"] = lock_event
    
    return payload

def send_heartbeat() -> bool:
    """Envía el heartbeat al servidor"""
    try:
        payload = build_heartbeat_payload()
        
        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"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.5 Started")
    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)
    
    consecutive_failures = 0
    max_failures = 5
    
    while True:
        try:
            success = send_heartbeat()
            
            if success:
                consecutive_failures = 0
            else:
                consecutive_failures += 1
                
                if consecutive_failures >= max_failures:
                    log.error(f"Too many failures ({consecutive_failures}), waiting 5 minutes...")
                    time.sleep(300)
                    consecutive_failures = 0
            
            time.sleep(DEFAULT_CONFIG['heartbeat_interval'])
            
        except KeyboardInterrupt:
            log.info("Agent stopped by user")
            break
        except Exception as e:
            log.error(f"Unexpected error: {e}")
            time.sleep(60)

if __name__ == "__main__":
    main()

