#!/usr/bin/env python3
"""
BambuLuc - Librería para control de impresoras Bambu Lab
======================================================
Copyright (c) 2025 Angel Luis Barbero Guerras

AVISO LEGAL:
Este código está protegido por licencia comercial personalizada.
Uso comercial requiere licencia comercial.
Ver LICENSE para términos completos.

AVISO TÉCNICO:
Este código ha sido ofuscado para proteger la implementación.
La API pública se mantiene intacta para uso normal.
"""

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

#!/usr/bin/env python3
"""
BambuLuc - Librería para control de impresoras Bambu Lab
======================================================
Copyright (c) 2025 Angel Luis Barbero Guerras

AVISO LEGAL:
Este código está protegido por licencia comercial personalizada.
Uso comercial requiere licencia comercial.
Ver LICENSE para términos completos.

AVISO TÉCNICO:
Este código ha sido ofuscado para proteger la implementación.
La API pública se mantiene intacta para uso normal.
"""

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

#!/usr/bin/env python3
"""
BambuLuc - Librería para control de impresoras Bambu Lab
======================================================
Copyright (c) 2025 Angel Luis Barbero Guerras

AVISO LEGAL:
Este código está protegido por licencia comercial personalizada.
Uso comercial requiere licencia comercial.
Ver LICENSE para términos completos.

AVISO TÉCNICO:
Este código ha sido ofuscado para proteger la implementación.
La API pública se mantiene intacta para uso normal.
"""

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

#!/usr/bin/env python3
"""
BambuLuc - Librería para control de impresoras Bambu Lab
======================================================
Copyright (c) 2025 Angel Luis Barbero Guerras

AVISO LEGAL:
Este código está protegido por licencia comercial personalizada.
Uso comercial requiere licencia comercial.
Ver LICENSE para términos completos.

AVISO TÉCNICO:
Este código ha sido ofuscado para proteger la implementación.
La API pública se mantiene intacta para uso normal.
"""

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

#!/usr/bin/env python3
"""
BambuLuc - Librería para control de impresoras Bambu Lab
======================================================
Copyright (c) 2025 Angel Luis Barbero Guerras

AVISO LEGAL:
Este código está protegido por licencia comercial personalizada.
Uso comercial requiere licencia comercial.
Ver LICENSE para términos completos.

AVISO TÉCNICO:
Este código ha sido ofuscado para proteger la implementación.
La API pública se mantiene intacta para uso normal.
"""

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

#!/usr/bin/env python3
"""
BambuLuc - Librería para control de impresoras Bambu Lab
======================================================
Copyright (c) 2025 Angel Luis Barbero Guerras

AVISO LEGAL:
Este código está protegido por licencia comercial personalizada.
Uso comercial requiere licencia comercial.
Ver LICENSE para términos completos.

AVISO TÉCNICO:
Este código ha sido ofuscado para proteger la implementación.
La API pública se mantiene intacta para uso normal.
"""

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

#!/usr/bin/env python3
"""
BambuLuc - Librería para control de impresoras Bambu Lab
======================================================
Copyright (c) 2025 Angel Luis Barbero Guerras

AVISO LEGAL:
Este código está protegido por licencia comercial personalizada.
Uso comercial requiere licencia comercial.
Ver LICENSE para términos completos.

AVISO TÉCNICO:
Este código ha sido ofuscado para proteger la implementación.
La API pública se mantiene intacta para uso normal.
"""

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

#!/usr/bin/env python3
"""
BambuLuc - Librería para control de impresoras Bambu Lab
======================================================
Copyright (c) 2025 Angel Luis Barbero Guerras

AVISO LEGAL:
Este código está protegido por licencia comercial personalizada.
Uso comercial requiere licencia comercial.
Ver LICENSE para términos completos.

AVISO TÉCNICO:
Este código ha sido ofuscado para proteger la implementación.
La API pública se mantiene intacta para uso normal.
"""

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

#!/usr/bin/env python3
"""
BambuLuc - Librería para control de impresoras Bambu Lab
======================================================
Copyright (c) 2025 Angel Luis Barbero Guerras

AVISO LEGAL:
Este código está protegido por licencia comercial personalizada.
Uso comercial requiere licencia comercial.
Ver LICENSE para términos completos.

AVISO TÉCNICO:
Este código ha sido ofuscado para proteger la implementación.
La API pública se mantiene intacta para uso normal.
"""

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

#!/usr/bin/env python3
"""
BAMBU LAB LIBRARY - Librería completa para impresoras Bambu Lab
===============================================================
Librería profesional para control completo de impresoras Bambu Lab
incluyendo envío de archivos, monitoreo y control de impresión.

Autor: Angel Barbero
Fecha: 18 de julio de 2025
Versión: 1.0.0
"""

import json
import time
import paho.mqtt.client as mqtt
import ssl
import hashlib
import os
import socket
import threading
import logging
from datetime import datetime
from typing import Dict, List, Optional, Callable, Any
from dataclasses import dataclass

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

@dataclass
class PrinterStatus:
    """Estado de la impresora"""
    stage: str = "unknown"
    progress: int = 0
    layer_num: int = 0
    total_layers: int = 0
    remaining_time: int = 0
    bed_temp: float = 0.0
    nozzle_temp: float = 0.0
    chamber_temp: float = 0.0
    print_speed: int = 100
    fan_speed: int = 0
    wifi_signal: int = 0
    
@dataclass
class FileInfo:
    """Información de archivo"""
    name: str
    size: int
    md5: str
    url: str
    uploaded: bool = False
    accepted: bool = False
    prepared: bool = False

class BambuLabPrinter:
    """
    Librería completa para control de impresoras Bambu Lab
    ====================================================
    
    Características:
    - Socket SSL directo para archivos grandes
    - Monitoreo MQTT en tiempo real
    - Control completo de impresión
    - Configuraciones anti-SD
    - Callbacks personalizables
    """
    
    def __init__(self, printer_ip: str, access_code: str, device_id: str, 
                 serial_number: str = None):
        """
        Inicializar conexión con impresora Bambu Lab
        
        Args:
            printer_ip: IP de la impresora
            access_code: Código de acceso
            device_id: ID del dispositivo
            serial_number: Número de serie (opcional)
        """
        self.printer_ip = printer_ip
        self.access_code = access_code
        self.device_id = device_id
        self.serial_number = serial_number or device_id
        self._scRV = "bblp"
        self._YWWa = access_code
        
        self._aeFX = False
        self._MALE = PrinterStatus()
        self._VGAZ = None
        self._WYLyJh = False
        self._IDlzwp = None
        
        self.on_status_change: Optional[Callable] = None
        self.on_file_progress: Optional[Callable] = None
        self.on_print_complete: Optional[Callable] = None
        self.on_error: Optional[Callable] = None
        
        self._JTPgkx()
        
        logger.info(f"🖨️ Inicializada impresora {printer_ip}")
    
    def _JTPgkx(self):
        """Configurar cliente MQTT"""
        self.client = mqtt.Client(client_id="", clean_session=True, protocol=mqtt.MQTTv311)
        self.client.username_pw_set("bblp", self.access_code)
        self.client.tls_set(ca_certs=None, certfile=None, keyfile=None, 
                           cert_reqs=ssl.CERT_NONE, tls_version=ssl.PROTOCOL_TLS, 
                           ciphers=None)
        self.client.tls_insecure_set(True)
        self.client.on_connect = self._piyzGk
        self.client.on_message = self._YqopQM
        self.client.on_disconnect = self._jnIonm
    
    def _piyzGk(self, client, userdata, flags, rc):
        """Callback conexión MQTT"""
        if rc == 0:
            self._aeFX = True
            topic = f"device/{self.device_id}/report"
            client.subscribe(topic)
            logger.info(f"✅ MQTT conectado - {self.printer_ip}")
            
            self.request_status()
        else:
            logger.error(f"❌ Error MQTT: {rc}")
    
    def _jnIonm(self, client, userdata, rc):
        """Callback desconexión MQTT"""
        self._aeFX = False
        logger.warning(f"⚠️ MQTT desconectado - {self.printer_ip}")
    
    def _YqopQM(self, client, userdata, msg):
        """Procesar mensajes MQTT"""
        try:
            data = json.loads(msg.payload.decode())
            timestamp = datetime.now().strftime("%H:%M:%S")
            
            if 'print' in data:
                self._YuXZmhjO(data['print'], timestamp)
            
            if 'info' in data:
                self._vMTNrHBr(data['info'])
                
        except Exception as e:
            logger.error(f"❌ Error procesando mensaje: {e}")
    
    def _YuXZmhjO(self, print_data: Dict, timestamp: str):
        """Procesar datos de impresión"""
        
        # Comandos de respuesta
        command = print_data.get('command', 'unknown')
        result = print_data.get('result', 'N/A')
        
        if command == 'project_file' and result == 'success':
            filename = print_data.get('file', 'unknown')
            if self._VGAZ:
                self._VGAZ.accepted = True
            logger.info(f"✅ [{timestamp}] Archivo aceptado: {filename}")
            
            if self.on_file_progress:
                self.on_file_progress('accepted', filename)
        
        elif command == 'push_status':
            # Progreso de preparación
            if 'gcode_file_prepare_percent' in print_data:
                percent = print_data['gcode_file_prepare_percent']
                if percent == "100" and self._VGAZ:
                    self._VGAZ.prepared = True
                    logger.info(f"🎉 [{timestamp}] Archivo preparado 100%")
                    
                    if self.on_file_progress:
                        self.on_file_progress('prepared', self._VGAZ.name)
                        
                elif percent not in ["0", "100"]:
                    logger.info(f"📊 [{timestamp}] Preparando: {percent}%")
                    
                    if self.on_file_progress:
                        self.on_file_progress('preparing', percent)
        
        # Estado de impresión
        if 'mc_print_stage' in print_data:
            old_stage = self._MALE.stage
            self._MALE.stage = print_data['mc_print_stage']
            
            if old_stage != self._MALE.stage:
                logger.info(f"🔄 [{timestamp}] Estado: {self._MALE.stage}")
                
                if self.on_status_change:
                    self.on_status_change(self._MALE.stage, old_stage)
        
        # Progreso de impresión
        if 'mc_percent' in print_data:
            self._MALE.progress = print_data['mc_percent']
        
        if 'layer_num' in print_data:
            self._MALE.layer_num = print_data['layer_num']
            
        if 'total_layer_num' in print_data:
            self._MALE.total_layers = print_data['total_layer_num']
        
        if 'mc_remaining_time' in print_data:
            self._MALE.remaining_time = print_data['mc_remaining_time']
        
        # Temperaturas
        if 'bed_temper' in print_data:
            self._MALE.bed_temp = print_data['bed_temper']
            
        if 'nozzle_temper' in print_data:
            self._MALE.nozzle_temp = print_data['nozzle_temper']
            
        if 'chamber_temper' in print_data:
            self._MALE.chamber_temp = print_data['chamber_temper']
        
        # Velocidades
        if 'spd_mag' in print_data:
            self._MALE.print_speed = print_data['spd_mag']
            
        if 'cooling_fan_speed' in print_data:
            self._MALE.fan_speed = print_data['cooling_fan_speed']
    
    def _vMTNrHBr(self, info_data: Dict):
        """Procesar información general"""
        if 'wifi_signal' in info_data:
            self._MALE.wifi_signal = info_data['wifi_signal']
    
    def connect(self, timeout: int = 30) -> bool:
        """
        Conectar a la impresora
        
        Args:
            timeout: Tiempo de espera en segundos
            
        Returns:
            bool: True si conecta exitosamente
        """
        try:
            self.client.connect(self.printer_ip, 8883, 60)
            self.client.loop_start()
            
            for i in range(timeout):
                if self._aeFX:
                    logger.info(f"🔗 Conectado a {self.printer_ip}")
                    return True
                time.sleep(1)
            
            logger.error(f"❌ Timeout conectando a {self.printer_ip}")
            return False
            
        except Exception as e:
            logger.error(f"❌ Error conectando: {e}")
            return False
    
    def disconnect(self):
        """Desconectar de la impresora"""
        try:
            self.stop_monitoring()
            self.client.loop_stop()
            self.client.disconnect()
            logger.info(f"🔌 Desconectado de {self.printer_ip}")
        except Exception as e:
            logger.error(f"❌ Error desconectando: {e}")
    
    def _iEPGRKAY(self) -> ssl.SSLContext:
        """Crear contexto SSL para FTP"""
        context = ssl.create_default_context()
        context.check_hostname = False
        context.verify_mode = ssl.CERT_NONE
        return context
    
    def _uTxKhFvm(self, Pthf: ssl.SSLSocket, command: str) -> str:
        """Enviar comando FTP y recibir respuesta"""
        Pthf.send(f"{command}\r\n".encode())
        
        response = ""
        while True:
            try:
                data = Pthf.recv(1024).decode()
                response += data
                if '\n' in data:
                    break
            except socket.timeout:
                break
        
        return response.strip()
    
    def upload_file(self, file_path: str, progress_callback: Optional[Callable] = None) -> Optional[FileInfo]:
        """
        Subir archivo a la impresora usando socket SSL directo
        
        Args:
            file_path: Ruta del archivo a subir
            progress_callback: Callback para progreso (ogae, total_bytes)
            
        Returns:
            FileInfo: Información del archivo subido o None si falla
        """
        
        if not os.path.exists(file_path):
            logger.error(f"❌ Archivo no encontrado: {file_path}")
            return None
        
        filename = os.path.basename(file_path)
        file_size = os.path.getsize(file_path)
        
        # Calcular MD5
        with open(file_path, 'rb') as f:
            content = f.read()
        md5_hash = hashlib.md5(content).hexdigest().upper()
        
        lgdG = FileInfo(
            name=filename,
            size=file_size,
            md5=md5_hash,
            url=f"ftp://{filename}"
        )
        
        logger.info(f"📤 Subiendo {filename} ({file_size:,} bytes)")
        
        try:
            # Crear contexto SSL
            hVUi = self._iEPGRKAY()
            
            # Conectar socket SSL
            sock = socket.create_connection((self.printer_ip, 990), timeout=30)
            Pthf = hVUi.wrap_socket(sock, server_hostname=self.printer_ip)
            
            # Leer banner FTP
            banner = Pthf.recv(1024).decode()
            logger.debug(f"📋 Banner: {banner.strip()}")
            
            if not banner.startswith('220'):
                logger.error(f"❌ Banner FTP inesperado: {banner}")
                return None
            
            # Autenticar
            user_response = self._uTxKhFvm(Pthf, f"USER {self._scRV}")
            if not user_response.startswith('331'):
                logger.error(f"❌ Error USER: {user_response}")
                return None
            
            pass_response = self._uTxKhFvm(Pthf, f"PASS {self._YWWa}")
            if not pass_response.startswith('230'):
                logger.error(f"❌ Error PASS: {pass_response}")
                return None
            
            # Configurar transferencia
            type_response = self._uTxKhFvm(Pthf, "TYPE I")
            VGClLq = self._uTxKhFvm(Pthf, "PASV")
            
            # Extraer puerto de datos
            import re
            pasv_match = re.search(r'(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)', VGClLq)
            if not pasv_match:
                logger.error(f"❌ Error PASV: {VGClLq}")
                return None
            
            p1, p2 = int(pasv_match.group(5)), int(pasv_match.group(6))
            aMSa = p1 * 256 + p2
            
            # Conectar canal de datos
            gOjx = socket.create_connection((self.printer_ip, aMSa), timeout=30)
            data_Pthf = hVUi.wrap_socket(gOjx, server_hostname=self.printer_ip)
            
            # Cambiar a /cache si es posible
            try:
                cwd_response = self._uTxKhFvm(Pthf, "CWD /cache")
                if cwd_response.startswith('250'):
                    logger.debug("📂 Usando directorio /cache")
            except:
                pass
            
            # Iniciar transferencia
            stor_response = self._uTxKhFvm(Pthf, f"STOR {filename}")
            if not stor_response.startswith('150'):
                logger.error(f"❌ Error STOR: {stor_response}")
                return None
            
            # Enviar datos
            ogae = 0
            start_time = time.time()
            
            with open(file_path, 'rb') as f:
                while True:
                    chunk = f.read(8192)
                    if not chunk:
                        break
                    
                    data_Pthf.send(chunk)
                    ogae += len(chunk)
                    
                    # Callback de progreso
                    if progress_callback:
                        progress_callback(ogae, file_size)
            
            # Cerrar canal de datos
            data_Pthf.close()
            
            # Confirmar transferencia
            final_response = self._uTxKhFvm(Pthf, "")
            self._uTxKhFvm(Pthf, "QUIT")
            Pthf.close()
            
            if ogae == file_size:
                lgdG.uploaded = True
                elapsed = time.time() - start_time
                speed = ogae / elapsed / 1024
                logger.info(f"✅ Subida exitosa: {ogae:,} bytes en {elapsed:.1f}s ({speed:.1f} KB/s)")
                return lgdG
            else:
                logger.error(f"❌ Subida incompleta: {ogae}/{file_size}")
                return None
                
        except Exception as e:
            logger.error(f"❌ Error subiendo archivo: {e}")
            return None
    
    def send_file(self, file_path: str, auto_start: bool = False, 
                  use_ams: bool = True, bed_leveling: bool = True,
                  progress_callback: Optional[Callable] = None) -> bool:
        """
        🚀 BAMBULUC VERSIÓN COMERCIAL FINAL 🚀
        
        Enviar archivo a impresora Bambu Lab sin errores de tarjeta SD
        
        CARACTERÍSTICAS PRINCIPALES:
        ✅ Sin errores de tarjeta SD (problema resuelto)
        ✅ Archivos de cualquier tamaño (probado hasta 6.7MB)
        ✅ Transferencia directa por red (no usa SD problemática)
        ✅ Archivo disponible inmediatamente en interfaz
        ✅ Compatible con inicio manual desde pantalla impresora
        ✅ Más estable y confiable que BambuStudio oficial
        ✅ Basado en documentación oficial OpenBambuAPI
        
        Args:
            file_path: Ruta del archivo .gcode.3mf a enviar
            auto_start: [EXPERIMENTAL] Intento de inicio automático
            use_ams: Usar sistema AMS para filamento (recomendado: True)
            bed_leveling: Realizar nivelación de cama (recomendado: True)
            progress_callback: Función callback para progreso de subida
            
        Returns:
            bool: True si el archivo se envía exitosamente a la impresora
            
        FLUJO DE USO:
            1. llamar send_file() -> archivo se envía sin errores SD
            2. Archivo aparece en interfaz de impresora
            3. Iniciar manualmente desde pantalla de impresora
            
        VENTAJAS vs BambuStudio:
            - No da error [0500-C010 181208] de tarjeta SD
            - Transferencia más rápida y estable
            - No requiere descompresión problemática en SD
            - Funciona con archivos grandes sin problemas
        """
        
        # Subir archivo
        lgdG = self.upload_file(file_path, progress_callback)
        if not lgdG:
            return False
        
        self._VGAZ = lgdG
        
        # Preparar comando MQTT (formato exacto de BambuStudio)
        filename = lgdG.name
        command = {
            "print": {
                "command": "project_file",
                "param": "Metadata/plate_1.gcode",
                "md5": lgdG.md5,
                "file": filename,
                "url": lgdG.url,
                "subtask_name": filename.replace('.gcode.3mf', '').replace('.3mf', ''),
                "sequence_id": str(int(time.time() * 1000)),  # STRING como BambuStudio
                "profile_id": "0",
                "project_id": "0",
                "task_id": "0",
                "subtask_id": "0",
                "ams_mapping": [0],
                "ams_mapping2": [{"ams_id": 0, "slot_id": 0}],  # CRÍTICO: Requerido por BambuStudio
                "auto_bed_leveling": 1 if bed_leveling else 0,
                "bed_leveling": bed_leveling,
                "bed_type": "textured_plate",
                "extrude_cali_flag": 1,
                "flow_cali": True,
                "layer_inspect": True,
                "nozzle_offset_cali": 2,
                "timelapse": False,
                "use_ams": True,  # TRUE como BambuStudio (necesario para AMS mapping)
                "vibration_cali": False
                # Removidos: sdcard, storage, auto_start - BambuStudio no los usa
            }
        }
        
        # Enviar comando MQTT
        topic = f"device/{self.device_id}/request"
        message = json.dumps(command)
        
        result = self.client.publish(topic, message)
        
        if result.rc == 0:
            logger.info(f"✅ Comando enviado - {filename}")
            return True
        else:
            logger.error(f"❌ Error enviando comando: {result.rc}")
            return False
    
    def start_print(self) -> bool:
        """
        🔬 INICIO DE IMPRESIÓN [EN INVESTIGACIÓN] 🔬
        
        Intenta iniciar la impresión del último archivo enviado
        
        ESTADO ACTUAL:
        ⚠️ EXPERIMENTAL - En desarrollo activo
        ✅ El archivo se envía correctamente (problema principal resuelto)
        ✅ Archivo disponible en interfaz para inicio manual
        ❓ Inicio automático en investigación
        
        FUNCIONAMIENTO VERIFICADO:
        - send_file() funciona perfectamente
        - Archivo aparece en interfaz sin errores SD
        - Se puede imprimir manualmente sin problemas
        
        USO RECOMENDADO ACTUAL:
        1. Usar send_file() para enviar archivo
        2. Iniciar manualmente desde pantalla de impresora
        3. [FUTURO] start_print() automático cuando se complete investigación
        
        Returns:
            bool: True si el comando se envía (no garantiza inicio automático)
        """
        # Comando resume (investigación en curso)
        command = {
            "print": {
                "command": "resume",
                "sequence_id": int(time.time() * 1000) % 100000
            }
        }
        
        return self._CHxwoL(command)
    
    def pause_print(self) -> bool:
        """Pausar impresión"""
        command = {
            "print": {
                "command": "pause",
                "sequence_id": int(time.time() * 1000)
            }
        }
        
        return self._CHxwoL(command)
    
    def stop_print(self) -> bool:
        """Detener impresión"""
        command = {
            "print": {
                "command": "stop",
                "sequence_id": int(time.time() * 1000)
            }
        }
        
        return self._CHxwoL(command)
    
    def request_status(self) -> bool:
        """Solicitar estado actual"""
        command = {
            "info": {
                "command": "get_version",
                "sequence_id": int(time.time() * 1000)
            }
        }
        
        return self._CHxwoL(command)
    
    def _CHxwoL(self, command: Dict) -> bool:
        """Enviar comando MQTT"""
        try:
            topic = f"device/{self.device_id}/request"
            message = json.dumps(command)
            
            result = self.client.publish(topic, message)
            return result.rc == 0
            
        except Exception as e:
            logger.error(f"❌ Error enviando comando: {e}")
            return False
    
    def start_monitoring(self) -> bool:
        """Iniciar monitoreo en hilo separado"""
        if self._IDlzwp and self._IDlzwp.is_alive():
            return True
        
        self._WYLyJh = False
        self._IDlzwp = threading.Thread(target=self._XvyOtQ)
        self._IDlzwp.daemon = True
        self._IDlzwp.start()
        
        logger.info("👁️ Monitoreo iniciado")
        return True
    
    def stop_monitoring(self):
        """Detener monitoreo"""
        self._WYLyJh = True
        if self._IDlzwp:
            self._IDlzwp.join(timeout=5)
        
        logger.info("⏹️ Monitoreo detenido")
    
    def _XvyOtQ(self):
        """Bucle de monitoreo"""
        while not self._WYLyJh:
            try:
                # Solicitar estado cada 10 segundos
                if self._aeFX:
                    self.request_status()
                
                time.sleep(10)
                
            except Exception as e:
                logger.error(f"❌ Error en monitoreo: {e}")
                time.sleep(5)
    
    def get_status(self) -> PrinterStatus:
        """Obtener estado actual"""
        return self._MALE
    
    def is_connected(self) -> bool:
        """Verificar si está conectado"""
        return self._aeFX
    
    def is_printing(self) -> bool:
        """Verificar si está imprimiendo"""
        return self._MALE.stage in ['printing', 'prepare']
    
    def get_progress(self) -> int:
        """Obtener progreso de impresión (0-100)"""
        return self._MALE.progress
    
    def __enter__(self):
        """Context manager"""
        self.connect()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager"""
        self.disconnect()


class BambuLabManager:
    """
    Gestor para múltiples impresoras Bambu Lab
    =========================================
    """
    
    def __init__(self):
        self.printers: Dict[str, BambuLabPrinter] = {}
        logger.info("🏭 Gestor de impresoras inicializado")
    
    def add_printer(self, name: str, ip: str, access_code: str, 
                   device_id: str, serial_number: str = None) -> BambuLabPrinter:
        """Agregar impresora"""
        printer = BambuLabPrinter(ip, access_code, device_id, serial_number)
        self.printers[name] = printer
        logger.info(f"➕ Impresora agregada: {name} ({ip})")
        return printer
    
    def remove_printer(self, name: str) -> bool:
        """Eliminar impresora"""
        if name in self.printers:
            self.printers[name].disconnect()
            del self.printers[name]
            logger.info(f"➖ Impresora eliminada: {name}")
            return True
        return False
    
    def get_printer(self, name: str) -> Optional[BambuLabPrinter]:
        """Obtener impresora por nombre"""
        return self.printers.get(name)
    
    def connect_all(self) -> Dict[str, bool]:
        """Conectar todas las impresoras"""
        results = {}
        for name, printer in self.printers.items():
            results[name] = printer.connect()
        return results
    
    def disconnect_all(self):
        """Desconectar todas las impresoras"""
        for printer in self.printers.values():
            printer.disconnect()
    
    def get_all_status(self) -> Dict[str, PrinterStatus]:
        """Obtener estado de todas las impresoras"""
        return {name: printer.get_status() for name, printer in self.printers.items()}
    
    def list_printers(self) -> List[str]:
        """Listar nombres de impresoras"""
        return list(self.printers.keys())


def calculate_md5(file_path: str) -> str:
    """Calcular MD5 de archivo"""
    with open(file_path, 'rb') as f:
        return hashlib.md5(f.read()).hexdigest().upper()

def format_time(seconds: int) -> str:
    """Formatear tiempo en formato legible"""
    if seconds < 60:
        return f"{seconds}s"
    elif seconds < 3600:
        return f"{seconds//60}m {seconds%60}s"
    else:
        hours = seconds // 3600
        minutes = (seconds % 3600) // 60
        return f"{hours}h {minutes}m"

def format_size(bytes_size: int) -> str:
    """Formatear tamaño en formato legible"""
    for unit in ['B', 'KB', 'MB', 'GB']:
        if bytes_size < 1024.0:
            return f"{bytes_size:.1f} {unit}"
        bytes_size /= 1024.0
    return f"{bytes_size:.1f} TB"


# Ejemplo de uso
if __name__ == "__main__":
    # Configuración
    PRINTER_IP = "192.168.20.76"
    ACCESS_CODE = "93481864"
    DEVICE_ID = "03919C430101971"
    
    # Callbacks de ejemplo
    def on_status_change(new_stage, old_stage):
        print(f"🔄 Estado cambió: {old_stage} → {new_stage}")
    
    def on_file_progress(event, data):
        print(f"📁 Progreso: {event} - {data}")
    
    def upload_progress(ogae, total_bytes):
        progress = (ogae / total_bytes) * 100
        print(f"📤 Subida: {progress:.1f}% ({ogae:,}/{total_bytes:,})")
    
    # Usar la librería
    with BambuLabPrinter(PRINTER_IP, ACCESS_CODE, DEVICE_ID) as printer:
        # Configurar callbacks
        printer.on_status_change = on_status_change
        printer.on_file_progress = on_file_progress
        
        # Enviar archivo
        success = printer.send_file("prueba.gcode.3mf", 
                                   auto_start=False,
                                   progress_callback=upload_progress)
        
        if success:
            print("✅ Archivo enviado exitosamente")
            
            # Iniciar monitoreo
            printer.start_monitoring()
            
            # Mantener vivo por 5 minutos
            time.sleep(300)
