"""
x7_update_CAx.py : Script que actualiza un archivo AML original basándose en las modificaciones 
hechas en el archivo Excel de IOs generado por x2_process_CAx.py

Pipeline:
1. Lee el AML original
2. Lee el Excel modificado 
3. Genera Excel de referencia desde el AML para comparar
4. Valida que la estructura base sea la misma (mismos nodos, nombres)
5. Identifica cambios en IOs, direcciones IP, etc.
6. Aplica los cambios al AML original
7. Genera un nuevo archivo AML con sufijo "_updated"
"""

import os
import sys
import tkinter as tk
from tkinter import filedialog, messagebox
import traceback
from lxml import etree as ET
import pandas as pd
from pathlib import Path
import re
import math
import tempfile

script_root = os.path.dirname(
    os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration

# Import functions from x2_process_CAx
from x2_process_CAx import (
    extract_aml_data, 
    generate_io_excel_report,
    sanitize_filename
)


def select_aml_file(title="Select Original AML File", initial_dir=None):
    """Abre un diálogo para seleccionar un archivo AML."""
    root = tk.Tk()
    root.withdraw()
    file_path = filedialog.askopenfilename(
        title=title,
        filetypes=[("AML Files", "*.aml"), ("All Files", "*.*")],
        initialdir=initial_dir
    )
    root.destroy()
    if not file_path:
        return None
    return file_path


def select_excel_file(title="Select Modified Excel File", initial_dir=None):
    """Abre un diálogo para seleccionar un archivo Excel."""
    root = tk.Tk()
    root.withdraw()
    file_path = filedialog.askopenfilename(
        title=title,
        filetypes=[("Excel Files", "*.xlsx"), ("Excel Files", "*.xls"), ("All Files", "*.*")],
        initialdir=initial_dir
    )
    root.destroy()
    if not file_path:
        return None
    return file_path


def generate_reference_excel_from_aml(aml_file_path, temp_dir):
    """
    Genera un Excel de referencia desde el AML para comparar con el Excel modificado.
    Retorna el path al Excel generado y los project_data.
    """
    print("Generando Excel de referencia desde AML original...")
    
    # Extraer datos del AML
    try:
        parser = ET.XMLParser(remove_blank_text=True, huge_tree=True)
        tree = ET.parse(aml_file_path, parser)
        root = tree.getroot()
        project_data = extract_aml_data(root)
    except Exception as e:
        print(f"ERROR procesando archivo AML {aml_file_path}: {e}")
        return None, None
    
    if not project_data or not project_data.get("plcs"):
        print("No se encontraron PLCs en el archivo AML.")
        return None, None
    
    # Generar Excel para cada PLC y combinar en uno solo
    all_excel_rows = []
    
    for plc_id, plc_data in project_data.get("plcs", {}).items():
        # Crear archivo temporal para este PLC
        temp_excel_path = os.path.join(temp_dir, f"temp_plc_{plc_id}.xlsx")
        
        # Generar Excel para este PLC (reutilizamos la función existente)
        generate_io_excel_report(project_data, temp_excel_path, plc_id, temp_dir)
        
        # Leer el Excel generado y agregar a la lista combinada
        if os.path.exists(temp_excel_path):
            try:
                df_plc = pd.read_excel(temp_excel_path, sheet_name='IO Report')
                # Agregar columna de ID único
                df_plc['Unique_ID'] = df_plc['PLC Name'] + "+" + df_plc['Device Name']
                all_excel_rows.append(df_plc)
                os.remove(temp_excel_path)  # Limpiar archivo temporal
            except Exception as e:
                print(f"ERROR leyendo Excel temporal para PLC {plc_id}: {e}")
    
    if not all_excel_rows:
        print("No se pudieron generar datos de Excel de referencia.")
        return None, None
    
    # Combinar todos los DataFrames
    combined_df = pd.concat(all_excel_rows, ignore_index=True)
    
    # Guardar Excel de referencia
    reference_excel_path = os.path.join(temp_dir, "reference_excel.xlsx")
    combined_df.to_excel(reference_excel_path, sheet_name='IO Report', index=False)
    
    return reference_excel_path, project_data


def compare_excel_files(reference_excel_path, modified_excel_path):
    """
    Compara el Excel de referencia con el Excel modificado.
    Retorna (is_valid, changes_dict) donde:
    - is_valid: True si la estructura básica es la misma
    - changes_dict: diccionario con los cambios detectados
    """
    print("Comparando archivos Excel...")
    
    try:
        # Leer ambos archivos
        df_ref = pd.read_excel(reference_excel_path, sheet_name='IO Report')
        df_mod = pd.read_excel(modified_excel_path, sheet_name='IO Report')
        
        # Agregar columna de ID único si no existe
        if 'Unique_ID' not in df_ref.columns:
            df_ref['Unique_ID'] = df_ref['PLC Name'] + "+" + df_ref['Device Name']
        if 'Unique_ID' not in df_mod.columns:
            df_mod['Unique_ID'] = df_mod['PLC Name'] + "+" + df_mod['Device Name']
        
    except Exception as e:
        print(f"ERROR leyendo archivos Excel: {e}")
        return False, {}
    
    # Validar estructura básica
    if len(df_ref) != len(df_mod):
        print(f"ERROR: Número de filas diferente. Referencia: {len(df_ref)}, Modificado: {len(df_mod)}")
        return False, {}
    
    # Verificar que todos los IDs únicos coincidan
    ref_ids = set(df_ref['Unique_ID'].tolist())
    mod_ids = set(df_mod['Unique_ID'].tolist())
    
    if ref_ids != mod_ids:
        missing_in_mod = ref_ids - mod_ids
        extra_in_mod = mod_ids - ref_ids
        print("ERROR: Los IDs únicos no coinciden entre archivos.")
        if missing_in_mod:
            print(f"  Faltantes en modificado: {missing_in_mod}")
        if extra_in_mod:
            print(f"  Extras en modificado: {extra_in_mod}")
        return False, {}
    
    print("Estructura básica validada correctamente.")
    
    # Detectar cambios
    changes = {}
    columns_to_monitor = [
        'Device Address', 'IO Input Start Address', 'IO Input Word Count', 
        'IO Output Start Address', 'IO Output Word Count',
        'Network Type', 'Device Type', 'Order Number', 'Firmware Version'
    ]
    
    for index, row_ref in df_ref.iterrows():
        unique_id = row_ref['Unique_ID']
        row_mod = df_mod[df_mod['Unique_ID'] == unique_id].iloc[0]
        
        row_changes = {}
        for col in columns_to_monitor:
            if col in df_ref.columns and col in df_mod.columns:
                # Manejo especial para valores numéricos
                ref_raw = row_ref[col]
                mod_raw = row_mod[col]
                
                # Convertir a string, manejando floats apropiadamente
                if pd.notna(ref_raw):
                    if isinstance(ref_raw, float) and ref_raw.is_integer():
                        ref_val = str(int(ref_raw))
                    else:
                        ref_val = str(ref_raw).strip()
                else:
                    ref_val = 'N/A'
                
                if pd.notna(mod_raw):
                    if isinstance(mod_raw, float) and mod_raw.is_integer():
                        mod_val = str(int(mod_raw))
                    else:
                        mod_val = str(mod_raw).strip()
                else:
                    mod_val = 'N/A'
                
                if ref_val != mod_val:
                    row_changes[col] = {
                        'original': ref_val,
                        'modified': mod_val
                    }
        
        if row_changes:
            changes[unique_id] = {
                'plc_name': row_ref['PLC Name'],
                'device_name': row_ref['Device Name'],
                'changes': row_changes
            }
    
    print(f"Detectados {len(changes)} dispositivos con cambios.")
    for unique_id, change_info in changes.items():
        print(f"  {unique_id}: {list(change_info['changes'].keys())}")
        # Debug: mostrar los primeros cambios para verificar valores
        if len(changes) <= 5:  # Solo mostrar detalles si son pocos cambios
            for field, change_data in change_info['changes'].items():
                print(f"    {field}: {change_data['original']} → {change_data['modified']}")
    
    return True, changes


def find_device_in_aml(project_data, plc_name, device_name):
    """
    Encuentra el device_id correspondiente en los datos del AML basándose en PLC y device name.
    """
    # Buscar el PLC por nombre
    target_plc_id = None
    for plc_id, plc_data in project_data.get("plcs", {}).items():
        if plc_data.get('name', plc_id) == plc_name:
            target_plc_id = plc_id
            break
    
    if not target_plc_id:
        return None
    
    # Buscar el dispositivo en las redes del PLC
    plc_info = project_data["plcs"][target_plc_id]
    plc_networks = plc_info.get("connected_networks", {})
    
    for net_id, plc_addr_on_net in plc_networks.items():
        net_info = project_data.get("networks", {}).get(net_id)
        if not net_info:
            continue
        
        devices_on_net = net_info.get("devices_on_net", {})
        
        # Identificar nodos que pertenecen al PLC
        plc_interface_and_node_ids = set()
        for node in plc_info.get("network_nodes", []):
            plc_interface_and_node_ids.add(node["id"])
            interface_id_lookup = project_data["devices"].get(node["id"], {}).get("parent_id")
            if interface_id_lookup:
                plc_interface_and_node_ids.add(interface_id_lookup)
        plc_interface_and_node_ids.add(target_plc_id)
        
        # Buscar en dispositivos de la red
        for node_id, node_addr in devices_on_net.items():
            if node_id in plc_interface_and_node_ids:
                continue
            
            node_info = project_data.get("devices", {}).get(node_id)
            if not node_info:
                continue
            
            # Determinar información del dispositivo
            interface_id = node_info.get("parent_id")
            interface_info = None
            actual_device_id = None
            actual_device_info = None
            
            if interface_id:
                interface_info = project_data.get("devices", {}).get(interface_id)
                if interface_info:
                    actual_device_id = interface_info.get("parent_id")
                    if actual_device_id:
                        actual_device_info = project_data.get("devices", {}).get(actual_device_id)
            
            display_info = actual_device_info if actual_device_info else (interface_info if interface_info else node_info)
            display_name = display_info.get("name", "Unknown")
            
            if display_name == device_name:
                return {
                    'node_id': node_id,
                    'display_id': actual_device_id if actual_device_info else (interface_id if interface_info else node_id),
                    'node_info': node_info,
                    'display_info': display_info,
                    'network_id': net_id
                }
    
    return None


def apply_changes_to_aml(aml_tree, project_data, changes_dict):
    """
    Aplica los cambios detectados al árbol XML del AML.
    """
    print("Aplicando cambios al archivo AML...")
    
    root = aml_tree.getroot()
    changes_applied = 0
    
    for unique_id, change_info in changes_dict.items():
        plc_name = change_info['plc_name']
        device_name = change_info['device_name']
        changes = change_info['changes']
        
        print(f"  Procesando cambios para: {unique_id}")
        
        # Encontrar el dispositivo en los datos del AML
        device_info = find_device_in_aml(project_data, plc_name, device_name)
        if not device_info:
            print(f"    ERROR: No se pudo encontrar el dispositivo '{device_name}' del PLC '{plc_name}' en el AML")
            continue
        
        print(f"    Debug: Encontrado dispositivo - node_id: {device_info['node_id']}, display_id: {device_info['display_id']}")
        
        # Encontrar el elemento XML correspondiente
        node_id = device_info['node_id']
        display_id = device_info['display_id']
        
        # Intentar primero con el display_id (el dispositivo real que contiene IOs)
        xml_element = root.xpath(f".//*[@ID='{display_id}']")
        
        if not xml_element:
            # Si no se encuentra, intentar con el node_id
            xml_element = root.xpath(f".//*[@ID='{node_id}']")
            
        if not xml_element:
            print(f"    ERROR: No se pudo encontrar el elemento XML con ID {display_id} o {node_id}")
            continue
        
        device_element = xml_element[0]
        print(f"    Debug: Usando elemento XML con ID {device_element.get('ID', 'N/A')}")
        
        # Aplicar cambios específicos
        for field, change_data in changes.items():
            original_val = change_data['original']
            modified_val = change_data['modified']
            
            if field == 'Device Address':
                # Cambiar dirección de red del dispositivo
                if apply_network_address_change(device_element, modified_val):
                    print(f"    ✓ Dirección de red actualizada: {original_val} -> {modified_val}")
                    changes_applied += 1
                else:
                    print(f"    ✗ Error actualizando dirección de red")
            
            elif field in ['IO Input Start Address', 'IO Input Word Count', 'IO Output Start Address', 'IO Output Word Count']:
                # Cambiar direcciones IO específicas
                if apply_io_address_change(device_element, field, original_val, modified_val, project_data, display_id):
                    print(f"    ✓ {field} actualizada: {original_val} -> {modified_val}")
                    changes_applied += 1
                else:
                    print(f"    ✗ Error actualizando {field}")
            
            elif field in ['Device Type', 'Order Number', 'Firmware Version']:
                # Cambiar atributos del dispositivo
                if apply_device_attribute_change(device_element, field, modified_val):
                    print(f"    ✓ {field} actualizado: {original_val} -> {modified_val}")
                    changes_applied += 1
                else:
                    print(f"    ✗ Error actualizando {field}")
    
    print(f"Total de cambios aplicados: {changes_applied}")
    return changes_applied > 0


def apply_network_address_change(device_element, new_address):
    """
    Aplica cambio de dirección de red a un elemento del dispositivo.
    """
    try:
        # Convertir new_address a string si es numérico
        if isinstance(new_address, (int, float)):
            address_str = str(new_address).rstrip('0').rstrip('.')
        else:
            address_str = str(new_address)
        
        # Buscar atributo NetworkAddress
        addr_attr = device_element.xpath("./*[local-name()='Attribute'][@Name='NetworkAddress']")
        if addr_attr:
            value_elem = addr_attr[0].xpath("./*[local-name()='Value']")
            if value_elem:
                value_elem[0].text = address_str
                return True
        
        # Si no existe, crear el atributo
        attr_elem = ET.SubElement(device_element, "Attribute")
        attr_elem.set("Name", "NetworkAddress")
        value_elem = ET.SubElement(attr_elem, "Value")
        value_elem.text = address_str
        return True
        
    except Exception as e:
        print(f"    Error aplicando cambio de dirección: {e}")
        return False


def apply_device_attribute_change(device_element, field, new_value):
    """
    Aplica cambio de atributo del dispositivo.
    """
    try:
        # Mapear campos a nombres de atributos XML
        attr_mapping = {
            'Device Type': 'TypeName',
            'Order Number': 'OrderNumber',
            'Firmware Version': 'FirmwareVersion'
        }
        
        attr_name = attr_mapping.get(field)
        if not attr_name:
            return False
        
        # Buscar atributo existente
        attr_elem = device_element.xpath(f"./*[local-name()='Attribute'][@Name='{attr_name}']")
        if attr_elem:
            value_elem = attr_elem[0].xpath("./*[local-name()='Value']")
            if value_elem:
                value_elem[0].text = new_value
                return True
        
        # Si no existe, crear el atributo
        attr_elem = ET.SubElement(device_element, "Attribute")
        attr_elem.set("Name", attr_name)
        value_elem = ET.SubElement(attr_elem, "Value")
        value_elem.text = new_value
        return True
        
    except Exception as e:
        print(f"    Error aplicando cambio de atributo {field}: {e}")
        return False


def apply_io_address_change(device_element, field, original_val, modified_val, project_data, device_id):
    """
    Aplica cambios a las direcciones IO usando Start Address + Word Count.
    Esto es más simple y directo que calcular End Addresses.
    """
    try:
        # Validar que el nuevo valor sea numérico o N/A
        if modified_val != 'N/A' and modified_val != '':
            try:
                # Manejar tanto floats como integers (pandas puede leer como float)
                if isinstance(modified_val, str):
                    # Si es string, intentar convertir
                    new_value = int(float(modified_val))
                else:
                    # Si ya es numérico, convertir directamente
                    new_value = int(modified_val)
            except (ValueError, TypeError):
                print(f"    Error: Valor no válido: {modified_val} (tipo: {type(modified_val)})")
                return False
        else:
            # Si es N/A, no hay dirección IO para este tipo
            return True
        
        # Determinar tipo de IO y si es start address o word count
        is_input = "Input" in field
        is_start_address = "Start Address" in field
        is_word_count = "Word Count" in field
        io_type = "Input" if is_input else "Output"
        
        # Buscar la estructura IO en el dispositivo - con múltiples estrategias
        print(f"    Debug: Buscando elementos Address en dispositivo {device_element.get('Name', 'N/A')}")
        
        # Estrategia 1: Buscar Address directamente en el elemento
        address_elements = device_element.xpath("./*[local-name()='Attribute'][@Name='Address']")
        
        if not address_elements:
            # Estrategia 2: Buscar en elementos hijos
            print(f"    Debug: No se encontró Address directo, buscando en elementos hijos...")
            address_elements = device_element.xpath(".//*[local-name()='Attribute'][@Name='Address']")
        
        if not address_elements:
            # Estrategia 3: Buscar en elementos padre (tal vez los IOs están en el parent)
            print(f"    Debug: No se encontró Address en hijos, buscando en elementos padre...")
            parent_elements = device_element.xpath("parent::*")
            if parent_elements:
                address_elements = parent_elements[0].xpath(".//*[local-name()='Attribute'][@Name='Address']")
        
        if not address_elements:
            print(f"    ERROR: No se encontró elemento Address en el dispositivo o sus alrededores")
            print(f"    Debug: Atributos disponibles en este elemento:")
            for attr in device_element.xpath("./*[local-name()='Attribute']"):
                attr_name = attr.get('Name', 'N/A')
                print(f"      - {attr_name}")
            return False
        
        print(f"    Debug: Encontrados {len(address_elements)} elemento(s) Address")
        
        # Buscar dentro del Address el sub-atributo correcto
        target_io_element = None
        
        # Buscar en todos los elementos Address encontrados
        for address_element in address_elements:
            print(f"    Debug: Explorando Address element...")
            io_subelements = address_element.xpath(f"./*[local-name()='Attribute']")
            
            print(f"    Debug: Encontrados {len(io_subelements)} sub-elementos en Address")
            
            for io_sub in io_subelements:
                sub_name = io_sub.get('Name', 'N/A')
                print(f"    Debug: Revisando sub-elemento: {sub_name}")
                
                # Verificar si es el tipo correcto (Input/Output)
                type_val = io_sub.xpath("./*[local-name()='Attribute'][@Name='IoType']/*[local-name()='Value']/text()")
                if type_val:
                    found_type = type_val[0]
                    print(f"    Debug: Encontrado IoType: {found_type}")
                    if found_type.lower() == io_type.lower():
                        target_io_element = io_sub
                        print(f"    Debug: ¡Encontrado elemento {io_type} compatible!")
                        break
                else:
                    # También revisar si el nombre del sub-elemento indica el tipo
                    if io_type.lower() in sub_name.lower():
                        print(f"    Debug: Sub-elemento {sub_name} parece ser de tipo {io_type}")
                        target_io_element = io_sub
                        break
            
            if target_io_element:
                break
        
        if not target_io_element:
            print(f"    ERROR: No se encontró elemento {io_type} en ningún Address")
            print(f"    Debug: Elementos Address disponibles:")
            for addr_elem in address_elements:
                sub_attrs = addr_elem.xpath(f"./*[local-name()='Attribute']")
                for sub in sub_attrs:
                    sub_name = sub.get('Name', 'N/A')
                    type_vals = sub.xpath("./*[local-name()='Attribute'][@Name='IoType']/*[local-name()='Value']/text()")
                    type_info = type_vals[0] if type_vals else "No IoType"
                    print(f"      - {sub_name} (IoType: {type_info})")
            return False
        
        if is_start_address:
            # Actualizar StartAddress
            start_attr = target_io_element.xpath("./*[local-name()='Attribute'][@Name='StartAddress']")
            if start_attr:
                value_elem = start_attr[0].xpath("./*[local-name()='Value']")
                if value_elem:
                    value_elem[0].text = str(new_value)
                    return True
            else:
                # Crear StartAddress si no existe
                start_attr_elem = ET.SubElement(target_io_element, "Attribute")
                start_attr_elem.set("Name", "StartAddress")
                value_elem = ET.SubElement(start_attr_elem, "Value")
                value_elem.text = str(new_value)
                return True
        
        elif is_word_count:
            # Actualizar Length basándose en Word Count
            # Convertir words a bits (1 word = 16 bits)
            length_bits = new_value * 16
            
            # Actualizar Length
            length_attr = target_io_element.xpath("./*[local-name()='Attribute'][@Name='Length']")
            if length_attr:
                value_elem = length_attr[0].xpath("./*[local-name()='Value']")
                if value_elem:
                    value_elem[0].text = str(length_bits)
                    return True
            else:
                # Crear Length si no existe
                length_attr_elem = ET.SubElement(target_io_element, "Attribute")
                length_attr_elem.set("Name", "Length")
                value_elem = ET.SubElement(length_attr_elem, "Value")
                value_elem.text = str(length_bits)
                return True
        
        return False
        
    except Exception as e:
        print(f"    Error aplicando cambio de dirección IO {field}: {e}")
        traceback.print_exc()
        return False


def save_updated_aml(aml_tree, original_aml_path):
    """
    Guarda el árbol XML modificado como un nuevo archivo AML con sufijo "_updated".
    """
    original_path = Path(original_aml_path)
    updated_path = original_path.parent / f"{original_path.stem}_updated{original_path.suffix}"
    
    try:
        # Escribir el XML actualizado
        aml_tree.write(
            str(updated_path), 
            pretty_print=True, 
            xml_declaration=True, 
            encoding='utf-8'
        )
        print(f"Archivo AML actualizado guardado en: {updated_path}")
        return str(updated_path)
        
    except Exception as e:
        print(f"ERROR guardando archivo AML actualizado: {e}")
        return None


def main():
    """Función principal del script."""
    try:
        configs = load_configuration()
        working_directory = configs.get("working_directory")
    except Exception as e:
        print(f"Warning: No se pudo cargar configuración: {e}")
        configs = {}
        working_directory = None

    script_version = "v1.4 - Enhanced Address Element Search with Debug"
    print(f"--- Actualizador de AML desde Excel Modificado ({script_version}) ---")

    # Validar directorio de trabajo
    if not working_directory or not os.path.isdir(working_directory):
        print("Directorio de trabajo no configurado. Usando directorio actual.")
        working_directory = os.getcwd()

    print(f"Directorio de trabajo: {working_directory}")

    # 1. Seleccionar archivo AML original
    print("\n1. Seleccione el archivo AML original:")
    aml_file_path = select_aml_file(initial_dir=working_directory)
    if not aml_file_path:
        print("No se seleccionó archivo AML. Saliendo.")
        return

    # 2. Seleccionar archivo Excel modificado
    print("\n2. Seleccione el archivo Excel modificado:")
    excel_file_path = select_excel_file(initial_dir=working_directory)
    if not excel_file_path:
        print("No se seleccionó archivo Excel. Saliendo.")
        return

    print(f"\nArchivo AML original: {aml_file_path}")
    print(f"Archivo Excel modificado: {excel_file_path}")

    # 3. Crear directorio temporal para archivos intermedios
    with tempfile.TemporaryDirectory() as temp_dir:
        print(f"\nUsando directorio temporal: {temp_dir}")

        # 4. Generar Excel de referencia desde AML
        reference_excel_path, project_data = generate_reference_excel_from_aml(aml_file_path, temp_dir)
        if not reference_excel_path or not project_data:
            print("ERROR: No se pudo generar Excel de referencia.")
            return

        # 5. Comparar archivos Excel
        is_valid, changes_dict = compare_excel_files(reference_excel_path, excel_file_path)
        if not is_valid:
            print("ERROR: El archivo Excel modificado no es compatible con el AML original.")
            return

        if not changes_dict:
            print("No se detectaron cambios en el Excel. No hay nada que actualizar.")
            return

        # 6. Cargar y parsear AML original
        print("\nCargando archivo AML original...")
        try:
            parser = ET.XMLParser(remove_blank_text=True, huge_tree=True)
            aml_tree = ET.parse(aml_file_path, parser)
        except Exception as e:
            print(f"ERROR parseando archivo AML: {e}")
            return

        # 7. Aplicar cambios al AML
        success = apply_changes_to_aml(aml_tree, project_data, changes_dict)
        if not success:
            print("No se pudieron aplicar cambios al AML.")
            return

        # 8. Guardar AML actualizado
        updated_aml_path = save_updated_aml(aml_tree, aml_file_path)
        if updated_aml_path:
            print(f"\n¡Proceso completado exitosamente!")
            print(f"Archivo AML actualizado: {updated_aml_path}")
        else:
            print("ERROR: No se pudo guardar el archivo AML actualizado.")


if __name__ == "__main__":
    main() 