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

🎉 NUEVA FUNCIONALIDAD v1.2.0:
- ✅ Impresión automática (BREAKTHROUGH!)
- ✅ Configuración AMS avanzada
- ✅ Comando print_project_file optimizado
- ✅ Anti-SD configuration mejorada

BREAKTHROUGH CONSEGUIDO: 21 de julio de 2025
URL mágica: "file:///sdcard/cache/filename.3mf"
"""

import asyncio
import ssl
import socket
import json
import zipfile
import os
import hashlib
import logging
from datetime import datetime
from pathlib import Path
import time
import struct
import threading

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

class BambuLuc:
    """
    BambuLuc v1.2.0 - Control avanzado de impresoras Bambu Lab
    
    🎉 NUEVA FUNCIONALIDAD:
    - Impresión automática sin intervención manual
    - Configuración AMS avanzada
    - Subida de archivos grandes optimizada
    
    Ejemplo de uso:
        bambu = BambuLuc("192.168.20.76", "93481864", "03919C430101971")
        bambu.upload_and_print_auto("archivo.3mf")  # ¡IMPRIME AUTOMÁTICAMENTE!
    """
    
    def __init__(self, ip_address, access_code, serial_number, enable_anti_sd=True):
        """
        Inicializar BambuLuc con configuración anti-SD por defecto
        
        Args:
            ip_address: IP de la impresora (ej: "192.168.20.76")
            access_code: Código de acceso (ej: "93481864") 
            serial_number: Número de serie (ej: "03919C430101971")
            enable_anti_sd: Habilitar configuración anti-SD (recomendado)
        """
        self.ip_address = ip_address
        self.access_code = access_code
        self.serial_number = serial_number
        self.enable_anti_sd = enable_anti_sd
        
        # Configuración SSL
        self.ssl_context = self._create_ssl_context()
        
        # Estado de la conexión
        self.connected = False
        self.last_response = None
        
        logger.info(f"🚀 BambuLuc v1.2.0 inicializado")
        logger.info(f"📡 IP: {ip_address}")
        logger.info(f"🔒 Anti-SD: {'Habilitado' if enable_anti_sd else 'Deshabilitado'}")
    
    def _create_ssl_context(self):
        """Crear contexto SSL seguro para conexiones"""
        context = ssl.create_default_context()
        context.check_hostname = False
        context.verify_mode = ssl.CERT_NONE
        return context
    
    def analyze_3mf_file(self, file_path):
        """
        Analizar archivo 3MF para extraer metadata crítica
        
        Returns:
            tuple: (plate_number, gcode_path, bed_type, filament_info)
        """
        try:
            with zipfile.ZipFile(file_path, 'r') as zip_file:
                available_files = zip_file.namelist()
                
                # Encontrar archivos gcode disponibles
                gcode_files = [f for f in available_files if f.endswith('.gcode') and 'plate_' in f]
                
                if not gcode_files:
                    raise ValueError("No se encontraron archivos gcode en el 3MF")
                
                # Extraer número de placa
                gcode_file = gcode_files[0]
                plate_number = gcode_file.split('plate_')[1].split('.')[0]
                
                # Leer configuración del JSON
                json_path = f"Metadata/plate_{plate_number}.json"
                bed_type = "auto"
                filament_info = {}
                
                if json_path in available_files:
                    plate_json = json.loads(zip_file.read(json_path).decode('utf-8'))
                    bed_type = plate_json.get('bed_type', 'auto')
                
                # Analizar slice_info.config para información de filamentos
                if 'Metadata/slice_info.config' in available_files:
                    slice_info = zip_file.read('Metadata/slice_info.config').decode('utf-8')
                    filament_info = self._parse_filament_info(slice_info)
                
                logger.info(f"📋 3MF Analizado:")
                logger.info(f"   - Placa: {plate_number}")
                logger.info(f"   - GCode: {gcode_file}")
                logger.info(f"   - Bed Type: {bed_type}")
                logger.info(f"   - Filamentos: {len(filament_info)} detectados")
                
                return plate_number, gcode_file, bed_type, filament_info
                
        except Exception as e:
            logger.error(f"❌ Error analizando 3MF: {e}")
            raise
    
    def _parse_filament_info(self, slice_info):
        """Extraer información de filamentos del slice_info.config"""
        filament_info = {}
        try:
            # Parsear XML para extraer información de filamentos
            import xml.etree.ElementTree as ET
            root = ET.fromstring(slice_info)
            
            plate = root.find('plate')
            if plate is not None:
                filament_count = 0
                for element in plate:
                    if element.tag == 'filament':
                        filament_id = element.get('id', str(filament_count))
                        filament_type = element.get('type', 'PLA')
                        filament_color = element.get('color', '#000000FF')
                        
                        filament_info[filament_id] = {
                            'type': filament_type,
                            'color': filament_color,
                            'used_m': float(element.get('used_m', 0)),
                            'used_g': float(element.get('used_g', 0))
                        }
                        filament_count += 1
        except Exception as e:
            logger.warning(f"⚠️ No se pudo parsear información de filamentos: {e}")
        
        return filament_info
    
    async def upload_file_ftp(self, file_path, remote_filename=None):
        """
        Subir archivo vía FTP usando socket SSL directo
        Implementación optimizada para archivos grandes
        
        Args:
            file_path: Ruta local del archivo
            remote_filename: Nombre remoto (opcional)
            
        Returns:
            bool: True si la subida fue exitosa
        """
        if remote_filename is None:
            remote_filename = os.path.basename(file_path)
        
        try:
            logger.info(f"📤 Subiendo archivo: {remote_filename}")
            file_size = os.path.getsize(file_path)
            logger.info(f"📊 Tamaño: {file_size / 1024 / 1024:.2f} MB")
            
            # Conectar al puerto FTP SSL (990)
            sock = socket.create_connection((self.ip_address, 990), timeout=60)
            ftp_sock = self.ssl_context.wrap_socket(sock, server_hostname=self.ip_address)
            
            # Autenticación FTP
            ftp_sock.send(b'USER bblp\r\n')
            response = ftp_sock.recv(1024).decode()
            logger.debug(f"USER: {response.strip()}")
            
            ftp_sock.send(f'PASS {self.access_code}\r\n'.encode())
            response = ftp_sock.recv(1024).decode()
            logger.debug(f"PASS: {response.strip()}")
            
            # Configurar modo binario
            ftp_sock.send(b'TYPE I\r\n')
            response = ftp_sock.recv(1024).decode()
            
            # Modo pasivo
            ftp_sock.send(b'PASV\r\n')
            response = ftp_sock.recv(1024).decode()
            
            # Extraer IP y puerto del modo pasivo
            import re
            pasv_match = re.search(r'\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)', response)
            if not pasv_match:
                raise Exception("No se pudo obtener información de modo pasivo")
            
            data_ip = f"{pasv_match.group(1)}.{pasv_match.group(2)}.{pasv_match.group(3)}.{pasv_match.group(4)}"
            data_port = int(pasv_match.group(5)) * 256 + int(pasv_match.group(6))
            
            # Conexión de datos
            data_sock = socket.create_connection((data_ip, data_port), timeout=60)
            data_ssl_sock = self.ssl_context.wrap_socket(data_sock, server_hostname=self.ip_address)
            
            # Comando STOR
            ftp_sock.send(f'STOR {remote_filename}\r\n'.encode())
            response = ftp_sock.recv(1024).decode()
            
            # Subir archivo con progreso
            with open(file_path, 'rb') as f:
                sent = 0
                chunk_size = 8192
                start_time = time.time()
                
                while True:
                    chunk = f.read(chunk_size)
                    if not chunk:
                        break
                    
                    data_ssl_sock.send(chunk)
                    sent += len(chunk)
                    
                    # Mostrar progreso cada 1MB
                    if sent % (1024 * 1024) == 0 or sent == file_size:
                        progress = (sent / file_size) * 100
                        elapsed = time.time() - start_time
                        speed = sent / elapsed / 1024 / 1024 if elapsed > 0 else 0
                        logger.info(f"⏳ Progreso: {progress:.1f}% - {speed:.2f} MB/s")
            
            data_ssl_sock.close()
            
            # Confirmar transferencia
            response = ftp_sock.recv(1024).decode()
            logger.info(f"✅ Archivo subido exitosamente: {response.strip()}")
            
            ftp_sock.send(b'QUIT\r\n')
            ftp_sock.close()
            
            return True
            
        except Exception as e:
            logger.error(f"❌ Error en subida FTP: {e}")
            return False
    
    def configure_ams(self, ams_id=0, tray_id=0, filament_type="PLA", 
                      filament_color="#000000FF", nozzle_temp_min=190, 
                      nozzle_temp_max=240, tray_info_idx="GFL99"):
        """
        🆕 Configurar AMS antes de la impresión
        
        Args:
            ams_id: ID del AMS (0-3)
            tray_id: ID de la bandeja (0-3)
            filament_type: Tipo de filamento (PLA, ABS, PETG, etc.)
            filament_color: Color en formato RRGGBBAA
            nozzle_temp_min: Temperatura mínima del nozzle
            nozzle_temp_max: Temperatura máxima del nozzle
            tray_info_idx: Índice de información del material
            
        Returns:
            dict: Respuesta del comando AMS
        """
        command = {
            "print": {
                "sequence_id": "0",
                "command": "ams_filament_setting",
                "ams_id": ams_id,
                "tray_id": tray_id,
                "tray_info_idx": tray_info_idx,
                "tray_color": filament_color,
                "nozzle_temp_min": nozzle_temp_min,
                "nozzle_temp_max": nozzle_temp_max,
                "tray_type": filament_type
            }
        }
        
        logger.info(f"🔧 Configurando AMS {ams_id}, Tray {tray_id}: {filament_type}")
        return asyncio.run(self._send_mqtt_command(command))
    
    async def send_print_command_auto(self, filename, gcode_param, bed_type="auto"):
        """
        🎉 NUEVA FUNCIONALIDAD: Enviar comando de impresión automática
        
        Esta es la implementación EXITOSA que inicia la impresión automáticamente
        usando la URL mágica: "file:///sdcard/cache/"
        
        Args:
            filename: Nombre del archivo en la impresora
            gcode_param: Parámetro gcode (ej: "Metadata/plate_15.gcode")
            bed_type: Tipo de cama (auto, textured_plate, etc.)
            
        Returns:
            bool: True si el comando fue exitoso
        """
        try:
            logger.info(f"🎯 Iniciando impresión automática: {filename}")
            
            # ¡COMANDO MÁGICO EXITOSO!
            command = {
                "print": {
                    "command": "project_file",
                    "url": f"file:///sdcard/cache/{filename}",  # ¡URL MÁGICA!
                    "param": gcode_param,
                    "subtask_id": "0",
                    "use_ams": not self.enable_anti_sd,  # Anti-SD configuration
                    "timelapse": False,
                    "flow_cali": False,
                    "bed_leveling": True,
                    "layer_inspect": True,
                    "vibration_cali": False,
                    "bed_type": bed_type
                }
            }
            
            logger.info(f"✨ Enviando comando mágico...")
            logger.debug(f"📋 Comando: {json.dumps(command, indent=2)}")
            
            response = await self._send_mqtt_command(command)
            
            if response and response.get('print', {}).get('result') == 'success':
                logger.info(f"🎉 ¡IMPRESIÓN INICIADA AUTOMÁTICAMENTE!")
                return True
            else:
                logger.warning(f"⚠️ Comando enviado pero respuesta inesperada: {response}")
                return False
                
        except Exception as e:
            logger.error(f"❌ Error en comando de impresión: {e}")
            return False
    
    async def upload_and_print_auto(self, file_path, configure_ams_auto=True):
        """
        🚀 FUNCIONALIDAD ESTRELLA: Subir archivo e imprimir automáticamente
        
        Esta función combina toda la funcionalidad en una sola llamada:
        1. Analiza el archivo 3MF
        2. Configura AMS automáticamente (si está habilitado)
        3. Sube el archivo vía FTP
        4. Inicia la impresión automáticamente
        
        Args:
            file_path: Ruta del archivo 3MF local
            configure_ams_auto: Configurar AMS automáticamente según el 3MF
            
        Returns:
            bool: True si todo el proceso fue exitoso
        """
        try:
            logger.info(f"🚀 INICIANDO PROCESO COMPLETO: {file_path}")
            
            # 1. Analizar 3MF
            logger.info(f"📋 PASO 1: Analizando 3MF...")
            plate_number, gcode_path, bed_type, filament_info = self.analyze_3mf_file(file_path)
            
            # 2. Configurar AMS automáticamente
            if configure_ams_auto and filament_info:
                logger.info(f"🔧 PASO 2: Configurando AMS...")
                for filament_id, info in filament_info.items():
                    self.configure_ams(
                        ams_id=0,
                        tray_id=int(filament_id) if filament_id.isdigit() else 0,
                        filament_type=info.get('type', 'PLA'),
                        filament_color=info.get('color', '#000000FF')
                    )
                    await asyncio.sleep(1)  # Esperar entre comandos
            
            # 3. Subir archivo
            logger.info(f"📤 PASO 3: Subiendo archivo...")
            remote_filename = f"{os.path.splitext(os.path.basename(file_path))[0]}_auto.3mf"
            upload_success = await self.upload_file_ftp(file_path, remote_filename)
            
            if not upload_success:
                logger.error(f"❌ Error en subida de archivo")
                return False
            
            # 4. Esperar procesamiento
            logger.info(f"⏳ PASO 4: Esperando procesamiento...")
            await asyncio.sleep(3)
            
            # 5. Iniciar impresión automática
            logger.info(f"🖨️ PASO 5: Iniciando impresión automática...")
            print_success = await self.send_print_command_auto(remote_filename, gcode_path, bed_type)
            
            if print_success:
                logger.info(f"🎉 ¡PROCESO COMPLETO EXITOSO!")
                logger.info(f"✅ Archivo subido: {remote_filename}")
                logger.info(f"✅ Impresión iniciada automáticamente")
                logger.info(f"📱 Verificar progreso en la pantalla de la impresora")
                return True
            else:
                logger.error(f"❌ Error iniciando impresión")
                return False
                
        except Exception as e:
            logger.error(f"❌ Error en proceso completo: {e}")
            return False
    
    async def _send_mqtt_command(self, command):
        """
        Enviar comando MQTT y recibir respuesta
        Implementación optimizada con manejo de errores
        """
        try:
            import paho.mqtt.client as mqtt
            
            # Crear cliente MQTT
            client = mqtt.Client()
            client.tls_set_context(self.ssl_context)
            client.username_pw_set("bblp", self.access_code)
            
            # Variables para capturar respuesta
            response_received = asyncio.Event()
            last_response = None
            
            def on_connect(client, userdata, flags, rc):
                if rc == 0:
                    logger.debug(f"🔗 MQTT Conectado")
                    client.subscribe(f"device/{self.serial_number}/report")
                    
                    # Enviar comando
                    topic = f"device/{self.serial_number}/request"
                    payload = json.dumps(command)
                    result = client.publish(topic, payload)
                    logger.debug(f"📤 Comando enviado (rc={result.rc})")
                else:
                    logger.error(f"❌ Error conectando MQTT: {rc}")
            
            def on_message(client, userdata, msg):
                nonlocal last_response
                try:
                    data = json.loads(msg.payload.decode())
                    if 'print' in data:
                        last_response = data
                        logger.debug(f"📥 Respuesta: {json.dumps(data, indent=2)}")
                        response_received.set()
                except Exception as e:
                    logger.debug(f"Error procesando mensaje: {e}")
            
            client.on_connect = on_connect
            client.on_message = on_message
            
            # Conectar y esperar respuesta
            client.connect(self.ip_address, 8883, 60)
            client.loop_start()
            
            try:
                await asyncio.wait_for(response_received.wait(), timeout=15.0)
                return last_response
            except asyncio.TimeoutError:
                logger.warning(f"⏰ Timeout esperando respuesta MQTT")
                return None
            finally:
                client.loop_stop()
                client.disconnect()
                
        except Exception as e:
            logger.error(f"❌ Error MQTT: {e}")
            return None
    
    # Métodos de compatibilidad con versiones anteriores
    def upload_file(self, file_path, remote_filename=None):
        """Método de compatibilidad - usar upload_file_ftp"""
        return asyncio.run(self.upload_file_ftp(file_path, remote_filename))
    
    def send_gcode(self, gcode_commands):
        """Enviar comandos G-code a la impresora"""
        if isinstance(gcode_commands, list):
            gcode_commands = '\n'.join(gcode_commands)
        
        command = {
            "print": {
                "sequence_id": "0",
                "command": "gcode_line",
                "param": gcode_commands
            }
        }
        
        return asyncio.run(self._send_mqtt_command(command))
    
    def get_status(self):
        """Obtener estado actual de la impresora"""
        command = {
            "pushing": {
                "sequence_id": "0",
                "command": "pushall"
            }
        }
        
        return asyncio.run(self._send_mqtt_command(command))
    
    def stop_print(self):
        """Detener impresión actual"""
        command = {
            "print": {
                "sequence_id": "0",
                "command": "stop"
            }
        }
        
        return asyncio.run(self._send_mqtt_command(command))
    
    def pause_print(self):
        """Pausar impresión actual"""
        command = {
            "print": {
                "sequence_id": "0",
                "command": "pause"
            }
        }
        
        return asyncio.run(self._send_mqtt_command(command))
    
    def resume_print(self):
        """Reanudar impresión pausada"""
        command = {
            "print": {
                "sequence_id": "0",
                "command": "resume"
            }
        }
        
        return asyncio.run(self._send_mqtt_command(command))

# Funciones de utilidad exportadas
def create_bambu_connection(ip_address, access_code, serial_number, **kwargs):
    """
    Función de conveniencia para crear conexión BambuLuc
    
    Args:
        ip_address: IP de la impresora
        access_code: Código de acceso  
        serial_number: Número de serie
        **kwargs: Argumentos adicionales para BambuLuc
        
    Returns:
        BambuLuc: Instancia configurada
    """
    return BambuLuc(ip_address, access_code, serial_number, **kwargs)

async def quick_print(ip_address, access_code, serial_number, file_path):
    """
    Función rápida para imprimir un archivo 3MF
    
    Args:
        ip_address: IP de la impresora
        access_code: Código de acceso
        serial_number: Número de serie  
        file_path: Ruta del archivo 3MF
        
    Returns:
        bool: True si la impresión se inició exitosamente
    """
    bambu = BambuLuc(ip_address, access_code, serial_number)
    return await bambu.upload_and_print_auto(file_path)

# Información de la versión
__version__ = "1.2.0"
__author__ = "Angel Luis Barbero Guerras"
__description__ = "Librería avanzada para control de impresoras Bambu Lab con impresión automática"

# Exportar API pública
__all__ = [
    'BambuLuc',
    'create_bambu_connection',
    'quick_print',
    '__version__'
]

if __name__ == "__main__":
    # Ejemplo de uso
    print(f"🚀 BambuLuc v{__version__}")
    print(f"📖 {__description__}")
    print(f"👨‍💻 {__author__}")
    print()
    print("Ejemplo de uso:")
    print("bambu = BambuLuc('192.168.20.76', '93481864', '03919C430101971')")
    print("await bambu.upload_and_print_auto('archivo.3mf')  # ¡Imprime automáticamente!")
