"""
export_cross_references_from_tia : Script para exportar las referencias cruzadas
de un proyecto TIA Portal a archivos (probablemente XML).
"""

import tkinter as tk
from tkinter import filedialog
import os
import sys
import traceback
import time
from pathlib import Path

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

# --- Configuration ---
# Supported TIA Portal versions mapping (extension -> version)
SUPPORTED_TIA_VERSIONS = {".ap18": "18.0", ".ap19": "19.0", ".ap20": "20.0"}

# Filter for cross-references. Based on documentation:
# 1: 'AllObjects', 2: 'ObjectsWithReferences', 3: 'ObjectsWithoutReferences', 4: 'UnusedObjects'
# Using 1 to export all. 0 might also work as a default in some API versions.
CROSS_REF_FILTER = 1

MAX_REOPEN_ATTEMPTS = (
    5  # Número máximo de re-aperturas permitidas para evitar bucles infinitos
)
BLOCK_TIMEOUT_SECONDS = 120  # Referencia de tiempo esperado para el procesamiento de cada bloque (para logging)


class PortalDisposedException(Exception):
    """Excepción lanzada cuando TIA Portal se ha cerrado inesperadamente o un objeto ha sido descartado."""

    def __init__(self, original_exception, failed_block: str | None = None):
        super().__init__(str(original_exception))
        self.failed_block = failed_block


def _is_disposed_exception(exc: Exception) -> bool:
    """Devuelve True si la excepción proviene de un objeto/portal ya cerrado o sin mensaje útil."""
    msg = str(exc).lower()
    return any(
        indicator in msg
        for indicator in (
            "disposed object",
            "tia portal has either been disposed",
            "unexpected exception - no exception message available",
        )
    )


# --- TIA Scripting Import Handling ---
if os.getenv("TIA_SCRIPTING"):
    sys.path.append(os.getenv("TIA_SCRIPTING"))
else:
    pass

try:
    import siemens_tia_scripting as ts
except ImportError:
    print("ERROR: Error al importar 'siemens_tia_scripting'.")
    print("Asegúrese de que:")
    print("1. TIA Portal Openness está instalado.")
    print(
        "2. El módulo 'siemens_tia_scripting' de Python está instalado (pip install ...) o"
    )
    print(
        "   la ruta a sus binarios está configurada en la variable de entorno 'TIA_SCRIPTING'."
    )
    print(
        "3. Está usando una versión compatible de Python (ej. 3.12.X según la documentación)."
    )
    sys.exit(1)
except Exception as e:
    print(f"Ocurrió un error inesperado durante la importación: {e}")
    traceback.print_exc()
    sys.exit(1)

# --- Functions ---


def get_supported_filetypes():
    """Returns the supported file types for TIA Portal projects."""
    filetypes = []
    for ext, version in SUPPORTED_TIA_VERSIONS.items():
        version_major = version.split(".")[0]
        filetypes.append((f"TIA Portal V{version_major} Projects", f"*{ext}"))

    # Add option to show all supported files
    all_extensions = " ".join([f"*{ext}" for ext in SUPPORTED_TIA_VERSIONS.keys()])
    filetypes.insert(0, ("All TIA Portal Projects", all_extensions))

    return filetypes


def detect_tia_version(project_file_path):
    """Detects TIA Portal version based on file extension."""
    file_path = Path(project_file_path)
    file_extension = file_path.suffix.lower()

    if file_extension in SUPPORTED_TIA_VERSIONS:
        detected_version = SUPPORTED_TIA_VERSIONS[file_extension]
        print(
            f"Versión de TIA Portal detectada: {detected_version} (de la extensión {file_extension})"
        )
        return detected_version
    else:
        print(
            f"ADVERTENCIA: Extensión de archivo no reconocida '{file_extension}'. Extensiones soportadas: {list(SUPPORTED_TIA_VERSIONS.keys())}"
        )
        # Default to version 18.0 for backward compatibility
        print("Usando por defecto TIA Portal V18.0")
        return "18.0"


def select_project_file():
    """Opens a dialog to select a TIA Portal project file."""
    root = tk.Tk()
    root.withdraw()
    file_path = filedialog.askopenfilename(
        title="Seleccionar archivo de proyecto TIA Portal",
        filetypes=get_supported_filetypes(),
    )
    root.destroy()
    if not file_path:
        print("No se seleccionó ningún archivo de proyecto. Saliendo.")
        sys.exit(0)
    return file_path


def _normalize_name(name: str) -> str:
    """Normaliza un nombre quitando espacios laterales y convirtiendo a minúsculas."""
    return name.strip().lower()


def _export_block_with_timeout(
    block, blocks_cr_path, block_name, timeout_seconds=BLOCK_TIMEOUT_SECONDS
):
    """
    Exporta las referencias cruzadas de un bloque con monitoreo de tiempo.

    Note: TIA Portal Openness no permite operaciones multi-hilo, por lo que
    implementamos un timeout conceptual que al menos registra cuánto tiempo toma.

    Returns:
        bool: True si se exportó exitosamente
    """
    start_time = time.time()

    try:
        # Realizar la exportación de forma directa (sin hilos debido a restricciones de TIA)
        block.export_cross_references(
            target_directorypath=str(blocks_cr_path),
            filter=CROSS_REF_FILTER,
        )

        elapsed_time = time.time() - start_time

        # Verificar si excedió el tiempo esperado (aunque ya terminó)
        if elapsed_time > timeout_seconds:
            print(
                f"      ADVERTENCIA: El bloque tardó {elapsed_time:.2f}s (>{timeout_seconds}s esperado)"
            )

        return True

    except Exception as e:
        elapsed_time = time.time() - start_time
        print(f"      Tiempo transcurrido antes del error: {elapsed_time:.2f} segundos")
        raise e


def export_plc_cross_references(
    plc, export_base_dir, exported_blocks=None, problematic_blocks=None
):
    """Exports cross-references for various elements from a given PLC.
    Parámetros
    ----------
    plc : objeto PLC actual
    export_base_dir : pathlib.Path directorio base para exportar
    exported_blocks : set[str]  bloques que ya se han exportado
    problematic_blocks : set[str]  bloques que se deben omitir tras fallos previos
    """
    if exported_blocks is None:
        exported_blocks = set()
    if problematic_blocks is None:
        problematic_blocks = set()

    plc_name = plc.get_name()
    print(f"\n--- Procesando PLC: {plc_name} ---")

    # Define base export path for this PLC's cross-references
    plc_export_dir = export_base_dir / plc_name
    plc_export_dir.mkdir(parents=True, exist_ok=True)

    # --- Export Program Block Cross-References ---
    blocks_cr_exported = 0
    blocks_cr_skipped = 0
    current_block_name = None  # Track current block being processed
    print(
        f"\n[PLC: {plc_name}] Exportando referencias cruzadas de bloques de programa..."
    )
    blocks_cr_path = plc_export_dir / "ProgramBlocks_CR"
    blocks_cr_path.mkdir(exist_ok=True)
    print(f"  Destino: {blocks_cr_path}")

    try:
        program_blocks = plc.get_program_blocks()
        print(f"  Se encontraron {len(program_blocks)} bloques de programa.")

        # Show which blocks will be skipped from the start
        if problematic_blocks:
            skipped_names = []
            for block in program_blocks:
                if _normalize_name(block.get_name()) in problematic_blocks:
                    skipped_names.append(block.get_name())
            if skipped_names:
                print(
                    f"  Bloques que serán omitidos (problemáticos previos): {', '.join(skipped_names)}"
                )

        for block in program_blocks:
            block_name = block.get_name()
            current_block_name = block_name  # Update current block being processed
            norm_block = _normalize_name(block_name)
            if norm_block in problematic_blocks:
                print(
                    f"    Omitiendo bloque problemático previamente detectado: {block_name}"
                )
                blocks_cr_skipped += 1
                continue
            if norm_block in exported_blocks:
                # Ya exportado en un intento anterior, no repetir
                print(f"    Omitiendo bloque ya exportado: {block_name}")
                continue
            print(f"    Procesando bloque: {block_name}...")
            try:
                print(f"      Exportando referencias cruzadas para {block_name}...")
                start_time = time.time()

                # Usar la función con monitoreo de tiempo
                _export_block_with_timeout(block, blocks_cr_path, block_name)

                elapsed_time = time.time() - start_time
                print(f"      Exportación completada en {elapsed_time:.2f} segundos")
                blocks_cr_exported += 1
                exported_blocks.add(norm_block)
            except RuntimeError as block_ex:
                print(
                    f"      ERROR TIA al exportar referencias cruzadas para el bloque {block_name}: {block_ex}"
                )
                problematic_blocks.add(norm_block)
                blocks_cr_skipped += 1
            except Exception as block_ex:
                print(
                    f"      ERROR GENERAL al exportar referencias cruzadas para el bloque {block_name}: {block_ex}"
                )
                traceback.print_exc()
                problematic_blocks.add(norm_block)  # Always mark as problematic
                blocks_cr_skipped += 1
                if _is_disposed_exception(block_ex):
                    # Escalamos para que el script pueda re-abrir el Portal y omitir el bloque
                    raise PortalDisposedException(block_ex, failed_block=block_name)
        print(
            f"  Resumen de exportación de referencias cruzadas de bloques: Exportados={blocks_cr_exported}, Omitidos/Errores={blocks_cr_skipped}"
        )
    except AttributeError:
        print(
            "  Error de atributo: No se pudo encontrar 'get_program_blocks' en el objeto PLC. Omitiendo bloques de programa."
        )
    except Exception as e:
        print(
            f"  ERROR al acceder a los bloques de programa para exportar referencias cruzadas: {e}"
        )
        traceback.print_exc()
        # If we know which block was being processed, mark it as problematic
        if current_block_name:
            problematic_blocks.add(_normalize_name(current_block_name))
            raise PortalDisposedException(e, failed_block=current_block_name)
        else:
            raise PortalDisposedException(e)

    # --- Export PLC Tag Table Cross-References ---
    tags_cr_exported = 0
    tags_cr_skipped = 0
    print(
        f"\n[PLC: {plc_name}] Exportando referencias cruzadas de tablas de variables..."
    )
    tags_cr_path = plc_export_dir / "PlcTags_CR"
    tags_cr_path.mkdir(exist_ok=True)
    print(f"  Destino: {tags_cr_path}")

    try:
        tag_tables = plc.get_plc_tag_tables()
        print(f"  Se encontraron {len(tag_tables)} tablas de variables.")
        for table in tag_tables:
            table_name = table.get_name()
            print(f"    Procesando tabla de variables: {table_name}...")
            try:
                print(f"      Exportando referencias cruzadas para {table_name}...")
                table.export_cross_references(
                    target_directorypath=str(tags_cr_path), filter=CROSS_REF_FILTER
                )
                tags_cr_exported += 1
            except RuntimeError as table_ex:
                print(
                    f"      ERROR TIA al exportar referencias cruzadas para la tabla {table_name}: {table_ex}"
                )
                tags_cr_skipped += 1
            except Exception as table_ex:
                print(
                    f"      ERROR GENERAL al exportar referencias cruzadas para la tabla {table_name}: {table_ex}"
                )
                traceback.print_exc()
                tags_cr_skipped += 1
        print(
            f"  Resumen de exportación de referencias cruzadas de tablas: Exportados={tags_cr_exported}, Omitidos/Errores={tags_cr_skipped}"
        )
    except AttributeError:
        print(
            "  Error de atributo: No se pudo encontrar 'get_plc_tag_tables' en el objeto PLC. Omitiendo tablas de variables."
        )
    except Exception as e:
        print(
            f"  ERROR al acceder a las tablas de variables para exportar referencias cruzadas: {e}"
        )
        traceback.print_exc()

    # --- Export PLC Data Type (UDT) Cross-References ---
    udts_cr_exported = 0
    udts_cr_skipped = 0
    print(
        f"\n[PLC: {plc_name}] Exportando referencias cruzadas de tipos de datos PLC (UDTs)..."
    )
    udts_cr_path = plc_export_dir / "PlcDataTypes_CR"
    udts_cr_path.mkdir(exist_ok=True)
    print(f"  Destino: {udts_cr_path}")

    try:
        udts = plc.get_user_data_types()
        print(f"  Se encontraron {len(udts)} UDTs.")
        for udt in udts:
            udt_name = udt.get_name()
            print(f"    Procesando UDT: {udt_name}...")
            try:
                print(f"      Exportando referencias cruzadas para {udt_name}...")
                udt.export_cross_references(
                    target_directorypath=str(udts_cr_path), filter=CROSS_REF_FILTER
                )
                udts_cr_exported += 1
            except RuntimeError as udt_ex:
                print(
                    f"      ERROR TIA al exportar referencias cruzadas para el UDT {udt_name}: {udt_ex}"
                )
                udts_cr_skipped += 1
            except Exception as udt_ex:
                print(
                    f"      ERROR GENERAL al exportar referencias cruzadas para el UDT {udt_name}: {udt_ex}"
                )
                traceback.print_exc()
                udts_cr_skipped += 1
        print(
            f"  Resumen de exportación de referencias cruzadas de UDTs: Exportados={udts_cr_exported}, Omitidos/Errores={udts_cr_skipped}"
        )
    except AttributeError:
        print(
            "  Error de atributo: No se pudo encontrar 'get_user_data_types' en el objeto PLC. Omitiendo UDTs."
        )
    except Exception as e:
        print(f"  ERROR al acceder a los UDTs para exportar referencias cruzadas: {e}")
        traceback.print_exc()

    # --- Export System Block Cross-References ---
    sys_blocks_cr_exported = 0
    sys_blocks_cr_skipped = 0
    print(
        f"\n[PLC: {plc_name}] Intentando exportar referencias cruzadas de bloques de sistema..."
    )
    sys_blocks_cr_path = plc_export_dir / "SystemBlocks_CR"
    sys_blocks_cr_path.mkdir(exist_ok=True)
    print(f"  Destino: {sys_blocks_cr_path}")

    try:
        if hasattr(plc, "get_system_blocks"):
            system_blocks = plc.get_system_blocks()
            print(f"  Se encontraron {len(system_blocks)} bloques de sistema.")
            for sys_block in system_blocks:
                sys_block_name = sys_block.get_name()
                print(f"    Procesando bloque de sistema: {sys_block_name}...")
                try:
                    print(
                        f"      Exportando referencias cruzadas para {sys_block_name}..."
                    )
                    sys_block.export_cross_references(
                        target_directorypath=str(sys_blocks_cr_path),
                        filter=CROSS_REF_FILTER,
                    )
                    sys_blocks_cr_exported += 1
                except RuntimeError as sys_ex:
                    print(
                        f"      ERROR TIA al exportar referencias cruzadas para el bloque de sistema {sys_block_name}: {sys_ex}"
                    )
                    sys_blocks_cr_skipped += 1
                except Exception as sys_ex:
                    print(
                        f"      ERROR GENERAL al exportar referencias cruzadas para el bloque de sistema {sys_block_name}: {sys_ex}"
                    )
                    traceback.print_exc()
                    sys_blocks_cr_skipped += 1
        else:
            print(
                "  Método 'get_system_blocks' no encontrado en el objeto PLC. Omitiendo bloques de sistema."
            )

        print(
            f"  Resumen de exportación de referencias cruzadas de bloques de sistema: Exportados={sys_blocks_cr_exported}, Omitidos/Errores={sys_blocks_cr_skipped}"
        )
    except AttributeError:
        print(
            "  Error de atributo durante el procesamiento de bloques de sistema. Omitiendo bloques de sistema restantes."
        )
        traceback.print_exc()
    except Exception as e:
        print(
            f"  ERROR al acceder/procesar bloques de sistema para exportar referencias cruzadas: {e}"
        )
        traceback.print_exc()

    # --- Export Software Unit Cross-References ---
    sw_units_cr_exported = 0
    sw_units_cr_skipped = 0
    print(
        f"\n[PLC: {plc_name}] Intentando exportar referencias cruzadas de unidades de software..."
    )
    sw_units_cr_path = plc_export_dir / "SoftwareUnits_CR"
    sw_units_cr_path.mkdir(exist_ok=True)
    print(f"  Destino: {sw_units_cr_path}")

    try:
        if hasattr(plc, "get_software_units"):
            software_units = plc.get_software_units()
            print(f"  Se encontraron {len(software_units)} unidades de software.")
            for unit in software_units:
                unit_name = unit.get_name()
                print(f"    Procesando unidad de software: {unit_name}...")
                try:
                    print(f"      Exportando referencias cruzadas para {unit_name}...")
                    unit.export_cross_references(
                        target_directorypath=str(sw_units_cr_path),
                        filter=CROSS_REF_FILTER,
                    )
                    sw_units_cr_exported += 1
                except RuntimeError as unit_ex:
                    print(
                        f"      ERROR TIA al exportar referencias cruzadas para la unidad de software {unit_name}: {unit_ex}"
                    )
                    sw_units_cr_skipped += 1
                except Exception as unit_ex:
                    print(
                        f"      ERROR GENERAL al exportar referencias cruzadas para la unidad de software {unit_name}: {unit_ex}"
                    )
                    traceback.print_exc()
                    sw_units_cr_skipped += 1
            print(
                f"  Resumen de exportación de referencias cruzadas de unidades de software: Exportados={sw_units_cr_exported}, Omitidos/Errores={sw_units_cr_skipped}"
            )
        else:
            print(
                "  Método 'get_software_units' no encontrado en el objeto PLC. Omitiendo unidades de software."
            )
    except AttributeError:
        print(
            "  Error de atributo durante el procesamiento de unidades de software. Omitiendo unidades restantes."
        )
        traceback.print_exc()
    except Exception as e:
        print(
            f"  ERROR al acceder/procesar unidades de software para exportar referencias cruzadas: {e}"
        )
        traceback.print_exc()

    print(f"\n--- Finalizado el procesamiento del PLC: {plc_name} ---")


def open_portal_and_project(tia_version: str, project_file_path: str):
    """Abre TIA Portal y el proyecto indicado, devolviendo el portal y el objeto proyecto."""
    print(f"\nConectando a TIA Portal V{tia_version}...")
    portal = ts.open_portal(
        version=tia_version,
        portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface,
    )
    print("Conectado a TIA Portal.")
    print(f"ID del proceso del Portal: {portal.get_process_id()}")

    project_obj = portal.open_project(project_file_path=str(project_file_path))
    if project_obj is None:
        project_obj = portal.get_project()
    if project_obj is None:
        raise Exception(
            "No se pudo abrir u obtener el proyecto especificado tras la reapertura."
        )
    return portal, project_obj


# --- Main Script ---

if __name__ == "__main__":
    configs = load_configuration()
    working_directory = configs.get("working_directory")

    print("--- Exportador de Referencias Cruzadas de TIA Portal ---")
    print(f"Configuración:")
    print(
        f"  - Tiempo esperado por bloque: {BLOCK_TIMEOUT_SECONDS} segundos (para logging)"
    )
    print(f"  - Máximo intentos de reapertura: {MAX_REOPEN_ATTEMPTS}")
    print(f"  - Filtro de referencias cruzadas: {CROSS_REF_FILTER}")
    print("")

    # Validate working directory
    if not working_directory or not os.path.isdir(working_directory):
        print("ERROR: Directorio de trabajo no configurado o inválido.")
        print(
            "Por favor configure el directorio de trabajo usando la aplicación principal."
        )
        sys.exit(1)

    # 1. Select Project File
    project_file = select_project_file()

    # 2. Detect TIA Portal version from project file
    tia_version = detect_tia_version(project_file)

    # 3. Define Export Directory using working_directory and subfolder
    export_base_dir = Path(working_directory)
    try:
        export_base_dir.mkdir(parents=True, exist_ok=True)
        print(f"\nProyecto seleccionado: {project_file}")
        print(f"Usando directorio base de exportación: {export_base_dir.resolve()}")
    except Exception as e:
        print(
            f"ERROR: No se pudo crear el directorio de exportación '{export_base_dir}'. Error: {e}"
        )
        sys.exit(1)

    portal_instance = None
    project_object = None

    try:
        # 4. Connect to TIA Portal with detected version
        portal_instance, project_object = open_portal_and_project(
            tia_version, project_file
        )

        # 5. Get PLCs
        plcs = project_object.get_plcs()
        if not plcs:
            print("No se encontraron dispositivos PLC en el proyecto.")
        else:
            print(
                f"Se encontraron {len(plcs)} PLC(s). Iniciando proceso de exportación de referencias cruzadas..."
            )

            # 7. Iterate and Export Cross-References for each PLC con lógica de re-apertura
            for plc_device in plcs:
                plc_name = plc_device.get_name()
                exported_blocks = set()
                problematic_blocks = set()
                skipped_blocks_report = []
                reopen_attempts = 0

                while True:
                    try:
                        export_plc_cross_references(
                            plc=plc_device,
                            export_base_dir=export_base_dir,
                            exported_blocks=exported_blocks,
                            problematic_blocks=problematic_blocks,
                        )
                        break  # Éxito
                    except PortalDisposedException as pd_ex:
                        reopen_attempts += 1
                        failed_block = pd_ex.failed_block
                        if failed_block:
                            norm_failed_block = _normalize_name(failed_block)
                            problematic_blocks.add(norm_failed_block)
                            skipped_blocks_report.append(failed_block)
                            print(f"Marcando bloque problemático: {failed_block}")
                        else:
                            print(
                                "Error general detectado sin bloque específico identificado"
                            )

                        if reopen_attempts > MAX_REOPEN_ATTEMPTS:
                            print(
                                f"Se alcanzó el máximo de re-aperturas permitidas ({MAX_REOPEN_ATTEMPTS}) para el PLC '{plc_name}'. Abortando."
                            )
                            break

                        # Intentamos cerrar el portal actual (si existe)
                        try:
                            print("Cerrando instancia actual de TIA Portal...")
                            portal_instance.close_portal()
                        except Exception:
                            pass

                        # Re-abrir portal y proyecto
                        print(
                            f"Re-abriendo TIA Portal (intento {reopen_attempts}/{MAX_REOPEN_ATTEMPTS})..."
                        )
                        portal_instance, project_object = open_portal_and_project(
                            tia_version, project_file
                        )

                        # Buscar de nuevo el PLC por nombre
                        plc_device = None
                        for _plc in project_object.get_plcs():
                            if _plc.get_name() == plc_name:
                                plc_device = _plc
                                break
                        if plc_device is None:
                            print(
                                f"No se encontró el PLC '{plc_name}' tras la re-apertura. Se aborta su procesamiento."
                            )
                            break
                        # Continuar con el while
                        continue

                if skipped_blocks_report:
                    print(
                        f"\nBloques problemáticos para el PLC '{plc_name}': {', '.join(set(skipped_blocks_report))}"
                    )
                    print(
                        f"Total de bloques problemáticos registrados: {len(problematic_blocks)}"
                    )

        print("\nProceso de exportación de referencias cruzadas completado.")

    except RuntimeError as tia_ex:
        print(f"\nError de TIA Portal Openness: {tia_ex}")
        traceback.print_exc()
    except FileNotFoundError:
        print(f"\nERROR: Archivo de proyecto no encontrado en {project_file}")
    except Exception as e:
        print(f"\nOcurrió un error inesperado: {e}")
        traceback.print_exc()
    finally:
        # 8. Cleanup
        if portal_instance:
            try:
                print("\nCerrando TIA Portal...")
                portal_instance.close_portal()
                print("TIA Portal cerrado.")
            except Exception as close_ex:
                print(f"Error durante la limpieza de TIA Portal: {close_ex}")

        print("\nScript finalizado.")
